| @@ -0,0 +1 @@ | |||
| save-prefix=~ | |||
| @@ -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" | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -27,6 +27,16 @@ export default [ | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| path: '/authorize', | |||
| layout: false, | |||
| component: './Authorize/index', | |||
| }, | |||
| { | |||
| path: '/gitlink', | |||
| layout: true, | |||
| component: './GitLink/index', | |||
| }, | |||
| { | |||
| path: '/user', | |||
| layout: false, | |||
| @@ -60,7 +60,7 @@ | |||
| "@antv/hierarchy": "^0.6.12", | |||
| "@types/crypto-js": "^4.2.2", | |||
| "@umijs/route-utils": "^4.0.1", | |||
| "antd": "^5.4.4", | |||
| "antd": "~5.21.4", | |||
| "classnames": "^2.3.2", | |||
| "crypto-js": "^4.2.0", | |||
| "echarts": "^5.5.0", | |||
| @@ -111,7 +111,7 @@ | |||
| "umi-presets-pro": "^2.0.0" | |||
| }, | |||
| "engines": { | |||
| "node": ">=12.0.0" | |||
| "node": ">=16.14.0" | |||
| }, | |||
| "create-umi": { | |||
| "ignoreScript": [ | |||
| @@ -7,7 +7,6 @@ import defaultSettings from '../config/defaultSettings'; | |||
| import '../public/fonts/font.css'; | |||
| import { getAccessToken } from './access'; | |||
| import './dayjsConfig'; | |||
| import { PageEnum } from './enums/pagesEnums'; | |||
| import './global.less'; | |||
| import { removeAllPageCacheState } from './hooks/pageCacheState'; | |||
| import { | |||
| @@ -23,6 +22,7 @@ export { requestConfig as request } from './requestConfig'; | |||
| import { type GlobalInitialState } from '@/types'; | |||
| import { menuItemRender } from '@/utils/menuRender'; | |||
| import ErrorBoundary from './components/ErrorBoundary'; | |||
| import { needAuth } from './utils'; | |||
| import { gotoLoginPage } from './utils/ui'; | |||
| /** | |||
| @@ -40,14 +40,17 @@ export async function getInitialState(): Promise<GlobalInitialState> { | |||
| roleNames: response.user.roles, | |||
| } as API.CurrentUser; | |||
| } catch (error) { | |||
| console.error(error); | |||
| console.error('1111', error); | |||
| gotoLoginPage(); | |||
| } | |||
| return undefined; | |||
| }; | |||
| // 如果不是登录页面,执行 | |||
| const { location } = history; | |||
| if (location.pathname !== PageEnum.LOGIN) { | |||
| console.log('getInitialState', needAuth(location.pathname)); | |||
| if (needAuth(location.pathname)) { | |||
| const currentUser = await fetchUserInfo(); | |||
| return { | |||
| fetchUserInfo, | |||
| @@ -94,7 +97,7 @@ export const layout: RuntimeConfig['layout'] = ({ initialState }) => { | |||
| onPageChange: () => { | |||
| const { location } = history; | |||
| // 如果没有登录,重定向到 login | |||
| if (!initialState?.currentUser && location.pathname !== PageEnum.LOGIN) { | |||
| if (!initialState?.currentUser && needAuth(location.pathname)) { | |||
| gotoLoginPage(); | |||
| } | |||
| }, | |||
| @@ -118,18 +121,10 @@ export const layout: RuntimeConfig['layout'] = ({ initialState }) => { | |||
| width: '331px', | |||
| }, | |||
| ], | |||
| // links: isDev | |||
| // ? [ | |||
| // <Link key="openapi" to="/umi/plugin/openapi" target="_blank"> | |||
| // <LinkOutlined /> | |||
| // <span>OpenAPI 文档</span> | |||
| // </Link>, | |||
| // ] | |||
| // : [], | |||
| // 自定义 403 页面 | |||
| // unAccessible: <div>unAccessible</div>, | |||
| // 增加一个 loading 的状态 | |||
| childrenRender: (children) => { | |||
| // 增加一个 loading 的状态 | |||
| // if (initialState?.loading) return <PageLoading />; | |||
| return ( | |||
| <div className="kf-page-container"> | |||
| @@ -167,8 +162,8 @@ export const layout: RuntimeConfig['layout'] = ({ initialState }) => { | |||
| export const onRouteChange: RuntimeConfig['onRouteChange'] = async (e) => { | |||
| const { location } = e; | |||
| const menus = getRemoteMenu(); | |||
| // console.log('onRouteChange', e); | |||
| if (menus === null && location.pathname !== PageEnum.LOGIN) { | |||
| console.log('onRouteChange', menus); | |||
| if (menus === null && needAuth(location.pathname)) { | |||
| history.go(0); | |||
| } | |||
| }; | |||
| @@ -178,12 +173,12 @@ export const patchRoutes: RuntimeConfig['patchRoutes'] = (e) => { | |||
| }; | |||
| export const patchClientRoutes: RuntimeConfig['patchClientRoutes'] = (e) => { | |||
| //console.log('patchClientRoutes', e); | |||
| console.log('patchClientRoutes', e); | |||
| patchRouteWithRemoteMenus(e.routes); | |||
| }; | |||
| export function render(oldRender: () => void) { | |||
| // console.log('render'); | |||
| console.log('render'); | |||
| const token = getAccessToken(); | |||
| if (!token || token?.length === 0) { | |||
| oldRender(); | |||
| @@ -236,6 +231,7 @@ export const antd: RuntimeAntdConfig = (memo) => { | |||
| memo.theme.components.Table = { | |||
| headerBg: 'rgba(242, 244, 247, 0.36)', | |||
| headerBorderRadius: 4, | |||
| rowSelectedBg: 'rgba(22, 100, 255, 0.05)', | |||
| }; | |||
| memo.theme.components.Tabs = { | |||
| titleFontSize: 16, | |||
| @@ -5,48 +5,49 @@ | |||
| gap: 20px 40px; | |||
| align-items: flex-start; | |||
| width: 80%; | |||
| } | |||
| .kf-basic-info-item { | |||
| display: flex; | |||
| align-items: flex-start; | |||
| width: calc(50% - 20px); | |||
| font-size: 16px; | |||
| line-height: 1.6; | |||
| &__item { | |||
| display: flex; | |||
| align-items: flex-start; | |||
| width: calc(50% - 20px); | |||
| &__label { | |||
| position: relative; | |||
| flex: none; | |||
| color: @text-color-secondary; | |||
| text-align: justify; | |||
| text-align-last: justify; | |||
| &__label { | |||
| position: relative; | |||
| flex: none; | |||
| color: @text-color-secondary; | |||
| font-size: @font-size-content; | |||
| line-height: 1.6; | |||
| text-align: justify; | |||
| text-align-last: justify; | |||
| &::after { | |||
| position: absolute; | |||
| content: ':'; | |||
| &::after { | |||
| position: absolute; | |||
| content: ':'; | |||
| } | |||
| } | |||
| } | |||
| &__list-value { | |||
| display: flex; | |||
| flex: 1; | |||
| flex-direction: column; | |||
| gap: 5px 0; | |||
| } | |||
| &__value-container { | |||
| display: flex; | |||
| flex: 1; | |||
| flex-direction: column; | |||
| gap: 5px 0; | |||
| } | |||
| &__value { | |||
| flex: 1; | |||
| margin-left: 16px; | |||
| white-space: pre-line; | |||
| word-break: break-all; | |||
| } | |||
| &__value { | |||
| flex: 1; | |||
| margin-left: 16px; | |||
| font-size: @font-size-content; | |||
| line-height: 1.6; | |||
| word-break: break-all; | |||
| &__text { | |||
| color: @text-color; | |||
| } | |||
| &__text { | |||
| color: @text-color; | |||
| } | |||
| &__link:hover { | |||
| text-decoration: underline @underline-color; | |||
| text-underline-offset: 3px; | |||
| &__link:hover { | |||
| text-decoration: underline @underline-color; | |||
| text-underline-offset: 3px; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -1,4 +1,5 @@ | |||
| import { Link } from '@umijs/max'; | |||
| import { Typography } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import './index.less'; | |||
| @@ -11,6 +12,7 @@ export type BasicInfoLink = { | |||
| export type BasicInfoData = { | |||
| label: string; | |||
| value?: any; | |||
| ellipsis?: boolean; | |||
| format?: (_value?: any) => string | BasicInfoLink | BasicInfoLink[] | undefined; | |||
| }; | |||
| @@ -18,45 +20,73 @@ type BasicInfoProps = { | |||
| datas: BasicInfoData[]; | |||
| className?: string; | |||
| style?: React.CSSProperties; | |||
| labelWidth?: number; | |||
| labelWidth: number; | |||
| }; | |||
| function BasicInfo({ datas, className, style, labelWidth = 100 }: BasicInfoProps) { | |||
| type BasicInfoItemProps = { | |||
| data: BasicInfoData; | |||
| labelWidth: number; | |||
| classPrefix: string; | |||
| }; | |||
| type BasicInfoItemValueProps = BasicInfoLink & { | |||
| ellipsis?: boolean; | |||
| classPrefix: string; | |||
| }; | |||
| export default function BasicInfo({ datas, className, style, labelWidth }: BasicInfoProps) { | |||
| return ( | |||
| <div className={classNames('kf-basic-info', className)} style={style}> | |||
| {datas.map((item) => ( | |||
| <BasicInfoItem key={item.label} data={item} labelWidth={labelWidth} /> | |||
| <BasicInfoItem | |||
| key={item.label} | |||
| data={item} | |||
| labelWidth={labelWidth} | |||
| classPrefix="kf-basic-info" | |||
| /> | |||
| ))} | |||
| </div> | |||
| ); | |||
| } | |||
| type BasicInfoItemProps = { | |||
| data: BasicInfoData; | |||
| labelWidth?: number; | |||
| }; | |||
| function BasicInfoItem({ data, labelWidth = 100 }: BasicInfoItemProps) { | |||
| const { label, value, format } = data; | |||
| export function BasicInfoItem({ data, labelWidth, classPrefix }: BasicInfoItemProps) { | |||
| const { label, value, format, ellipsis } = data; | |||
| const formatValue = format ? format(value) : value; | |||
| const myClassName = `${classPrefix}__item`; | |||
| let valueComponent = undefined; | |||
| if (Array.isArray(formatValue)) { | |||
| valueComponent = ( | |||
| <div className="kf-basic-info-item__list-value"> | |||
| <div className={`${myClassName}__value-container`}> | |||
| {formatValue.map((item: BasicInfoLink) => ( | |||
| <BasicInfoItemValue key={item.value} value={item.value} link={item.link} url={item.url} /> | |||
| <BasicInfoItemValue | |||
| key={item.value} | |||
| value={item.value} | |||
| link={item.link} | |||
| url={item.url} | |||
| ellipsis={ellipsis} | |||
| classPrefix={classPrefix} | |||
| /> | |||
| ))} | |||
| </div> | |||
| ); | |||
| } else if (typeof formatValue === 'object' && formatValue) { | |||
| valueComponent = ( | |||
| <BasicInfoItemValue value={formatValue.value} link={formatValue.link} url={formatValue.url} /> | |||
| <BasicInfoItemValue | |||
| value={formatValue.value} | |||
| link={formatValue.link} | |||
| url={formatValue.url} | |||
| ellipsis={ellipsis} | |||
| classPrefix={classPrefix} | |||
| /> | |||
| ); | |||
| } else { | |||
| valueComponent = <BasicInfoItemValue value={formatValue} />; | |||
| valueComponent = ( | |||
| <BasicInfoItemValue value={formatValue} ellipsis={ellipsis} classPrefix={classPrefix} /> | |||
| ); | |||
| } | |||
| return ( | |||
| <div className="kf-basic-info-item" key={label}> | |||
| <div className="kf-basic-info-item__label" style={{ width: labelWidth }}> | |||
| <div className={myClassName} key={label}> | |||
| <div className={`${myClassName}__label`} style={{ width: labelWidth }}> | |||
| {label} | |||
| </div> | |||
| {valueComponent} | |||
| @@ -64,35 +94,36 @@ function BasicInfoItem({ data, labelWidth = 100 }: BasicInfoItemProps) { | |||
| ); | |||
| } | |||
| type BasicInfoItemValueProps = { | |||
| value: string; | |||
| link?: string; | |||
| url?: string; | |||
| }; | |||
| function BasicInfoItemValue({ value, link, url }: BasicInfoItemValueProps) { | |||
| export function BasicInfoItemValue({ | |||
| value, | |||
| link, | |||
| url, | |||
| ellipsis, | |||
| classPrefix, | |||
| }: BasicInfoItemValueProps) { | |||
| const myClassName = `${classPrefix}__item__value`; | |||
| let component = undefined; | |||
| if (url && value) { | |||
| return ( | |||
| <a | |||
| className="kf-basic-info-item__value kf-basic-info-item__link" | |||
| href={url} | |||
| target="_blank" | |||
| rel="noopener noreferrer" | |||
| > | |||
| component = ( | |||
| <a className={`${myClassName}__link`} href={url} target="_blank" rel="noopener noreferrer"> | |||
| {value} | |||
| </a> | |||
| ); | |||
| } else if (link && value) { | |||
| return ( | |||
| <Link to={link} className="kf-basic-info-item__value kf-basic-info-item__link"> | |||
| component = ( | |||
| <Link to={link} className={`${myClassName}__link`}> | |||
| {value} | |||
| </Link> | |||
| ); | |||
| } else { | |||
| return ( | |||
| <div className="kf-basic-info-item__value kf-basic-info-item__text">{value ?? '--'}</div> | |||
| ); | |||
| component = <span className={`${myClassName}__text`}>{value ?? '--'}</span>; | |||
| } | |||
| } | |||
| export default BasicInfo; | |||
| return ( | |||
| <div className={myClassName}> | |||
| <Typography.Text ellipsis={ellipsis ? { tooltip: value } : false}> | |||
| {component} | |||
| </Typography.Text> | |||
| </div> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,60 @@ | |||
| .kf-basic-table-info { | |||
| display: flex; | |||
| flex-direction: row; | |||
| flex-wrap: wrap; | |||
| align-items: stretch; | |||
| width: 100%; | |||
| border: 1px solid @border-color-base; | |||
| border-bottom: none; | |||
| border-radius: 4px; | |||
| &__item { | |||
| display: flex; | |||
| align-items: stretch; | |||
| width: 25%; | |||
| border-bottom: 1px solid @border-color-base; | |||
| &__label { | |||
| flex: none; | |||
| padding: 12px 20px; | |||
| color: @text-color-secondary; | |||
| font-size: 14px; | |||
| text-align: left; | |||
| background-color: .addAlpha(#606b7a, 0.05) []; | |||
| } | |||
| &__value-container { | |||
| display: flex; | |||
| flex: 1; | |||
| flex-direction: column; | |||
| align-items: flex-start; | |||
| min-width: 0; | |||
| } | |||
| &__value { | |||
| flex: 1; | |||
| min-width: 0; | |||
| margin: 0 !important; | |||
| padding: 12px 20px 4px; | |||
| font-size: @font-size; | |||
| word-break: break-all; | |||
| & + & { | |||
| padding-top: 0; | |||
| } | |||
| &:last-child { | |||
| padding-bottom: 12px; | |||
| } | |||
| &__text { | |||
| color: @text-color; | |||
| } | |||
| &__link:hover { | |||
| text-decoration: underline @underline-color; | |||
| text-underline-offset: 3px; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -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; | |||
| @@ -12,6 +12,7 @@ export enum IframePageType { | |||
| DatasetAnnotation = 'DatasetAnnotation', // 数据标注 | |||
| AppDevelopment = 'AppDevelopment', // 应用开发 | |||
| DevEnv = 'DevEnv', // 开发环境 | |||
| GitLink = 'GitLink', | |||
| } | |||
| const getRequestAPI = (type: IframePageType): (() => Promise<any>) => { | |||
| @@ -26,6 +27,8 @@ const getRequestAPI = (type: IframePageType): (() => Promise<any>) => { | |||
| code: 200, | |||
| data: SessionStorage.getItem(SessionStorage.editorUrlKey) || '', | |||
| }); | |||
| case IframePageType.GitLink: | |||
| return () => Promise.resolve({ code: 200, data: 'http://172.20.32.201:4000' }); | |||
| } | |||
| }; | |||
| @@ -20,7 +20,7 @@ | |||
| height: 40px; | |||
| padding: 0 30px; | |||
| font-size: @font-size-content; | |||
| border-radius: 10px; | |||
| border-radius: 6px; | |||
| } | |||
| .ant-btn-default { | |||
| border-color: transparent; | |||
| @@ -1,6 +1,8 @@ | |||
| import { clearSessionToken } from '@/access'; | |||
| import { setRemoteMenu } from '@/services/session'; | |||
| import { logout } from '@/services/system/auth'; | |||
| import { ClientInfo } from '@/types'; | |||
| import SessionStorage from '@/utils/sessionStorage'; | |||
| import { gotoLoginPage } from '@/utils/ui'; | |||
| import { LogoutOutlined, UserOutlined } from '@ant-design/icons'; | |||
| import { setAlpha } from '@ant-design/pro-components'; | |||
| @@ -64,6 +66,11 @@ const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => { | |||
| clearSessionToken(); | |||
| setRemoteMenu(null); | |||
| gotoLoginPage(); | |||
| const clientInfo: ClientInfo = SessionStorage.getItem(SessionStorage.clientInfoKey, true); | |||
| if (clientInfo) { | |||
| const { logoutUri } = clientInfo; | |||
| location.replace(logoutUri); | |||
| } | |||
| }; | |||
| const actionClassName = useEmotionCss(({ token }) => { | |||
| return { | |||
| @@ -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> | |||
| ); | |||
| } | |||
| @@ -1,3 +1,4 @@ | |||
| export enum PageEnum { | |||
| LOGIN = '/user/login', | |||
| Authorize = '/authorize', | |||
| } | |||
| @@ -5,7 +5,7 @@ | |||
| */ | |||
| import { FormInstance } from 'antd'; | |||
| import { debounce } from 'lodash'; | |||
| import { useCallback, useEffect, useRef, useState } from 'react'; | |||
| import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; | |||
| /** | |||
| * 生成具有初始值的状态引用 | |||
| * | |||
| @@ -156,3 +156,45 @@ export const useEffectWhen = (effect: () => void, deps: React.DependencyList, wh | |||
| } | |||
| }, [when]); | |||
| }; | |||
| // 选择、全选操作 | |||
| export const useCheck = <T>(list: T[]) => { | |||
| const [selected, setSelected] = useState<T[]>([]); | |||
| const checked = useMemo(() => { | |||
| return selected.length === list.length && selected.length > 0; | |||
| }, [selected, list]); | |||
| const indeterminate = useMemo(() => { | |||
| return selected.length > 0 && selected.length < list.length; | |||
| }, [selected, list]); | |||
| const checkAll = useCallback(() => { | |||
| setSelected(checked ? [] : list); | |||
| }, [list, checked]); | |||
| const isSingleChecked = useCallback((item: T) => selected.includes(item), [selected]); | |||
| const checkSingle = useCallback( | |||
| (item: T) => { | |||
| setSelected((prev) => { | |||
| if (isSingleChecked(item)) { | |||
| return prev.filter((i) => i !== item); | |||
| } else { | |||
| return [...prev, item]; | |||
| } | |||
| }); | |||
| }, | |||
| [selected, isSingleChecked], | |||
| ); | |||
| return [ | |||
| selected, | |||
| setSelected, | |||
| checked, | |||
| indeterminate, | |||
| checkAll, | |||
| isSingleChecked, | |||
| checkSingle, | |||
| ] as const; | |||
| }; | |||
| @@ -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; | |||
| } | |||
| @@ -162,7 +168,7 @@ | |||
| height: 40px; | |||
| padding: 0 30px; | |||
| font-size: @font-size-content; | |||
| border-radius: 10px; | |||
| border-radius: 6px; | |||
| } | |||
| .ant-btn-default { | |||
| border-color: transparent; | |||
| @@ -0,0 +1,50 @@ | |||
| import { setSessionToken } from '@/access'; | |||
| import { loginByOauth2Req } from '@/services/auth'; | |||
| import { to } from '@/utils/promise'; | |||
| import { history, useModel, useSearchParams } from '@umijs/max'; | |||
| import { message } from 'antd'; | |||
| import { useEffect } from 'react'; | |||
| import { flushSync } from 'react-dom'; | |||
| import styles from './index.less'; | |||
| function Authorize() { | |||
| const { initialState, setInitialState } = useModel('@@initialState'); | |||
| const [searchParams] = useSearchParams(); | |||
| const code = searchParams.get('code'); | |||
| const redirect = searchParams.get('redirect'); | |||
| useEffect(() => { | |||
| loginByOauth2(); | |||
| }, []); | |||
| // 登录 | |||
| const loginByOauth2 = async () => { | |||
| const params = { | |||
| code, | |||
| }; | |||
| const [res] = await to(loginByOauth2Req(params)); | |||
| debugger; | |||
| if (res && res.data) { | |||
| const { access_token, expires_in } = res.data; | |||
| setSessionToken(access_token, access_token, expires_in); | |||
| message.success('登录成功!'); | |||
| await fetchUserInfo(); | |||
| history.push(redirect || '/'); | |||
| } | |||
| }; | |||
| const fetchUserInfo = async () => { | |||
| const userInfo = await initialState?.fetchUserInfo?.(); | |||
| if (userInfo) { | |||
| flushSync(() => { | |||
| setInitialState((s) => ({ | |||
| ...s, | |||
| currentUser: userInfo, | |||
| })); | |||
| }); | |||
| } | |||
| }; | |||
| return <div className={styles.container}></div>; | |||
| } | |||
| export default Authorize; | |||
| @@ -1,47 +1,46 @@ | |||
| .code-config-list { | |||
| display: flex; | |||
| flex: 1; | |||
| flex-direction: column; | |||
| .code-config { | |||
| height: 100%; | |||
| height: 100%; | |||
| padding: 20px 0; | |||
| background: white; | |||
| box-shadow: 0px 3px 6px rgba(146, 146, 146, 0.09); | |||
| &__header { | |||
| &__list { | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: space-between; | |||
| height: 32px; | |||
| margin-bottom: 30px; | |||
| padding: 0 30px; | |||
| color: @text-color; | |||
| font-size: 15px; | |||
| } | |||
| flex-direction: column; | |||
| height: calc(100% - 60px); | |||
| margin-top: 10px; | |||
| padding: 30px 30px 0; | |||
| background: white; | |||
| border-radius: 10px; | |||
| box-shadow: 0px 2px 12px rgba(180, 182, 191, 0.09); | |||
| &__content { | |||
| display: flex; | |||
| flex: 1; | |||
| flex-wrap: wrap; | |||
| gap: 20px; | |||
| align-content: flex-start; | |||
| width: 100%; | |||
| margin-bottom: 30px; | |||
| padding: 0 30px; | |||
| overflow-y: auto; | |||
| } | |||
| &__header { | |||
| display: flex; | |||
| align-items: center; | |||
| height: 32px; | |||
| color: @text-color; | |||
| font-size: 15px; | |||
| } | |||
| &__empty { | |||
| display: flex; | |||
| flex: 1; | |||
| align-items: center; | |||
| justify-content: center; | |||
| } | |||
| &__content { | |||
| display: flex; | |||
| flex: 1 1 0%; | |||
| flex-wrap: wrap; | |||
| gap: 20px; | |||
| align-content: flex-start; | |||
| width: 100%; | |||
| margin: 25px 0; | |||
| overflow-y: auto; | |||
| } | |||
| &__empty { | |||
| display: flex; | |||
| flex: 1; | |||
| align-items: center; | |||
| justify-content: center; | |||
| } | |||
| :global { | |||
| .ant-pagination { | |||
| margin-right: 30px; | |||
| text-align: right; | |||
| :global { | |||
| .ant-pagination { | |||
| margin-bottom: 25px; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -1,5 +1,12 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2024-10-10 09:55:12 | |||
| * @Description: 代码配置 | |||
| */ | |||
| import KFEmpty, { EmptyType } from '@/components/KFEmpty'; | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import PageTitle from '@/components/PageTitle'; | |||
| import { deleteCodeConfigReq, getCodeConfigListReq } from '@/services/codeConfig'; | |||
| import { getGitUrl } from '@/utils'; | |||
| import { openAntdModal } from '@/utils/modal'; | |||
| @@ -127,64 +134,69 @@ function CodeConfigList() { | |||
| }; | |||
| return ( | |||
| <div className={styles['code-config-list']}> | |||
| <div className={styles['code-config-list__header']}> | |||
| <span>数据总数:{total} 个</span> | |||
| <div> | |||
| <div className={styles['code-config']}> | |||
| <PageTitle title="代码配置"></PageTitle> | |||
| <div className={styles['code-config__list']}> | |||
| <div className={styles['code-config__list__header']}> | |||
| <span>数据总数:{total} 个</span> | |||
| <Input.Search | |||
| placeholder="按代码仓库名称筛选" | |||
| allowClear | |||
| onSearch={handleSearch} | |||
| style={{ | |||
| width: 300, | |||
| marginRight: '20px', | |||
| marginLeft: 'auto', | |||
| }} | |||
| onChange={(e) => setInputText(e.target.value)} | |||
| value={inputText} | |||
| /> | |||
| <Button | |||
| type="default" | |||
| style={{ marginLeft: '20px' }} | |||
| style={{ marginRight: 0 }} | |||
| onClick={createCodeConfig} | |||
| icon={<KFIcon type="icon-xinjian2" />} | |||
| > | |||
| 新建代码配置 | |||
| </Button> | |||
| </div> | |||
| </div> | |||
| {dataList && dataList.length !== 0 && ( | |||
| <> | |||
| <div className={styles['code-config-list__content']}> | |||
| {dataList.map((item) => ( | |||
| <CodeConfigItem | |||
| item={item} | |||
| key={item.id} | |||
| onRemove={handleRemove} | |||
| onEdit={handleEdit} | |||
| onClick={handleClick} | |||
| /> | |||
| ))} | |||
| </div> | |||
| <Pagination | |||
| total={total} | |||
| showSizeChanger | |||
| defaultPageSize={20} | |||
| pageSizeOptions={[20, 40, 60, 80, 100]} | |||
| showQuickJumper | |||
| onChange={handlePageChange} | |||
| {...pagination} | |||
| {dataList && dataList.length !== 0 && ( | |||
| <> | |||
| <div className={styles['code-config__list__content']}> | |||
| {dataList.map((item) => ( | |||
| <CodeConfigItem | |||
| item={item} | |||
| key={item.id} | |||
| onRemove={handleRemove} | |||
| onEdit={handleEdit} | |||
| onClick={handleClick} | |||
| /> | |||
| ))} | |||
| </div> | |||
| <Pagination | |||
| align="end" | |||
| total={total} | |||
| showSizeChanger | |||
| defaultPageSize={20} | |||
| pageSizeOptions={[20, 40, 60, 80, 100]} | |||
| showQuickJumper | |||
| onChange={handlePageChange} | |||
| {...pagination} | |||
| /> | |||
| </> | |||
| )} | |||
| {dataList && dataList.length === 0 && ( | |||
| <KFEmpty | |||
| className={styles['code-config__list__empty']} | |||
| type={EmptyType.NoData} | |||
| title="暂无数据" | |||
| content={'很抱歉,没有搜索到您想要的内容\n建议刷新试试'} | |||
| hasFooter={true} | |||
| onRefresh={getDataList} | |||
| /> | |||
| </> | |||
| )} | |||
| {dataList && dataList.length === 0 && ( | |||
| <KFEmpty | |||
| className={styles['code-config-list__empty']} | |||
| type={EmptyType.NoData} | |||
| title="暂无数据" | |||
| content={'很抱歉,没有搜索到您想要的内容\n建议刷新试试'} | |||
| hasFooter={true} | |||
| onRefresh={getDataList} | |||
| /> | |||
| )} | |||
| )} | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| @@ -2,51 +2,97 @@ | |||
| position: relative; | |||
| width: calc(25% - 15px); | |||
| padding: 20px; | |||
| background: white; | |||
| border: 1px solid #eaeaea; | |||
| background: linear-gradient(180deg, #f7faff 0%, #ffffff 100%); | |||
| border: 2px solid white; | |||
| border-radius: 4px; | |||
| box-shadow: 0px 3px 10px rgba(164, 169, 181, 0.13); | |||
| cursor: pointer; | |||
| &:hover { | |||
| border-color: @primary-color; | |||
| } | |||
| @media screen and (max-width: 1860px) { | |||
| & { | |||
| width: calc(33.33% - 13.33px); | |||
| } | |||
| } | |||
| &__name { | |||
| &__icon { | |||
| flex: none; | |||
| width: 16px; | |||
| height: 16px; | |||
| margin-right: 10px; | |||
| } | |||
| &__name { | |||
| position: relative; | |||
| margin-right: 20px; | |||
| margin-bottom: 0 !important; | |||
| color: @text-color; | |||
| font-weight: 500; | |||
| font-size: 16px; | |||
| &::after { | |||
| position: absolute; | |||
| top: 14px; | |||
| left: 0; | |||
| width: 100%; | |||
| height: 6px; | |||
| background: linear-gradient( | |||
| to right, | |||
| .addAlpha(@primary-color, 0.4) [] 0, | |||
| .addAlpha(@primary-color, 0) [] 100% | |||
| ); | |||
| content: ''; | |||
| } | |||
| } | |||
| &:hover &__name { | |||
| color: @primary-color; | |||
| } | |||
| &__tag { | |||
| padding: 2px 11px; | |||
| font-size: 12px; | |||
| border-radius: 1000px; | |||
| flex: none; | |||
| padding: 1px 10px; | |||
| font-size: 13px; | |||
| border-radius: 2px; | |||
| &--public { | |||
| color: @primary-color; | |||
| background-color: .addAlpha(@primary-color, 0.08) []; | |||
| border-color: .addAlpha(@primary-color, 0.5) []; | |||
| background-color: .addAlpha(@primary-color, 0.1) []; | |||
| border: 1px solid .addAlpha(@primary-color, 0.5) []; | |||
| } | |||
| &--private { | |||
| color: @warning-color; | |||
| background-color: .addAlpha(@warning-color, 0.08) []; | |||
| border-color: .addAlpha(@warning-color, 0.5) []; | |||
| background-color: .addAlpha(@warning-color, 0.1) []; | |||
| border: 1px solid .addAlpha(@warning-color, 0.5) []; | |||
| } | |||
| } | |||
| :global { | |||
| .ant-btn { | |||
| flex: none; | |||
| color: #808080; | |||
| } | |||
| } | |||
| &__url-box { | |||
| margin-bottom: 15px; | |||
| padding: 14px; | |||
| background-color: .addAlpha(@primary-color, 0.04) []; | |||
| border-radius: 4px; | |||
| } | |||
| &__url { | |||
| margin-bottom: 10px; | |||
| color: @text-color-secondary; | |||
| margin-bottom: 15px !important; | |||
| color: @text-color; | |||
| font-size: 14px; | |||
| } | |||
| &__branch { | |||
| margin-bottom: 20px; | |||
| color: @text-color-tertiary; | |||
| color: @text-color-secondary; | |||
| font-size: 14px; | |||
| } | |||
| @@ -59,13 +105,4 @@ | |||
| color: #808080; | |||
| font-size: 13px; | |||
| } | |||
| &:hover { | |||
| border-color: @primary-color; | |||
| box-shadow: 0px 0px 6px 1px rgba(0, 0, 0, 0.1); | |||
| } | |||
| &:hover &__name { | |||
| color: @primary-color; | |||
| } | |||
| } | |||
| @@ -19,6 +19,11 @@ function CodeConfigItem({ item, onClick, onEdit, onRemove }: CodeConfigItemProps | |||
| return ( | |||
| <div className={styles['code-config-item']} onClick={() => onClick?.(item)}> | |||
| <Flex justify="space-between" align="center" style={{ marginBottom: '20px', height: '32px' }}> | |||
| <img | |||
| className={styles['code-config-item__icon']} | |||
| src={require('@/assets/img/code-name-icon.png')} | |||
| alt="" | |||
| /> | |||
| <Typography.Paragraph | |||
| className={styles['code-config-item__name']} | |||
| ellipsis={{ tooltip: item.code_repo_name }} | |||
| @@ -58,17 +63,19 @@ function CodeConfigItem({ item, onClick, onEdit, onRemove }: CodeConfigItemProps | |||
| <KFIcon type="icon-shanchu" font={17} /> | |||
| </Button> | |||
| </Flex> | |||
| <Typography.Paragraph | |||
| className={styles['code-config-item__url']} | |||
| ellipsis={{ tooltip: item.git_url }} | |||
| > | |||
| {item.git_url} | |||
| </Typography.Paragraph> | |||
| <div className={styles['code-config-item__branch']}>{item.git_branch}</div> | |||
| <div className={styles['code-config-item__url-box']}> | |||
| <Typography.Paragraph | |||
| className={styles['code-config-item__url']} | |||
| ellipsis={{ tooltip: item.git_url }} | |||
| > | |||
| {item.git_url} | |||
| </Typography.Paragraph> | |||
| <div className={styles['code-config-item__branch']}>{item.git_branch}</div> | |||
| </div> | |||
| <Flex justify="space-between"> | |||
| <div className={styles['code-config-item__user']}> | |||
| <img | |||
| style={{ width: '17px', marginRight: '6px' }} | |||
| style={{ width: '16px', marginRight: '6px' }} | |||
| src={creatByImg} | |||
| alt="" | |||
| draggable={false} | |||
| @@ -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> | |||
| ); | |||
| } | |||
| @@ -13,13 +13,37 @@ | |||
| } | |||
| } | |||
| &:hover { | |||
| border-color: @primary-color; | |||
| box-shadow: 0px 0px 6px 1px rgba(0, 0, 0, 0.1); | |||
| } | |||
| &__name { | |||
| position: relative; | |||
| display: inline-block; | |||
| height: 24px; | |||
| margin: 0 10px 0 0 !important; | |||
| color: @text-color; | |||
| font-weight: 500; | |||
| font-size: 16px; | |||
| &::after { | |||
| position: absolute; | |||
| top: 14px; | |||
| left: 0; | |||
| width: 100%; | |||
| height: 6px; | |||
| background: linear-gradient( | |||
| to right, | |||
| .addAlpha(@primary-color, 0.3) [] 0, | |||
| .addAlpha(@primary-color, 0) [] 100% | |||
| ); | |||
| content: ''; | |||
| } | |||
| } | |||
| &:hover &__name { | |||
| color: @primary-color; | |||
| } | |||
| &__description { | |||
| @@ -37,25 +61,4 @@ | |||
| color: #808080; | |||
| font-size: 13px; | |||
| } | |||
| &:hover { | |||
| border-color: @primary-color; | |||
| box-shadow: 0px 0px 6px 1px rgba(0, 0, 0, 0.1); | |||
| .resource-item__name { | |||
| color: @primary-color; | |||
| } | |||
| } | |||
| } | |||
| .resource-item__name { | |||
| &::after { | |||
| position: absolute; | |||
| top: 14px; | |||
| left: 0; | |||
| width: 100%; | |||
| height: 6px; | |||
| background: linear-gradient(to right, rgba(22, 100, 255, 0.3) 0, rgba(22, 100, 255, 0) 100%); | |||
| content: ''; | |||
| } | |||
| } | |||
| @@ -33,7 +33,6 @@ | |||
| :global { | |||
| .ant-pagination { | |||
| margin-right: 30px; | |||
| text-align: right; | |||
| } | |||
| } | |||
| @@ -204,6 +204,7 @@ function ResourceList( | |||
| ))} | |||
| </div> | |||
| <Pagination | |||
| align="end" | |||
| total={total} | |||
| showSizeChanger | |||
| defaultPageSize={20} | |||
| @@ -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,12 @@ function EditorList() { | |||
| }; | |||
| // 分页切换 | |||
| const handleTableChange: TableProps['onChange'] = (pagination, filters, sorter, { action }) => { | |||
| const handleTableChange: TableProps<EditorData>['onChange'] = ( | |||
| pagination, | |||
| _filters, | |||
| _sorter, | |||
| { action }, | |||
| ) => { | |||
| if (action === 'paginate') { | |||
| setPagination(pagination); | |||
| } | |||
| @@ -186,21 +190,21 @@ function EditorList() { | |||
| dataIndex: 'computing_resource', | |||
| key: 'computing_resource', | |||
| width: '20%', | |||
| render: CommonTableCell(), | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '创建者', | |||
| dataIndex: 'update_by', | |||
| key: 'update_by', | |||
| width: '20%', | |||
| render: CommonTableCell(), | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '创建时间', | |||
| dataIndex: 'create_time', | |||
| key: 'create_time', | |||
| width: '20%', | |||
| render: DateTableCell, | |||
| render: tableCellRender(false, TableCellValueType.Date), | |||
| }, | |||
| { | |||
| title: '操作', | |||
| @@ -14,10 +14,30 @@ | |||
| &__table { | |||
| height: calc(100% - 60px); | |||
| padding: 20px 30px 0; | |||
| padding: 20px 30px; | |||
| background-color: white; | |||
| border-radius: 10px; | |||
| &__footer { | |||
| display: flex; | |||
| align-items: center; | |||
| padding-top: 20px; | |||
| color: @text-color-secondary; | |||
| font-size: 12px; | |||
| background-color: white; | |||
| div { | |||
| flex: 1; | |||
| height: 1px; | |||
| background-color: @border-color-base; | |||
| } | |||
| p { | |||
| flex: none; | |||
| margin: 0 8px; | |||
| } | |||
| } | |||
| :global { | |||
| .ant-table-container { | |||
| border: none !important; | |||
| @@ -25,7 +45,7 @@ | |||
| .ant-table-thead { | |||
| .ant-table-cell { | |||
| background-color: rgb(247, 247, 247); | |||
| border-color: #e8e8e8 !important; | |||
| border-color: @border-color-base !important; | |||
| } | |||
| } | |||
| .ant-table-tbody { | |||
| @@ -34,6 +54,13 @@ | |||
| border-left: none !important; | |||
| } | |||
| } | |||
| .ant-table-tbody-virtual::after { | |||
| border-bottom: none !important; | |||
| } | |||
| .ant-table-footer { | |||
| padding: 0; | |||
| border: none !important; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -1,14 +1,20 @@ | |||
| // import { useCacheState } from '@/hooks/pageCacheState'; | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2024-10-10 09:55:12 | |||
| * @Description: 实验对比 | |||
| */ | |||
| import { useDomSize } from '@/hooks'; | |||
| import { | |||
| getExpEvaluateInfosReq, | |||
| getExpMetricsReq, | |||
| getExpTrainInfosReq, | |||
| } from '@/services/experiment'; | |||
| import { tableSorter } from '@/utils'; | |||
| import { to } from '@/utils/promise'; | |||
| import tableCellRender, { arrayFormatter, dateFormatter } from '@/utils/table'; | |||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||
| import { useSearchParams } from '@umijs/max'; | |||
| import { App, Button, Table, /* TablePaginationConfig,*/ TableProps, Tooltip } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { useEffect, useMemo, useState } from 'react'; | |||
| import ExperimentStatusCell from '../components/ExperimentStatusCell'; | |||
| import { ComparisonType, comparisonConfig } from './config'; | |||
| @@ -23,43 +29,60 @@ type TableData = { | |||
| metrics_names: string[]; | |||
| metrics: Record<string, number>; | |||
| params_names: string[]; | |||
| params: Record<string, string>; | |||
| params: Record<string, number>; | |||
| }; | |||
| const pageSize = 30; | |||
| // function Footer() { | |||
| // return ( | |||
| // <div className={styles['experiment-comparison__table__footer']}> | |||
| // <div></div> | |||
| // <p>我是有底线的</p> | |||
| // <div></div> | |||
| // </div> | |||
| // ); | |||
| // } | |||
| function ExperimentComparison() { | |||
| const [searchParams] = useSearchParams(); | |||
| const comparisonType = searchParams.get('type') as ComparisonType; | |||
| const experimentId = searchParams.get('id'); | |||
| const [tableData, setTableData] = useState<TableData[]>([]); | |||
| // const [cacheState, setCacheState] = useCacheState(); | |||
| // const [total, setTotal] = useState(0); | |||
| const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]); | |||
| // const [loading, setLoading] = useState(false); | |||
| const { message } = App.useApp(); | |||
| const config = useMemo(() => comparisonConfig[comparisonType], [comparisonType]); | |||
| // const [pagination, setPagination] = useState<TablePaginationConfig>( | |||
| // cacheState?.pagination ?? { | |||
| // current: 1, | |||
| // pageSize: 10, | |||
| // }, | |||
| // ); | |||
| const [tableRef, { width: tableWidth, height: tableHeight }] = useDomSize<HTMLDivElement>( | |||
| 0, | |||
| 0, | |||
| [], | |||
| ); | |||
| const [loadCompleted, setLoadCompleted] = useState(false); | |||
| const [loading, setLoading] = useState(false); // 避免误触发加载更多 | |||
| useEffect(() => { | |||
| getComparisonData(); | |||
| }, [experimentId]); | |||
| // 获取对比数据列表 | |||
| const getComparisonData = async () => { | |||
| // setLoading(true); | |||
| const getComparisonData = async (offset: string = '') => { | |||
| const request = | |||
| comparisonType === ComparisonType.Train ? getExpTrainInfosReq : getExpEvaluateInfosReq; | |||
| const [res] = await to(request(experimentId)); | |||
| // setLoading(false); | |||
| const [res] = await to(request(experimentId, { offset: offset, limit: pageSize })); | |||
| if (res && res.data) { | |||
| // const { content = [], totalElements = 0 } = res.data; | |||
| setTableData(res.data); | |||
| // setTotal(totalElements); | |||
| setTableData((prev) => [...prev, ...res.data]); | |||
| if (res.data.length === 0) { | |||
| setLoadCompleted(true); | |||
| const ele = document.getElementsByClassName('ant-table-body')[0]; | |||
| if (ele) { | |||
| const div = document.createElement('div'); | |||
| div.className = styles['experiment-comparison__table__footer']; | |||
| div.innerHTML = '<div></div><p>我是有底线的</p><div></div>'; | |||
| ele.appendChild(div); | |||
| } | |||
| } | |||
| } | |||
| setLoading(false); | |||
| }; | |||
| // 获取对比 url | |||
| @@ -80,17 +103,10 @@ function ExperimentComparison() { | |||
| getExpMetrics(); | |||
| }; | |||
| // 分页切换 | |||
| // const handleTableChange: TableProps['onChange'] = (pagination, filters, sorter, { action }) => { | |||
| // if (action === 'paginate') { | |||
| // setPagination(pagination); | |||
| // } | |||
| // // console.log(pagination, filters, sorter, action); | |||
| // }; | |||
| // 选择行 | |||
| const rowSelection: TableProps['rowSelection'] = { | |||
| const rowSelection: TableProps<TableData>['rowSelection'] = { | |||
| type: 'checkbox', | |||
| columnWidth: 48, | |||
| fixed: 'left', | |||
| selectedRowKeys, | |||
| onChange: (selectedRowKeys: React.Key[]) => { | |||
| @@ -98,11 +114,27 @@ function ExperimentComparison() { | |||
| }, | |||
| }; | |||
| const columns: TableProps['columns'] = useMemo(() => { | |||
| const handleTableScroll = (e: React.UIEvent<HTMLDivElement, UIEvent>) => { | |||
| const target = e.target as HTMLDivElement; | |||
| const { scrollTop, scrollHeight, clientHeight } = target; | |||
| // 实现自动加载更多 | |||
| if (!loadCompleted && !loading && scrollHeight - scrollTop - clientHeight <= 0) { | |||
| const last = tableData[tableData.length - 1]; | |||
| setLoading(true); | |||
| getComparisonData(last?.run_id); | |||
| } | |||
| }; | |||
| const columns: TableProps<TableData>['columns'] = useMemo(() => { | |||
| const first: TableData | undefined = tableData[0]; | |||
| const metricsNames = first?.metrics_names ?? []; | |||
| const paramsNames = first?.params_names ?? []; | |||
| return [ | |||
| { | |||
| title: '基本信息', | |||
| align: 'center', | |||
| children: [ | |||
| { | |||
| title: '实例 ID', | |||
| @@ -120,7 +152,7 @@ function ExperimentComparison() { | |||
| width: 180, | |||
| fixed: 'left', | |||
| align: 'center', | |||
| render: tableCellRender(false, dateFormatter), | |||
| render: tableCellRender(false, TableCellValueType.Date), | |||
| }, | |||
| { | |||
| title: '运行状态', | |||
| @@ -128,7 +160,7 @@ function ExperimentComparison() { | |||
| key: 'status', | |||
| width: 100, | |||
| fixed: 'left', | |||
| align: 'center', | |||
| // align: 'center', | |||
| render: ExperimentStatusCell, | |||
| }, | |||
| { | |||
| @@ -138,7 +170,7 @@ function ExperimentComparison() { | |||
| width: 180, | |||
| fixed: 'left', | |||
| align: 'center', | |||
| render: tableCellRender(true, arrayFormatter()), | |||
| render: tableCellRender(true, TableCellValueType.Array), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| ], | |||
| @@ -146,7 +178,7 @@ function ExperimentComparison() { | |||
| { | |||
| title: `${config.title}参数`, | |||
| align: 'center', | |||
| children: first?.params_names.map((name) => ({ | |||
| children: paramsNames.map((name) => ({ | |||
| title: ( | |||
| <Tooltip title={name}> | |||
| <span>{name}</span> | |||
| @@ -158,14 +190,14 @@ function ExperimentComparison() { | |||
| align: 'center', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| sorter: (a, b) => a.params[name] - b.params[name], | |||
| sorter: (a, b) => tableSorter(a.params[name], b.params[name]), | |||
| showSorterTooltip: false, | |||
| })), | |||
| }, | |||
| { | |||
| title: `${config.title}指标`, | |||
| align: 'center', | |||
| children: first?.metrics_names.map((name) => ({ | |||
| children: metricsNames.map((name) => ({ | |||
| title: ( | |||
| <Tooltip title={name}> | |||
| <span>{name}</span> | |||
| @@ -177,7 +209,7 @@ function ExperimentComparison() { | |||
| align: 'center', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| sorter: (a, b) => a.metrics[name] - b.metrics[name], | |||
| sorter: (a, b) => tableSorter(a.metrics[name], b.metrics[name]), | |||
| showSorterTooltip: false, | |||
| })), | |||
| }, | |||
| @@ -191,27 +223,15 @@ function ExperimentComparison() { | |||
| 可视化对比 | |||
| </Button> | |||
| </div> | |||
| <div | |||
| className={classNames( | |||
| 'vertical-scroll-table-no-page', | |||
| styles['experiment-comparison__table'], | |||
| )} | |||
| > | |||
| <div className={styles['experiment-comparison__table']} ref={tableRef}> | |||
| <Table | |||
| dataSource={tableData} | |||
| columns={columns} | |||
| rowSelection={rowSelection} | |||
| scroll={{ y: 'calc(100% - 55px)', x: '100%' }} | |||
| scroll={{ y: tableHeight - 150, x: tableWidth - 60 }} | |||
| pagination={false} | |||
| bordered={true} | |||
| // loading={loading} | |||
| // pagination={{ | |||
| // ...pagination, | |||
| // total: total, | |||
| // showSizeChanger: true, | |||
| // showQuickJumper: true, | |||
| // }} | |||
| // onChange={handleTableChange} | |||
| onScroll={handleTableScroll} | |||
| rowKey="run_id" | |||
| /> | |||
| </div> | |||
| @@ -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={isAdd ? 'primary' : 'default'} onClick={() => handleRun(false)}> | |||
| 确定 | |||
| </Button>, | |||
| ]; | |||
| if (!isAdd) { | |||
| footer.push( | |||
| <Button key="run" type="primary" onClick={() => handleRun(true)}> | |||
| 确定并运行 | |||
| </Button>, | |||
| ); | |||
| } | |||
| return ( | |||
| <KFModal | |||
| className={styles['add-experiment-modal']} | |||
| title={modalTitle} | |||
| image={modalIcon} | |||
| open={open} | |||
| okButtonProps={{ | |||
| htmlType: 'submit', | |||
| form: 'form', | |||
| }} | |||
| onCancel={onCancel} | |||
| destroyOnClose={true} | |||
| width={825} | |||
| footer={footer} | |||
| > | |||
| <Form | |||
| name="form" | |||
| layout="horizontal" | |||
| initialValues={initialValues} | |||
| onFinish={onFinish} | |||
| autoComplete="off" | |||
| form={form} | |||
| {...layout} | |||
| @@ -4,14 +4,18 @@ | |||
| width: 100%; | |||
| padding: 0 0 0 33px; | |||
| color: @text-color; | |||
| font-size: 15px; | |||
| font-size: 14px; | |||
| & > div { | |||
| padding: 0 16px; | |||
| } | |||
| .check { | |||
| width: calc((100% + 32px + 33px) / 6.25 / 2); | |||
| } | |||
| .index { | |||
| width: calc((100% + 32px + 33px) / 6.25); | |||
| width: calc((100% + 32px + 33px) / 6.25 / 2); | |||
| } | |||
| .tensorBoard { | |||
| @@ -33,6 +37,7 @@ | |||
| } | |||
| .operation { | |||
| position: relative; | |||
| width: 344px; | |||
| } | |||
| } | |||
| @@ -1,7 +1,9 @@ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { ExperimentStatus } from '@/enums'; | |||
| import { useCheck } from '@/hooks'; | |||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | |||
| import { | |||
| deleteManyExperimentIns, | |||
| deleteQueryByExperimentInsId, | |||
| putQueryByExperimentInsId, | |||
| } from '@/services/experiment/index.js'; | |||
| @@ -11,8 +13,9 @@ import { elapsedTime, formatDate } from '@/utils/date'; | |||
| import { to } from '@/utils/promise'; | |||
| import { modalConfirm } from '@/utils/ui'; | |||
| import { DoubleRightOutlined } from '@ant-design/icons'; | |||
| import { App, Button, ConfigProvider, Tooltip } from 'antd'; | |||
| import { App, Button, Checkbox, ConfigProvider, Tooltip } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { useEffect, useMemo } from 'react'; | |||
| import TensorBoardStatusCell from '../TensorBoardStatus'; | |||
| import styles from './index.less'; | |||
| @@ -36,6 +39,25 @@ function ExperimentInstanceComponent({ | |||
| onLoadMore, | |||
| }: ExperimentInstanceProps) { | |||
| const { message } = App.useApp(); | |||
| const allIntanceIds = useMemo(() => { | |||
| return experimentInList?.map((item) => item.id) || []; | |||
| }, [experimentInList]); | |||
| const [ | |||
| selectedIns, | |||
| setSelectedIns, | |||
| checked, | |||
| indeterminate, | |||
| checkAll, | |||
| isSingleChecked, | |||
| checkSingle, | |||
| ] = useCheck(allIntanceIds); | |||
| useEffect(() => { | |||
| // 关闭时清空 | |||
| if (allIntanceIds.length === 0) { | |||
| setSelectedIns([]); | |||
| } | |||
| }, [experimentInList]); | |||
| // 删除实验实例确认 | |||
| const handleRemove = (instance: ExperimentInstance) => { | |||
| @@ -56,6 +78,26 @@ function ExperimentInstanceComponent({ | |||
| } | |||
| }; | |||
| // 批量删除实验实例确认 | |||
| const handleDeleteAll = () => { | |||
| modalConfirm({ | |||
| title: '确定批量删除选中的实例吗?', | |||
| onOk: () => { | |||
| batchDeleteExperimentInstances(); | |||
| }, | |||
| }); | |||
| }; | |||
| // 批量删除实验实例 | |||
| const batchDeleteExperimentInstances = async () => { | |||
| const [res] = await to(deleteManyExperimentIns(selectedIns)); | |||
| if (res) { | |||
| message.success('删除成功'); | |||
| setSelectedIns([]); | |||
| onRemove?.(); | |||
| } | |||
| }; | |||
| // 终止实验实例 | |||
| const terminateExperimentInstance = async (instance: ExperimentInstance) => { | |||
| const [res] = await to(putQueryByExperimentInsId(instance.id)); | |||
| @@ -72,6 +114,9 @@ function ExperimentInstanceComponent({ | |||
| return ( | |||
| <div> | |||
| <div className={styles.tableExpandBox} style={{ paddingBottom: '16px' }}> | |||
| <div className={styles.check}> | |||
| <Checkbox checked={checked} indeterminate={indeterminate} onChange={checkAll}></Checkbox> | |||
| </div> | |||
| <div className={styles.index}>序号</div> | |||
| <div className={styles.tensorBoard}>可视化</div> | |||
| <div className={styles.description}> | |||
| @@ -79,7 +124,21 @@ function ExperimentInstanceComponent({ | |||
| <div style={{ width: '50%' }}>开始时间</div> | |||
| </div> | |||
| <div className={styles.status}>状态</div> | |||
| <div className={styles.operation}>操作</div> | |||
| <div className={styles.operation}> | |||
| <span>操作</span> | |||
| {selectedIns.length > 0 && ( | |||
| <Button | |||
| style={{ position: 'absolute', right: '0' }} | |||
| color="primary" | |||
| variant="filled" | |||
| size="small" | |||
| onClick={handleDeleteAll} | |||
| icon={<KFIcon type="icon-shanchu" />} | |||
| > | |||
| 删除 | |||
| </Button> | |||
| )} | |||
| </div> | |||
| </div> | |||
| {experimentInList.map((item, index) => ( | |||
| @@ -87,6 +146,12 @@ function ExperimentInstanceComponent({ | |||
| key={item.id} | |||
| className={classNames(styles.tableExpandBox, styles.tableExpandBoxContent)} | |||
| > | |||
| <div className={styles.check}> | |||
| <Checkbox | |||
| checked={isSingleChecked(item.id)} | |||
| onChange={() => checkSingle(item.id)} | |||
| ></Checkbox> | |||
| </div> | |||
| <a | |||
| className={styles.index} | |||
| style={{ padding: '0 16px' }} | |||
| @@ -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 && | |||
| @@ -5,7 +5,7 @@ | |||
| &__label { | |||
| color: rgba(29, 29, 32, 0.75); | |||
| font-size: 15px; | |||
| font-size: 14px; | |||
| &--running { | |||
| color: @success-color; | |||
| @@ -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> | |||
| @@ -0,0 +1,7 @@ | |||
| import IframePage, { IframePageType } from '@/components/IFramePage'; | |||
| function GitLink() { | |||
| return <IframePage type={IframePageType.GitLink}></IframePage>; | |||
| } | |||
| export default GitLink; | |||
| @@ -3,8 +3,6 @@ | |||
| * @Date: 2024-04-16 13:58:08 | |||
| * @Description: 镜像详情 | |||
| */ | |||
| import CommonTableCell from '@/components/CommonTableCell'; | |||
| import DateTableCell from '@/components/DateTableCell'; | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import PageTitle from '@/components/PageTitle'; | |||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||
| @@ -20,6 +18,7 @@ import themes from '@/styles/theme.less'; | |||
| import { formatDate } from '@/utils/date'; | |||
| import { to } from '@/utils/promise'; | |||
| import SessionStorage from '@/utils/sessionStorage'; | |||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||
| import { modalConfirm } from '@/utils/ui'; | |||
| import { useNavigate, useParams } from '@umijs/max'; | |||
| import { | |||
| @@ -125,7 +124,12 @@ function MirrorInfo() { | |||
| }; | |||
| // 分页切换 | |||
| const handleTableChange: TableProps['onChange'] = (pagination, filters, sorter, { action }) => { | |||
| const handleTableChange: TableProps<MirrorVersionData>['onChange'] = ( | |||
| pagination, | |||
| _filters, | |||
| _sorter, | |||
| { action }, | |||
| ) => { | |||
| if (action === 'paginate') { | |||
| setPagination(pagination); | |||
| } | |||
| @@ -156,13 +160,13 @@ function MirrorInfo() { | |||
| dataIndex: 'tag_name', | |||
| key: 'tag_name', | |||
| width: '25%', | |||
| render: CommonTableCell(), | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '镜像地址', | |||
| dataIndex: 'url', | |||
| key: 'url', | |||
| render: CommonTableCell(), | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '状态', | |||
| @@ -176,14 +180,14 @@ function MirrorInfo() { | |||
| dataIndex: 'file_size', | |||
| key: 'file_size', | |||
| width: 150, | |||
| render: CommonTableCell(), | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '创建时间', | |||
| dataIndex: 'create_time', | |||
| key: 'create_time', | |||
| width: 200, | |||
| render: DateTableCell, | |||
| render: tableCellRender(false, TableCellValueType.Date), | |||
| }, | |||
| { | |||
| title: '操作', | |||
| @@ -3,8 +3,6 @@ | |||
| * @Date: 2024-04-16 13:58:08 | |||
| * @Description: 镜像列表 | |||
| */ | |||
| import CommonTableCell from '@/components/CommonTableCell'; | |||
| import DateTableCell from '@/components/DateTableCell'; | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { CommonTabKeys } from '@/enums'; | |||
| import { useCacheState } from '@/hooks/pageCacheState'; | |||
| @@ -12,6 +10,7 @@ import { deleteMirrorReq, getMirrorListReq } from '@/services/mirror'; | |||
| import themes from '@/styles/theme.less'; | |||
| import { to } from '@/utils/promise'; | |||
| import SessionStorage from '@/utils/sessionStorage'; | |||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||
| import { modalConfirm } from '@/utils/ui'; | |||
| import { useNavigate } from '@umijs/max'; | |||
| import { | |||
| @@ -156,7 +155,12 @@ function MirrorList() { | |||
| }; | |||
| // 分页切换 | |||
| const handleTableChange: TableProps['onChange'] = (pagination, filters, sorter, { action }) => { | |||
| const handleTableChange: TableProps<MirrorData>['onChange'] = ( | |||
| pagination, | |||
| _filters, | |||
| _sorter, | |||
| { action }, | |||
| ) => { | |||
| if (action === 'paginate') { | |||
| setPagination(pagination); | |||
| } | |||
| @@ -169,21 +173,21 @@ function MirrorList() { | |||
| dataIndex: 'name', | |||
| key: 'name', | |||
| width: '30%', | |||
| render: CommonTableCell(), | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '版本数据', | |||
| dataIndex: 'version_count', | |||
| key: 'version_count', | |||
| width: '15%', | |||
| render: CommonTableCell(), | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '镜像描述', | |||
| dataIndex: 'description', | |||
| key: 'description', | |||
| width: '35%', | |||
| render: CommonTableCell(true), | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| { | |||
| @@ -191,7 +195,7 @@ function MirrorList() { | |||
| dataIndex: 'create_time', | |||
| key: 'create_time', | |||
| width: '20%', | |||
| render: DateTableCell, | |||
| render: tableCellRender(false, TableCellValueType.Date), | |||
| }, | |||
| { | |||
| title: '操作', | |||
| @@ -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,267 @@ | |||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||
| import { useCheck } from '@/hooks'; | |||
| import { getModelPageVersionsReq, getModelVersionsMetricsReq } from '@/services/dataset'; | |||
| import { tableSorter } from '@/utils'; | |||
| import { to } from '@/utils/promise'; | |||
| import tableCellRender from '@/utils/table'; | |||
| import { Checkbox, Table, Tooltip, type TablePaginationConfig, type TableProps } from 'antd'; | |||
| import { useEffect, useMemo, useState } from 'react'; | |||
| import MetricsChart, { MetricsChatData } from '../MetricsChart'; | |||
| import styles from './index.less'; | |||
| enum MetricsType { | |||
| Train = 'train', // 训练 | |||
| Evaluate = 'evaluate', // 评估 | |||
| } | |||
| type TableData = { | |||
| name: string; | |||
| metrics_names?: string[]; | |||
| metrics?: Record<string, number>; | |||
| params_names?: string[]; | |||
| params?: Record<string, number>; | |||
| }; | |||
| type ModelMetricsProps = { | |||
| resourceId: number; | |||
| identifier: string; | |||
| owner: string; | |||
| version: string; | |||
| }; | |||
| function ModelMetrics({ resourceId, identifier, owner, version }: ModelMetricsProps) { | |||
| const [pagination, setPagination] = useState<TablePaginationConfig>({ | |||
| current: 1, | |||
| pageSize: 10, | |||
| }); | |||
| const [total, setTotal] = useState(0); | |||
| const [tableData, setTableData] = useState<TableData[]>([]); | |||
| const [chartData, setChartData] = useState<Record<string, MetricsChatData[]> | undefined>( | |||
| undefined, | |||
| ); | |||
| const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]); | |||
| // 获取所有的指标名称 | |||
| const allMetricsNames = useMemo(() => { | |||
| const first: TableData | undefined = tableData.find( | |||
| (item) => item.metrics_names && item.metrics_names.length > 0, | |||
| ); | |||
| return first?.metrics_names ?? []; | |||
| }, [tableData]); | |||
| const [ | |||
| selectedMetrics, | |||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | |||
| _setSelectedMetrics, | |||
| metricsChecked, | |||
| metricsIndeterminate, | |||
| checkAllMetrics, | |||
| isSingleMetricsChecked, | |||
| checkSingleMetrics, | |||
| ] = useCheck(allMetricsNames); | |||
| useEffect(() => { | |||
| getModelPageVersions(); | |||
| }, []); | |||
| useEffect(() => { | |||
| if (selectedMetrics.length !== 0 && selectedRowKeys.length !== 0) { | |||
| getModelVersionsMetrics(); | |||
| } else { | |||
| setChartData(undefined); | |||
| } | |||
| }, [selectedMetrics, selectedRowKeys]); | |||
| useEffect(() => { | |||
| const curRow = tableData.find((item) => item.name === version); | |||
| if ( | |||
| curRow && | |||
| curRow.metrics_names && | |||
| curRow.metrics_names.length > 0 && | |||
| !selectedRowKeys.includes(version) | |||
| ) { | |||
| setSelectedRowKeys([version, ...selectedRowKeys]); | |||
| } | |||
| }, [version]); | |||
| // 获取模型版本列表,带有参数和指标数据 | |||
| const getModelPageVersions = async () => { | |||
| const params = { | |||
| page: pagination.current! - 1, | |||
| size: pagination.pageSize, | |||
| identifier: identifier, | |||
| owner: owner, | |||
| type: MetricsType.Train, | |||
| }; | |||
| const [res] = await to(getModelPageVersionsReq(params)); | |||
| if (res && res.data) { | |||
| const { content = [], totalElements = 0 } = res.data; | |||
| setTableData(content); | |||
| setTotal(totalElements); | |||
| } | |||
| }; | |||
| const getModelVersionsMetrics = async () => { | |||
| const params = { | |||
| versions: selectedRowKeys, | |||
| metrics: selectedMetrics, | |||
| type: MetricsType.Train, | |||
| identifier: identifier, | |||
| repo_id: resourceId, | |||
| }; | |||
| const [res] = await to(getModelVersionsMetricsReq(params)); | |||
| if (res && res.data) { | |||
| setChartData(res.data); | |||
| } | |||
| }; | |||
| // 分页切换 | |||
| const handleTableChange: TableProps<TableData>['onChange'] = ( | |||
| pagination, | |||
| _filters, | |||
| _sorter, | |||
| { action }, | |||
| ) => { | |||
| if (action === 'paginate') { | |||
| setPagination(pagination); | |||
| } | |||
| }; | |||
| const rowSelection: TableProps<TableData>['rowSelection'] = { | |||
| type: 'checkbox', | |||
| fixed: 'left', | |||
| selectedRowKeys, | |||
| onChange: (selectedRowKeys: React.Key[]) => { | |||
| setSelectedRowKeys(selectedRowKeys); | |||
| }, | |||
| getCheckboxProps: (record: TableData) => ({ | |||
| disabled: !record.metrics_names || record.metrics_names.length === 0, | |||
| }), | |||
| }; | |||
| const showTableData = useMemo(() => { | |||
| const index = tableData.findIndex((item) => item.name === version); | |||
| if (index !== -1) { | |||
| const rowData = tableData[index]; | |||
| const newTableData = tableData.filter((_, idx) => idx !== index); | |||
| return [rowData, ...newTableData]; | |||
| } | |||
| }, [version, tableData]); | |||
| // 表头 | |||
| const columns: TableProps<TableData>['columns'] = useMemo(() => { | |||
| const first: TableData | undefined = tableData.find( | |||
| (item) => item.metrics_names && item.metrics_names.length > 0, | |||
| ); | |||
| const metricsNames = first?.metrics_names ?? []; | |||
| const paramsNames = first?.params_names ?? []; | |||
| return [ | |||
| { | |||
| title: '基本信息', | |||
| align: 'center', | |||
| children: [ | |||
| { | |||
| title: '版本号', | |||
| dataIndex: 'name', | |||
| key: 'name', | |||
| width: 180, | |||
| fixed: 'left', | |||
| align: 'center', | |||
| render: tableCellRender(false), | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| title: `训练参数`, | |||
| align: 'center', | |||
| children: paramsNames.map((name) => ({ | |||
| title: ( | |||
| <Tooltip title={name}> | |||
| <span>{name}</span> | |||
| </Tooltip> | |||
| ), | |||
| dataIndex: ['params', name], | |||
| key: name, | |||
| width: 120, | |||
| align: 'center', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| sorter: (a, b) => tableSorter(a.params?.[name], b.params?.[name]), | |||
| showSorterTooltip: false, | |||
| })), | |||
| }, | |||
| { | |||
| title: () => ( | |||
| <div> | |||
| <Checkbox | |||
| checked={metricsChecked} | |||
| indeterminate={metricsIndeterminate} | |||
| onChange={checkAllMetrics} | |||
| disabled={metricsNames.length === 0} | |||
| ></Checkbox>{' '} | |||
| <span>训练指标</span> | |||
| </div> | |||
| ), | |||
| align: 'center', | |||
| children: metricsNames.map((name) => ({ | |||
| title: ( | |||
| <div> | |||
| <Checkbox | |||
| checked={isSingleMetricsChecked(name)} | |||
| onChange={(e) => { | |||
| e.stopPropagation(); | |||
| checkSingleMetrics(name); | |||
| }} | |||
| onClick={(e) => e.stopPropagation()} | |||
| ></Checkbox>{' '} | |||
| <Tooltip title={name}> | |||
| <span>{name}</span> | |||
| </Tooltip> | |||
| </div> | |||
| ), | |||
| dataIndex: ['metrics', name], | |||
| key: name, | |||
| width: 120, | |||
| align: 'center', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| sorter: (a, b) => tableSorter(a.metrics?.[name], b.metrics?.[name]), | |||
| showSorterTooltip: false, | |||
| })), | |||
| }, | |||
| ]; | |||
| }, [tableData, selectedMetrics]); | |||
| return ( | |||
| <div className={styles['model-metrics']}> | |||
| <div className={styles['model-metrics__table']}> | |||
| <SubAreaTitle | |||
| title="指标参数差异对比" | |||
| image={require('@/assets/img/model-metrics.png')} | |||
| style={{ marginBottom: '15px' }} | |||
| ></SubAreaTitle> | |||
| <Table | |||
| dataSource={showTableData} | |||
| columns={columns} | |||
| rowSelection={rowSelection} | |||
| bordered={true} | |||
| pagination={{ | |||
| ...pagination, | |||
| total: total, | |||
| showSizeChanger: true, | |||
| showQuickJumper: true, | |||
| showTotal: () => `共${total}条`, | |||
| }} | |||
| onChange={handleTableChange} | |||
| rowKey="name" | |||
| /> | |||
| </div> | |||
| <div className={styles['model-metrics__chart']}> | |||
| {chartData && | |||
| Object.keys(chartData).map((key) => ( | |||
| <MetricsChart key={key} name={key} chartData={chartData[key]}></MetricsChart> | |||
| ))} | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| export default ModelMetrics; | |||
| @@ -3,8 +3,6 @@ | |||
| * @Date: 2024-04-16 13:58:08 | |||
| * @Description: 模型部署服务列表 | |||
| */ | |||
| import CommonTableCell from '@/components/CommonTableCell'; | |||
| import DateTableCell from '@/components/DateTableCell'; | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import PageTitle from '@/components/PageTitle'; | |||
| import { serviceTypeOptions } from '@/enums'; | |||
| @@ -13,6 +11,7 @@ import { deleteServiceReq, getServiceListReq } from '@/services/modelDeployment' | |||
| import themes from '@/styles/theme.less'; | |||
| import { to } from '@/utils/promise'; | |||
| import SessionStorage from '@/utils/sessionStorage'; | |||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||
| import { modalConfirm } from '@/utils/ui'; | |||
| import { useNavigate } from '@umijs/max'; | |||
| import { | |||
| @@ -177,7 +176,12 @@ function ModelDeployment() { | |||
| }; | |||
| // 分页切换 | |||
| const handleTableChange: TableProps['onChange'] = (pagination, _filters, _sorter, { action }) => { | |||
| const handleTableChange: TableProps<ServiceData>['onChange'] = ( | |||
| pagination, | |||
| _filters, | |||
| _sorter, | |||
| { action }, | |||
| ) => { | |||
| if (action === 'paginate') { | |||
| setPagination(pagination); | |||
| } | |||
| @@ -190,42 +194,39 @@ function ModelDeployment() { | |||
| dataIndex: 'index', | |||
| key: 'index', | |||
| width: '20%', | |||
| render(_text, _record, index) { | |||
| return <span>{(pagination.current! - 1) * pagination.pageSize! + index + 1}</span>; | |||
| }, | |||
| render: tableCellRender(false, TableCellValueType.Index, { | |||
| page: pagination.current! - 1, | |||
| pageSize: pagination.pageSize!, | |||
| }), | |||
| }, | |||
| { | |||
| title: '服务名称', | |||
| dataIndex: 'service_name', | |||
| key: 'service_name', | |||
| width: '20%', | |||
| render: (text, record) => { | |||
| return ( | |||
| <a className="kf-table-row-link" onClick={() => toDetail(record)}> | |||
| {text} | |||
| </a> | |||
| ); | |||
| }, | |||
| render: tableCellRender(false, TableCellValueType.Link, { | |||
| onClick: toDetail, | |||
| }), | |||
| }, | |||
| { | |||
| title: '服务类型', | |||
| dataIndex: 'service_type_name', | |||
| key: 'service_type_name', | |||
| width: '20%', | |||
| render: CommonTableCell(), | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '版本数量', | |||
| dataIndex: 'version_count', | |||
| key: 'version_count', | |||
| width: '20%', | |||
| render: CommonTableCell(), | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '服务描述', | |||
| dataIndex: 'description', | |||
| key: 'description', | |||
| render: CommonTableCell(), | |||
| render: tableCellRender(), | |||
| width: '20%', | |||
| }, | |||
| { | |||
| @@ -233,7 +234,7 @@ function ModelDeployment() { | |||
| dataIndex: 'update_time', | |||
| key: 'update_time', | |||
| width: '20%', | |||
| render: DateTableCell, | |||
| render: tableCellRender(false, TableCellValueType.Date), | |||
| }, | |||
| { | |||
| title: '操作', | |||
| @@ -4,7 +4,6 @@ | |||
| * @Description: 模型部署列表 | |||
| */ | |||
| import BasicInfo from '@/components/BasicInfo'; | |||
| import CommonTableCell from '@/components/CommonTableCell'; | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import PageTitle from '@/components/PageTitle'; | |||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||
| @@ -19,8 +18,10 @@ import { | |||
| } from '@/services/modelDeployment'; | |||
| import themes from '@/styles/theme.less'; | |||
| import { formatDate } from '@/utils/date'; | |||
| import { openAntdModal } from '@/utils/modal'; | |||
| import { to } from '@/utils/promise'; | |||
| import SessionStorage from '@/utils/sessionStorage'; | |||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||
| import { modalConfirm } from '@/utils/ui'; | |||
| import { useNavigate, useParams } from '@umijs/max'; | |||
| import { | |||
| @@ -30,7 +31,6 @@ import { | |||
| Input, | |||
| Select, | |||
| Table, | |||
| Tooltip, | |||
| type TablePaginationConfig, | |||
| type TableProps, | |||
| } from 'antd'; | |||
| @@ -38,6 +38,7 @@ import { type SearchProps } from 'antd/es/input'; | |||
| import classNames from 'classnames'; | |||
| import { useEffect, useState } from 'react'; | |||
| import ServiceRunStatusCell from '../components/ModelDeployStatusCell'; | |||
| import VersionCompareModal from '../components/VersionCompareModal'; | |||
| import { | |||
| CreateServiceVersionFrom, | |||
| ServiceData, | |||
| @@ -57,6 +58,7 @@ function ServiceInfo() { | |||
| const [inputText, setInputText] = useState(cacheState?.searchText); | |||
| const [tableData, setTableData] = useState<ServiceVersionData[]>([]); | |||
| const [total, setTotal] = useState(0); | |||
| const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]); | |||
| const [pagination, setPagination] = useState<TablePaginationConfig>( | |||
| cacheState?.pagination ?? { | |||
| current: 1, | |||
| @@ -209,11 +211,39 @@ function ServiceInfo() { | |||
| }; | |||
| // 分页切换 | |||
| const handleTableChange: TableProps['onChange'] = (pagination, _filters, _sorter, { action }) => { | |||
| const handleTableChange: TableProps<ServiceVersionData>['onChange'] = ( | |||
| pagination, | |||
| _filters, | |||
| _sorter, | |||
| { action }, | |||
| ) => { | |||
| if (action === 'paginate') { | |||
| setPagination(pagination); | |||
| } | |||
| // console.log(pagination, filters, sorter, action); | |||
| }; | |||
| // 版本对比 | |||
| const handleVersionCompare = () => { | |||
| if (selectedRowKeys.length !== 2) { | |||
| message.error('请选择两个版本进行对比'); | |||
| return; | |||
| } | |||
| openAntdModal(VersionCompareModal, { | |||
| version1: selectedRowKeys[0] as string, | |||
| version2: selectedRowKeys[1] as string, | |||
| }); | |||
| }; | |||
| // 选择行 | |||
| const rowSelection: TableProps<ServiceVersionData>['rowSelection'] = { | |||
| type: 'checkbox', | |||
| columnWidth: 48, | |||
| fixed: 'left', | |||
| selectedRowKeys, | |||
| onChange: (selectedRowKeys: React.Key[]) => { | |||
| setSelectedRowKeys(selectedRowKeys); | |||
| }, | |||
| }; | |||
| const columns: TableProps<ServiceVersionData>['columns'] = [ | |||
| @@ -222,31 +252,24 @@ function ServiceInfo() { | |||
| dataIndex: 'index', | |||
| key: 'index', | |||
| width: '20%', | |||
| render(_text, _record, index) { | |||
| return <span>{(pagination.current! - 1) * pagination.pageSize! + index + 1}</span>; | |||
| }, | |||
| render: tableCellRender(false, TableCellValueType.Index, { | |||
| page: pagination.current! - 1, | |||
| pageSize: pagination.pageSize!, | |||
| }), | |||
| }, | |||
| { | |||
| title: '服务版本', | |||
| dataIndex: 'version', | |||
| key: 'version', | |||
| width: '20%', | |||
| render: CommonTableCell(), | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '模型版本', | |||
| dataIndex: 'model', | |||
| dataIndex: ['model', 'show_value'], | |||
| key: 'model', | |||
| width: '20%', | |||
| render: (_text: string, record: ServiceVersionData) => ( | |||
| <Tooltip | |||
| title={record.model.show_value} | |||
| placement="topLeft" | |||
| overlayStyle={{ maxWidth: '400px' }} | |||
| > | |||
| <span>{record.model.show_value}</span> | |||
| </Tooltip> | |||
| ), | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| { | |||
| @@ -261,14 +284,14 @@ function ServiceInfo() { | |||
| dataIndex: 'image', | |||
| key: 'image', | |||
| width: '20%', | |||
| render: CommonTableCell(true), | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| { | |||
| title: '副本数量', | |||
| dataIndex: 'replicas', | |||
| key: 'replicas', | |||
| render: CommonTableCell(), | |||
| render: tableCellRender(), | |||
| width: '20%', | |||
| }, | |||
| { | |||
| @@ -276,15 +299,9 @@ function ServiceInfo() { | |||
| dataIndex: 'resource', | |||
| key: 'resource', | |||
| width: '20%', | |||
| render: (resource: string) => ( | |||
| <Tooltip | |||
| title={getResourceDescription(resource)} | |||
| placement="topLeft" | |||
| overlayStyle={{ maxWidth: '400px' }} | |||
| > | |||
| <span>{resource ? getResourceDescription(resource) : '--'}</span> | |||
| </Tooltip> | |||
| ), | |||
| render: tableCellRender(true, TableCellValueType.Custom, { | |||
| format: getResourceDescription, | |||
| }), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| { | |||
| @@ -393,13 +410,16 @@ function ServiceInfo() { | |||
| allowClear | |||
| ></Select> | |||
| <Button | |||
| style={{ marginRight: '20px', marginLeft: 'auto' }} | |||
| style={{ marginRight: '15px', marginLeft: 'auto' }} | |||
| type="default" | |||
| onClick={() => createServiceVersion(ServiceOperationType.Create)} | |||
| icon={<KFIcon type="icon-xinjian2" />} | |||
| > | |||
| 新增版本 | |||
| </Button> | |||
| <Button style={{ marginRight: '15px' }} type="default" onClick={handleVersionCompare}> | |||
| 版本对比 | |||
| </Button> | |||
| <Button | |||
| style={{ marginRight: 0 }} | |||
| type="default" | |||
| @@ -424,6 +444,7 @@ function ServiceInfo() { | |||
| showTotal: () => `共${total}条`, | |||
| }} | |||
| onChange={handleTableChange} | |||
| rowSelection={rowSelection} | |||
| rowKey="id" | |||
| /> | |||
| </div> | |||
| @@ -0,0 +1,117 @@ | |||
| @purple-color: #6516ff; | |||
| .title(@color, @background) { | |||
| width: 100%; | |||
| margin-bottom: 20px; | |||
| color: @color; | |||
| font-weight: 500; | |||
| font-size: @font-size; | |||
| line-height: 42px; | |||
| text-align: center; | |||
| background: @background; | |||
| .singleLine(); | |||
| } | |||
| .text() { | |||
| margin-bottom: 20px !important; | |||
| color: @text-color-secondary; | |||
| font-size: 13px; | |||
| word-break: break-all; | |||
| .singleLine(); | |||
| } | |||
| .version-container(@background) { | |||
| flex: 1; | |||
| min-width: 0; | |||
| background: @background; | |||
| border-radius: 4px; | |||
| } | |||
| .version-compare { | |||
| :global { | |||
| .ant-modal-content { | |||
| padding: 40px 40px 25px !important; | |||
| } | |||
| .ant-modal-header { | |||
| margin-bottom: 20px !important; | |||
| } | |||
| .kf-modal-title { | |||
| color: @text-color; | |||
| font-weight: 500; | |||
| font-size: 20px; | |||
| } | |||
| } | |||
| &__container { | |||
| display: flex; | |||
| flex-wrap: nowrap; | |||
| gap: 0 5px; | |||
| align-items: stretch; | |||
| height: 100%; | |||
| } | |||
| &__fields { | |||
| flex: none; | |||
| width: 117px; | |||
| padding: 0 15px; | |||
| background: white; | |||
| border: 1px solid .addAlpha(@primary-color, 0.2) []; | |||
| border-radius: 4px; | |||
| &__title { | |||
| margin-bottom: 20px; | |||
| color: @text-color; | |||
| font-size: @font-size; | |||
| line-height: 42px; | |||
| } | |||
| &__text { | |||
| .text(); | |||
| &--different { | |||
| color: @error-color; | |||
| } | |||
| } | |||
| } | |||
| &__left { | |||
| .version-container(.addAlpha(@primary-color, 0.04) []); | |||
| &__title { | |||
| .title(@primary-color, linear-gradient( | |||
| 159.9deg,rgba(138, 177, 255, 0.5) 0%, | |||
| rgba(22, 100, 255, 0.5) 100% | |||
| )); | |||
| } | |||
| &__text { | |||
| padding: 0 15px; | |||
| text-align: center; | |||
| .text(); | |||
| &--different { | |||
| color: @primary-color; | |||
| } | |||
| } | |||
| } | |||
| &__right { | |||
| .version-container(rgba(100, 30, 237, 0.04)); | |||
| &__title { | |||
| .title(@purple-color, linear-gradient( | |||
| 159.9deg, | |||
| rgba(193, 138, 255, 0.5) 0%, | |||
| rgba(146, 22, 255, 0.5) 100% | |||
| )); | |||
| } | |||
| &__text { | |||
| padding: 0 15px; | |||
| text-align: center; | |||
| .text(); | |||
| &--different { | |||
| color: @purple-color; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,197 @@ | |||
| import KFModal from '@/components/KFModal'; | |||
| import { ServiceRunStatus } from '@/enums'; | |||
| import { useComputingResource } from '@/hooks/resource'; | |||
| import { type ServiceVersionData } from '@/pages/ModelDeployment/types'; | |||
| import { getServiceVersionCompareReq } from '@/services/modelDeployment'; | |||
| import { to } from '@/utils/promise'; | |||
| import { Typography, type ModalProps } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { useEffect, useMemo, useState } from 'react'; | |||
| import { statusInfo } from '../ModelDeployStatusCell'; | |||
| import styles from './index.less'; | |||
| type CompareData = { | |||
| differences: Record<string, any>; | |||
| version1: ServiceVersionData; | |||
| version2: ServiceVersionData; | |||
| }; | |||
| type ServiceVersionDataKey = keyof ServiceVersionData; | |||
| type FiledType = { | |||
| key: ServiceVersionDataKey; | |||
| text: string; | |||
| format?: (data: any) => any; | |||
| }; | |||
| interface CreateMirrorModalProps extends Omit<ModalProps, 'onOk'> { | |||
| version1: string; | |||
| version2: string; | |||
| } | |||
| // 格式化环境变量 | |||
| const formatEnvText = (env: Record<string, string>) => { | |||
| if (!env || Object.keys(env).length === 0) { | |||
| return '--'; | |||
| } | |||
| return Object.entries(env) | |||
| .map(([key, value]) => `${key} = ${value}`) | |||
| .join(','); | |||
| }; | |||
| function VersionCompareModal({ version1, version2, ...rest }: CreateMirrorModalProps) { | |||
| const [compareData, setCompareData] = useState<CompareData | undefined>(undefined); | |||
| const getResourceDescription = useComputingResource()[2]; | |||
| const fields: FiledType[] = useMemo( | |||
| () => [ | |||
| { | |||
| key: 'service_name', | |||
| text: '服务名称', | |||
| }, | |||
| { | |||
| key: 'run_state', | |||
| text: '状态', | |||
| format: (data: any) => { | |||
| return data ? statusInfo[data as ServiceRunStatus].text : '--'; | |||
| }, | |||
| }, | |||
| { | |||
| key: 'image', | |||
| text: '镜像', | |||
| }, | |||
| { | |||
| key: 'code_config', | |||
| text: '代码配置', | |||
| format: (data: any) => { | |||
| return data?.show_value; | |||
| }, | |||
| }, | |||
| { | |||
| key: 'model', | |||
| text: '模型', | |||
| format: (data: any) => { | |||
| return data?.show_value; | |||
| }, | |||
| }, | |||
| { | |||
| key: 'resource', | |||
| text: '资源规格', | |||
| format: getResourceDescription, | |||
| }, | |||
| { | |||
| key: 'replicas', | |||
| text: '副本数', | |||
| }, | |||
| { | |||
| key: 'mount_path', | |||
| text: '挂载路径', | |||
| }, | |||
| { | |||
| key: 'url', | |||
| text: '服务URL', | |||
| }, | |||
| { | |||
| key: 'env_variables', | |||
| text: '环境变量', | |||
| format: formatEnvText, | |||
| }, | |||
| { | |||
| key: 'description', | |||
| text: '描述', | |||
| }, | |||
| ], | |||
| [getResourceDescription], | |||
| ); | |||
| useEffect(() => { | |||
| getServiceVersionCompare(); | |||
| }, []); | |||
| // 获取对比数据 | |||
| const getServiceVersionCompare = async () => { | |||
| const params = { | |||
| id1: version1, | |||
| id2: version2, | |||
| }; | |||
| const [res] = await to(getServiceVersionCompareReq(params)); | |||
| if (res && res.data) { | |||
| setCompareData(res.data); | |||
| } | |||
| }; | |||
| const { | |||
| version1: v1 = {} as ServiceVersionData, | |||
| version2: v2 = {} as ServiceVersionData, | |||
| differences = {}, | |||
| } = compareData || {}; | |||
| const isDifferent = (key: ServiceVersionDataKey) => { | |||
| const keys = Object.keys(differences); | |||
| return keys.includes(key); | |||
| }; | |||
| return ( | |||
| <KFModal | |||
| {...rest} | |||
| title="服务版本对比" | |||
| width={825} | |||
| footer={null} | |||
| className={styles['version-compare']} | |||
| > | |||
| <div className={styles['version-compare__container']}> | |||
| <div className={styles['version-compare__fields']}> | |||
| <div className={styles['version-compare__fields__title']}>基础版本号</div> | |||
| {fields.map(({ key, text }) => ( | |||
| <div | |||
| className={classNames(styles['version-compare__fields__text'], { | |||
| [styles['version-compare__fields__text--different']]: isDifferent(key), | |||
| })} | |||
| key={key} | |||
| > | |||
| {text} | |||
| </div> | |||
| ))} | |||
| </div> | |||
| <div className={styles['version-compare__left']}> | |||
| <div className={styles['version-compare__left__title']}>{v1.version}</div> | |||
| {fields.map(({ key, format }) => { | |||
| const text = format ? format(v1[key]) : v1[key]; | |||
| return ( | |||
| <div | |||
| key={key} | |||
| className={classNames(styles['version-compare__left__text'], { | |||
| [styles['version-compare__left__text--different']]: isDifferent(key), | |||
| })} | |||
| > | |||
| <Typography.Text ellipsis={{ tooltip: text }} style={{ color: 'inherit' }}> | |||
| {text} | |||
| </Typography.Text> | |||
| </div> | |||
| ); | |||
| })} | |||
| </div> | |||
| <div className={styles['version-compare__right']}> | |||
| <div className={styles['version-compare__right__title']}>{v2.version}</div> | |||
| {fields.map(({ key, format }) => { | |||
| const text = format ? format(v2[key]) : v2[key]; | |||
| return ( | |||
| <div | |||
| key={key} | |||
| className={classNames(styles['version-compare__right__text'], { | |||
| [styles['version-compare__right__text--different']]: isDifferent(key), | |||
| })} | |||
| > | |||
| <Typography.Text ellipsis={{ tooltip: text }} style={{ color: 'inherit' }}> | |||
| {text} | |||
| </Typography.Text> | |||
| </div> | |||
| ); | |||
| })} | |||
| </div> | |||
| </div> | |||
| </KFModal> | |||
| ); | |||
| } | |||
| export default VersionCompareModal; | |||
| @@ -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)); | |||
| } | |||
| } | |||
| }; | |||
| @@ -20,10 +20,6 @@ | |||
| } | |||
| } | |||
| .ant-pagination { | |||
| text-align: center; | |||
| } | |||
| .ant-input-group-addon { | |||
| display: none; | |||
| } | |||
| @@ -97,6 +97,7 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { | |||
| ))} | |||
| </div> | |||
| <Pagination | |||
| align="center" | |||
| total={total} | |||
| showSizeChanger | |||
| defaultPageSize={20} | |||
| @@ -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,10 +1,12 @@ | |||
| import { clearSessionToken, setSessionToken } from '@/access'; | |||
| import { getClientInfoReq } from '@/services/auth'; | |||
| import { getCaptchaImg, login } from '@/services/system/auth'; | |||
| import { safeInvoke } from '@/utils/functional'; | |||
| import LocalStorage from '@/utils/localStorage'; | |||
| import { to } from '@/utils/promise'; | |||
| import SessionStorage from '@/utils/sessionStorage'; | |||
| import { gotoOAuth2 } from '@/utils/ui'; | |||
| import { history, useModel } from '@umijs/max'; | |||
| import { Button, Checkbox, Flex, Form, Image, Input, message, type InputRef } from 'antd'; | |||
| import { Form, message, type InputRef } from 'antd'; | |||
| import CryptoJS from 'crypto-js'; | |||
| import { useEffect, useRef, useState } from 'react'; | |||
| import { flushSync } from 'react-dom'; | |||
| @@ -30,25 +32,35 @@ const Login = () => { | |||
| const captchaInputRef = useRef<InputRef>(null); | |||
| useEffect(() => { | |||
| getCaptchaCode(); | |||
| const autoLogin = LocalStorage.getItem(LocalStorage.rememberPasswordKey) ?? 'false'; | |||
| if (autoLogin === 'true') { | |||
| const userStorage = LocalStorage.getItem(LocalStorage.loginUserKey); | |||
| const userJson = safeInvoke((text: string) => | |||
| CryptoJS.AES.decrypt(text, AESKEY).toString(CryptoJS.enc.Utf8), | |||
| )(userStorage); | |||
| const user = safeInvoke(JSON.parse)(userJson); | |||
| if (user && typeof user === 'object' && user.version === VERSION) { | |||
| const { username, password } = user; | |||
| form.setFieldsValue({ username: username, password: password, autoLogin: true }); | |||
| } else { | |||
| form.setFieldsValue({ username: '', password: '', autoLogin: true }); | |||
| LocalStorage.removeItem(LocalStorage.loginUserKey); | |||
| } | |||
| } else { | |||
| form.setFieldsValue({ username: '', password: '', autoLogin: false }); | |||
| } | |||
| // getCaptchaCode(); | |||
| // const autoLogin = LocalStorage.getItem(LocalStorage.rememberPasswordKey) ?? 'false'; | |||
| // if (autoLogin === 'true') { | |||
| // const userStorage = LocalStorage.getItem(LocalStorage.loginUserKey); | |||
| // const userJson = safeInvoke((text: string) => | |||
| // CryptoJS.AES.decrypt(text, AESKEY).toString(CryptoJS.enc.Utf8), | |||
| // )(userStorage); | |||
| // const user = safeInvoke(parseJsonText)(userJson); | |||
| // if (user && typeof user === 'object' && user.version === VERSION) { | |||
| // const { username, password } = user; | |||
| // form.setFieldsValue({ username: username, password: password, autoLogin: true }); | |||
| // } else { | |||
| // form.setFieldsValue({ username: '', password: '', autoLogin: true }); | |||
| // LocalStorage.removeItem(LocalStorage.loginUserKey); | |||
| // } | |||
| // } else { | |||
| // form.setFieldsValue({ username: '', password: '', autoLogin: false }); | |||
| // } | |||
| getClientInfo(); | |||
| }, []); | |||
| const getClientInfo = async () => { | |||
| const [res] = await to(getClientInfoReq()); | |||
| if (res && res.data) { | |||
| const clientInfo = res.data; | |||
| SessionStorage.setItem(SessionStorage.clientInfoKey, clientInfo, true); | |||
| gotoOAuth2(); | |||
| } | |||
| }; | |||
| const getCaptchaCode = async () => { | |||
| const [res] = await to(getCaptchaImg()); | |||
| if (res) { | |||
| @@ -70,6 +82,12 @@ const Login = () => { | |||
| } | |||
| }; | |||
| const handleSubmit2 = async (values: API.LoginParams) => { | |||
| const url = | |||
| 'http://172.20.32.106:8080/oauth/authorize?client_id=ci4s&response_type=code&grant_type=authorization_code'; | |||
| window.location.href = url; | |||
| }; | |||
| // 登录 | |||
| const handleSubmit = async (values: API.LoginParams) => { | |||
| const [res, error] = await to(login({ ...values, uuid })); | |||
| @@ -108,113 +126,115 @@ const Login = () => { | |||
| } | |||
| }; | |||
| return ( | |||
| <div className={styles['user-login']}> | |||
| <div className={styles['user-login__left']}> | |||
| <div className={styles['user-login__left__top']}> | |||
| <img | |||
| src={require('@/assets/img/logo.png')} | |||
| style={{ width: '32px', marginRight: '12px' }} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| 智能材料科研平台 | |||
| </div> | |||
| <div className={styles['user-login__left__title']}> | |||
| <span>智能材料科研平台</span> | |||
| <img | |||
| src={require('@/assets/img/login-ai-logo.png')} | |||
| className={styles['user-login__left__title__img']} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| </div> | |||
| <div className={styles['user-login__left__message']}> | |||
| <span>大语言模型运维 统一管理平台</span> | |||
| </div> | |||
| <img | |||
| className={styles['user-login__left__bottom-img']} | |||
| src={require('@/assets/img/login-left-image.png')} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| </div> | |||
| <div className={styles['user-login__right']}> | |||
| <div> | |||
| <div className={styles['user-login__right__title']}> | |||
| <span style={{ color: '#111111' }}>欢迎登录</span> | |||
| <span>智能材料科研平台</span> | |||
| </div> | |||
| <div className={styles['user-login__right__content']}> | |||
| <div className={styles['user-login__right__content__title']}>账号登录</div> | |||
| <div className={styles['user-login__right__content__form']}> | |||
| <Form | |||
| labelCol={{ span: 0 }} | |||
| wrapperCol={{ span: 24 }} | |||
| initialValues={{ autoLogin: true }} | |||
| onFinish={handleSubmit} | |||
| autoComplete="off" | |||
| form={form} | |||
| > | |||
| <Form.Item name="username" rules={[{ required: true, message: '请输入用户名' }]}> | |||
| <Input | |||
| placeholder="请输入用户名" | |||
| prefix={<LoginInputPrefix icon={require('@/assets/img/login-user.png')} />} | |||
| allowClear | |||
| /> | |||
| </Form.Item> | |||
| <Form.Item name="password" rules={[{ required: true, message: '请输入密码' }]}> | |||
| <Input.Password | |||
| placeholder="请输入密码" | |||
| prefix={<LoginInputPrefix icon={require('@/assets/img/login-password.png')} />} | |||
| allowClear | |||
| /> | |||
| </Form.Item> | |||
| <Flex align="start" style={{ height: '98px' }}> | |||
| <div style={{ flex: 1 }}> | |||
| <Form.Item name="code" rules={[{ required: true, message: '请输入验证码' }]}> | |||
| <Input | |||
| placeholder="请输入验证码" | |||
| prefix={ | |||
| <LoginInputPrefix icon={require('@/assets/img/login-captcha.png')} /> | |||
| } | |||
| ref={captchaInputRef} | |||
| allowClear | |||
| /> | |||
| </Form.Item> | |||
| </div> | |||
| <Image | |||
| className={styles['user-login__right__content__form__captcha']} | |||
| src={captchaCode} | |||
| alt="验证码" | |||
| preview={false} | |||
| onClick={() => getCaptchaCode()} | |||
| /> | |||
| </Flex> | |||
| <Form.Item | |||
| name="autoLogin" | |||
| valuePropName="checked" | |||
| labelCol={{ span: 0 }} | |||
| wrapperCol={{ span: 16 }} | |||
| > | |||
| <Checkbox>记住密码</Checkbox> | |||
| </Form.Item> | |||
| <Form.Item labelCol={{ span: 0 }} wrapperCol={{ span: 24 }}> | |||
| <Button type="primary" htmlType="submit"> | |||
| 登录 | |||
| </Button> | |||
| </Form.Item> | |||
| </Form> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| ); | |||
| return <div className={styles['user-login']}></div>; | |||
| // return ( | |||
| // <div className={styles['user-login']}> | |||
| // <div className={styles['user-login__left']}> | |||
| // <div className={styles['user-login__left__top']}> | |||
| // <img | |||
| // src={require('@/assets/img/logo.png')} | |||
| // style={{ width: '32px', marginRight: '12px' }} | |||
| // draggable={false} | |||
| // alt="" | |||
| // /> | |||
| // 智能材料科研平台 | |||
| // </div> | |||
| // <div className={styles['user-login__left__title']}> | |||
| // <span>智能材料科研平台</span> | |||
| // <img | |||
| // src={require('@/assets/img/login-ai-logo.png')} | |||
| // className={styles['user-login__left__title__img']} | |||
| // draggable={false} | |||
| // alt="" | |||
| // /> | |||
| // </div> | |||
| // <div className={styles['user-login__left__message']}> | |||
| // <span>大语言模型运维 统一管理平台</span> | |||
| // </div> | |||
| // <img | |||
| // className={styles['user-login__left__bottom-img']} | |||
| // src={require('@/assets/img/login-left-image.png')} | |||
| // draggable={false} | |||
| // alt="" | |||
| // /> | |||
| // </div> | |||
| // <div className={styles['user-login__right']}> | |||
| // <div> | |||
| // <div className={styles['user-login__right__title']}> | |||
| // <span style={{ color: '#111111' }}>欢迎登录</span> | |||
| // <span>智能材料科研平台</span> | |||
| // </div> | |||
| // <div className={styles['user-login__right__content']}> | |||
| // <div className={styles['user-login__right__content__title']}>账号登录</div> | |||
| // <div className={styles['user-login__right__content__form']}> | |||
| // <Form | |||
| // labelCol={{ span: 0 }} | |||
| // wrapperCol={{ span: 24 }} | |||
| // initialValues={{ autoLogin: true }} | |||
| // onFinish={handleSubmit2} | |||
| // autoComplete="off" | |||
| // form={form} | |||
| // > | |||
| // <Form.Item name="username" rules={[{ required: false, message: '请输入用户名' }]}> | |||
| // <Input | |||
| // placeholder="请输入用户名" | |||
| // prefix={<LoginInputPrefix icon={require('@/assets/img/login-user.png')} />} | |||
| // allowClear | |||
| // /> | |||
| // </Form.Item> | |||
| // <Form.Item name="password" rules={[{ required: false, message: '请输入密码' }]}> | |||
| // <Input.Password | |||
| // placeholder="请输入密码" | |||
| // prefix={<LoginInputPrefix icon={require('@/assets/img/login-password.png')} />} | |||
| // allowClear | |||
| // /> | |||
| // </Form.Item> | |||
| // <Flex align="start" style={{ height: '98px' }}> | |||
| // <div style={{ flex: 1 }}> | |||
| // <Form.Item name="code" rules={[{ required: false, message: '请输入验证码' }]}> | |||
| // <Input | |||
| // placeholder="请输入验证码" | |||
| // prefix={ | |||
| // <LoginInputPrefix icon={require('@/assets/img/login-captcha.png')} /> | |||
| // } | |||
| // ref={captchaInputRef} | |||
| // allowClear | |||
| // /> | |||
| // </Form.Item> | |||
| // </div> | |||
| // <Image | |||
| // className={styles['user-login__right__content__form__captcha']} | |||
| // src={captchaCode} | |||
| // alt="验证码" | |||
| // preview={false} | |||
| // onClick={() => getCaptchaCode()} | |||
| // /> | |||
| // </Flex> | |||
| // <Form.Item | |||
| // name="autoLogin" | |||
| // valuePropName="checked" | |||
| // labelCol={{ span: 0 }} | |||
| // wrapperCol={{ span: 16 }} | |||
| // > | |||
| // <Checkbox>记住密码</Checkbox> | |||
| // </Form.Item> | |||
| // <Form.Item labelCol={{ span: 0 }} wrapperCol={{ span: 24 }}> | |||
| // <Button type="primary" htmlType="submit"> | |||
| // 登录 | |||
| // </Button> | |||
| // </Form.Item> | |||
| // </Form> | |||
| // </div> | |||
| // </div> | |||
| // </div> | |||
| // </div> | |||
| // </div> | |||
| // ); | |||
| }; | |||
| export default Login; | |||
| @@ -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: '运行中' }, | |||
| ], | |||
| }, | |||
| { | |||
| @@ -47,13 +47,11 @@ | |||
| &__robot-img { | |||
| position: fixed; | |||
| right: 30px; | |||
| bottom: 20px; | |||
| right: 20px; | |||
| bottom: 90px; | |||
| z-index: 99; | |||
| width: 64px; | |||
| height: 64px; | |||
| background-color: white; | |||
| border-radius: 10px; | |||
| width: 56px; | |||
| height: 56px; | |||
| cursor: pointer; | |||
| } | |||
| } | |||
| @@ -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; | |||
| }; | |||
| } | |||
| @@ -0,0 +1,16 @@ | |||
| import { request } from '@umijs/max'; | |||
| // 单点登录 | |||
| export function loginByOauth2Req(data) { | |||
| return request(`/api/auth/loginByOauth2`, { | |||
| method: 'POST', | |||
| data, | |||
| }); | |||
| } | |||
| // 登录获取客户端信息 | |||
| export function getClientInfoReq() { | |||
| return request(`/api/auth/oauth2ClientInfo`, { | |||
| method: 'GET', | |||
| }); | |||
| } | |||
| @@ -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 | |||
| }); | |||
| } | |||
| @@ -104,3 +104,11 @@ export function getServiceVersionLogReq(params: any) { | |||
| params, | |||
| }); | |||
| } | |||
| // 获取服务版本对比 | |||
| export function getServiceVersionCompareReq(params: any) { | |||
| return request(`/api/mmp/service/serviceVersionCompare`, { | |||
| 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 || {}), | |||
| }); | |||
| } | |||
| @@ -188,4 +188,227 @@ declare namespace API { | |||
| filter?: string; | |||
| sorter?: string; | |||
| }; | |||
| type CurrentUser = UserInfo & { | |||
| signature?: string; | |||
| title?: string; | |||
| group?: string; | |||
| tags?: { key?: string; label?: string }[]; | |||
| notifyCount?: number; | |||
| unreadCount?: number; | |||
| country?: string; | |||
| access?: string; | |||
| geographic?: { | |||
| province?: { label?: string; key?: string }; | |||
| city?: { label?: string; key?: string }; | |||
| }; | |||
| address?: string; | |||
| phone?: string; | |||
| roleNames?: { | |||
| roleName?: string; | |||
| }[]; | |||
| }; | |||
| type ErrorResponse = { | |||
| /** 业务约定的错误码 */ | |||
| errorCode: string; | |||
| /** 业务上的错误信息 */ | |||
| errorMessage?: string; | |||
| /** 业务上的请求是否成功 */ | |||
| success?: boolean; | |||
| }; | |||
| type FakeCaptcha = { | |||
| code?: number; | |||
| status?: string; | |||
| }; | |||
| type getFakeCaptchaParams = { | |||
| /** 手机号 */ | |||
| phone?: string; | |||
| }; | |||
| type LoginParams = { | |||
| username?: string; | |||
| password?: string; | |||
| uuid?: string; | |||
| autoLogin?: boolean; | |||
| type?: string; | |||
| }; | |||
| type LoginResult = { | |||
| code: number; | |||
| msg?: string; | |||
| type?: string; | |||
| data?: { | |||
| access_token?: string; | |||
| expires_in?: number; | |||
| }; | |||
| }; | |||
| type NoticeIconItem = { | |||
| id?: string; | |||
| extra?: string; | |||
| key?: string; | |||
| read?: boolean; | |||
| avatar?: string; | |||
| title?: string; | |||
| status?: string; | |||
| datetime?: string; | |||
| description?: string; | |||
| type?: NoticeIconItemType; | |||
| }; | |||
| type NoticeIconItemType = 'notification' | 'message' | 'event'; | |||
| type NoticeIconList = { | |||
| data?: NoticeIconItem[]; | |||
| /** 列表的内容总数 */ | |||
| total?: number; | |||
| success?: boolean; | |||
| }; | |||
| type PageParams = { | |||
| current?: number; | |||
| pageSize?: number; | |||
| }; | |||
| type RuleList = { | |||
| data?: RuleListItem[]; | |||
| /** 列表的内容总数 */ | |||
| total?: number; | |||
| success?: boolean; | |||
| }; | |||
| type RuleListItem = { | |||
| key?: number; | |||
| disabled?: boolean; | |||
| href?: string; | |||
| avatar?: string; | |||
| name?: string; | |||
| owner?: string; | |||
| desc?: string; | |||
| callNo?: number; | |||
| status?: number; | |||
| updatedAt?: string; | |||
| createdAt?: string; | |||
| progress?: number; | |||
| }; | |||
| type ruleParams = { | |||
| /** 当前的页码 */ | |||
| current?: number; | |||
| /** 页面的容量 */ | |||
| pageSize?: number; | |||
| }; | |||
| type ApiResponse = { | |||
| code?: number; | |||
| type?: string; | |||
| message?: string; | |||
| }; | |||
| type Category = { | |||
| id?: number; | |||
| name?: string; | |||
| }; | |||
| type deleteOrderParams = { | |||
| /** ID of the order that needs to be deleted */ | |||
| orderId: number; | |||
| }; | |||
| type deletePetParams = { | |||
| api_key?: string; | |||
| /** Pet id to delete */ | |||
| petId: number; | |||
| }; | |||
| type deleteUserParams = { | |||
| /** The name that needs to be deleted */ | |||
| username: string; | |||
| }; | |||
| type findPetsByStatusParams = { | |||
| /** Status values that need to be considered for filter */ | |||
| status: ('available' | 'pending' | 'sold')[]; | |||
| }; | |||
| type findPetsByTagsParams = { | |||
| /** Tags to filter by */ | |||
| tags: string[]; | |||
| }; | |||
| type getOrderByIdParams = { | |||
| /** ID of pet that needs to be fetched */ | |||
| orderId: number; | |||
| }; | |||
| type getPetByIdParams = { | |||
| /** ID of pet to return */ | |||
| petId: number; | |||
| }; | |||
| type getUserByNameParams = { | |||
| /** The name that needs to be fetched. Use user1 for testing. */ | |||
| username: string; | |||
| }; | |||
| type loginUserParams = { | |||
| /** The user name for login */ | |||
| username: string; | |||
| /** The password for login in clear text */ | |||
| password: string; | |||
| }; | |||
| type Order = { | |||
| id?: number; | |||
| petId?: number; | |||
| quantity?: number; | |||
| shipDate?: string; | |||
| /** Order Status */ | |||
| status?: 'placed' | 'approved' | 'delivered'; | |||
| complete?: boolean; | |||
| }; | |||
| type Pet = { | |||
| id?: number; | |||
| category?: Category; | |||
| name: string; | |||
| photoUrls: string[]; | |||
| tags?: Tag[]; | |||
| /** pet status in the store */ | |||
| status?: 'available' | 'pending' | 'sold'; | |||
| }; | |||
| type Tag = { | |||
| id?: number; | |||
| name?: string; | |||
| }; | |||
| type updatePetWithFormParams = { | |||
| /** ID of pet that needs to be updated */ | |||
| petId: number; | |||
| }; | |||
| type updateUserParams = { | |||
| /** name that need to be updated */ | |||
| username: string; | |||
| }; | |||
| type uploadFileParams = { | |||
| /** ID of pet to update */ | |||
| petId: number; | |||
| }; | |||
| type User = { | |||
| id?: number; | |||
| username?: string; | |||
| firstName?: string; | |||
| lastName?: string; | |||
| email?: string; | |||
| password?: string; | |||
| phone?: string; | |||
| /** User Status */ | |||
| userStatus?: number; | |||
| }; | |||
| } | |||
| @@ -7,6 +7,17 @@ | |||
| import { ExperimentStatus, TensorBoardStatus } from '@/enums'; | |||
| import type { Settings as LayoutSettings } from '@ant-design/pro-components'; | |||
| export type ClientInfo = { | |||
| accessTokenUri: string; | |||
| checkTokenUri: string; | |||
| clientId: string; | |||
| clientSecret: string; | |||
| loginPage: string; | |||
| logoutUri: string; | |||
| redirectUri: string; | |||
| userAuthorizationUri: string; | |||
| }; | |||
| // 全局初始状态类型 | |||
| export type GlobalInitialState = { | |||
| settings?: Partial<LayoutSettings>; | |||
| @@ -14,6 +25,7 @@ export type GlobalInitialState = { | |||
| fetchUserInfo?: () => Promise<API.CurrentUser | undefined>; | |||
| loading?: boolean; | |||
| collapsed?: boolean; | |||
| clientInfo?: ClientInfo; | |||
| }; | |||
| // 流水线全局参数 | |||
| @@ -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 === '') { | |||
| @@ -4,6 +4,7 @@ | |||
| * @Description: 工具类 | |||
| */ | |||
| import { PageEnum } from '@/enums/pagesEnums'; | |||
| import G6 from '@antv/g6'; | |||
| // 生成 8 位随机数 | |||
| @@ -21,7 +22,7 @@ export function getNameByCode(list: any[], code: any) { | |||
| // 解析 json 字符串 | |||
| export function parseJsonText(text?: string | null): any | null { | |||
| if (!text) { | |||
| if (text === undefined || text === null || text === '') { | |||
| return null; | |||
| } | |||
| try { | |||
| @@ -218,3 +219,25 @@ export const getGitUrl = (url: string, branch: string): string => { | |||
| const gitUrl = url.replace(/\.git$/, ''); | |||
| return branch ? `${gitUrl}/tree/${branch}` : gitUrl; | |||
| }; | |||
| // 判断是否需要登录 | |||
| export const needAuth = (pathname: string) => { | |||
| return pathname !== PageEnum.LOGIN && pathname !== PageEnum.Authorize; | |||
| }; | |||
| // 表格排序 | |||
| export const tableSorter = (a: any, b: any) => { | |||
| if (b === null || b === undefined) { | |||
| return -1; | |||
| } | |||
| if (a === null || a === undefined) { | |||
| return 1; | |||
| } | |||
| if (typeof a === 'number' && typeof b === 'number') { | |||
| return a - b; | |||
| } | |||
| if (typeof a === 'string' && typeof b === 'string') { | |||
| return a.localeCompare(b); | |||
| } | |||
| return 0; | |||
| }; | |||
| @@ -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,8 @@ export default class SessionStorage { | |||
| static readonly serviceVersionInfoKey = 'service-version-info'; | |||
| // 编辑器 url | |||
| static readonly editorUrlKey = 'editor-url'; | |||
| // 数据集、模型资源 | |||
| static readonly resourceItemKey = 'resource-item'; | |||
| // 客户端信息 | |||
| static readonly clientInfoKey = 'client-info'; | |||
| static getItem(key: string, isObject: boolean = false) { | |||
| const jsonStr = sessionStorage.getItem(key); | |||
| @@ -16,13 +18,9 @@ export default class SessionStorage { | |||
| return jsonStr; | |||
| } | |||
| if (jsonStr) { | |||
| try { | |||
| return JSON.parse(jsonStr); | |||
| } catch (error) { | |||
| return undefined; | |||
| } | |||
| return parseJsonText(jsonStr); | |||
| } | |||
| return undefined; | |||
| return null; | |||
| } | |||
| static setItem(key: string, state?: any, isObject: boolean = false) { | |||
| @@ -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; | |||
| @@ -6,9 +6,11 @@ | |||
| import { PageEnum } from '@/enums/pagesEnums'; | |||
| import { removeAllPageCacheState } from '@/hooks/pageCacheState'; | |||
| import themes from '@/styles/theme.less'; | |||
| import { type ClientInfo } from '@/types'; | |||
| import { history } from '@umijs/max'; | |||
| import { Modal, message, type ModalFuncProps, type UploadFile } from 'antd'; | |||
| import { closeAllModals } from './modal'; | |||
| import SessionStorage from './sessionStorage'; | |||
| type ModalConfirmProps = ModalFuncProps & { | |||
| isDelete?: boolean; | |||
| @@ -75,7 +77,7 @@ export const gotoLoginPage = (toHome: boolean = true) => { | |||
| const { pathname, search } = location; | |||
| const urlParams = new URLSearchParams(); | |||
| urlParams.append('redirect', pathname + search); | |||
| const newSearch = toHome && pathname !== '/' ? '' : urlParams.toString(); | |||
| const newSearch = toHome || pathname === '/' ? '' : urlParams.toString(); | |||
| // console.log('pathname', pathname); | |||
| // console.log('search', search); | |||
| if (pathname !== PageEnum.LOGIN) { | |||
| @@ -88,6 +90,15 @@ export const gotoLoginPage = (toHome: boolean = true) => { | |||
| } | |||
| }; | |||
| export const gotoOAuth2 = () => { | |||
| const clientInfo = SessionStorage.getItem(SessionStorage.clientInfoKey, true) as ClientInfo; | |||
| if (clientInfo) { | |||
| const { clientId, userAuthorizationUri } = clientInfo; | |||
| const url = `${userAuthorizationUri}?client_id=${clientId}&response_type=code&grant_type=authorization_code`; | |||
| location.replace(url); | |||
| } | |||
| }; | |||
| /** | |||
| * 验证文件上传 | |||
| * | |||