| @@ -58,3 +58,5 @@ mvnw | |||||
| # web | # web | ||||
| **/node_modules | **/node_modules | ||||
| *storybook.log | |||||
| @@ -41,10 +41,5 @@ screenshot | |||||
| build | build | ||||
| pnpm-lock.yaml | pnpm-lock.yaml | ||||
| /src/services/codeConfig/index.js | |||||
| /src/pages/CodeConfig/components/AddCodeConfigModal/index.less | |||||
| /src/pages/CodeConfig/List/index.less | |||||
| /src/pages/Dataset/components/ResourceItem/index.less | |||||
| /src/pages/CodeConfig/components/AddCodeConfigModal/index.tsx | |||||
| /src/pages/CodeConfig/components/CodeConfigItem/index.tsx | |||||
| /src/pages/Dataset/components/ResourceItem/index.tsx | |||||
| *storybook.log | |||||
| @@ -0,0 +1 @@ | |||||
| v18.16.0 | |||||
| @@ -0,0 +1,16 @@ | |||||
| export default function(babel) { | |||||
| const { types: t } = babel; | |||||
| return { | |||||
| visitor: { | |||||
| ImportDeclaration(path) { | |||||
| const source = path.node.source.value; | |||||
| // console.log("zzzz", source); | |||||
| if (source.endsWith('.less')) { | |||||
| if (path.node.specifiers.length > 0) { | |||||
| path.node.source.value += "?modules"; | |||||
| } | |||||
| } | |||||
| }, | |||||
| }, | |||||
| }; | |||||
| }; | |||||
| @@ -0,0 +1,117 @@ | |||||
| import type { StorybookConfig } from '@storybook/react-webpack5'; | |||||
| import path from 'path'; | |||||
| import webpack from 'webpack'; | |||||
| const config: StorybookConfig = { | |||||
| stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], | |||||
| addons: [ | |||||
| // '@storybook/addon-webpack5-compiler-swc', | |||||
| '@storybook/addon-webpack5-compiler-babel', | |||||
| '@storybook/addon-onboarding', | |||||
| '@storybook/addon-essentials', | |||||
| '@chromatic-com/storybook', | |||||
| '@storybook/addon-interactions', | |||||
| ], | |||||
| framework: { | |||||
| name: '@storybook/react-webpack5', | |||||
| options: {}, | |||||
| }, | |||||
| staticDirs: ['../public'], | |||||
| docs: { | |||||
| defaultName: 'Documentation', | |||||
| }, | |||||
| webpackFinal: async (config) => { | |||||
| if (config.resolve) { | |||||
| config.resolve.alias = { | |||||
| ...config.resolve.alias, | |||||
| '@': path.resolve(__dirname, '../src'), | |||||
| '@umijs/max$': path.resolve(__dirname, './mock/umijs.mock.tsx'), | |||||
| }; | |||||
| } | |||||
| if (config.module && config.module.rules) { | |||||
| config.module.rules.push( | |||||
| { | |||||
| test: /\.less$/, | |||||
| oneOf: [ | |||||
| { | |||||
| resourceQuery: /modules/, | |||||
| use: [ | |||||
| 'style-loader', | |||||
| { | |||||
| loader: 'css-loader', | |||||
| options: { | |||||
| importLoaders: 1, | |||||
| import: true, | |||||
| esModule: true, | |||||
| modules: { | |||||
| localIdentName: '[local]___[hash:base64:5]', | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| { | |||||
| loader: 'less-loader', | |||||
| options: { | |||||
| lessOptions: { | |||||
| javascriptEnabled: true, // 如果需要支持 Ant Design 的 Less 变量,开启此项 | |||||
| modifyVars: { | |||||
| hack: 'true; @import "@/styles/theme.less";', | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| ], | |||||
| include: path.resolve(__dirname, '../src'), // 限制范围,避免处理 node_modules | |||||
| }, | |||||
| { | |||||
| use: [ | |||||
| 'style-loader', | |||||
| 'css-loader', | |||||
| { | |||||
| loader: 'less-loader', | |||||
| options: { | |||||
| lessOptions: { | |||||
| javascriptEnabled: true, // 如果需要支持 Ant Design 的 Less 变量,开启此项 | |||||
| modifyVars: { | |||||
| hack: 'true; @import "@/styles/theme.less";', | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| ], | |||||
| include: path.resolve(__dirname, '../src'), // 限制范围,避免处理 node_modules | |||||
| }, | |||||
| ], | |||||
| }, | |||||
| { | |||||
| test: /\.(tsx?|jsx?)$/, | |||||
| loader: 'ts-loader', | |||||
| options: { | |||||
| transpileOnly: true, | |||||
| }, | |||||
| include: [ | |||||
| path.resolve(__dirname, '../src'), // 限制范围,避免处理 node_modules | |||||
| path.resolve(__dirname, './'), | |||||
| ], | |||||
| }, | |||||
| ); | |||||
| } | |||||
| if (config.plugins) { | |||||
| config.plugins.push( | |||||
| new webpack.ProvidePlugin({ | |||||
| React: 'react', // 全局注入 React | |||||
| }), | |||||
| ); | |||||
| } | |||||
| return config; | |||||
| }, | |||||
| babel: async (config: any) => { | |||||
| if (!config.plugins) { | |||||
| config.plugins = []; | |||||
| } | |||||
| config.plugins.push(path.resolve(__dirname, './babel-plugin-auto-css-modules.js')); | |||||
| return config; | |||||
| }, | |||||
| }; | |||||
| export default config; | |||||
| @@ -0,0 +1,19 @@ | |||||
| export const Link = ({ to, children, ...props }: any) => ( | |||||
| <a href={typeof to === 'string' ? to : '#'} {...props}> | |||||
| {children} | |||||
| </a> | |||||
| ); | |||||
| export const request = (url: string, options: any) => { | |||||
| return fetch(url, options) | |||||
| .then((res) => { | |||||
| if (!res.ok) { | |||||
| throw new Error(res.statusText); | |||||
| } | |||||
| return res; | |||||
| }) | |||||
| .then((res) => res.json()); | |||||
| }; | |||||
| export { useNavigate, useParams, useSearchParams } from 'react-router-dom'; | |||||
| export const history = window.history; | |||||
| @@ -0,0 +1,107 @@ | |||||
| import '@/global.less'; | |||||
| import '@/overrides.less'; | |||||
| import themes from '@/styles/theme.less'; | |||||
| import type { Preview } from '@storybook/react'; | |||||
| import { App, ConfigProvider } from 'antd'; | |||||
| import zhCN from 'antd/locale/zh_CN'; | |||||
| import { initialize, mswLoader } from 'msw-storybook-addon'; | |||||
| import './storybook.css'; | |||||
| /* | |||||
| * Initializes MSW | |||||
| * See https://github.com/mswjs/msw-storybook-addon#configuring-msw | |||||
| * to learn how to customize it | |||||
| */ | |||||
| initialize(); | |||||
| const preview: Preview = { | |||||
| parameters: { | |||||
| controls: { | |||||
| expanded: true, | |||||
| sort: 'requiredFirst', | |||||
| matchers: { | |||||
| color: /(background|color)$/i, | |||||
| date: /Date$/i, | |||||
| }, | |||||
| }, | |||||
| backgrounds: { | |||||
| values: [ | |||||
| { name: 'Dark', value: '#000' }, | |||||
| { name: 'Gray', value: '#f9fafb' }, | |||||
| { name: 'Light', value: '#FFF' }, | |||||
| ], | |||||
| default: 'Light', | |||||
| }, | |||||
| options: { | |||||
| storySort: { | |||||
| method: 'alphabetical', | |||||
| order: ['Documentation', 'Components'], | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| decorators: [ | |||||
| (Story) => ( | |||||
| <ConfigProvider | |||||
| locale={zhCN} | |||||
| theme={{ | |||||
| cssVar: true, | |||||
| token: { | |||||
| colorPrimary: themes['primaryColor'], | |||||
| colorSuccess: themes['successColor'], | |||||
| colorError: themes['errorColor'], | |||||
| colorWarning: themes['warningColor'], | |||||
| colorLink: themes['primaryColor'], | |||||
| colorText: themes['textColor'], | |||||
| controlHeightLG: 46, | |||||
| }, | |||||
| components: { | |||||
| Button: { | |||||
| defaultBg: 'rgba(22, 100, 255, 0.06)', | |||||
| defaultBorderColor: 'rgba(22, 100, 255, 0.11)', | |||||
| defaultColor: themes['textColor'], | |||||
| defaultHoverBg: 'rgba(22, 100, 255, 0.06)', | |||||
| defaultHoverBorderColor: 'rgba(22, 100, 255, 0.5)', | |||||
| defaultHoverColor: '#3F7FFF ', | |||||
| defaultActiveBg: 'rgba(22, 100, 255, 0.12)', | |||||
| defaultActiveBorderColor: 'rgba(22, 100, 255, 0.75)', | |||||
| defaultActiveColor: themes['primaryColor'], | |||||
| contentFontSize: parseInt(themes['fontSize']), | |||||
| }, | |||||
| Input: { | |||||
| inputFontSize: parseInt(themes['fontSizeInput']), | |||||
| inputFontSizeLG: parseInt(themes['fontSizeInputLg']), | |||||
| paddingBlockLG: 10, | |||||
| }, | |||||
| Select: { | |||||
| singleItemHeightLG: 46, | |||||
| optionSelectedColor: themes['primaryColor'], | |||||
| }, | |||||
| Table: { | |||||
| headerBg: 'rgba(242, 244, 247, 0.36)', | |||||
| headerBorderRadius: 4, | |||||
| rowSelectedBg: 'rgba(22, 100, 255, 0.05)', | |||||
| }, | |||||
| Tabs: { | |||||
| titleFontSize: 16, | |||||
| }, | |||||
| Form: { | |||||
| labelColor: 'rgba(29, 29, 32, 0.8);', | |||||
| }, | |||||
| Breadcrumb: { | |||||
| iconFontSize: parseInt(themes['fontSize']), | |||||
| linkColor: 'rgba(29, 29, 32, 0.7)', | |||||
| separatorColor: 'rgba(29, 29, 32, 0.7)', | |||||
| }, | |||||
| }, | |||||
| }} | |||||
| > | |||||
| <App message={{ maxCount: 3 }}> | |||||
| <Story /> | |||||
| </App> | |||||
| </ConfigProvider> | |||||
| ), | |||||
| ], | |||||
| loaders: [mswLoader], // 👈 Add the MSW loader to all stories | |||||
| }; | |||||
| export default preview; | |||||
| @@ -0,0 +1,19 @@ | |||||
| html, | |||||
| body, | |||||
| #root { | |||||
| min-width: unset; | |||||
| height: 100%; | |||||
| margin: 0; | |||||
| padding: 0; | |||||
| overflow-y: visible; | |||||
| } | |||||
| .ant-input-search-large .ant-input-affix-wrapper, .ant-input-search-large .ant-input-search-button { | |||||
| height: 46px; | |||||
| } | |||||
| *, | |||||
| *::before, | |||||
| *::after { | |||||
| box-sizing: border-box; | |||||
| } | |||||
| @@ -0,0 +1,26 @@ | |||||
| { | |||||
| "compilerOptions": { | |||||
| "target": "esnext", // 指定ECMAScript目标版本 | |||||
| "lib": ["dom", "dom.iterable", "esnext"], // 要包含在编译中的库文件列表 | |||||
| "allowJs": true, // 允许编译JavaScript文件 | |||||
| "skipLibCheck": true, // 跳过所有声明文件的类型检查 | |||||
| "esModuleInterop": true, // 禁用命名空间导入(import * as fs from "fs"),并启用CJS/AMD/UMD样式的导入(import fs from "fs") | |||||
| "allowSyntheticDefaultImports": true, // 允许从没有默认导出的模块进行默认导入 | |||||
| "strict": true, // 启用所有严格类型检查选项 | |||||
| "forceConsistentCasingInFileNames": false, // 允许对同一文件的引用使用不一致的大小写 | |||||
| "module": "esnext", // 指定模块代码生成 | |||||
| "moduleResolution": "bundler", // 使用bundlers样式解析模块 | |||||
| "isolatedModules": true, // 无条件地为未解析的文件发出导入 | |||||
| "resolveJsonModule": true, // 包含.json扩展名的模块 | |||||
| "noEmit": true, // 不发出输出(即不编译代码,只进行类型检查) | |||||
| "jsx": "react-jsx", // 在.tsx文件中支持JSX | |||||
| "sourceMap": true, // 生成相应的.map文件 | |||||
| "declaration": true, // 生成相应的.d.ts文件 | |||||
| "noUnusedLocals": true, // 报告未使用的局部变量错误 | |||||
| "noUnusedParameters": true, // 报告未使用的参数错误 | |||||
| "incremental": true, // 通过读写磁盘上的文件来启用增量编译 | |||||
| "noFallthroughCasesInSwitch": true, // 报告switch语句中的fallthrough案例错误 | |||||
| "strictNullChecks": true, // 启用严格的null检查 | |||||
| "baseUrl": "./" | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,20 @@ | |||||
| declare module 'slash2'; | |||||
| declare module '*.css'; | |||||
| declare module '*.less'; | |||||
| declare module '*.scss'; | |||||
| declare module '*.sass'; | |||||
| declare module '*.svg'; | |||||
| declare module '*.png'; | |||||
| declare module '*.jpg'; | |||||
| declare module '*.jpeg'; | |||||
| declare module '*.gif'; | |||||
| declare module '*.bmp'; | |||||
| declare module '*.tiff'; | |||||
| declare module 'omit.js'; | |||||
| declare module 'numeral'; | |||||
| declare module '@antv/data-set'; | |||||
| declare module 'mockjs'; | |||||
| declare module 'react-fittext'; | |||||
| declare module 'bizcharts-plugin-slider'; | |||||
| declare const REACT_APP_ENV: 'test' | 'dev' | 'pre' | false; | |||||
| @@ -75,7 +75,7 @@ export default defineConfig({ | |||||
| * @name layout 插件 | * @name layout 插件 | ||||
| * @doc https://umijs.org/docs/max/layout-menu | * @doc https://umijs.org/docs/max/layout-menu | ||||
| */ | */ | ||||
| title: '复杂智能软件', | |||||
| title: '智能材料科研平台', | |||||
| layout: { | layout: { | ||||
| ...defaultSettings, | ...defaultSettings, | ||||
| }, | }, | ||||
| @@ -17,7 +17,7 @@ const Settings: ProLayoutProps & { | |||||
| fixSiderbar: false, | fixSiderbar: false, | ||||
| splitMenus: false, | splitMenus: false, | ||||
| colorWeak: false, | colorWeak: false, | ||||
| title: '复杂智能软件', | |||||
| title: '智能材料科研平台', | |||||
| pwa: true, | pwa: true, | ||||
| token: { | token: { | ||||
| // 参见ts声明,demo 见文档,通过token 修改样式 | // 参见ts声明,demo 见文档,通过token 修改样式 | ||||
| @@ -44,7 +44,7 @@ export default [ | |||||
| { | { | ||||
| name: 'login', | name: 'login', | ||||
| path: '/user/login', | path: '/user/login', | ||||
| component: './User/Login/login', | |||||
| component: process.env.NO_SSO ? './User/Login/login' : './User/Login', | |||||
| }, | }, | ||||
| ], | ], | ||||
| }, | }, | ||||
| @@ -181,6 +181,42 @@ export default [ | |||||
| }, | }, | ||||
| ], | ], | ||||
| }, | }, | ||||
| { | |||||
| name: '超参数自动寻优', | |||||
| path: 'hyperparameter', | |||||
| routes: [ | |||||
| { | |||||
| name: '超参数寻优', | |||||
| path: '', | |||||
| component: './HyperParameter/List/index', | |||||
| }, | |||||
| { | |||||
| name: '实验详情', | |||||
| path: 'info/:id', | |||||
| component: './HyperParameter/Info/index', | |||||
| }, | |||||
| { | |||||
| name: '创建实验', | |||||
| path: 'create', | |||||
| component: './HyperParameter/Create/index', | |||||
| }, | |||||
| { | |||||
| name: '编辑实验', | |||||
| path: 'edit/:id', | |||||
| component: './HyperParameter/Create/index', | |||||
| }, | |||||
| { | |||||
| name: '复制实验', | |||||
| path: 'copy/:id', | |||||
| component: './HyperParameter/Create/index', | |||||
| }, | |||||
| { | |||||
| name: '实验实例详情', | |||||
| path: 'instance/:autoMLId/:id', | |||||
| component: './HyperParameter/Instance/index', | |||||
| }, | |||||
| ], | |||||
| }, | |||||
| ], | ], | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -8,6 +8,7 @@ | |||||
| "build": "max build", | "build": "max build", | ||||
| "deploy": "npm run build && npm run gh-pages", | "deploy": "npm run build && npm run gh-pages", | ||||
| "dev": "npm run start:dev", | "dev": "npm run start:dev", | ||||
| "dev-no-sso": "NO_SSO=true npm run start:dev", | |||||
| "docker-hub:build": "docker build -f Dockerfile.hub -t ant-design-pro ./", | "docker-hub:build": "docker build -f Dockerfile.hub -t ant-design-pro ./", | ||||
| "docker-prod:build": "docker-compose -f ./docker/docker-compose.yml build", | "docker-prod:build": "docker-compose -f ./docker/docker-compose.yml build", | ||||
| "docker-prod:dev": "docker-compose -f ./docker/docker-compose.yml up", | "docker-prod:dev": "docker-compose -f ./docker/docker-compose.yml up", | ||||
| @@ -36,6 +37,10 @@ | |||||
| "start:mock": "cross-env REACT_APP_ENV=dev UMI_ENV=dev max dev", | "start:mock": "cross-env REACT_APP_ENV=dev UMI_ENV=dev max dev", | ||||
| "start:pre": "cross-env REACT_APP_ENV=pre UMI_ENV=dev max dev", | "start:pre": "cross-env REACT_APP_ENV=pre UMI_ENV=dev max dev", | ||||
| "start:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_ENV=dev max dev", | "start:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_ENV=dev max dev", | ||||
| "storybook": "storybook dev -p 6006", | |||||
| "storybook-build": "storybook build", | |||||
| "storybook-docs": "storybook dev --docs", | |||||
| "storybook-docs-build": "storybook build --docs", | |||||
| "test": "jest", | "test": "jest", | ||||
| "test:coverage": "npm run jest -- --coverage", | "test:coverage": "npm run jest -- --coverage", | ||||
| "test:update": "npm run jest -- -u", | "test:update": "npm run jest -- -u", | ||||
| @@ -83,6 +88,17 @@ | |||||
| }, | }, | ||||
| "devDependencies": { | "devDependencies": { | ||||
| "@ant-design/pro-cli": "^3.1.0", | "@ant-design/pro-cli": "^3.1.0", | ||||
| "@chromatic-com/storybook": "~3.2.4", | |||||
| "@storybook/addon-essentials": "~8.5.3", | |||||
| "@storybook/addon-interactions": "~8.5.3", | |||||
| "@storybook/addon-onboarding": "~8.5.3", | |||||
| "@storybook/addon-styling-webpack": "~1.0.1", | |||||
| "@storybook/addon-webpack5-compiler-babel": "~3.0.5", | |||||
| "@storybook/addon-webpack5-compiler-swc": "~2.0.0", | |||||
| "@storybook/blocks": "~8.5.3", | |||||
| "@storybook/react": "~8.5.3", | |||||
| "@storybook/react-webpack5": "~8.5.3", | |||||
| "@storybook/test": "~8.5.3", | |||||
| "@testing-library/react": "^14.0.0", | "@testing-library/react": "^14.0.0", | ||||
| "@types/antd": "^1.0.0", | "@types/antd": "^1.0.0", | ||||
| "@types/express": "^4.17.14", | "@types/express": "^4.17.14", | ||||
| @@ -96,15 +112,22 @@ | |||||
| "@umijs/max": "^4.0.66", | "@umijs/max": "^4.0.66", | ||||
| "cross-env": "^7.0.3", | "cross-env": "^7.0.3", | ||||
| "eslint": "^8.39.0", | "eslint": "^8.39.0", | ||||
| "eslint-plugin-storybook": "~0.11.2", | |||||
| "express": "^4.18.2", | "express": "^4.18.2", | ||||
| "gh-pages": "^5.0.0", | "gh-pages": "^5.0.0", | ||||
| "husky": "^8.0.3", | "husky": "^8.0.3", | ||||
| "jest": "^29.5.0", | "jest": "^29.5.0", | ||||
| "jest-environment-jsdom": "^29.5.0", | "jest-environment-jsdom": "^29.5.0", | ||||
| "less": "~4.2.2", | |||||
| "less-loader": "~12.2.0", | |||||
| "lint-staged": "^13.2.0", | "lint-staged": "^13.2.0", | ||||
| "mockjs": "^1.1.0", | "mockjs": "^1.1.0", | ||||
| "msw": "~2.7.0", | |||||
| "msw-storybook-addon": "~2.0.4", | |||||
| "prettier": "^2.8.1", | "prettier": "^2.8.1", | ||||
| "storybook": "~8.5.3", | |||||
| "swagger-ui-dist": "^4.18.2", | "swagger-ui-dist": "^4.18.2", | ||||
| "ts-loader": "~9.5.2", | |||||
| "ts-node": "^10.9.1", | "ts-node": "^10.9.1", | ||||
| "typescript": "^5.0.4", | "typescript": "^5.0.4", | ||||
| "umi-presets-pro": "^2.0.0" | "umi-presets-pro": "^2.0.0" | ||||
| @@ -140,5 +163,10 @@ | |||||
| "CNAME", | "CNAME", | ||||
| "create-umi" | "create-umi" | ||||
| ] | ] | ||||
| }, | |||||
| "msw": { | |||||
| "workerDirectory": [ | |||||
| "public" | |||||
| ] | |||||
| } | } | ||||
| } | } | ||||
| @@ -0,0 +1,307 @@ | |||||
| /* eslint-disable */ | |||||
| /* tslint:disable */ | |||||
| /** | |||||
| * Mock Service Worker. | |||||
| * @see https://github.com/mswjs/msw | |||||
| * - Please do NOT modify this file. | |||||
| * - Please do NOT serve this file on production. | |||||
| */ | |||||
| const PACKAGE_VERSION = '2.7.0' | |||||
| const INTEGRITY_CHECKSUM = '00729d72e3b82faf54ca8b9621dbb96f' | |||||
| const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') | |||||
| const activeClientIds = new Set() | |||||
| self.addEventListener('install', function () { | |||||
| self.skipWaiting() | |||||
| }) | |||||
| self.addEventListener('activate', function (event) { | |||||
| event.waitUntil(self.clients.claim()) | |||||
| }) | |||||
| self.addEventListener('message', async function (event) { | |||||
| const clientId = event.source.id | |||||
| if (!clientId || !self.clients) { | |||||
| return | |||||
| } | |||||
| const client = await self.clients.get(clientId) | |||||
| if (!client) { | |||||
| return | |||||
| } | |||||
| const allClients = await self.clients.matchAll({ | |||||
| type: 'window', | |||||
| }) | |||||
| switch (event.data) { | |||||
| case 'KEEPALIVE_REQUEST': { | |||||
| sendToClient(client, { | |||||
| type: 'KEEPALIVE_RESPONSE', | |||||
| }) | |||||
| break | |||||
| } | |||||
| case 'INTEGRITY_CHECK_REQUEST': { | |||||
| sendToClient(client, { | |||||
| type: 'INTEGRITY_CHECK_RESPONSE', | |||||
| payload: { | |||||
| packageVersion: PACKAGE_VERSION, | |||||
| checksum: INTEGRITY_CHECKSUM, | |||||
| }, | |||||
| }) | |||||
| break | |||||
| } | |||||
| case 'MOCK_ACTIVATE': { | |||||
| activeClientIds.add(clientId) | |||||
| sendToClient(client, { | |||||
| type: 'MOCKING_ENABLED', | |||||
| payload: { | |||||
| client: { | |||||
| id: client.id, | |||||
| frameType: client.frameType, | |||||
| }, | |||||
| }, | |||||
| }) | |||||
| break | |||||
| } | |||||
| case 'MOCK_DEACTIVATE': { | |||||
| activeClientIds.delete(clientId) | |||||
| break | |||||
| } | |||||
| case 'CLIENT_CLOSED': { | |||||
| activeClientIds.delete(clientId) | |||||
| const remainingClients = allClients.filter((client) => { | |||||
| return client.id !== clientId | |||||
| }) | |||||
| // Unregister itself when there are no more clients | |||||
| if (remainingClients.length === 0) { | |||||
| self.registration.unregister() | |||||
| } | |||||
| break | |||||
| } | |||||
| } | |||||
| }) | |||||
| self.addEventListener('fetch', function (event) { | |||||
| const { request } = event | |||||
| // Bypass navigation requests. | |||||
| if (request.mode === 'navigate') { | |||||
| return | |||||
| } | |||||
| // Opening the DevTools triggers the "only-if-cached" request | |||||
| // that cannot be handled by the worker. Bypass such requests. | |||||
| if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { | |||||
| return | |||||
| } | |||||
| // Bypass all requests when there are no active clients. | |||||
| // Prevents the self-unregistered worked from handling requests | |||||
| // after it's been deleted (still remains active until the next reload). | |||||
| if (activeClientIds.size === 0) { | |||||
| return | |||||
| } | |||||
| // Generate unique request ID. | |||||
| const requestId = crypto.randomUUID() | |||||
| event.respondWith(handleRequest(event, requestId)) | |||||
| }) | |||||
| async function handleRequest(event, requestId) { | |||||
| const client = await resolveMainClient(event) | |||||
| const response = await getResponse(event, client, requestId) | |||||
| // Send back the response clone for the "response:*" life-cycle events. | |||||
| // Ensure MSW is active and ready to handle the message, otherwise | |||||
| // this message will pend indefinitely. | |||||
| if (client && activeClientIds.has(client.id)) { | |||||
| ;(async function () { | |||||
| const responseClone = response.clone() | |||||
| sendToClient( | |||||
| client, | |||||
| { | |||||
| type: 'RESPONSE', | |||||
| payload: { | |||||
| requestId, | |||||
| isMockedResponse: IS_MOCKED_RESPONSE in response, | |||||
| type: responseClone.type, | |||||
| status: responseClone.status, | |||||
| statusText: responseClone.statusText, | |||||
| body: responseClone.body, | |||||
| headers: Object.fromEntries(responseClone.headers.entries()), | |||||
| }, | |||||
| }, | |||||
| [responseClone.body], | |||||
| ) | |||||
| })() | |||||
| } | |||||
| return response | |||||
| } | |||||
| // Resolve the main client for the given event. | |||||
| // Client that issues a request doesn't necessarily equal the client | |||||
| // that registered the worker. It's with the latter the worker should | |||||
| // communicate with during the response resolving phase. | |||||
| async function resolveMainClient(event) { | |||||
| const client = await self.clients.get(event.clientId) | |||||
| if (activeClientIds.has(event.clientId)) { | |||||
| return client | |||||
| } | |||||
| if (client?.frameType === 'top-level') { | |||||
| return client | |||||
| } | |||||
| const allClients = await self.clients.matchAll({ | |||||
| type: 'window', | |||||
| }) | |||||
| return allClients | |||||
| .filter((client) => { | |||||
| // Get only those clients that are currently visible. | |||||
| return client.visibilityState === 'visible' | |||||
| }) | |||||
| .find((client) => { | |||||
| // Find the client ID that's recorded in the | |||||
| // set of clients that have registered the worker. | |||||
| return activeClientIds.has(client.id) | |||||
| }) | |||||
| } | |||||
| async function getResponse(event, client, requestId) { | |||||
| const { request } = event | |||||
| // Clone the request because it might've been already used | |||||
| // (i.e. its body has been read and sent to the client). | |||||
| const requestClone = request.clone() | |||||
| function passthrough() { | |||||
| // Cast the request headers to a new Headers instance | |||||
| // so the headers can be manipulated with. | |||||
| const headers = new Headers(requestClone.headers) | |||||
| // Remove the "accept" header value that marked this request as passthrough. | |||||
| // This prevents request alteration and also keeps it compliant with the | |||||
| // user-defined CORS policies. | |||||
| const acceptHeader = headers.get('accept') | |||||
| if (acceptHeader) { | |||||
| const values = acceptHeader.split(',').map((value) => value.trim()) | |||||
| const filteredValues = values.filter( | |||||
| (value) => value !== 'msw/passthrough', | |||||
| ) | |||||
| if (filteredValues.length > 0) { | |||||
| headers.set('accept', filteredValues.join(', ')) | |||||
| } else { | |||||
| headers.delete('accept') | |||||
| } | |||||
| } | |||||
| return fetch(requestClone, { headers }) | |||||
| } | |||||
| // Bypass mocking when the client is not active. | |||||
| if (!client) { | |||||
| return passthrough() | |||||
| } | |||||
| // Bypass initial page load requests (i.e. static assets). | |||||
| // The absence of the immediate/parent client in the map of the active clients | |||||
| // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet | |||||
| // and is not ready to handle requests. | |||||
| if (!activeClientIds.has(client.id)) { | |||||
| return passthrough() | |||||
| } | |||||
| // Notify the client that a request has been intercepted. | |||||
| const requestBuffer = await request.arrayBuffer() | |||||
| const clientMessage = await sendToClient( | |||||
| client, | |||||
| { | |||||
| type: 'REQUEST', | |||||
| payload: { | |||||
| id: requestId, | |||||
| url: request.url, | |||||
| mode: request.mode, | |||||
| method: request.method, | |||||
| headers: Object.fromEntries(request.headers.entries()), | |||||
| cache: request.cache, | |||||
| credentials: request.credentials, | |||||
| destination: request.destination, | |||||
| integrity: request.integrity, | |||||
| redirect: request.redirect, | |||||
| referrer: request.referrer, | |||||
| referrerPolicy: request.referrerPolicy, | |||||
| body: requestBuffer, | |||||
| keepalive: request.keepalive, | |||||
| }, | |||||
| }, | |||||
| [requestBuffer], | |||||
| ) | |||||
| switch (clientMessage.type) { | |||||
| case 'MOCK_RESPONSE': { | |||||
| return respondWithMock(clientMessage.data) | |||||
| } | |||||
| case 'PASSTHROUGH': { | |||||
| return passthrough() | |||||
| } | |||||
| } | |||||
| return passthrough() | |||||
| } | |||||
| function sendToClient(client, message, transferrables = []) { | |||||
| return new Promise((resolve, reject) => { | |||||
| const channel = new MessageChannel() | |||||
| channel.port1.onmessage = (event) => { | |||||
| if (event.data && event.data.error) { | |||||
| return reject(event.data.error) | |||||
| } | |||||
| resolve(event.data) | |||||
| } | |||||
| client.postMessage( | |||||
| message, | |||||
| [channel.port2].concat(transferrables.filter(Boolean)), | |||||
| ) | |||||
| }) | |||||
| } | |||||
| async function respondWithMock(response) { | |||||
| // Setting response status code to 0 is a no-op. | |||||
| // However, when responding with a "Response.error()", the produced Response | |||||
| // instance will have status code set to 0. Since it's not possible to create | |||||
| // a Response instance with status code 0, handle that use-case separately. | |||||
| if (response.status === 0) { | |||||
| return Response.error() | |||||
| } | |||||
| const mockedResponse = new Response(response.body, response) | |||||
| Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { | |||||
| value: true, | |||||
| enumerable: true, | |||||
| }) | |||||
| return mockedResponse | |||||
| } | |||||
| @@ -7,7 +7,6 @@ import defaultSettings from '../config/defaultSettings'; | |||||
| import '../public/fonts/font.css'; | import '../public/fonts/font.css'; | ||||
| import { getAccessToken } from './access'; | import { getAccessToken } from './access'; | ||||
| import './dayjsConfig'; | import './dayjsConfig'; | ||||
| import './global.less'; | |||||
| import { removeAllPageCacheState } from './hooks/pageCacheState'; | import { removeAllPageCacheState } from './hooks/pageCacheState'; | ||||
| import { | import { | ||||
| getRemoteMenu, | getRemoteMenu, | ||||
| @@ -41,7 +40,7 @@ export async function getInitialState(): Promise<GlobalInitialState> { | |||||
| roleNames: response.user.roles, | roleNames: response.user.roles, | ||||
| } as API.CurrentUser; | } as API.CurrentUser; | ||||
| } catch (error) { | } catch (error) { | ||||
| console.error('1111', error); | |||||
| console.error('getInitialState', error); | |||||
| gotoLoginPage(); | gotoLoginPage(); | ||||
| } | } | ||||
| return undefined; | return undefined; | ||||
| @@ -215,7 +214,7 @@ export const antd: RuntimeAntdConfig = (memo) => { | |||||
| defaultColor: themes['textColor'], | defaultColor: themes['textColor'], | ||||
| defaultHoverBg: 'rgba(22, 100, 255, 0.06)', | defaultHoverBg: 'rgba(22, 100, 255, 0.06)', | ||||
| defaultHoverBorderColor: 'rgba(22, 100, 255, 0.5)', | defaultHoverBorderColor: 'rgba(22, 100, 255, 0.5)', | ||||
| defaultHoverColor: '#3F7FFF ', | |||||
| defaultHoverColor: '#3F7FFF', | |||||
| defaultActiveBg: 'rgba(22, 100, 255, 0.12)', | defaultActiveBg: 'rgba(22, 100, 255, 0.12)', | ||||
| defaultActiveBorderColor: 'rgba(22, 100, 255, 0.75)', | defaultActiveBorderColor: 'rgba(22, 100, 255, 0.75)', | ||||
| defaultActiveColor: themes['primaryColor'], | defaultActiveColor: themes['primaryColor'], | ||||
| @@ -228,6 +227,7 @@ export const antd: RuntimeAntdConfig = (memo) => { | |||||
| }; | }; | ||||
| memo.theme.components.Select = { | memo.theme.components.Select = { | ||||
| singleItemHeightLG: 46, | singleItemHeightLG: 46, | ||||
| optionSelectedColor: themes['primaryColor'], | |||||
| }; | }; | ||||
| memo.theme.components.Table = { | memo.theme.components.Table = { | ||||
| headerBg: 'rgba(242, 244, 247, 0.36)', | headerBg: 'rgba(242, 244, 247, 0.36)', | ||||
| @@ -0,0 +1,86 @@ | |||||
| /* | |||||
| * @Author: 赵伟 | |||||
| * @Date: 2024-11-29 09:27:19 | |||||
| * @Description: 用于 BasicInfo 和 BasicTableInfo 组件的子组件 | |||||
| */ | |||||
| import { Typography } from 'antd'; | |||||
| import React from 'react'; | |||||
| import BasicInfoItemValue from './BasicInfoItemValue'; | |||||
| import { type BasicInfoData, type BasicInfoLink } from './types'; | |||||
| type BasicInfoItemProps = { | |||||
| /** 基础信息 */ | |||||
| data: BasicInfoData; | |||||
| /** 标题宽度 */ | |||||
| labelWidth: number; | |||||
| /** 自定义类名前缀 */ | |||||
| classPrefix: string; | |||||
| /** 标题是否显示省略号 */ | |||||
| labelEllipsis?: boolean; | |||||
| /** 标签对齐方式 */ | |||||
| labelAlign?: 'start' | 'end' | 'justify'; | |||||
| }; | |||||
| function BasicInfoItem({ | |||||
| data, | |||||
| labelWidth, | |||||
| classPrefix, | |||||
| labelEllipsis = true, | |||||
| labelAlign = 'start', | |||||
| }: BasicInfoItemProps) { | |||||
| const { label, value, format, ellipsis } = data; | |||||
| const formatValue = format ? format(value) : value; | |||||
| const myClassName = `${classPrefix}__item`; | |||||
| let valueComponent = undefined; | |||||
| if (React.isValidElement(formatValue)) { | |||||
| valueComponent = <div className={`${myClassName}__node`}>{formatValue}</div>; | |||||
| } else if (Array.isArray(formatValue)) { | |||||
| valueComponent = ( | |||||
| <div className={`${myClassName}__value-container`}> | |||||
| {formatValue.map((item: BasicInfoLink) => ( | |||||
| <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} | |||||
| ellipsis={ellipsis} | |||||
| classPrefix={classPrefix} | |||||
| /> | |||||
| ); | |||||
| } else { | |||||
| valueComponent = ( | |||||
| <BasicInfoItemValue value={formatValue} ellipsis={ellipsis} classPrefix={classPrefix} /> | |||||
| ); | |||||
| } | |||||
| return ( | |||||
| <div className={myClassName} key={label}> | |||||
| <div | |||||
| className={`${myClassName}__label`} | |||||
| style={{ width: labelWidth, textAlign: labelAlign, textAlignLast: labelAlign }} | |||||
| > | |||||
| <Typography.Text | |||||
| ellipsis={labelEllipsis !== false ? { tooltip: label } : false} | |||||
| style={{ width: labelAlign === 'justify' ? '100%' : 'auto' }} | |||||
| > | |||||
| {label} | |||||
| </Typography.Text> | |||||
| </div> | |||||
| {valueComponent} | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default BasicInfoItem; | |||||
| @@ -0,0 +1,58 @@ | |||||
| /* | |||||
| * @Author: 赵伟 | |||||
| * @Date: 2024-11-29 09:27:19 | |||||
| * @Description: 用于 BasicInfoItem 的组件 | |||||
| */ | |||||
| import { isEmpty } from '@/utils'; | |||||
| import { Link } from '@umijs/max'; | |||||
| import { Typography } from 'antd'; | |||||
| type BasicInfoItemValueProps = { | |||||
| /** 值是否显示省略号 */ | |||||
| ellipsis?: boolean; | |||||
| /** 自定义类名前缀 */ | |||||
| classPrefix: string; | |||||
| /** 值 */ | |||||
| value?: string; | |||||
| /** 内部链接 */ | |||||
| link?: string; | |||||
| /** 外部链接 */ | |||||
| url?: string; | |||||
| }; | |||||
| function BasicInfoItemValue({ | |||||
| value, | |||||
| link, | |||||
| url, | |||||
| classPrefix, | |||||
| ellipsis = true, | |||||
| }: BasicInfoItemValueProps) { | |||||
| const myClassName = `${classPrefix}__item__value`; | |||||
| let component = undefined; | |||||
| if (url && value) { | |||||
| component = ( | |||||
| <a className={`${myClassName}__link`} href={url} target="_blank" rel="noopener noreferrer"> | |||||
| {value} | |||||
| </a> | |||||
| ); | |||||
| } else if (link && value) { | |||||
| component = ( | |||||
| <Link to={link} className={`${myClassName}__link`}> | |||||
| {value} | |||||
| </Link> | |||||
| ); | |||||
| } else { | |||||
| component = <span className={`${myClassName}__text`}>{!isEmpty(value) ? value : '--'}</span>; | |||||
| } | |||||
| return ( | |||||
| <div className={myClassName}> | |||||
| <Typography.Text ellipsis={ellipsis !== false ? { tooltip: value } : false}> | |||||
| {component} | |||||
| </Typography.Text> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default BasicInfoItemValue; | |||||
| @@ -1,113 +0,0 @@ | |||||
| /* | |||||
| * @Author: 赵伟 | |||||
| * @Date: 2024-11-29 09:27:19 | |||||
| * @Description: 用于 BasicInfo 和 BasicTableInfo 组件的子组件 | |||||
| */ | |||||
| import { Link } from '@umijs/max'; | |||||
| import { Typography } from 'antd'; | |||||
| import React from 'react'; | |||||
| import { type BasicInfoData, type BasicInfoLink } from './types'; | |||||
| type BasicInfoItemProps = { | |||||
| data: BasicInfoData; | |||||
| labelWidth: number; | |||||
| classPrefix: string; | |||||
| }; | |||||
| 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={`${myClassName}__value-container`}> | |||||
| {formatValue.map((item: BasicInfoLink) => ( | |||||
| <BasicInfoItemValue | |||||
| key={item.value} | |||||
| value={item.value} | |||||
| link={item.link} | |||||
| url={item.url} | |||||
| ellipsis={ellipsis} | |||||
| classPrefix={classPrefix} | |||||
| /> | |||||
| ))} | |||||
| </div> | |||||
| ); | |||||
| } else if (React.isValidElement(formatValue)) { | |||||
| // 这个判断必须在下面的判断之前 | |||||
| valueComponent = ( | |||||
| <BasicInfoItemValue value={formatValue} ellipsis={ellipsis} classPrefix={classPrefix} /> | |||||
| ); | |||||
| } else if (typeof formatValue === 'object' && formatValue) { | |||||
| valueComponent = ( | |||||
| <BasicInfoItemValue | |||||
| value={formatValue.value} | |||||
| link={formatValue.link} | |||||
| url={formatValue.url} | |||||
| ellipsis={ellipsis} | |||||
| classPrefix={classPrefix} | |||||
| /> | |||||
| ); | |||||
| } else { | |||||
| valueComponent = ( | |||||
| <BasicInfoItemValue value={formatValue} ellipsis={ellipsis} classPrefix={classPrefix} /> | |||||
| ); | |||||
| } | |||||
| return ( | |||||
| <div className={myClassName} key={label}> | |||||
| <div className={`${myClassName}__label`} style={{ width: labelWidth }}> | |||||
| {label} | |||||
| </div> | |||||
| {valueComponent} | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| type BasicInfoItemValueProps = { | |||||
| ellipsis?: boolean; | |||||
| classPrefix: string; | |||||
| value: string | React.ReactNode; | |||||
| link?: string; | |||||
| url?: string; | |||||
| }; | |||||
| export function BasicInfoItemValue({ | |||||
| value, | |||||
| link, | |||||
| url, | |||||
| ellipsis, | |||||
| classPrefix, | |||||
| }: BasicInfoItemValueProps) { | |||||
| const myClassName = `${classPrefix}__item__value`; | |||||
| let component = undefined; | |||||
| if (url && value) { | |||||
| component = ( | |||||
| <a className={`${myClassName}__link`} href={url} target="_blank" rel="noopener noreferrer"> | |||||
| {value} | |||||
| </a> | |||||
| ); | |||||
| } else if (link && value) { | |||||
| component = ( | |||||
| <Link to={link} className={`${myClassName}__link`}> | |||||
| {value} | |||||
| </Link> | |||||
| ); | |||||
| } else if (React.isValidElement(value)) { | |||||
| return value; | |||||
| } else { | |||||
| component = <span className={`${myClassName}__text`}>{value ?? '--'}</span>; | |||||
| } | |||||
| return ( | |||||
| <div className={myClassName}> | |||||
| <Typography.Text | |||||
| ellipsis={ellipsis ? { tooltip: value } : false} | |||||
| style={{ fontSize: 'inherit' }} | |||||
| > | |||||
| {component} | |||||
| </Typography.Text> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| @@ -1,48 +0,0 @@ | |||||
| /* | |||||
| * @Author: 赵伟 | |||||
| * @Date: 2024-11-29 09:27:19 | |||||
| * @Description: 用于 BasicInfo 和 BasicTableInfo 组件的常用转化格式 | |||||
| */ | |||||
| // 格式化日期 | |||||
| export { formatDate } from '@/utils/date'; | |||||
| /** | |||||
| * 格式化字符串数组 | |||||
| * @param value - 字符串数组 | |||||
| * @returns 逗号分隔的字符串 | |||||
| */ | |||||
| export const formatList = (value: string[] | null | undefined): string => { | |||||
| if ( | |||||
| value === undefined || | |||||
| value === null || | |||||
| Array.isArray(value) === false || | |||||
| value.length === 0 | |||||
| ) { | |||||
| return '--'; | |||||
| } | |||||
| return value.join(','); | |||||
| }; | |||||
| /** | |||||
| * 格式化布尔值 | |||||
| * @param value - 布尔值 | |||||
| * @returns "是" 或 "否" | |||||
| */ | |||||
| export const formatBoolean = (value: boolean): string => { | |||||
| return value ? '是' : '否'; | |||||
| }; | |||||
| type FormatEnum = (value: string | number) => string; | |||||
| /** | |||||
| * 格式化枚举 | |||||
| * @param options - 枚举选项 | |||||
| * @returns 格式化枚举函数 | |||||
| */ | |||||
| export const formatEnum = (options: { value: string | number; label: string }[]): FormatEnum => { | |||||
| return (value: string | number) => { | |||||
| const option = options.find((item) => item.value === value); | |||||
| return option ? option.label : '--'; | |||||
| }; | |||||
| }; | |||||
| @@ -17,8 +17,6 @@ | |||||
| color: @text-color-secondary; | color: @text-color-secondary; | ||||
| font-size: @font-size-content; | font-size: @font-size-content; | ||||
| line-height: 1.6; | line-height: 1.6; | ||||
| text-align: justify; | |||||
| text-align-last: justify; | |||||
| &::after { | &::after { | ||||
| position: absolute; | position: absolute; | ||||
| @@ -31,10 +29,12 @@ | |||||
| flex: 1; | flex: 1; | ||||
| flex-direction: column; | flex-direction: column; | ||||
| gap: 5px 0; | gap: 5px 0; | ||||
| min-width: 0; | |||||
| } | } | ||||
| &__value { | &__value { | ||||
| flex: 1; | flex: 1; | ||||
| min-width: 0; | |||||
| margin-left: 16px; | margin-left: 16px; | ||||
| font-size: @font-size-content; | font-size: @font-size-content; | ||||
| line-height: 1.6; | line-height: 1.6; | ||||
| @@ -49,5 +49,29 @@ | |||||
| text-underline-offset: 3px; | text-underline-offset: 3px; | ||||
| } | } | ||||
| } | } | ||||
| &__node { | |||||
| flex: 1; | |||||
| min-width: 0; | |||||
| margin-left: 16px; | |||||
| font-size: @font-size-content; | |||||
| line-height: 1.6; | |||||
| word-break: break-all; | |||||
| } | |||||
| } | |||||
| } | |||||
| .kf-basic-info--three-columns { | |||||
| width: 100%; | |||||
| .kf-basic-info__item { | |||||
| width: calc((100% - 80px) / 3); | |||||
| &__label { | |||||
| font-size: @font-size; | |||||
| } | |||||
| &__value { | |||||
| font-size: @font-size; | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -1,27 +1,56 @@ | |||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import React from 'react'; | import React from 'react'; | ||||
| import { BasicInfoItem } from './components'; | |||||
| import BasicInfoItem from './BasicInfoItem'; | |||||
| import './index.less'; | import './index.less'; | ||||
| import type { BasicInfoData, BasicInfoLink } from './types'; | import type { BasicInfoData, BasicInfoLink } from './types'; | ||||
| export * from './format'; | |||||
| export type { BasicInfoData, BasicInfoLink }; | export type { BasicInfoData, BasicInfoLink }; | ||||
| type BasicInfoProps = { | |||||
| export type BasicInfoProps = { | |||||
| /** 基础信息 */ | |||||
| datas: BasicInfoData[]; | datas: BasicInfoData[]; | ||||
| /** 标题宽度 */ | |||||
| labelWidth: number; | |||||
| /** 标题是否显示省略号 */ | |||||
| labelEllipsis?: boolean; | |||||
| /** 是否一行三列 */ | |||||
| threeColumns?: boolean; | |||||
| /** 标签对齐方式 */ | |||||
| labelAlign?: 'start' | 'end' | 'justify'; | |||||
| /** 自定义类名 */ | |||||
| className?: string; | className?: string; | ||||
| /** 自定义样式 */ | |||||
| style?: React.CSSProperties; | style?: React.CSSProperties; | ||||
| labelWidth: number; | |||||
| }; | }; | ||||
| export default function BasicInfo({ datas, className, style, labelWidth }: BasicInfoProps) { | |||||
| /** | |||||
| * 基础信息展示组件,用于展示基础信息,支持一行两列或一行三列,支持数据格式化 | |||||
| */ | |||||
| export default function BasicInfo({ | |||||
| datas, | |||||
| className, | |||||
| style, | |||||
| labelWidth, | |||||
| labelEllipsis = true, | |||||
| threeColumns = false, | |||||
| labelAlign = 'start', | |||||
| }: BasicInfoProps) { | |||||
| return ( | return ( | ||||
| <div className={classNames('kf-basic-info', className)} style={style}> | |||||
| <div | |||||
| className={classNames( | |||||
| 'kf-basic-info', | |||||
| { 'kf-basic-info--three-columns': threeColumns }, | |||||
| className, | |||||
| )} | |||||
| style={style} | |||||
| > | |||||
| {datas.map((item) => ( | {datas.map((item) => ( | ||||
| <BasicInfoItem | <BasicInfoItem | ||||
| key={item.label} | key={item.label} | ||||
| data={item} | data={item} | ||||
| labelWidth={labelWidth} | labelWidth={labelWidth} | ||||
| classPrefix="kf-basic-info" | classPrefix="kf-basic-info" | ||||
| labelEllipsis={labelEllipsis} | |||||
| labelAlign={labelAlign} | |||||
| /> | /> | ||||
| ))} | ))} | ||||
| </div> | </div> | ||||
| @@ -3,12 +3,12 @@ export type BasicInfoData = { | |||||
| label: string; | label: string; | ||||
| value?: any; | value?: any; | ||||
| ellipsis?: boolean; | ellipsis?: boolean; | ||||
| format?: (_value?: any) => string | BasicInfoLink | BasicInfoLink[] | undefined; | |||||
| format?: (_value?: any) => string | React.ReactNode | BasicInfoLink | BasicInfoLink[] | undefined; | |||||
| }; | }; | ||||
| // 值为链接的类型 | // 值为链接的类型 | ||||
| export type BasicInfoLink = { | export type BasicInfoLink = { | ||||
| value: string; | |||||
| value?: string; | |||||
| link?: string; | link?: string; | ||||
| url?: string; | url?: string; | ||||
| }; | }; | ||||
| @@ -34,7 +34,6 @@ | |||||
| &__value { | &__value { | ||||
| flex: 1; | flex: 1; | ||||
| min-width: 0; | min-width: 0; | ||||
| margin: 0 !important; | |||||
| padding: 12px 20px 4px; | padding: 12px 20px 4px; | ||||
| font-size: @font-size; | font-size: @font-size; | ||||
| word-break: break-all; | word-break: break-all; | ||||
| @@ -56,5 +55,13 @@ | |||||
| text-underline-offset: 3px; | text-underline-offset: 3px; | ||||
| } | } | ||||
| } | } | ||||
| &__node { | |||||
| flex: 1; | |||||
| min-width: 0; | |||||
| padding: 12px 20px; | |||||
| font-size: @font-size; | |||||
| word-break: break-all; | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -1,30 +1,27 @@ | |||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import { BasicInfoItem } from '../BasicInfo/components'; | |||||
| import { BasicInfoProps } from '../BasicInfo'; | |||||
| import BasicInfoItem from '../BasicInfo/BasicInfoItem'; | |||||
| import { type BasicInfoData, type BasicInfoLink } from '../BasicInfo/types'; | import { type BasicInfoData, type BasicInfoLink } from '../BasicInfo/types'; | ||||
| import './index.less'; | import './index.less'; | ||||
| export * from '../BasicInfo/format'; | |||||
| export type { BasicInfoData, BasicInfoLink }; | export type { BasicInfoData, BasicInfoLink }; | ||||
| type BasicTableInfoProps = { | |||||
| datas: BasicInfoData[]; | |||||
| className?: string; | |||||
| style?: React.CSSProperties; | |||||
| labelWidth: number; | |||||
| }; | |||||
| /** | |||||
| * 表格基础信息展示组件,用于展示基础信息,一行四列,支持数据格式化 | |||||
| */ | |||||
| export default function BasicTableInfo({ | export default function BasicTableInfo({ | ||||
| datas, | datas, | ||||
| className, | className, | ||||
| style, | style, | ||||
| labelWidth, | labelWidth, | ||||
| }: BasicTableInfoProps) { | |||||
| labelEllipsis, | |||||
| }: BasicInfoProps) { | |||||
| const remainder = datas.length % 4; | const remainder = datas.length % 4; | ||||
| const array = []; | const array = []; | ||||
| if (remainder > 0) { | if (remainder > 0) { | ||||
| for (let i = 0; i < 4 - remainder; i++) { | for (let i = 0; i < 4 - remainder; i++) { | ||||
| array.push({ | array.push({ | ||||
| label: '', | label: '', | ||||
| value: '', | |||||
| value: false, // 用于区分是否是空数据,不能是空字符串、null、undefined | |||||
| }); | }); | ||||
| } | } | ||||
| } | } | ||||
| @@ -37,6 +34,7 @@ export default function BasicTableInfo({ | |||||
| key={`${item.label}-${index}`} | key={`${item.label}-${index}`} | ||||
| data={item} | data={item} | ||||
| labelWidth={labelWidth} | labelWidth={labelWidth} | ||||
| labelEllipsis={labelEllipsis} | |||||
| classPrefix="kf-basic-table-info" | classPrefix="kf-basic-table-info" | ||||
| /> | /> | ||||
| ))} | ))} | ||||
| @@ -4,18 +4,32 @@ | |||||
| * @Description: 流水线选择代码配置表单 | * @Description: 流水线选择代码配置表单 | ||||
| */ | */ | ||||
| import CodeSelectorModal from '@/components/CodeSelectorModal'; | |||||
| import KFIcon from '@/components/KFIcon'; | import KFIcon from '@/components/KFIcon'; | ||||
| import CodeSelectorModal from '@/pages/Pipeline/components/CodeSelectorModal'; | |||||
| import { openAntdModal } from '@/utils/modal'; | import { openAntdModal } from '@/utils/modal'; | ||||
| import { Button } from 'antd'; | import { Button } from 'antd'; | ||||
| import classNames from 'classnames'; | |||||
| import ParameterInput, { type ParameterInputProps } from '../ParameterInput'; | import ParameterInput, { type ParameterInputProps } from '../ParameterInput'; | ||||
| import './index.less'; | import './index.less'; | ||||
| export { requiredValidator, type ParameterInputObject } from '../ParameterInput'; | |||||
| export { | |||||
| requiredValidator, | |||||
| type ParameterInputObject, | |||||
| type ParameterInputValue, | |||||
| } from '../ParameterInput'; | |||||
| type CodeSelectProps = ParameterInputProps; | type CodeSelectProps = ParameterInputProps; | ||||
| function CodeSelect({ value, onChange, disabled, ...rest }: CodeSelectProps) { | |||||
| /** 代码配置选择表单组件 */ | |||||
| function CodeSelect({ | |||||
| value, | |||||
| size, | |||||
| disabled, | |||||
| className, | |||||
| style, | |||||
| onChange, | |||||
| ...rest | |||||
| }: CodeSelectProps) { | |||||
| const selectResource = () => { | const selectResource = () => { | ||||
| const { close } = openAntdModal(CodeSelectorModal, { | const { close } = openAntdModal(CodeSelectorModal, { | ||||
| onOk: (res) => { | onOk: (res) => { | ||||
| @@ -46,9 +60,10 @@ function CodeSelect({ value, onChange, disabled, ...rest }: CodeSelectProps) { | |||||
| }; | }; | ||||
| return ( | return ( | ||||
| <div className="kf-code-select"> | |||||
| <div className={classNames('kf-code-select', className)} style={style}> | |||||
| <ParameterInput | <ParameterInput | ||||
| {...rest} | {...rest} | ||||
| size={size} | |||||
| disabled={disabled} | disabled={disabled} | ||||
| value={value} | value={value} | ||||
| onChange={onChange} | onChange={onChange} | ||||
| @@ -56,7 +71,7 @@ function CodeSelect({ value, onChange, disabled, ...rest }: CodeSelectProps) { | |||||
| ></ParameterInput> | ></ParameterInput> | ||||
| <Button | <Button | ||||
| className="kf-code-select__button" | className="kf-code-select__button" | ||||
| size="large" | |||||
| size={size} | |||||
| type="link" | type="link" | ||||
| icon={<KFIcon type="icon-xuanzedaimapeizhi" font={16} />} | icon={<KFIcon type="icon-xuanzedaimapeizhi" font={16} />} | ||||
| disabled={disabled} | disabled={disabled} | ||||
| @@ -0,0 +1,49 @@ | |||||
| .kf-code-selector-modal { | |||||
| width: 100%; | |||||
| height: 100%; | |||||
| &__search { | |||||
| width: 100%; | |||||
| } | |||||
| &__content { | |||||
| display: flex; | |||||
| flex-direction: row; | |||||
| flex-wrap: wrap; | |||||
| gap: 10px; | |||||
| width: 100%; | |||||
| max-height: 50vh; | |||||
| margin-top: 24px; | |||||
| margin-bottom: 30px; | |||||
| overflow-x: hidden; | |||||
| overflow-y: auto; | |||||
| } | |||||
| &__empty { | |||||
| padding-top: 40px; | |||||
| } | |||||
| // 覆盖 antd 样式 | |||||
| .ant-input-affix-wrapper { | |||||
| border-radius: 23px !important; | |||||
| .ant-input-prefix { | |||||
| margin-inline-end: 12px; | |||||
| } | |||||
| .ant-input-suffix { | |||||
| margin-inline-end: 12px; | |||||
| } | |||||
| .ant-input-clear-icon { | |||||
| font-size: 16px; | |||||
| } | |||||
| } | |||||
| .ant-input-group-addon { | |||||
| display: none; | |||||
| } | |||||
| .ant-pagination { | |||||
| .ant-select-single { | |||||
| height: 32px !important; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -4,16 +4,16 @@ | |||||
| * @Description: 选择代码 | * @Description: 选择代码 | ||||
| */ | */ | ||||
| import KFIcon from '@/components/KFIcon'; | |||||
| import KFModal from '@/components/KFModal'; | import KFModal from '@/components/KFModal'; | ||||
| import { type CodeConfigData } from '@/pages/CodeConfig/List'; | import { type CodeConfigData } from '@/pages/CodeConfig/List'; | ||||
| import { getCodeConfigListReq } from '@/services/codeConfig'; | import { getCodeConfigListReq } from '@/services/codeConfig'; | ||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import { Icon } from '@umijs/max'; | |||||
| import type { ModalProps, PaginationProps } from 'antd'; | import type { ModalProps, PaginationProps } from 'antd'; | ||||
| import { Empty, Input, Pagination } from 'antd'; | import { Empty, Input, Pagination } from 'antd'; | ||||
| import { useEffect, useState } from 'react'; | import { useEffect, useState } from 'react'; | ||||
| import CodeConfigItem from '../CodeConfigItem'; | import CodeConfigItem from '../CodeConfigItem'; | ||||
| import styles from './index.less'; | |||||
| import './index.less'; | |||||
| export { type CodeConfigData }; | export { type CodeConfigData }; | ||||
| @@ -21,6 +21,7 @@ export interface CodeSelectorModalProps extends Omit<ModalProps, 'onOk'> { | |||||
| onOk?: (params: CodeConfigData | undefined) => void; | onOk?: (params: CodeConfigData | undefined) => void; | ||||
| } | } | ||||
| /** 选择代码配置的弹窗,推荐使用函数的方式打开 */ | |||||
| function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { | function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { | ||||
| const [dataList, setDataList] = useState<CodeConfigData[]>([]); | const [dataList, setDataList] = useState<CodeConfigData[]>([]); | ||||
| const [total, setTotal] = useState(0); | const [total, setTotal] = useState(0); | ||||
| @@ -79,9 +80,9 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { | |||||
| footer={null} | footer={null} | ||||
| destroyOnClose | destroyOnClose | ||||
| > | > | ||||
| <div className={styles['code-selector']}> | |||||
| <div className="kf-code-selector-modal"> | |||||
| <Input.Search | <Input.Search | ||||
| className={styles['code-selector__search']} | |||||
| className="kf-code-selector-modal__search" | |||||
| placeholder="按代码仓库名称筛选" | placeholder="按代码仓库名称筛选" | ||||
| allowClear | allowClear | ||||
| onSearch={handleSearch} | onSearch={handleSearch} | ||||
| @@ -90,12 +91,15 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { | |||||
| suffix={null} | suffix={null} | ||||
| value={inputText} | value={inputText} | ||||
| prefix={ | prefix={ | ||||
| <Icon icon="local:magnifying-glass" style={{ marginLeft: '10px', marginTop: '2px' }} /> | |||||
| <KFIcon type="icon-sousuo" color="rgba(22,100,255,0.4" style={{ marginLeft: '10px' }} /> | |||||
| } | } | ||||
| // prefix={ | |||||
| // <Icon icon="local:magnifying-glass" style={{ marginLeft: '10px', marginTop: '2px' }} /> | |||||
| // } | |||||
| /> | /> | ||||
| {dataList?.length !== 0 ? ( | {dataList?.length !== 0 ? ( | ||||
| <> | <> | ||||
| <div className={styles['code-selector__content']}> | |||||
| <div className="kf-code-selector-modal__content"> | |||||
| {dataList?.map((item) => ( | {dataList?.map((item) => ( | ||||
| <CodeConfigItem item={item} key={item.id} onClick={handleClick} /> | <CodeConfigItem item={item} key={item.id} onClick={handleClick} /> | ||||
| ))} | ))} | ||||
| @@ -112,7 +116,7 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { | |||||
| /> | /> | ||||
| </> | </> | ||||
| ) : ( | ) : ( | ||||
| <div className={styles['code-selector__empty']}> | |||||
| <div className="kf-code-selector-modal__empty"> | |||||
| <Empty image={Empty.PRESENTED_IMAGE_SIMPLE}></Empty> | <Empty image={Empty.PRESENTED_IMAGE_SIMPLE}></Empty> | ||||
| </div> | </div> | ||||
| )} | )} | ||||
| @@ -0,0 +1,41 @@ | |||||
| import BasicInfo, { type BasicInfoData, type BasicInfoProps } from '@/components/BasicInfo'; | |||||
| import InfoGroup from '@/components/InfoGroup'; | |||||
| import classNames from 'classnames'; | |||||
| export type { BasicInfoData }; | |||||
| interface ConfigInfoProps extends BasicInfoProps { | |||||
| /** 标题 */ | |||||
| title: string; | |||||
| /** 子元素 */ | |||||
| children?: React.ReactNode; | |||||
| } | |||||
| /** 详情基本信息块,目前主要用于主动机器学习、超参数寻优、自主学习详情中 */ | |||||
| function ConfigInfo({ | |||||
| title, | |||||
| datas, | |||||
| labelWidth, | |||||
| labelAlign = 'start', | |||||
| labelEllipsis = true, | |||||
| threeColumns = true, | |||||
| className, | |||||
| style, | |||||
| children, | |||||
| }: ConfigInfoProps) { | |||||
| return ( | |||||
| <InfoGroup title={title} className={classNames('kf-config-info', className)} style={style}> | |||||
| <div className={'kf-config-info__content'}> | |||||
| <BasicInfo | |||||
| datas={datas} | |||||
| labelWidth={labelWidth} | |||||
| labelAlign={labelAlign} | |||||
| labelEllipsis={labelEllipsis} | |||||
| threeColumns={threeColumns} | |||||
| /> | |||||
| {children} | |||||
| </div> | |||||
| </InfoGroup> | |||||
| ); | |||||
| } | |||||
| export default ConfigInfo; | |||||
| @@ -1,6 +1,6 @@ | |||||
| .disabled-input { | .disabled-input { | ||||
| padding: 4px 11px; | padding: 4px 11px; | ||||
| color: rgba(0, 0, 0, 0.25); | |||||
| color: @text-disabled-color; | |||||
| font-size: @font-size-input; | font-size: @font-size-input; | ||||
| background-color: rgba(0, 0, 0, 0.04); | background-color: rgba(0, 0, 0, 0.04); | ||||
| border: 1px solid #d9d9d9; | border: 1px solid #d9d9d9; | ||||
| @@ -6,13 +6,14 @@ type DisabledInputProps = { | |||||
| valuePropName?: string; | valuePropName?: string; | ||||
| }; | }; | ||||
| /** | |||||
| * 模拟禁用的输入框,但是完全显示内容 | |||||
| */ | |||||
| function DisabledInput({ value, valuePropName }: DisabledInputProps) { | function DisabledInput({ value, valuePropName }: DisabledInputProps) { | ||||
| const data = valuePropName ? value[valuePropName] : value; | const data = valuePropName ? value[valuePropName] : value; | ||||
| return ( | return ( | ||||
| <div className={styles['disabled-input']}> | <div className={styles['disabled-input']}> | ||||
| <Typography.Text ellipsis={{ tooltip: data }} style={{ color: 'inherit' }}> | |||||
| {data} | |||||
| </Typography.Text> | |||||
| <Typography.Text ellipsis={{ tooltip: data }}>{data}</Typography.Text> | |||||
| </div> | </div> | ||||
| ); | ); | ||||
| } | } | ||||
| @@ -2,22 +2,30 @@ import classNames from 'classnames'; | |||||
| import './index.less'; | import './index.less'; | ||||
| type FullScreenFrameProps = { | type FullScreenFrameProps = { | ||||
| /** URL */ | |||||
| url: string; | url: string; | ||||
| /** 自定义类名 */ | |||||
| className?: string; | className?: string; | ||||
| /** 自定义样式 */ | |||||
| style?: React.CSSProperties; | style?: React.CSSProperties; | ||||
| onload?: (e?: React.SyntheticEvent<HTMLIFrameElement, Event>) => void; | |||||
| onerror?: (e?: React.SyntheticEvent<HTMLIFrameElement, Event>) => void; | |||||
| /** 加载完成回调 */ | |||||
| onLoad?: (e?: React.SyntheticEvent<HTMLIFrameElement, Event>) => void; | |||||
| /** 加载失败回调 */ | |||||
| onError?: (e?: React.SyntheticEvent<HTMLIFrameElement, Event>) => void; | |||||
| }; | }; | ||||
| function FullScreenFrame({ url, className, style, onload, onerror }: FullScreenFrameProps) { | |||||
| /** | |||||
| * 全屏 iframe,IFramePage 组件的子组件,开发中应该使用 IFramePage | |||||
| */ | |||||
| function FullScreenFrame({ url, className, style, onLoad, onError }: FullScreenFrameProps) { | |||||
| return ( | return ( | ||||
| <div className={classNames('kf-full-screen-frame', className ?? '')} style={style}> | <div className={classNames('kf-full-screen-frame', className ?? '')} style={style}> | ||||
| {url && ( | {url && ( | ||||
| <iframe | <iframe | ||||
| src={url} | src={url} | ||||
| className="kf-full-screen-frame__iframe" | className="kf-full-screen-frame__iframe" | ||||
| onLoad={onload} | |||||
| onError={onerror} | |||||
| onLoad={onLoad} | |||||
| onError={onError} | |||||
| ></iframe> | ></iframe> | ||||
| )} | )} | ||||
| </div> | </div> | ||||
| @@ -1,6 +1,6 @@ | |||||
| import FullScreenFrame from '@/components/FullScreenFrame'; | import FullScreenFrame from '@/components/FullScreenFrame'; | ||||
| import KFSpin from '@/components/KFSpin'; | import KFSpin from '@/components/KFSpin'; | ||||
| // import { getLabelStudioUrl } from '@/services/developmentEnvironment'; | |||||
| import { getLabelStudioUrl } from '@/services/developmentEnvironment'; | |||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import SessionStorage from '@/utils/sessionStorage'; | import SessionStorage from '@/utils/sessionStorage'; | ||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| @@ -12,32 +12,36 @@ export enum IframePageType { | |||||
| DatasetAnnotation = 'DatasetAnnotation', // 数据标注 | DatasetAnnotation = 'DatasetAnnotation', // 数据标注 | ||||
| AppDevelopment = 'AppDevelopment', // 应用开发 | AppDevelopment = 'AppDevelopment', // 应用开发 | ||||
| DevEnv = 'DevEnv', // 开发环境 | DevEnv = 'DevEnv', // 开发环境 | ||||
| GitLink = 'GitLink', | |||||
| GitLink = 'GitLink', // git link | |||||
| } | } | ||||
| const getRequestAPI = (type: IframePageType): (() => Promise<any>) => { | const getRequestAPI = (type: IframePageType): (() => Promise<any>) => { | ||||
| switch (type) { | switch (type) { | ||||
| case IframePageType.DatasetAnnotation: | |||||
| return () => Promise.resolve({ code: 200, data: 'http://172.20.32.181:18888/oauth/login' }); //getLabelStudioUrl; | |||||
| case IframePageType.AppDevelopment: | |||||
| case IframePageType.DatasetAnnotation: // 数据标注 | |||||
| return getLabelStudioUrl; | |||||
| case IframePageType.AppDevelopment: // 应用开发 | |||||
| return () => Promise.resolve({ code: 200, data: 'http://172.20.32.185:30080/' }); | return () => Promise.resolve({ code: 200, data: 'http://172.20.32.185:30080/' }); | ||||
| case IframePageType.DevEnv: | |||||
| case IframePageType.DevEnv: // 开发环境 | |||||
| return () => | return () => | ||||
| Promise.resolve({ | Promise.resolve({ | ||||
| code: 200, | code: 200, | ||||
| data: SessionStorage.getItem(SessionStorage.editorUrlKey) || '', | data: SessionStorage.getItem(SessionStorage.editorUrlKey) || '', | ||||
| }); | }); | ||||
| case IframePageType.GitLink: | |||||
| case IframePageType.GitLink: // git link | |||||
| return () => Promise.resolve({ code: 200, data: 'http://172.20.32.201:4000' }); | return () => Promise.resolve({ code: 200, data: 'http://172.20.32.201:4000' }); | ||||
| } | } | ||||
| }; | }; | ||||
| type IframePageProps = { | type IframePageProps = { | ||||
| /** 子系统 */ | |||||
| type: IframePageType; | type: IframePageType; | ||||
| /** 自定义样式类名 */ | |||||
| className?: string; | className?: string; | ||||
| /** 自定义样式 */ | |||||
| style?: React.CSSProperties; | style?: React.CSSProperties; | ||||
| }; | }; | ||||
| /** 系统内嵌 iframe,目前系统有数据标注、应用开发、开发环境、GitLink 四个子系统,使用时可以添加其他子系统 */ | |||||
| function IframePage({ type, className, style }: IframePageProps) { | function IframePage({ type, className, style }: IframePageProps) { | ||||
| const [iframeUrl, setIframeUrl] = useState(''); | const [iframeUrl, setIframeUrl] = useState(''); | ||||
| const [loading, setLoading] = useState(false); | const [loading, setLoading] = useState(false); | ||||
| @@ -66,7 +70,7 @@ function IframePage({ type, className, style }: IframePageProps) { | |||||
| return ( | return ( | ||||
| <div className={classNames('kf-iframe-page', className)} style={style}> | <div className={classNames('kf-iframe-page', className)} style={style}> | ||||
| {loading && createPortal(<KFSpin size="large" />, document.body)} | {loading && createPortal(<KFSpin size="large" />, document.body)} | ||||
| <FullScreenFrame url={iframeUrl} onload={hideLoading} onerror={hideLoading} /> | |||||
| <FullScreenFrame url={iframeUrl} onLoad={hideLoading} onError={hideLoading} /> | |||||
| </div> | </div> | ||||
| ); | ); | ||||
| } | } | ||||
| @@ -1,7 +1,7 @@ | |||||
| .kf-info-group-title { | .kf-info-group-title { | ||||
| width: 100%; | width: 100%; | ||||
| height: 56px; | height: 56px; | ||||
| padding-left: @content-padding; | |||||
| padding: 0 @content-padding; | |||||
| background: linear-gradient( | background: linear-gradient( | ||||
| 179.03deg, | 179.03deg, | ||||
| rgba(199, 223, 255, 0.12) 0%, | rgba(199, 223, 255, 0.12) 0%, | ||||
| @@ -21,6 +21,7 @@ | |||||
| color: @text-color; | color: @text-color; | ||||
| font-weight: 500; | font-weight: 500; | ||||
| font-size: @font-size-title; | font-size: @font-size-title; | ||||
| .singleLine(); | |||||
| &::after { | &::after { | ||||
| position: absolute; | position: absolute; | ||||
| @@ -1,13 +1,19 @@ | |||||
| import { Flex } from 'antd'; | import { Flex } from 'antd'; | ||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import './index.less'; | |||||
| import './InfoGroupTitle.less'; | |||||
| type InfoGroupTitleProps = { | type InfoGroupTitleProps = { | ||||
| /** 标题 */ | |||||
| title: string; | title: string; | ||||
| /** 自定义类名 */ | |||||
| className?: string; | className?: string; | ||||
| /** 自定义样式 */ | |||||
| style?: React.CSSProperties; | style?: React.CSSProperties; | ||||
| }; | }; | ||||
| /** | |||||
| * 信息组标题 | |||||
| */ | |||||
| function InfoGroupTitle({ title, style, className }: InfoGroupTitleProps) { | function InfoGroupTitle({ title, style, className }: InfoGroupTitleProps) { | ||||
| return ( | return ( | ||||
| <Flex align="center" className={classNames('kf-info-group-title', className)} style={style}> | <Flex align="center" className={classNames('kf-info-group-title', className)} style={style}> | ||||
| @@ -1,16 +1,25 @@ | |||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import InfoGroupTitle from '../InfoGroupTitle'; | |||||
| import InfoGroupTitle from './InfoGroupTitle'; | |||||
| import './index.less'; | import './index.less'; | ||||
| type InfoGroupProps = { | type InfoGroupProps = { | ||||
| /** 标题 */ | |||||
| title: string; | title: string; | ||||
| height?: string | number; // 如果要纵向滚动,需要设置高度 | |||||
| width?: string | number; // 如果要横向滚动,需要设置宽度 | |||||
| /** 高度, 如果要纵向滚动,需要设置高度 */ | |||||
| height?: string | number; | |||||
| /** 宽度, 如果要横向滚动,需要设置宽度 */ | |||||
| width?: string | number; | |||||
| /** 自定义类名 */ | |||||
| className?: string; | className?: string; | ||||
| /** 自定义样式 */ | |||||
| style?: React.CSSProperties; | style?: React.CSSProperties; | ||||
| /** 子元素 */ | |||||
| children?: React.ReactNode; | children?: React.ReactNode; | ||||
| }; | }; | ||||
| /** | |||||
| * 信息组,用于展示基本信息,支持横向、纵向滚动。自动机器学习、超参数寻优都是使用这个组件 | |||||
| */ | |||||
| function InfoGroup({ title, height, width, className, style, children }: InfoGroupProps) { | function InfoGroup({ title, height, width, className, style, children }: InfoGroupProps) { | ||||
| const contentStyle: React.CSSProperties = {}; | const contentStyle: React.CSSProperties = {}; | ||||
| if (height) { | if (height) { | ||||
| @@ -33,7 +33,7 @@ | |||||
| margin-top: 20px; | margin-top: 20px; | ||||
| margin-bottom: 30px; | margin-bottom: 30px; | ||||
| &__back-btn { | |||||
| &__button { | |||||
| height: 32px; | height: 32px; | ||||
| } | } | ||||
| } | } | ||||
| @@ -9,15 +9,24 @@ export enum EmptyType { | |||||
| } | } | ||||
| type EmptyProps = { | type EmptyProps = { | ||||
| className?: string; | |||||
| style?: React.CSSProperties; | |||||
| /** 类型 */ | |||||
| type: EmptyType; | type: EmptyType; | ||||
| /** 标题 */ | |||||
| title?: string; | title?: string; | ||||
| /** 内容 */ | |||||
| content?: string; | content?: string; | ||||
| /** 是否有页脚,如果有默认是一个按钮 */ | |||||
| hasFooter?: boolean; | hasFooter?: boolean; | ||||
| footer?: () => React.ReactNode; | |||||
| /** 按钮标题,默认是"刷新" */ | |||||
| buttonTitle?: string; | buttonTitle?: string; | ||||
| onRefresh?: () => void; | |||||
| /** 按钮点击回调 */ | |||||
| onButtonClick?: () => void; | |||||
| /** 自定义页脚内容 */ | |||||
| footer?: () => React.ReactNode; | |||||
| /** 自定义类名 */ | |||||
| className?: string; | |||||
| /** 自定义样式 */ | |||||
| style?: React.CSSProperties; | |||||
| }; | }; | ||||
| function getEmptyImage(type: EmptyType) { | function getEmptyImage(type: EmptyType) { | ||||
| @@ -31,6 +40,7 @@ function getEmptyImage(type: EmptyType) { | |||||
| } | } | ||||
| } | } | ||||
| /** 空状态 */ | |||||
| function KFEmpty({ | function KFEmpty({ | ||||
| className, | className, | ||||
| style, | style, | ||||
| @@ -40,7 +50,7 @@ function KFEmpty({ | |||||
| hasFooter = true, | hasFooter = true, | ||||
| footer, | footer, | ||||
| buttonTitle = '刷新', | buttonTitle = '刷新', | ||||
| onRefresh, | |||||
| onButtonClick, | |||||
| }: EmptyProps) { | }: EmptyProps) { | ||||
| const image = getEmptyImage(type); | const image = getEmptyImage(type); | ||||
| @@ -54,7 +64,7 @@ function KFEmpty({ | |||||
| {footer ? ( | {footer ? ( | ||||
| footer() | footer() | ||||
| ) : ( | ) : ( | ||||
| <Button className="kf-empty__footer__back-btn" type="primary" onClick={onRefresh}> | |||||
| <Button className="kf-empty__footer__button" type="primary" onClick={onButtonClick}> | |||||
| {buttonTitle} | {buttonTitle} | ||||
| </Button> | </Button> | ||||
| )} | )} | ||||
| @@ -14,14 +14,20 @@ const Icon = createFromIconfontCN({ | |||||
| type IconFontProps = Parameters<typeof Icon>[0]; | type IconFontProps = Parameters<typeof Icon>[0]; | ||||
| interface KFIconProps extends IconFontProps { | interface KFIconProps extends IconFontProps { | ||||
| /** 图标 */ | |||||
| type: string; | type: string; | ||||
| /** 字体大小 */ | |||||
| font?: number; | font?: number; | ||||
| /** 字体颜色 */ | |||||
| color?: string; | color?: string; | ||||
| style?: React.CSSProperties; | |||||
| /** 自定义类名 */ | |||||
| className?: string; | className?: string; | ||||
| /** 自定义样式 */ | |||||
| style?: React.CSSProperties; | |||||
| } | } | ||||
| function KFIcon({ type, font = 15, color = '', style = {}, className, ...rest }: KFIconProps) { | |||||
| /** 封装 iconfont 图标 */ | |||||
| function KFIcon({ type, font = 15, color, className, style, ...rest }: KFIconProps) { | |||||
| const iconStyle = { | const iconStyle = { | ||||
| ...style, | ...style, | ||||
| fontSize: font, | fontSize: font, | ||||
| @@ -6,12 +6,16 @@ | |||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import React from 'react'; | import React from 'react'; | ||||
| import './index.less'; | |||||
| import './KFModalTitle.less'; | |||||
| type ModalTitleProps = { | type ModalTitleProps = { | ||||
| /** 标题 */ | |||||
| title: React.ReactNode; | title: React.ReactNode; | ||||
| /** 图片 */ | |||||
| image?: string; | image?: string; | ||||
| /** 自定义样式 */ | |||||
| style?: React.CSSProperties; | style?: React.CSSProperties; | ||||
| /** 自定义类名 */ | |||||
| className?: string; | className?: string; | ||||
| }; | }; | ||||
| @@ -4,19 +4,22 @@ | |||||
| * @Description: 自定义 Modal | * @Description: 自定义 Modal | ||||
| */ | */ | ||||
| import ModalTitle from '@/components/ModalTitle'; | |||||
| import { Modal, type ModalProps } from 'antd'; | import { Modal, type ModalProps } from 'antd'; | ||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import KFModalTitle from './KFModalTitle'; | |||||
| import './index.less'; | import './index.less'; | ||||
| export interface KFModalProps extends ModalProps { | export interface KFModalProps extends ModalProps { | ||||
| /** 标题图片 */ | |||||
| image?: string; | image?: string; | ||||
| } | } | ||||
| /** 自定义 Modal,应用中的业务 Modal 应该使用它进行封装,推荐使用函数的方式打开 */ | |||||
| function KFModal({ | function KFModal({ | ||||
| title, | title, | ||||
| image, | image, | ||||
| children, | children, | ||||
| className = '', | |||||
| className, | |||||
| centered, | centered, | ||||
| maskClosable, | maskClosable, | ||||
| ...rest | ...rest | ||||
| @@ -27,7 +30,7 @@ function KFModal({ | |||||
| {...rest} | {...rest} | ||||
| centered={centered ?? true} | centered={centered ?? true} | ||||
| maskClosable={maskClosable ?? false} | maskClosable={maskClosable ?? false} | ||||
| title={<ModalTitle title={title} image={image}></ModalTitle>} | |||||
| title={<KFModalTitle title={title} image={image} />} | |||||
| > | > | ||||
| {children} | {children} | ||||
| </Modal> | </Modal> | ||||
| @@ -8,32 +8,40 @@ import classNames from 'classnames'; | |||||
| import './index.less'; | import './index.less'; | ||||
| export type KFRadioItem = { | export type KFRadioItem = { | ||||
| key: string; | |||||
| title: string; | title: string; | ||||
| value: string; | |||||
| icon?: React.ReactNode; | icon?: React.ReactNode; | ||||
| }; | }; | ||||
| type KFRadioProps = { | type KFRadioProps = { | ||||
| /** 选项 */ | |||||
| items: KFRadioItem[]; | items: KFRadioItem[]; | ||||
| /** 当前选中项 */ | |||||
| value?: string; | value?: string; | ||||
| /** 自定义样式 */ | |||||
| style?: React.CSSProperties; | style?: React.CSSProperties; | ||||
| /** 自定义类名 */ | |||||
| className?: string; | className?: string; | ||||
| /** 选中回调 */ | |||||
| onChange?: (value: string) => void; | onChange?: (value: string) => void; | ||||
| }; | }; | ||||
| /** | |||||
| * 自定义 Radio | |||||
| */ | |||||
| function KFRadio({ items, value, style, className, onChange }: KFRadioProps) { | function KFRadio({ items, value, style, className, onChange }: KFRadioProps) { | ||||
| return ( | return ( | ||||
| <span className={classNames('kf-radio', className)} style={style}> | <span className={classNames('kf-radio', className)} style={style}> | ||||
| {items.map((item) => { | {items.map((item) => { | ||||
| return ( | return ( | ||||
| <span | <span | ||||
| key={item.key} | |||||
| key={item.value} | |||||
| className={ | className={ | ||||
| value === item.key | |||||
| value === item.value | |||||
| ? classNames('kf-radio__item', 'kf-radio__item--active') | ? classNames('kf-radio__item', 'kf-radio__item--active') | ||||
| : 'kf-radio__item' | : 'kf-radio__item' | ||||
| } | } | ||||
| onClick={() => onChange?.(item.key)} | |||||
| onClick={() => onChange?.(item.value)} | |||||
| > | > | ||||
| {item.icon} | {item.icon} | ||||
| <span style={{ marginLeft: '5px' }}>{item.title}</span> | <span style={{ marginLeft: '5px' }}>{item.title}</span> | ||||
| @@ -5,13 +5,19 @@ | |||||
| */ | */ | ||||
| import { Spin, SpinProps } from 'antd'; | import { Spin, SpinProps } from 'antd'; | ||||
| import styles from './index.less'; | |||||
| import './index.less'; | |||||
| function KFSpin(props: SpinProps) { | |||||
| interface KFSpinProps extends SpinProps { | |||||
| /** 加载文本 */ | |||||
| label: string; | |||||
| } | |||||
| /** 自定义 Spin */ | |||||
| function KFSpin({ label = '加载中', ...rest }: KFSpinProps) { | |||||
| return ( | return ( | ||||
| <div className={styles['kf-spin']}> | |||||
| <Spin {...props} /> | |||||
| <div className={styles['kf-spin__label']}>加载中</div> | |||||
| <div className={'kf-spin'}> | |||||
| <Spin {...rest} /> | |||||
| <div className={'kf-spin__label'}>{label}</div> | |||||
| </div> | </div> | ||||
| ); | ); | ||||
| } | } | ||||
| @@ -1,19 +0,0 @@ | |||||
| .kf-label-value { | |||||
| display: flex; | |||||
| align-items: flex-start; | |||||
| font-size: 16px; | |||||
| line-height: 1.6; | |||||
| &__label { | |||||
| flex: none; | |||||
| width: 80px; | |||||
| color: @text-color-secondary; | |||||
| } | |||||
| &__value { | |||||
| flex: 1; | |||||
| color: @text-color; | |||||
| white-space: pre-line; | |||||
| word-break: break-all; | |||||
| } | |||||
| } | |||||
| @@ -1,20 +0,0 @@ | |||||
| import classNames from 'classnames'; | |||||
| import './index.less'; | |||||
| type labelValueProps = { | |||||
| label: string; | |||||
| value?: any; | |||||
| className?: string; | |||||
| style?: React.CSSProperties; | |||||
| }; | |||||
| function LabelValue({ label, value, className, style }: labelValueProps) { | |||||
| return ( | |||||
| <div className={classNames('kf-label-value', className)} style={style}> | |||||
| <div className="kf-label-value__label">{label}</div> | |||||
| <div className="kf-label-value__value">{value ?? '--'}</div> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default LabelValue; | |||||
| @@ -1,5 +1,4 @@ | |||||
| .menu-icon-selector { | .menu-icon-selector { | ||||
| // grid 布局,每行显示 8 个图标 | |||||
| display: grid; | display: grid; | ||||
| grid-template-columns: repeat(4, 80px); | grid-template-columns: repeat(4, 80px); | ||||
| gap: 20px; | gap: 20px; | ||||
| @@ -10,7 +9,7 @@ | |||||
| display: flex; | display: flex; | ||||
| align-items: center; | align-items: center; | ||||
| justify-content: center; | justify-content: center; | ||||
| width: 80x; | |||||
| width: 80px; | |||||
| height: 80px; | height: 80px; | ||||
| border: 1px solid transparent; | border: 1px solid transparent; | ||||
| border-radius: 4px; | border-radius: 4px; | ||||
| @@ -12,7 +12,9 @@ import { useEffect, useState } from 'react'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| interface MenuIconSelectorProps extends Omit<ModalProps, 'onOk'> { | interface MenuIconSelectorProps extends Omit<ModalProps, 'onOk'> { | ||||
| /** 选中的图标 */ | |||||
| selectedIcon?: string; | selectedIcon?: string; | ||||
| /** 选择回调 */ | |||||
| onOk: (param: string) => void; | onOk: (param: string) => void; | ||||
| } | } | ||||
| @@ -21,6 +23,7 @@ type IconObject = { | |||||
| font_class: string; | font_class: string; | ||||
| }; | }; | ||||
| /** 菜单图标选择器 */ | |||||
| function MenuIconSelector({ open, selectedIcon, onOk, ...rest }: MenuIconSelectorProps) { | function MenuIconSelector({ open, selectedIcon, onOk, ...rest }: MenuIconSelectorProps) { | ||||
| const [icons, setIcons] = useState<IconObject[]>([]); | const [icons, setIcons] = useState<IconObject[]>([]); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| @@ -8,10 +8,17 @@ import React from 'react'; | |||||
| import './index.less'; | import './index.less'; | ||||
| type PageTitleProps = { | type PageTitleProps = { | ||||
| title: string; | |||||
| /** 标题 */ | |||||
| title: React.ReactNode; | |||||
| /** 自定义类名 */ | |||||
| className?: string; | className?: string; | ||||
| /** 自定义样式 */ | |||||
| style?: React.CSSProperties; | style?: React.CSSProperties; | ||||
| }; | }; | ||||
| /** | |||||
| * 页面标题 | |||||
| */ | |||||
| function PageTitle({ title, style, className = '' }: PageTitleProps) { | function PageTitle({ title, style, className = '' }: PageTitleProps) { | ||||
| return ( | return ( | ||||
| <div className={classNames('kf-page-title', className)} style={style}> | <div className={classNames('kf-page-title', className)} style={style}> | ||||
| @@ -39,7 +39,7 @@ | |||||
| &__placeholder { | &__placeholder { | ||||
| min-height: 22px; | min-height: 22px; | ||||
| color: rgba(0, 0, 0, 0.25); | |||||
| color: @text-placeholder-color; | |||||
| font-size: @font-size-input; | font-size: @font-size-input; | ||||
| line-height: 1.5714285714285714; | line-height: 1.5714285714285714; | ||||
| } | } | ||||
| @@ -49,18 +49,31 @@ | |||||
| padding: 10px 11px; | padding: 10px 11px; | ||||
| font-size: @font-size-input-lg; | font-size: @font-size-input-lg; | ||||
| .parameter-input__placeholder { | |||||
| .parameter-input__placeholder, | |||||
| .parameter-input__content__value { | |||||
| min-height: 24px; | |||||
| font-size: @font-size-input-lg; | font-size: @font-size-input-lg; | ||||
| line-height: 1.5; | line-height: 1.5; | ||||
| } | } | ||||
| .parameter-input__content__close-icon { | |||||
| font-size: 12px; | |||||
| } | |||||
| } | |||||
| .parameter-input.parameter-input--small { | |||||
| padding: 0 7px; | |||||
| font-size: @font-size-input; | |||||
| .parameter-input__placeholder, | |||||
| .parameter-input__content__value { | .parameter-input__content__value { | ||||
| font-size: @font-size-input-lg; | |||||
| line-height: 1.5; | |||||
| min-height: 22px; | |||||
| font-size: @font-size-input; | |||||
| line-height: 1.5714285714285714; | |||||
| } | } | ||||
| .parameter-input__content__close-icon { | .parameter-input__content__close-icon { | ||||
| font-size: 12px; | |||||
| font-size: 10px; | |||||
| } | } | ||||
| } | } | ||||
| @@ -1,21 +1,22 @@ | |||||
| /* | /* | ||||
| * @Author: 赵伟 | * @Author: 赵伟 | ||||
| * @Date: 2024-04-16 08:42:57 | * @Date: 2024-04-16 08:42:57 | ||||
| * @Description: 参数输入组件 | |||||
| * @Description: 参数输入表单组件,支持手动输入,也支持选择全局参数 | |||||
| */ | */ | ||||
| import { CommonTabKeys } from '@/enums'; | |||||
| import { CloseOutlined } from '@ant-design/icons'; | import { CloseOutlined } from '@ant-design/icons'; | ||||
| import { Form, Input } from 'antd'; | import { Form, Input } from 'antd'; | ||||
| import { RuleObject } from 'antd/es/form'; | import { RuleObject } from 'antd/es/form'; | ||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import './index.less'; | import './index.less'; | ||||
| // 对象 | |||||
| // 如果值是对象时的类型 | |||||
| export type ParameterInputObject = { | export type ParameterInputObject = { | ||||
| value?: any; // 值 | value?: any; // 值 | ||||
| showValue?: any; // 显示值 | showValue?: any; // 显示值 | ||||
| fromSelect?: boolean; // 是否来自选择 | fromSelect?: boolean; // 是否来自选择 | ||||
| activeTab?: string; // 选择镜像、数据集、模型时,保存当前激活的tab | |||||
| activeTab?: CommonTabKeys; // 选择镜像、数据集、模型时,保存当前激活的tab | |||||
| expandedKeys?: string[]; // 选择镜像、数据集、模型时,保存展开的keys | expandedKeys?: string[]; // 选择镜像、数据集、模型时,保存展开的keys | ||||
| checkedKeys?: string[]; // 选择镜像、数据集、模型时,保存选中的keys | checkedKeys?: string[]; // 选择镜像、数据集、模型时,保存选中的keys | ||||
| [key: string]: any; | [key: string]: any; | ||||
| @@ -25,18 +26,34 @@ export type ParameterInputObject = { | |||||
| export type ParameterInputValue = ParameterInputObject | string; | export type ParameterInputValue = ParameterInputObject | string; | ||||
| export interface ParameterInputProps { | export interface ParameterInputProps { | ||||
| /** 值,可以是字符串,也可以是 ParameterInputObject 对象 */ | |||||
| value?: ParameterInputValue; | value?: ParameterInputValue; | ||||
| /** | |||||
| * 值变化时的回调 | |||||
| * @param value 值,可以是字符串,也可以是 ParameterInputObject 对象 | |||||
| */ | |||||
| onChange?: (value?: ParameterInputValue) => void; | onChange?: (value?: ParameterInputValue) => void; | ||||
| /** 点击时的回调 */ | |||||
| onClick?: () => void; | onClick?: () => void; | ||||
| /** 删除时的回调 */ | |||||
| onRemove?: () => void; | onRemove?: () => void; | ||||
| /** 是否可以手动输入 */ | |||||
| canInput?: boolean; | canInput?: boolean; | ||||
| /** 是否是文本框 */ | |||||
| textArea?: boolean; | textArea?: boolean; | ||||
| /** 占位符 */ | |||||
| placeholder?: string; | placeholder?: string; | ||||
| /** 是否允许清除 */ | |||||
| allowClear?: boolean; | allowClear?: boolean; | ||||
| /** 自定义类名 */ | |||||
| className?: string; | className?: string; | ||||
| /** 自定义样式 */ | |||||
| style?: React.CSSProperties; | style?: React.CSSProperties; | ||||
| /** 大小 */ | |||||
| size?: 'middle' | 'small' | 'large'; | size?: 'middle' | 'small' | 'large'; | ||||
| /** 是否禁用 */ | |||||
| disabled?: boolean; | disabled?: boolean; | ||||
| /** 元素 id */ | |||||
| id?: string; | id?: string; | ||||
| } | } | ||||
| @@ -88,6 +105,7 @@ function ParameterInput({ | |||||
| className={classNames( | className={classNames( | ||||
| 'parameter-input', | 'parameter-input', | ||||
| { 'parameter-input--large': size === 'large' }, | { 'parameter-input--large': size === 'large' }, | ||||
| { 'parameter-input--small': size === 'small' }, | |||||
| { [`parameter-input--${status}`]: status }, | { [`parameter-input--${status}`]: status }, | ||||
| className, | className, | ||||
| )} | )} | ||||
| @@ -1,7 +1,7 @@ | |||||
| /* | /* | ||||
| * @Author: 赵伟 | * @Author: 赵伟 | ||||
| * @Date: 2024-04-16 08:42:57 | * @Date: 2024-04-16 08:42:57 | ||||
| * @Description: 参数选择组件 | |||||
| * @Description: 参数下拉选择组件,支持资源规格、数据集、模型、服务 | |||||
| */ | */ | ||||
| import { PipelineNodeModelParameter } from '@/types'; | import { PipelineNodeModelParameter } from '@/types'; | ||||
| @@ -9,30 +9,71 @@ import ResourceSelectorModal, { | |||||
| ResourceSelectorResponse, | ResourceSelectorResponse, | ||||
| ResourceSelectorType, | ResourceSelectorType, | ||||
| selectorTypeConfig, | selectorTypeConfig, | ||||
| } from '@/pages/Pipeline/components/ResourceSelectorModal'; | |||||
| } from '@/components/ResourceSelectorModal'; | |||||
| import { openAntdModal } from '@/utils/modal'; | import { openAntdModal } from '@/utils/modal'; | ||||
| import { Button } from 'antd'; | import { Button } from 'antd'; | ||||
| import { useState } from 'react'; | |||||
| import classNames from 'classnames'; | |||||
| import { pick } from 'lodash'; | |||||
| import { useEffect, useState } from 'react'; | |||||
| import ParameterInput, { type ParameterInputProps } from '../ParameterInput'; | import ParameterInput, { type ParameterInputProps } from '../ParameterInput'; | ||||
| import './index.less'; | import './index.less'; | ||||
| export { requiredValidator, type ParameterInputObject } from '../ParameterInput'; | |||||
| export { | |||||
| requiredValidator, | |||||
| type ParameterInputObject, | |||||
| type ParameterInputValue, | |||||
| } from '../ParameterInput'; | |||||
| export { ResourceSelectorType, selectorTypeConfig, type ResourceSelectorResponse }; | export { ResourceSelectorType, selectorTypeConfig, type ResourceSelectorResponse }; | ||||
| type ResourceSelectProps = { | |||||
| interface ResourceSelectProps extends ParameterInputProps { | |||||
| /** 类型,数据集、模型、镜像 */ | |||||
| type: ResourceSelectorType; | type: ResourceSelectorType; | ||||
| } & ParameterInputProps; | |||||
| } | |||||
| // 获取选择数据集、模型后面按钮 icon | |||||
| // 获取选择数据集、模型、镜像后面按钮 icon | |||||
| const getSelectBtnIcon = (type: ResourceSelectorType) => { | const getSelectBtnIcon = (type: ResourceSelectorType) => { | ||||
| return <KFIcon type={selectorTypeConfig[type].buttonIcon} font={16} />; | return <KFIcon type={selectorTypeConfig[type].buttonIcon} font={16} />; | ||||
| }; | }; | ||||
| function ResourceSelect({ type, value, onChange, disabled, ...rest }: ResourceSelectProps) { | |||||
| /** 数据集、模型、镜像选择表单组件 */ | |||||
| function ResourceSelect({ | |||||
| type, | |||||
| value, | |||||
| size, | |||||
| disabled, | |||||
| className, | |||||
| style, | |||||
| onChange, | |||||
| ...rest | |||||
| }: ResourceSelectProps) { | |||||
| const [selectedResource, setSelectedResource] = useState<ResourceSelectorResponse | undefined>( | const [selectedResource, setSelectedResource] = useState<ResourceSelectorResponse | undefined>( | ||||
| undefined, | undefined, | ||||
| ); | ); | ||||
| useEffect(() => { | |||||
| if ( | |||||
| value && | |||||
| typeof value === 'object' && | |||||
| value.activeTab && | |||||
| value.id && | |||||
| value.name && | |||||
| value.version && | |||||
| value.path && | |||||
| (type === ResourceSelectorType.Mirror || (value.identifier && value.owner)) | |||||
| ) { | |||||
| const originResource = pick(value, [ | |||||
| 'activeTab', | |||||
| 'id', | |||||
| 'identifier', | |||||
| 'name', | |||||
| 'owner', | |||||
| 'version', | |||||
| 'path', | |||||
| ]) as ResourceSelectorResponse; | |||||
| setSelectedResource(originResource); | |||||
| } | |||||
| }, [value]); | |||||
| const selectResource = () => { | const selectResource = () => { | ||||
| const resource = selectedResource; | const resource = selectedResource; | ||||
| const { close } = openAntdModal(ResourceSelectorModal, { | const { close } = openAntdModal(ResourceSelectorModal, { | ||||
| @@ -50,8 +91,10 @@ function ResourceSelect({ type, value, onChange, disabled, ...rest }: ResourceSe | |||||
| showValue: path, | showValue: path, | ||||
| fromSelect: true, | fromSelect: true, | ||||
| activeTab, | activeTab, | ||||
| expandedKeys: [`${id}`], | |||||
| checkedKeys: [`${id}-${version}`], | |||||
| id, | |||||
| name, | |||||
| version, | |||||
| path, | |||||
| }); | }); | ||||
| } else { | } else { | ||||
| const jsonObj = { | const jsonObj = { | ||||
| @@ -69,8 +112,6 @@ function ResourceSelect({ type, value, onChange, disabled, ...rest }: ResourceSe | |||||
| showValue, | showValue, | ||||
| fromSelect: true, | fromSelect: true, | ||||
| activeTab, | activeTab, | ||||
| expandedKeys: [`${id}`], | |||||
| checkedKeys: [`${id}-${version}`], | |||||
| ...jsonObj, | ...jsonObj, | ||||
| }); | }); | ||||
| } | } | ||||
| @@ -80,8 +121,6 @@ function ResourceSelect({ type, value, onChange, disabled, ...rest }: ResourceSe | |||||
| showValue: undefined, | showValue: undefined, | ||||
| fromSelect: false, | fromSelect: false, | ||||
| activeTab: undefined, | activeTab: undefined, | ||||
| expandedKeys: [], | |||||
| checkedKeys: [], | |||||
| }); | }); | ||||
| } | } | ||||
| close(); | close(); | ||||
| @@ -90,18 +129,19 @@ function ResourceSelect({ type, value, onChange, disabled, ...rest }: ResourceSe | |||||
| }; | }; | ||||
| return ( | return ( | ||||
| <div className="kf-resource-select"> | |||||
| <div className={classNames('kf-resource-select', className)} style={style}> | |||||
| <ParameterInput | <ParameterInput | ||||
| {...rest} | {...rest} | ||||
| disabled={disabled} | disabled={disabled} | ||||
| value={value} | value={value} | ||||
| size={size} | |||||
| onChange={onChange} | onChange={onChange} | ||||
| onRemove={() => setSelectedResource(undefined)} | onRemove={() => setSelectedResource(undefined)} | ||||
| onClick={selectResource} | onClick={selectResource} | ||||
| ></ParameterInput> | ></ParameterInput> | ||||
| <Button | <Button | ||||
| className="kf-resource-select__button" | className="kf-resource-select__button" | ||||
| size="large" | |||||
| size={size} | |||||
| type="link" | type="link" | ||||
| icon={getSelectBtnIcon(type)} | icon={getSelectBtnIcon(type)} | ||||
| disabled={disabled} | disabled={disabled} | ||||
| @@ -4,11 +4,11 @@ | |||||
| * @Description: 选择数据集、模型、镜像 | * @Description: 选择数据集、模型、镜像 | ||||
| */ | */ | ||||
| import KFIcon from '@/components/KFIcon'; | |||||
| import KFModal from '@/components/KFModal'; | import KFModal from '@/components/KFModal'; | ||||
| import { CommonTabKeys } from '@/enums'; | import { CommonTabKeys } from '@/enums'; | ||||
| import { ResourceFileData } from '@/pages/Dataset/config'; | import { ResourceFileData } from '@/pages/Dataset/config'; | ||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import { Icon } from '@umijs/max'; | |||||
| import type { GetRef, ModalProps, TreeDataNode, TreeProps } from 'antd'; | import type { GetRef, ModalProps, TreeDataNode, TreeProps } from 'antd'; | ||||
| import { Input, Tabs, Tree } from 'antd'; | import { Input, Tabs, Tree } from 'antd'; | ||||
| import React, { useEffect, useMemo, useRef, useState } from 'react'; | import React, { useEffect, useMemo, useRef, useState } from 'react'; | ||||
| @@ -16,22 +16,30 @@ import { ResourceSelectorType, selectorTypeConfig } from './config'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| export { ResourceSelectorType, selectorTypeConfig }; | export { ResourceSelectorType, selectorTypeConfig }; | ||||
| // 选择数据集\模型\镜像的返回类型 | |||||
| // 选择数据集、模型、镜像的返回类型 | |||||
| export type ResourceSelectorResponse = { | export type ResourceSelectorResponse = { | ||||
| activeTab: CommonTabKeys; // 是我的还是公开的 | |||||
| id: string; // 数据集\模型\镜像 id | id: string; // 数据集\模型\镜像 id | ||||
| name: string; // 数据集\模型\镜像 name | name: string; // 数据集\模型\镜像 name | ||||
| version: string; // 数据集\模型\镜像版本 | version: string; // 数据集\模型\镜像版本 | ||||
| path: string; // 数据集\模型\镜像版本路径 | path: string; // 数据集\模型\镜像版本路径 | ||||
| identifier: string; // 数据集\模型 identifier | |||||
| owner: string; // 数据集\模型 owner | |||||
| activeTab: CommonTabKeys; // 是我的还是公开的 | |||||
| identifier: string; // 数据集\模型 identifier,镜像这个字段为空 | |||||
| owner: string; // 数据集\模型 owner,镜像这个字段为空 | |||||
| }; | }; | ||||
| export interface ResourceSelectorModalProps extends Omit<ModalProps, 'onOk'> { | export interface ResourceSelectorModalProps extends Omit<ModalProps, 'onOk'> { | ||||
| type: ResourceSelectorType; // 数据集\模型\镜像 | |||||
| /** 类型,数据集、模型、镜像 */ | |||||
| type: ResourceSelectorType; | |||||
| /** 默认展开的节点 */ | |||||
| defaultExpandedKeys?: React.Key[]; | defaultExpandedKeys?: React.Key[]; | ||||
| /** 默认展开的节点 */ | |||||
| defaultCheckedKeys?: React.Key[]; | defaultCheckedKeys?: React.Key[]; | ||||
| /** 默认激活的 Tab */ | |||||
| defaultActiveTab?: CommonTabKeys; | defaultActiveTab?: CommonTabKeys; | ||||
| /** | |||||
| * 确认回调 | |||||
| * @param params 选择的数据 | |||||
| */ | |||||
| onOk?: (params: ResourceSelectorResponse | undefined) => void; | onOk?: (params: ResourceSelectorResponse | undefined) => void; | ||||
| } | } | ||||
| @@ -61,6 +69,7 @@ const getIdAndVersion = (versionKey: string) => { | |||||
| }; | }; | ||||
| }; | }; | ||||
| /** 选择数据集、模型、镜像的弹框,推荐使用函数的方式打开 */ | |||||
| function ResourceSelectorModal({ | function ResourceSelectorModal({ | ||||
| type, | type, | ||||
| defaultExpandedKeys = [], | defaultExpandedKeys = [], | ||||
| @@ -69,7 +78,7 @@ function ResourceSelectorModal({ | |||||
| onOk, | onOk, | ||||
| ...rest | ...rest | ||||
| }: ResourceSelectorModalProps) { | }: ResourceSelectorModalProps) { | ||||
| const [activeTab, setActiveTab] = useState<string>(defaultActiveTab); | |||||
| const [activeTab, setActiveTab] = useState<CommonTabKeys>(defaultActiveTab); | |||||
| const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]); | const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]); | ||||
| const [checkedKeys, setCheckedKeys] = useState<React.Key[]>([]); | const [checkedKeys, setCheckedKeys] = useState<React.Key[]>([]); | ||||
| const [loadedKeys, setLoadedKeys] = useState<React.Key[]>([]); | const [loadedKeys, setLoadedKeys] = useState<React.Key[]>([]); | ||||
| @@ -234,7 +243,7 @@ function ResourceSelectorModal({ | |||||
| version, | version, | ||||
| identifier, | identifier, | ||||
| owner, | owner, | ||||
| activeTab: activeTab as CommonTabKeys, | |||||
| activeTab: activeTab, | |||||
| }; | }; | ||||
| onOk?.(res); | onOk?.(res); | ||||
| } else { | } else { | ||||
| @@ -255,7 +264,7 @@ function ResourceSelectorModal({ | |||||
| <Tabs | <Tabs | ||||
| activeKey={activeTab} | activeKey={activeTab} | ||||
| items={tabItems} | items={tabItems} | ||||
| onChange={setActiveTab} | |||||
| onChange={(e) => setActiveTab(e as CommonTabKeys)} | |||||
| className={styles['model-tabs']} | className={styles['model-tabs']} | ||||
| /> | /> | ||||
| <div className={styles['model-selector']}> | <div className={styles['model-selector']}> | ||||
| @@ -267,7 +276,14 @@ function ResourceSelectorModal({ | |||||
| variant="borderless" | variant="borderless" | ||||
| value={searchText} | value={searchText} | ||||
| onChange={(e) => setSearchText(e.target.value)} | onChange={(e) => setSearchText(e.target.value)} | ||||
| prefix={<Icon icon="local:magnifying-glass" style={{ height: '15px' }} />} | |||||
| prefix={ | |||||
| <KFIcon | |||||
| type="icon-sousuo" | |||||
| color="rgba(22,100,255,0.4)" | |||||
| style={{ height: '15px' }} | |||||
| /> | |||||
| } | |||||
| // prefix={<Icon icon="local:magnifying-glass" style={{ height: '15px' }} />} | |||||
| /> | /> | ||||
| <Tree | <Tree | ||||
| ref={treeRef} | ref={treeRef} | ||||
| @@ -1,6 +1,8 @@ | |||||
| import { clearSessionToken } from '@/access'; | import { clearSessionToken } from '@/access'; | ||||
| import { setRemoteMenu } from '@/services/session'; | import { setRemoteMenu } from '@/services/session'; | ||||
| import { logout } from '@/services/system/auth'; | import { logout } from '@/services/system/auth'; | ||||
| import { ClientInfo } from '@/types'; | |||||
| import SessionStorage from '@/utils/sessionStorage'; | |||||
| import { gotoLoginPage } from '@/utils/ui'; | import { gotoLoginPage } from '@/utils/ui'; | ||||
| import { LogoutOutlined, UserOutlined } from '@ant-design/icons'; | import { LogoutOutlined, UserOutlined } from '@ant-design/icons'; | ||||
| import { setAlpha } from '@ant-design/pro-components'; | import { setAlpha } from '@ant-design/pro-components'; | ||||
| @@ -64,11 +66,11 @@ const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => { | |||||
| clearSessionToken(); | clearSessionToken(); | ||||
| setRemoteMenu(null); | setRemoteMenu(null); | ||||
| gotoLoginPage(); | gotoLoginPage(); | ||||
| // const clientInfo: ClientInfo = SessionStorage.getItem(SessionStorage.clientInfoKey, true); | |||||
| // if (clientInfo) { | |||||
| // const { logoutUri } = clientInfo; | |||||
| // location.replace(logoutUri); | |||||
| // } | |||||
| const clientInfo: ClientInfo = SessionStorage.getItem(SessionStorage.clientInfoKey, true); | |||||
| if (clientInfo) { | |||||
| const { logoutUri } = clientInfo; | |||||
| location.replace(logoutUri); | |||||
| } | |||||
| }; | }; | ||||
| const actionClassName = useEmotionCss(({ token }) => { | const actionClassName = useEmotionCss(({ token }) => { | ||||
| return { | return { | ||||
| @@ -1,9 +1,8 @@ | |||||
| import { useModel } from '@umijs/max'; | |||||
| import React from 'react'; | |||||
| // import KFBreadcrumb from '../KFBreadcrumb'; | |||||
| import KFIcon from '@/components/KFIcon'; | import KFIcon from '@/components/KFIcon'; | ||||
| import { ProBreadcrumb } from '@ant-design/pro-components'; | import { ProBreadcrumb } from '@ant-design/pro-components'; | ||||
| import { useModel } from '@umijs/max'; | |||||
| import { Button } from 'antd'; | import { Button } from 'antd'; | ||||
| import React from 'react'; | |||||
| import Avatar from './AvatarDropdown'; | import Avatar from './AvatarDropdown'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| // import { SelectLang } from '@umijs/max'; | // import { SelectLang } from '@umijs/max'; | ||||
| @@ -44,8 +43,6 @@ const GlobalHeaderRight: React.FC = () => { | |||||
| <ProBreadcrumb></ProBreadcrumb> | <ProBreadcrumb></ProBreadcrumb> | ||||
| {/* <KFBreadcrumb /> */} | |||||
| <Avatar menu={true} /> | <Avatar menu={true} /> | ||||
| {/* <SelectLang className={actionClassName} /> */} | {/* <SelectLang className={actionClassName} /> */} | ||||
| </div> | </div> | ||||
| @@ -8,13 +8,20 @@ import classNames from 'classnames'; | |||||
| import './index.less'; | import './index.less'; | ||||
| type SubAreaTitleProps = { | type SubAreaTitleProps = { | ||||
| /** 标题 */ | |||||
| title: string; | title: string; | ||||
| /** 图片 */ | |||||
| image?: string; | image?: string; | ||||
| style?: React.CSSProperties; | |||||
| /** 自定义类名 */ | |||||
| className?: string; | className?: string; | ||||
| /** 自定义样式 */ | |||||
| style?: React.CSSProperties; | |||||
| }; | }; | ||||
| function SubAreaTitle({ title, image, style, className }: SubAreaTitleProps) { | |||||
| /** | |||||
| * 表单或者详情页的分区标题 | |||||
| */ | |||||
| function SubAreaTitle({ title, image, className, style }: SubAreaTitleProps) { | |||||
| return ( | return ( | ||||
| <div className={classNames('kf-subarea-title', className)} style={style}> | <div className={classNames('kf-subarea-title', className)} style={style}> | ||||
| {image && ( | {image && ( | ||||
| @@ -118,3 +118,14 @@ export const autoMLResamplingStrategyOptions = [ | |||||
| { label: 'holdout', value: AutoMLResamplingStrategy.Holdout }, | { label: 'holdout', value: AutoMLResamplingStrategy.Holdout }, | ||||
| { label: 'crossValid', value: AutoMLResamplingStrategy.CrossValid }, | { label: 'crossValid', value: AutoMLResamplingStrategy.CrossValid }, | ||||
| ]; | ]; | ||||
| // 超参数自动寻优优化方向 | |||||
| export enum hyperParameterOptimizedMode { | |||||
| Min = 'min', | |||||
| Max = 'max', | |||||
| } | |||||
| export const hyperParameterOptimizedModeOptions = [ | |||||
| { label: '越大越好', value: hyperParameterOptimizedMode.Max }, | |||||
| { label: '越小越好', value: hyperParameterOptimizedMode.Min }, | |||||
| ]; | |||||
| @@ -5,7 +5,7 @@ body, | |||||
| height: 100%; | height: 100%; | ||||
| margin: 0; | margin: 0; | ||||
| padding: 0; | padding: 0; | ||||
| overflow-y: hidden; | |||||
| overflow-y: visible; | |||||
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, | ||||
| 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', | 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', | ||||
| 'Noto Color Emoji'; | 'Noto Color Emoji'; | ||||
| @@ -1,6 +1,6 @@ | |||||
| { | { | ||||
| "id": "4511326", | "id": "4511326", | ||||
| "name": "复杂智能软件-导航", | |||||
| "name": "智能材料科研平台-导航", | |||||
| "font_family": "iconfont", | "font_family": "iconfont", | ||||
| "css_prefix_text": "icon-", | "css_prefix_text": "icon-", | ||||
| "description": "", | "description": "", | ||||
| @@ -261,3 +261,8 @@ | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| .ant-typography { | |||||
| color: inherit; | |||||
| font-size: inherit; | |||||
| } | |||||
| @@ -12,7 +12,7 @@ const NoFoundPage = () => { | |||||
| content={'很抱歉,您访问的页面地址有误,\n或者该页面不存在。'} | content={'很抱歉,您访问的页面地址有误,\n或者该页面不存在。'} | ||||
| hasFooter={true} | hasFooter={true} | ||||
| buttonTitle="返回首页" | buttonTitle="返回首页" | ||||
| onRefresh={() => navigate('/')} | |||||
| onButtonClick={() => navigate('/')} | |||||
| ></KFEmpty> | ></KFEmpty> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -33,7 +33,7 @@ | |||||
| } | } | ||||
| .ant-btn-variant-text:disabled { | .ant-btn-variant-text:disabled { | ||||
| color: rgba(0, 0, 0, 0.25); | |||||
| color: @text-disabled-color; | |||||
| } | } | ||||
| .ant-btn-variant-text { | .ant-btn-variant-text { | ||||
| @@ -1,7 +1,7 @@ | |||||
| /* | /* | ||||
| * @Author: 赵伟 | * @Author: 赵伟 | ||||
| * @Date: 2024-04-16 13:58:08 | * @Date: 2024-04-16 13:58:08 | ||||
| * @Description: 创建服务版本 | |||||
| * @Description: 创建实验 | |||||
| */ | */ | ||||
| import PageTitle from '@/components/PageTitle'; | import PageTitle from '@/components/PageTitle'; | ||||
| import { AutoMLEnsembleClass, AutoMLTaskType } from '@/enums'; | import { AutoMLEnsembleClass, AutoMLTaskType } from '@/enums'; | ||||
| @@ -11,7 +11,6 @@ import { safeInvoke } from '@/utils/functional'; | |||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import { useLocation, useNavigate, useParams } from '@umijs/max'; | import { useLocation, useNavigate, useParams } from '@umijs/max'; | ||||
| import { App, Button, Form } from 'antd'; | import { App, Button, Form } from 'antd'; | ||||
| import { omit } from 'lodash'; | |||||
| import { useEffect } from 'react'; | import { useEffect } from 'react'; | ||||
| import BasicConfig from '../components/CreateForm/BasicConfig'; | import BasicConfig from '../components/CreateForm/BasicConfig'; | ||||
| import DatasetConfig from '../components/CreateForm/DatasetConfig'; | import DatasetConfig from '../components/CreateForm/DatasetConfig'; | ||||
| @@ -106,7 +105,7 @@ function CreateAutoML() { | |||||
| // 根据后台要求,修改表单数据 | // 根据后台要求,修改表单数据 | ||||
| const object = { | const object = { | ||||
| ...omit(formData), | |||||
| ...formData, | |||||
| include_classifier: convertEmptyStringToUndefined(include_classifier), | include_classifier: convertEmptyStringToUndefined(include_classifier), | ||||
| include_feature_preprocessor: convertEmptyStringToUndefined(include_feature_preprocessor), | include_feature_preprocessor: convertEmptyStringToUndefined(include_feature_preprocessor), | ||||
| include_regressor: convertEmptyStringToUndefined(include_regressor), | include_regressor: convertEmptyStringToUndefined(include_regressor), | ||||
| @@ -191,7 +190,7 @@ function CreateAutoML() { | |||||
| <TrialConfig /> | <TrialConfig /> | ||||
| <DatasetConfig /> | <DatasetConfig /> | ||||
| <Form.Item wrapperCol={{ offset: 0, span: 16 }}> | |||||
| <Form.Item wrapperCol={{ offset: 0, span: 16 }} style={{ marginTop: '40px' }}> | |||||
| <Button type="primary" htmlType="submit"> | <Button type="primary" htmlType="submit"> | ||||
| {buttonText} | {buttonText} | ||||
| </Button> | </Button> | ||||
| @@ -3,9 +3,7 @@ | |||||
| * @Date: 2024-04-16 13:58:08 | * @Date: 2024-04-16 13:58:08 | ||||
| * @Description: 自主机器学习详情 | * @Description: 自主机器学习详情 | ||||
| */ | */ | ||||
| import KFIcon from '@/components/KFIcon'; | |||||
| import PageTitle from '@/components/PageTitle'; | import PageTitle from '@/components/PageTitle'; | ||||
| import { CommonTabKeys } from '@/enums'; | |||||
| import { getAutoMLInfoReq } from '@/services/autoML'; | import { getAutoMLInfoReq } from '@/services/autoML'; | ||||
| import { safeInvoke } from '@/utils/functional'; | import { safeInvoke } from '@/utils/functional'; | ||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| @@ -16,24 +14,10 @@ import { AutoMLData } from '../types'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| function AutoMLInfo() { | function AutoMLInfo() { | ||||
| const [activeTab, setActiveTab] = useState<string>(CommonTabKeys.Public); | |||||
| const params = useParams(); | const params = useParams(); | ||||
| const autoMLId = safeInvoke(Number)(params.id); | const autoMLId = safeInvoke(Number)(params.id); | ||||
| const [autoMLInfo, setAutoMLInfo] = useState<AutoMLData | undefined>(undefined); | const [autoMLInfo, setAutoMLInfo] = useState<AutoMLData | undefined>(undefined); | ||||
| const tabItems = [ | |||||
| { | |||||
| key: CommonTabKeys.Public, | |||||
| label: '基本信息', | |||||
| icon: <KFIcon type="icon-jibenxinxi" />, | |||||
| }, | |||||
| { | |||||
| key: CommonTabKeys.Private, | |||||
| label: 'Trial列表', | |||||
| icon: <KFIcon type="icon-Trialliebiao" />, | |||||
| }, | |||||
| ]; | |||||
| useEffect(() => { | useEffect(() => { | ||||
| if (autoMLId) { | if (autoMLId) { | ||||
| getAutoMLInfo(); | getAutoMLInfo(); | ||||
| @@ -3,419 +3,11 @@ | |||||
| * @Date: 2024-04-16 13:58:08 | * @Date: 2024-04-16 13:58:08 | ||||
| * @Description: 自主机器学习列表 | * @Description: 自主机器学习列表 | ||||
| */ | */ | ||||
| import KFIcon from '@/components/KFIcon'; | |||||
| import PageTitle from '@/components/PageTitle'; | |||||
| import { ExperimentStatus } from '@/enums'; | |||||
| import { useCacheState } from '@/hooks/pageCacheState'; | |||||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | |||||
| import { | |||||
| deleteAutoMLReq, | |||||
| getAutoMLListReq, | |||||
| getExperimentInsListReq, | |||||
| runAutoMLReq, | |||||
| } from '@/services/autoML'; | |||||
| import themes from '@/styles/theme.less'; | |||||
| import { type ExperimentInstance as ExperimentInstanceData } from '@/types'; | |||||
| import { to } from '@/utils/promise'; | |||||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||||
| import { modalConfirm } from '@/utils/ui'; | |||||
| import { useNavigate } from '@umijs/max'; | |||||
| import { | |||||
| App, | |||||
| Button, | |||||
| ConfigProvider, | |||||
| Input, | |||||
| Table, | |||||
| Tooltip, | |||||
| type TablePaginationConfig, | |||||
| type TableProps, | |||||
| } from 'antd'; | |||||
| import { type SearchProps } from 'antd/es/input'; | |||||
| import classNames from 'classnames'; | |||||
| import { useEffect, useState } from 'react'; | |||||
| import ExperimentInstance from '../components/ExperimentInstance'; | |||||
| import { AutoMLData } from '../types'; | |||||
| import styles from './index.less'; | |||||
| function AutoMLList() { | |||||
| const navigate = useNavigate(); | |||||
| const { message } = App.useApp(); | |||||
| const [cacheState, setCacheState] = useCacheState(); | |||||
| const [searchText, setSearchText] = useState(cacheState?.searchText); | |||||
| const [inputText, setInputText] = useState(cacheState?.searchText); | |||||
| const [tableData, setTableData] = useState<AutoMLData[]>([]); | |||||
| const [total, setTotal] = useState(0); | |||||
| const [experimentInsList, setExperimentInsList] = useState<ExperimentInstanceData[]>([]); | |||||
| const [expandedRowKeys, setExpandedRowKeys] = useState<number[]>([]); | |||||
| const [experimentInsTotal, setExperimentInsTotal] = useState(0); | |||||
| const [pagination, setPagination] = useState<TablePaginationConfig>( | |||||
| cacheState?.pagination ?? { | |||||
| current: 1, | |||||
| pageSize: 10, | |||||
| }, | |||||
| ); | |||||
| useEffect(() => { | |||||
| getAutoMLList(); | |||||
| }, [pagination, searchText]); | |||||
| // 获取自主机器学习列表 | |||||
| const getAutoMLList = async () => { | |||||
| const params: Record<string, any> = { | |||||
| page: pagination.current! - 1, | |||||
| size: pagination.pageSize, | |||||
| ml_name: searchText || undefined, | |||||
| }; | |||||
| const [res] = await to(getAutoMLListReq(params)); | |||||
| if (res && res.data) { | |||||
| const { content = [], totalElements = 0 } = res.data; | |||||
| setTableData(content); | |||||
| setTotal(totalElements); | |||||
| } | |||||
| }; | |||||
| // 搜索 | |||||
| const onSearch: SearchProps['onSearch'] = (value) => { | |||||
| setSearchText(value); | |||||
| setPagination((prev) => ({ | |||||
| ...prev, | |||||
| current: 1, | |||||
| })); | |||||
| }; | |||||
| // 删除模型部署 | |||||
| const deleteAutoML = async (record: AutoMLData) => { | |||||
| const [res] = await to(deleteAutoMLReq(record.id)); | |||||
| if (res) { | |||||
| message.success('删除成功'); | |||||
| // 如果是一页的唯一数据,删除时,请求第一页的数据 | |||||
| // 否则直接刷新这一页的数据 | |||||
| // 避免回到第一页 | |||||
| if (tableData.length > 1) { | |||||
| setPagination((prev) => ({ | |||||
| ...prev, | |||||
| current: 1, | |||||
| })); | |||||
| } else { | |||||
| getAutoMLList(); | |||||
| } | |||||
| } | |||||
| }; | |||||
| // 处理删除 | |||||
| const handleAutoMLDelete = (record: AutoMLData) => { | |||||
| modalConfirm({ | |||||
| title: '删除后,该实验将不可恢复', | |||||
| content: '是否确认删除?', | |||||
| onOk: () => { | |||||
| deleteAutoML(record); | |||||
| }, | |||||
| }); | |||||
| }; | |||||
| // 创建、编辑、复制自动机器学习 | |||||
| const createAutoML = (record?: AutoMLData, isCopy: boolean = false) => { | |||||
| setCacheState({ | |||||
| pagination, | |||||
| searchText, | |||||
| }); | |||||
| if (record) { | |||||
| if (isCopy) { | |||||
| navigate(`/pipeline/autoML/copy/${record.id}`); | |||||
| } else { | |||||
| navigate(`/pipeline/autoML/edit/${record.id}`); | |||||
| } | |||||
| } else { | |||||
| navigate(`/pipeline/autoML/create`); | |||||
| } | |||||
| }; | |||||
| // 查看自动机器学习详情 | |||||
| const gotoDetail = (record: AutoMLData) => { | |||||
| setCacheState({ | |||||
| pagination, | |||||
| searchText, | |||||
| }); | |||||
| navigate(`/pipeline/autoML/info/${record.id}`); | |||||
| }; | |||||
| // 启动自动机器学习 | |||||
| const startAutoML = async (record: AutoMLData) => { | |||||
| const [res] = await to(runAutoMLReq(record.id)); | |||||
| if (res) { | |||||
| message.success('运行成功'); | |||||
| setExpandedRowKeys([record.id]); | |||||
| refreshExperimentList(); | |||||
| refreshExperimentIns(record.id); | |||||
| } | |||||
| }; | |||||
| import ExperimentList, { ExperimentListType } from '../components/ExperimentList'; | |||||
| // --------------------------- 实验实例 --------------------------- | |||||
| // 获取实验实例列表 | |||||
| const getExperimentInsList = async (autoMLId: number, page: number) => { | |||||
| const params = { | |||||
| autoMlId: autoMLId, | |||||
| page: page, | |||||
| size: 5, | |||||
| }; | |||||
| const [res] = await to(getExperimentInsListReq(params)); | |||||
| if (res && res.data) { | |||||
| const { content = [], totalElements = 0 } = res.data; | |||||
| try { | |||||
| if (page === 0) { | |||||
| setExperimentInsList(content); | |||||
| } else { | |||||
| setExperimentInsList((prev) => [...prev, ...content]); | |||||
| } | |||||
| setExperimentInsTotal(totalElements); | |||||
| } catch (error) { | |||||
| console.error('JSON parse error: ', error); | |||||
| } | |||||
| } | |||||
| }; | |||||
| // 展开实例 | |||||
| const handleExpandChange = (expanded: boolean, record: AutoMLData) => { | |||||
| setExperimentInsList([]); | |||||
| if (expanded) { | |||||
| setExpandedRowKeys([record.id]); | |||||
| getExperimentInsList(record.id, 0); | |||||
| } else { | |||||
| setExpandedRowKeys([]); | |||||
| } | |||||
| }; | |||||
| // 跳转到实验实例详情 | |||||
| const gotoInstanceInfo = (autoML: AutoMLData, record: ExperimentInstanceData) => { | |||||
| navigate({ pathname: `/pipeline/automl/instance/${autoML.id}/${record.id}` }); | |||||
| }; | |||||
| // 刷新实验实例列表 | |||||
| const refreshExperimentIns = (experimentId: number) => { | |||||
| getExperimentInsList(experimentId, 0); | |||||
| }; | |||||
| // 加载更多实验实例 | |||||
| const loadMoreExperimentIns = () => { | |||||
| const page = Math.round(experimentInsList.length / 5); | |||||
| const autoMLId = expandedRowKeys[0]; | |||||
| getExperimentInsList(autoMLId, page); | |||||
| }; | |||||
| // 实验实例终止 | |||||
| const handleInstanceTerminate = async (experimentIns: ExperimentInstanceData) => { | |||||
| // 刷新实验列表 | |||||
| refreshExperimentList(); | |||||
| setExperimentInsList((prevList) => { | |||||
| return prevList.map((item) => { | |||||
| if (item.id === experimentIns.id) { | |||||
| return { | |||||
| ...item, | |||||
| status: ExperimentStatus.Terminated, | |||||
| }; | |||||
| } | |||||
| return item; | |||||
| }); | |||||
| }); | |||||
| }; | |||||
| // 刷新实验列表状态, | |||||
| // 目前是直接刷新实验列表,后续需要优化,只刷新状态 | |||||
| const refreshExperimentList = () => { | |||||
| getAutoMLList(); | |||||
| }; | |||||
| // --------------------------- Table --------------------------- | |||||
| // 分页切换 | |||||
| const handleTableChange: TableProps<AutoMLData>['onChange'] = ( | |||||
| pagination, | |||||
| _filters, | |||||
| _sorter, | |||||
| { action }, | |||||
| ) => { | |||||
| if (action === 'paginate') { | |||||
| setPagination(pagination); | |||||
| } | |||||
| }; | |||||
| const columns: TableProps<AutoMLData>['columns'] = [ | |||||
| { | |||||
| title: '实验名称', | |||||
| dataIndex: 'ml_name', | |||||
| key: 'ml_name', | |||||
| width: '16%', | |||||
| render: tableCellRender(false, TableCellValueType.Link, { | |||||
| onClick: gotoDetail, | |||||
| }), | |||||
| }, | |||||
| { | |||||
| title: '实验描述', | |||||
| dataIndex: 'ml_description', | |||||
| key: 'ml_description', | |||||
| render: tableCellRender(true), | |||||
| ellipsis: { showTitle: false }, | |||||
| }, | |||||
| { | |||||
| title: '创建时间', | |||||
| dataIndex: 'update_time', | |||||
| key: 'update_time', | |||||
| width: '20%', | |||||
| render: tableCellRender(true, TableCellValueType.Date), | |||||
| ellipsis: { showTitle: false }, | |||||
| }, | |||||
| { | |||||
| title: '最近五次运行状态', | |||||
| dataIndex: 'status_list', | |||||
| key: 'status_list', | |||||
| width: 200, | |||||
| render: (text) => { | |||||
| const newText: string[] = text && text.replace(/\s+/g, '').split(','); | |||||
| return ( | |||||
| <> | |||||
| {newText && newText.length > 0 | |||||
| ? newText.map((item, index) => { | |||||
| return ( | |||||
| <Tooltip | |||||
| key={index} | |||||
| placement="top" | |||||
| title={experimentStatusInfo[item as ExperimentStatus].label} | |||||
| > | |||||
| <img | |||||
| style={{ width: '17px', marginRight: '6px' }} | |||||
| src={experimentStatusInfo[item as ExperimentStatus].icon} | |||||
| draggable={false} | |||||
| alt="" | |||||
| /> | |||||
| </Tooltip> | |||||
| ); | |||||
| }) | |||||
| : null} | |||||
| </> | |||||
| ); | |||||
| }, | |||||
| }, | |||||
| { | |||||
| title: '操作', | |||||
| dataIndex: 'operation', | |||||
| width: 360, | |||||
| key: 'operation', | |||||
| render: (_: any, record: AutoMLData) => ( | |||||
| <div> | |||||
| <Button | |||||
| type="link" | |||||
| size="small" | |||||
| key="start" | |||||
| icon={<KFIcon type="icon-yunhang" />} | |||||
| onClick={() => startAutoML(record)} | |||||
| > | |||||
| 运行 | |||||
| </Button> | |||||
| <Button | |||||
| type="link" | |||||
| size="small" | |||||
| key="edit" | |||||
| icon={<KFIcon type="icon-bianji" />} | |||||
| onClick={() => createAutoML(record, false)} | |||||
| > | |||||
| 编辑 | |||||
| </Button> | |||||
| <Button | |||||
| type="link" | |||||
| size="small" | |||||
| key="copy" | |||||
| icon={<KFIcon type="icon-fuzhi" />} | |||||
| onClick={() => createAutoML(record, true)} | |||||
| > | |||||
| 复制 | |||||
| </Button> | |||||
| <ConfigProvider | |||||
| theme={{ | |||||
| token: { | |||||
| colorLink: themes['warningColor'], | |||||
| }, | |||||
| }} | |||||
| > | |||||
| <Button | |||||
| type="link" | |||||
| size="small" | |||||
| key="remove" | |||||
| icon={<KFIcon type="icon-shanchu" />} | |||||
| onClick={() => handleAutoMLDelete(record)} | |||||
| > | |||||
| 删除 | |||||
| </Button> | |||||
| </ConfigProvider> | |||||
| </div> | |||||
| ), | |||||
| }, | |||||
| ]; | |||||
| return ( | |||||
| <div className={styles['auto-ml-list']}> | |||||
| <PageTitle title="自动机器学习列表"></PageTitle> | |||||
| <div className={styles['auto-ml-list__content']}> | |||||
| <div className={styles['auto-ml-list__content__filter']}> | |||||
| <Input.Search | |||||
| placeholder="按实验名称筛选" | |||||
| onSearch={onSearch} | |||||
| onChange={(e) => setInputText(e.target.value)} | |||||
| style={{ width: 300 }} | |||||
| value={inputText} | |||||
| allowClear | |||||
| /> | |||||
| <Button | |||||
| style={{ marginLeft: '20px' }} | |||||
| type="default" | |||||
| onClick={() => createAutoML()} | |||||
| icon={<KFIcon type="icon-xinjian2" />} | |||||
| > | |||||
| 新建实验 | |||||
| </Button> | |||||
| </div> | |||||
| <div | |||||
| className={classNames('vertical-scroll-table', styles['auto-ml-list__content__table'])} | |||||
| > | |||||
| <Table | |||||
| dataSource={tableData} | |||||
| columns={columns} | |||||
| scroll={{ y: 'calc(100% - 55px)' }} | |||||
| pagination={{ | |||||
| ...pagination, | |||||
| total: total, | |||||
| showSizeChanger: true, | |||||
| showQuickJumper: true, | |||||
| showTotal: () => `共${total}条`, | |||||
| }} | |||||
| onChange={handleTableChange} | |||||
| expandable={{ | |||||
| expandedRowRender: (record) => ( | |||||
| <ExperimentInstance | |||||
| experimentInsList={experimentInsList} | |||||
| experimentInsTotal={experimentInsTotal} | |||||
| onClickInstance={(item) => gotoInstanceInfo(record, item)} | |||||
| onRemove={() => { | |||||
| refreshExperimentIns(record.id); | |||||
| refreshExperimentList(); | |||||
| }} | |||||
| onTerminate={handleInstanceTerminate} | |||||
| onLoadMore={() => loadMoreExperimentIns()} | |||||
| ></ExperimentInstance> | |||||
| ), | |||||
| onExpand: (e, a) => { | |||||
| handleExpandChange(e, a); | |||||
| }, | |||||
| expandedRowKeys: expandedRowKeys, | |||||
| rowExpandable: () => true, | |||||
| }} | |||||
| rowKey="id" | |||||
| /> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| ); | |||||
| function AutoMLList() { | |||||
| return <ExperimentList type={ExperimentListType.AutoML} />; | |||||
| } | } | ||||
| export default AutoMLList; | export default AutoMLList; | ||||
| @@ -1,28 +1,16 @@ | |||||
| import ConfigInfo, { type BasicInfoData } from '@/components/ConfigInfo'; | |||||
| import { AutoMLTaskType, autoMLEnsembleClassOptions, autoMLTaskTypeOptions } from '@/enums'; | import { AutoMLTaskType, autoMLEnsembleClassOptions, autoMLTaskTypeOptions } from '@/enums'; | ||||
| import { AutoMLData } from '@/pages/AutoML/types'; | import { AutoMLData } from '@/pages/AutoML/types'; | ||||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | import { experimentStatusInfo } from '@/pages/Experiment/status'; | ||||
| import { type NodeStatus } from '@/types'; | import { type NodeStatus } from '@/types'; | ||||
| import { parseJsonText } from '@/utils'; | import { parseJsonText } from '@/utils'; | ||||
| import { elapsedTime } from '@/utils/date'; | import { elapsedTime } from '@/utils/date'; | ||||
| import { formatBoolean, formatDataset, formatDate, formatEnum } from '@/utils/format'; | |||||
| import { Flex } from 'antd'; | import { Flex } from 'antd'; | ||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import { useMemo } from 'react'; | import { useMemo } from 'react'; | ||||
| import ConfigInfo, { | |||||
| formatBoolean, | |||||
| formatDate, | |||||
| formatEnum, | |||||
| type BasicInfoData, | |||||
| } from '../ConfigInfo'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| // 格式化数据集 | |||||
| const formatDataset = (dataset: { name: string; version: string }) => { | |||||
| if (!dataset || !dataset.name || !dataset.version) { | |||||
| return '--'; | |||||
| } | |||||
| return `${dataset.name}:${dataset.version}`; | |||||
| }; | |||||
| // 格式化优化方向 | // 格式化优化方向 | ||||
| const formatOptimizeMode = (value: boolean) => { | const formatOptimizeMode = (value: boolean) => { | ||||
| return value ? '越大越好' : '越小越好'; | return value ? '越大越好' : '越小越好'; | ||||
| @@ -58,28 +46,23 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB | |||||
| { | { | ||||
| label: '实验名称', | label: '实验名称', | ||||
| value: info.ml_name, | value: info.ml_name, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '实验描述', | label: '实验描述', | ||||
| value: info.ml_description, | value: info.ml_description, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '创建人', | label: '创建人', | ||||
| value: info.create_by, | value: info.create_by, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '创建时间', | label: '创建时间', | ||||
| value: info.create_time, | value: info.create_time, | ||||
| ellipsis: true, | |||||
| format: formatDate, | format: formatDate, | ||||
| }, | }, | ||||
| { | { | ||||
| label: '更新时间', | label: '更新时间', | ||||
| value: info.update_time, | value: info.update_time, | ||||
| ellipsis: true, | |||||
| format: formatDate, | format: formatDate, | ||||
| }, | }, | ||||
| ]; | ]; | ||||
| @@ -93,18 +76,15 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB | |||||
| { | { | ||||
| label: '任务类型', | label: '任务类型', | ||||
| value: info.task_type, | value: info.task_type, | ||||
| ellipsis: true, | |||||
| format: formatEnum(autoMLTaskTypeOptions), | format: formatEnum(autoMLTaskTypeOptions), | ||||
| }, | }, | ||||
| { | { | ||||
| label: '特征预处理算法', | label: '特征预处理算法', | ||||
| value: info.include_feature_preprocessor, | value: info.include_feature_preprocessor, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '排除的特征预处理算法', | label: '排除的特征预处理算法', | ||||
| value: info.exclude_feature_preprocessor, | value: info.exclude_feature_preprocessor, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: info.task_type === AutoMLTaskType.Regression ? '回归算法' : '分类算法', | label: info.task_type === AutoMLTaskType.Regression ? '回归算法' : '分类算法', | ||||
| @@ -112,7 +92,6 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB | |||||
| info.task_type === AutoMLTaskType.Regression | info.task_type === AutoMLTaskType.Regression | ||||
| ? info.include_regressor | ? info.include_regressor | ||||
| : info.include_classifier, | : info.include_classifier, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: info.task_type === AutoMLTaskType.Regression ? '排除的回归算法' : '排除的分类算法', | label: info.task_type === AutoMLTaskType.Regression ? '排除的回归算法' : '排除的分类算法', | ||||
| @@ -120,91 +99,73 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB | |||||
| info.task_type === AutoMLTaskType.Regression | info.task_type === AutoMLTaskType.Regression | ||||
| ? info.exclude_regressor | ? info.exclude_regressor | ||||
| : info.exclude_classifier, | : info.exclude_classifier, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '集成方式', | label: '集成方式', | ||||
| value: info.ensemble_class, | value: info.ensemble_class, | ||||
| ellipsis: true, | |||||
| format: formatEnum(autoMLEnsembleClassOptions), | format: formatEnum(autoMLEnsembleClassOptions), | ||||
| }, | }, | ||||
| { | { | ||||
| label: '集成模型数量', | label: '集成模型数量', | ||||
| value: info.ensemble_size, | value: info.ensemble_size, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '集成最佳模型数量', | label: '集成最佳模型数量', | ||||
| value: info.ensemble_nbest, | value: info.ensemble_nbest, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '最大数量', | label: '最大数量', | ||||
| value: info.max_models_on_disc, | value: info.max_models_on_disc, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '内存限制(MB)', | label: '内存限制(MB)', | ||||
| value: info.memory_limit, | value: info.memory_limit, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '单次时间限制(秒)', | label: '单次时间限制(秒)', | ||||
| value: info.per_run_time_limit, | value: info.per_run_time_limit, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '搜索时间限制(秒)', | label: '搜索时间限制(秒)', | ||||
| value: info.time_left_for_this_task, | value: info.time_left_for_this_task, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '重采样策略', | label: '重采样策略', | ||||
| value: info.resampling_strategy, | value: info.resampling_strategy, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '交叉验证折数', | label: '交叉验证折数', | ||||
| value: info.folds, | value: info.folds, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '是否打乱', | label: '是否打乱', | ||||
| value: info.shuffle, | value: info.shuffle, | ||||
| ellipsis: true, | |||||
| format: formatBoolean, | format: formatBoolean, | ||||
| }, | }, | ||||
| { | { | ||||
| label: '训练集比率', | label: '训练集比率', | ||||
| value: info.train_size, | value: info.train_size, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '测试集比率', | label: '测试集比率', | ||||
| value: info.test_size, | value: info.test_size, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '计算指标', | label: '计算指标', | ||||
| value: info.scoring_functions, | value: info.scoring_functions, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '随机种子', | label: '随机种子', | ||||
| value: info.seed, | value: info.seed, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '数据集', | label: '数据集', | ||||
| value: info.dataset, | value: info.dataset, | ||||
| ellipsis: true, | |||||
| format: formatDataset, | format: formatDataset, | ||||
| }, | }, | ||||
| { | { | ||||
| label: '预测目标列', | label: '预测目标列', | ||||
| value: info.target_columns, | value: info.target_columns, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| ]; | ]; | ||||
| }, [info]); | }, [info]); | ||||
| @@ -217,18 +178,15 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB | |||||
| { | { | ||||
| label: '指标名称', | label: '指标名称', | ||||
| value: info.metric_name, | value: info.metric_name, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '优化方向', | label: '优化方向', | ||||
| value: info.greater_is_better, | value: info.greater_is_better, | ||||
| ellipsis: true, | |||||
| format: formatOptimizeMode, | format: formatOptimizeMode, | ||||
| }, | }, | ||||
| { | { | ||||
| label: '指标权重', | label: '指标权重', | ||||
| value: info.metrics, | value: info.metrics, | ||||
| ellipsis: true, | |||||
| format: formatMetricsWeight, | format: formatMetricsWeight, | ||||
| }, | }, | ||||
| ]; | ]; | ||||
| @@ -243,12 +201,10 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB | |||||
| { | { | ||||
| label: '启动时间', | label: '启动时间', | ||||
| value: formatDate(runStatus.startedAt), | value: formatDate(runStatus.startedAt), | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '执行时长', | label: '执行时长', | ||||
| value: elapsedTime(runStatus.startedAt, runStatus.finishedAt), | value: elapsedTime(runStatus.startedAt, runStatus.finishedAt), | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '状态', | label: '状态', | ||||
| @@ -271,7 +227,6 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB | |||||
| </div> | </div> | ||||
| </Flex> | </Flex> | ||||
| ), | ), | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| ]; | ]; | ||||
| }, [runStatus]); | }, [runStatus]); | ||||
| @@ -281,7 +236,7 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB | |||||
| {isInstance && runStatus && ( | {isInstance && runStatus && ( | ||||
| <ConfigInfo | <ConfigInfo | ||||
| title="运行信息" | title="运行信息" | ||||
| data={instanceDatas} | |||||
| datas={instanceDatas} | |||||
| labelWidth={70} | labelWidth={70} | ||||
| style={{ marginBottom: '20px' }} | style={{ marginBottom: '20px' }} | ||||
| /> | /> | ||||
| @@ -289,18 +244,18 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB | |||||
| {!isInstance && ( | {!isInstance && ( | ||||
| <ConfigInfo | <ConfigInfo | ||||
| title="基本信息" | title="基本信息" | ||||
| data={basicDatas} | |||||
| datas={basicDatas} | |||||
| labelWidth={70} | labelWidth={70} | ||||
| style={{ marginBottom: '20px' }} | style={{ marginBottom: '20px' }} | ||||
| /> | /> | ||||
| )} | )} | ||||
| <ConfigInfo | <ConfigInfo | ||||
| title="配置信息" | title="配置信息" | ||||
| data={configDatas} | |||||
| datas={configDatas} | |||||
| labelWidth={150} | labelWidth={150} | ||||
| style={{ marginBottom: '20px' }} | style={{ marginBottom: '20px' }} | ||||
| /> | /> | ||||
| <ConfigInfo title="优化指标" data={metricsData} labelWidth={70} /> | |||||
| <ConfigInfo title="优化指标" datas={metricsData} labelWidth={70} /> | |||||
| </div> | </div> | ||||
| ); | ); | ||||
| } | } | ||||
| @@ -1,20 +0,0 @@ | |||||
| .config-info { | |||||
| :global { | |||||
| .kf-basic-info { | |||||
| width: 100%; | |||||
| &__item { | |||||
| width: calc((100% - 80px) / 3); | |||||
| &__label { | |||||
| font-size: @font-size; | |||||
| text-align: left; | |||||
| text-align-last: left; | |||||
| } | |||||
| &__value { | |||||
| min-width: 0; | |||||
| font-size: @font-size; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -1,26 +0,0 @@ | |||||
| import BasicInfo, { type BasicInfoData } from '@/components/BasicInfo'; | |||||
| import InfoGroup from '@/components/InfoGroup'; | |||||
| import classNames from 'classnames'; | |||||
| import styles from './index.less'; | |||||
| export * from '@/components/BasicInfo/format'; | |||||
| export type { BasicInfoData }; | |||||
| type ConfigInfoProps = { | |||||
| title: string; | |||||
| data: BasicInfoData[]; | |||||
| labelWidth: number; | |||||
| className?: string; | |||||
| style?: React.CSSProperties; | |||||
| }; | |||||
| function ConfigInfo({ title, data, labelWidth, className, style }: ConfigInfoProps) { | |||||
| return ( | |||||
| <InfoGroup title={title} className={classNames(styles['config-info'], className)} style={style}> | |||||
| <div className={styles['config-info__content']}> | |||||
| <BasicInfo datas={data} labelWidth={labelWidth} /> | |||||
| </div> | |||||
| </InfoGroup> | |||||
| ); | |||||
| } | |||||
| export default ConfigInfo; | |||||
| @@ -9,11 +9,7 @@ export type CopyingTextProps = { | |||||
| function CopyingText({ text }: CopyingTextProps) { | function CopyingText({ text }: CopyingTextProps) { | ||||
| return ( | return ( | ||||
| <div className={styles['copying-text']}> | <div className={styles['copying-text']}> | ||||
| <Typography.Text | |||||
| ellipsis={{ tooltip: text }} | |||||
| style={{ color: 'inherit' }} | |||||
| className={styles['copying-text__text']} | |||||
| > | |||||
| <Typography.Text ellipsis={{ tooltip: text }} className={styles['copying-text__text']}> | |||||
| {text} | {text} | ||||
| </Typography.Text> | </Typography.Text> | ||||
| <KFIcon | <KFIcon | ||||
| @@ -62,10 +62,7 @@ function TrialConfig() { | |||||
| > | > | ||||
| <InputNumber placeholder="请输入指标权重" min={0} precision={0} /> | <InputNumber placeholder="请输入指标权重" min={0} precision={0} /> | ||||
| </Form.Item> | </Form.Item> | ||||
| <Flex | |||||
| style={{ width: '76px', marginLeft: '18px', height: '46px' }} | |||||
| align="center" | |||||
| > | |||||
| <Flex className={styles['metrics-weight__operation']} align="center"> | |||||
| <Button | <Button | ||||
| style={{ | style={{ | ||||
| marginRight: '3px', | marginRight: '3px', | ||||
| @@ -1,9 +1,18 @@ | |||||
| .metrics-weight { | .metrics-weight { | ||||
| position: relative; | |||||
| margin-bottom: 20px; | margin-bottom: 20px; | ||||
| &:last-child { | &:last-child { | ||||
| margin-bottom: 0; | margin-bottom: 0; | ||||
| } | } | ||||
| &__operation { | |||||
| position: absolute; | |||||
| left: calc(100% + 10px); | |||||
| width: 76px; | |||||
| height: 46px; | |||||
| margin-left: 6px; | |||||
| } | |||||
| } | } | ||||
| .add-weight { | .add-weight { | ||||
| @@ -14,7 +23,7 @@ | |||||
| border-color: .addAlpha(@primary-color, 0.5) []; | border-color: .addAlpha(@primary-color, 0.5) []; | ||||
| box-shadow: none !important; | box-shadow: none !important; | ||||
| &:hover { | &:hover { | ||||
| border-style: solid; | |||||
| border-style: solid !important; | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -2,11 +2,6 @@ import KFIcon from '@/components/KFIcon'; | |||||
| import { ExperimentStatus } from '@/enums'; | import { ExperimentStatus } from '@/enums'; | ||||
| import { useCheck } from '@/hooks'; | import { useCheck } from '@/hooks'; | ||||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | import { experimentStatusInfo } from '@/pages/Experiment/status'; | ||||
| import { | |||||
| batchDeleteExperimentInsReq, | |||||
| deleteExperimentInsReq, | |||||
| stopExperimentInsReq, | |||||
| } from '@/services/autoML'; | |||||
| import themes from '@/styles/theme.less'; | import themes from '@/styles/theme.less'; | ||||
| import { type ExperimentInstance } from '@/types'; | import { type ExperimentInstance } from '@/types'; | ||||
| import { elapsedTime, formatDate } from '@/utils/date'; | import { elapsedTime, formatDate } from '@/utils/date'; | ||||
| @@ -16,9 +11,11 @@ import { DoubleRightOutlined } from '@ant-design/icons'; | |||||
| import { App, Button, Checkbox, ConfigProvider, Tooltip } from 'antd'; | import { App, Button, Checkbox, ConfigProvider, Tooltip } from 'antd'; | ||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import { useEffect, useMemo } from 'react'; | import { useEffect, useMemo } from 'react'; | ||||
| import { ExperimentListType, experimentListConfig } from '../ExperimentList/config'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| type ExperimentInstanceProps = { | type ExperimentInstanceProps = { | ||||
| type: ExperimentListType; | |||||
| experimentInsList?: ExperimentInstance[]; | experimentInsList?: ExperimentInstance[]; | ||||
| experimentInsTotal: number; | experimentInsTotal: number; | ||||
| onClickInstance?: (instance: ExperimentInstance) => void; | onClickInstance?: (instance: ExperimentInstance) => void; | ||||
| @@ -28,6 +25,7 @@ type ExperimentInstanceProps = { | |||||
| }; | }; | ||||
| function ExperimentInstanceComponent({ | function ExperimentInstanceComponent({ | ||||
| type, | |||||
| experimentInsList, | experimentInsList, | ||||
| experimentInsTotal, | experimentInsTotal, | ||||
| onClickInstance, | onClickInstance, | ||||
| @@ -48,6 +46,7 @@ function ExperimentInstanceComponent({ | |||||
| isSingleChecked, | isSingleChecked, | ||||
| checkSingle, | checkSingle, | ||||
| ] = useCheck(allIntanceIds); | ] = useCheck(allIntanceIds); | ||||
| const config = experimentListConfig[type]; | |||||
| useEffect(() => { | useEffect(() => { | ||||
| // 关闭时清空 | // 关闭时清空 | ||||
| @@ -68,7 +67,8 @@ function ExperimentInstanceComponent({ | |||||
| // 删除实验实例 | // 删除实验实例 | ||||
| const deleteExperimentInstance = async (id: number) => { | const deleteExperimentInstance = async (id: number) => { | ||||
| const [res] = await to(deleteExperimentInsReq(id)); | |||||
| const request = config.deleteInsReq; | |||||
| const [res] = await to(request(id)); | |||||
| if (res) { | if (res) { | ||||
| message.success('删除成功'); | message.success('删除成功'); | ||||
| onRemove?.(); | onRemove?.(); | ||||
| @@ -87,7 +87,8 @@ function ExperimentInstanceComponent({ | |||||
| // 批量删除实验实例 | // 批量删除实验实例 | ||||
| const batchDeleteExperimentInstances = async () => { | const batchDeleteExperimentInstances = async () => { | ||||
| const [res] = await to(batchDeleteExperimentInsReq(selectedIns)); | |||||
| const request = config.batchDeleteInsReq; | |||||
| const [res] = await to(request(selectedIns)); | |||||
| if (res) { | if (res) { | ||||
| message.success('删除成功'); | message.success('删除成功'); | ||||
| setSelectedIns([]); | setSelectedIns([]); | ||||
| @@ -97,7 +98,8 @@ function ExperimentInstanceComponent({ | |||||
| // 终止实验实例 | // 终止实验实例 | ||||
| const terminateExperimentInstance = async (instance: ExperimentInstance) => { | const terminateExperimentInstance = async (instance: ExperimentInstance) => { | ||||
| const [res] = await to(stopExperimentInsReq(instance.id)); | |||||
| const request = config.stopInsReq; | |||||
| const [res] = await to(request(instance.id)); | |||||
| if (res) { | if (res) { | ||||
| message.success('终止成功'); | message.success('终止成功'); | ||||
| onTerminate?.(instance); | onTerminate?.(instance); | ||||
| @@ -0,0 +1,75 @@ | |||||
| /* | |||||
| * @Author: 赵伟 | |||||
| * @Date: 2025-01-08 14:30:58 | |||||
| * @Description: 实验列表组件配置 | |||||
| */ | |||||
| import { | |||||
| batchDeleteExperimentInsReq, | |||||
| deleteAutoMLReq, | |||||
| deleteExperimentInsReq, | |||||
| getAutoMLListReq, | |||||
| getExperimentInsListReq, | |||||
| runAutoMLReq, | |||||
| stopExperimentInsReq, | |||||
| } from '@/services/autoML'; | |||||
| import { | |||||
| batchDeleteRayInsReq, | |||||
| deleteRayInsReq, | |||||
| deleteRayReq, | |||||
| getRayInsListReq, | |||||
| getRayListReq, | |||||
| runRayReq, | |||||
| stopRayInsReq, | |||||
| } from '@/services/hyperParameter'; | |||||
| export enum ExperimentListType { | |||||
| AutoML = 'AutoML', | |||||
| HyperParameter = 'HyperParameter', | |||||
| } | |||||
| type ExperimentListInfo = { | |||||
| getListReq: (params: any) => Promise<any>; // 获取列表 | |||||
| getInsListReq: (params: any) => Promise<any>; // 获取实例列表 | |||||
| deleteRecordReq: (params: any) => Promise<any>; // 删除 | |||||
| runRecordReq: (params: any) => Promise<any>; // 运行 | |||||
| deleteInsReq: (params: any) => Promise<any>; // 删除实例 | |||||
| batchDeleteInsReq: (params: any) => Promise<any>; // 批量删除实例 | |||||
| stopInsReq: (params: any) => Promise<any>; // 终止实例 | |||||
| title: string; // 标题 | |||||
| pathPrefix: string; // 路由路径前缀 | |||||
| idProperty: string; // ID属性 | |||||
| nameProperty: string; // 名称属性 | |||||
| descProperty: string; // 描述属性 | |||||
| }; | |||||
| export const experimentListConfig: Record<ExperimentListType, ExperimentListInfo> = { | |||||
| [ExperimentListType.AutoML]: { | |||||
| getListReq: getAutoMLListReq, | |||||
| getInsListReq: getExperimentInsListReq, | |||||
| deleteRecordReq: deleteAutoMLReq, | |||||
| runRecordReq: runAutoMLReq, | |||||
| deleteInsReq: deleteExperimentInsReq, | |||||
| batchDeleteInsReq: batchDeleteExperimentInsReq, | |||||
| stopInsReq: stopExperimentInsReq, | |||||
| title: '自主机器学习', | |||||
| pathPrefix: 'automl', | |||||
| nameProperty: 'ml_name', | |||||
| descProperty: 'ml_description', | |||||
| idProperty: 'autoMlId', | |||||
| }, | |||||
| [ExperimentListType.HyperParameter]: { | |||||
| getListReq: getRayListReq, | |||||
| getInsListReq: getRayInsListReq, | |||||
| deleteRecordReq: deleteRayReq, | |||||
| runRecordReq: runRayReq, | |||||
| deleteInsReq: deleteRayInsReq, | |||||
| batchDeleteInsReq: batchDeleteRayInsReq, | |||||
| stopInsReq: stopRayInsReq, | |||||
| title: '超参数自动寻优', | |||||
| pathPrefix: 'hyperparameter', | |||||
| nameProperty: 'name', | |||||
| descProperty: 'description', | |||||
| idProperty: 'rayId', | |||||
| }, | |||||
| }; | |||||
| @@ -1,4 +1,4 @@ | |||||
| .auto-ml-list { | |||||
| .experiment-list { | |||||
| height: 100%; | height: 100%; | ||||
| &__content { | &__content { | ||||
| height: calc(100% - 60px); | height: calc(100% - 60px); | ||||
| @@ -0,0 +1,429 @@ | |||||
| /* | |||||
| * @Author: 赵伟 | |||||
| * @Date: 2025-01-08 13:58:08 | |||||
| * @Description: 自主机器学习和超参数寻优列表组件 | |||||
| */ | |||||
| import KFIcon from '@/components/KFIcon'; | |||||
| import PageTitle from '@/components/PageTitle'; | |||||
| import { ExperimentStatus } from '@/enums'; | |||||
| import { useCacheState } from '@/hooks/pageCacheState'; | |||||
| import { AutoMLData } from '@/pages/AutoML/types'; | |||||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | |||||
| import themes from '@/styles/theme.less'; | |||||
| import { type ExperimentInstance as ExperimentInstanceData } from '@/types'; | |||||
| import { to } from '@/utils/promise'; | |||||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||||
| import { modalConfirm } from '@/utils/ui'; | |||||
| import { useNavigate } from '@umijs/max'; | |||||
| import { | |||||
| App, | |||||
| Button, | |||||
| ConfigProvider, | |||||
| Input, | |||||
| Table, | |||||
| Tooltip, | |||||
| type TablePaginationConfig, | |||||
| type TableProps, | |||||
| } from 'antd'; | |||||
| import { type SearchProps } from 'antd/es/input'; | |||||
| import classNames from 'classnames'; | |||||
| import { useEffect, useState } from 'react'; | |||||
| import ExperimentInstance from '../ExperimentInstance'; | |||||
| import { ExperimentListType, experimentListConfig } from './config'; | |||||
| import styles from './index.less'; | |||||
| export { ExperimentListType }; | |||||
| type ExperimentListProps = { | |||||
| type: ExperimentListType; | |||||
| }; | |||||
| function ExperimentList({ type }: ExperimentListProps) { | |||||
| const navigate = useNavigate(); | |||||
| const { message } = App.useApp(); | |||||
| const [cacheState, setCacheState] = useCacheState(); | |||||
| const [searchText, setSearchText] = useState(cacheState?.searchText); | |||||
| const [inputText, setInputText] = useState(cacheState?.searchText); | |||||
| const [tableData, setTableData] = useState<AutoMLData[]>([]); | |||||
| const [total, setTotal] = useState(0); | |||||
| const [experimentInsList, setExperimentInsList] = useState<ExperimentInstanceData[]>([]); | |||||
| const [expandedRowKeys, setExpandedRowKeys] = useState<number[]>([]); | |||||
| const [experimentInsTotal, setExperimentInsTotal] = useState(0); | |||||
| const [pagination, setPagination] = useState<TablePaginationConfig>( | |||||
| cacheState?.pagination ?? { | |||||
| current: 1, | |||||
| pageSize: 10, | |||||
| }, | |||||
| ); | |||||
| const config = experimentListConfig[type]; | |||||
| useEffect(() => { | |||||
| getAutoMLList(); | |||||
| }, [pagination, searchText]); | |||||
| // 获取自主机器学习或超参数自动优化列表 | |||||
| const getAutoMLList = async () => { | |||||
| const params: Record<string, any> = { | |||||
| page: pagination.current! - 1, | |||||
| size: pagination.pageSize, | |||||
| ml_name: searchText || undefined, | |||||
| }; | |||||
| const request = config.getListReq; | |||||
| const [res] = await to(request(params)); | |||||
| if (res && res.data) { | |||||
| const { content = [], totalElements = 0 } = res.data; | |||||
| setTableData(content); | |||||
| setTotal(totalElements); | |||||
| } | |||||
| }; | |||||
| // 搜索 | |||||
| const onSearch: SearchProps['onSearch'] = (value) => { | |||||
| setSearchText(value); | |||||
| setPagination((prev) => ({ | |||||
| ...prev, | |||||
| current: 1, | |||||
| })); | |||||
| }; | |||||
| // 删除一条记录 | |||||
| const deleteAutoML = async (record: AutoMLData) => { | |||||
| const request = config.deleteRecordReq; | |||||
| const [res] = await to(request(record.id)); | |||||
| if (res) { | |||||
| message.success('删除成功'); | |||||
| // 如果是一页的唯一数据,删除时,请求第一页的数据 | |||||
| // 否则直接刷新这一页的数据 | |||||
| // 避免回到第一页 | |||||
| if (tableData.length > 1) { | |||||
| setPagination((prev) => ({ | |||||
| ...prev, | |||||
| current: 1, | |||||
| })); | |||||
| } else { | |||||
| getAutoMLList(); | |||||
| } | |||||
| } | |||||
| }; | |||||
| // 处理删除 | |||||
| const handleAutoMLDelete = (record: AutoMLData) => { | |||||
| modalConfirm({ | |||||
| title: '删除后,该实验将不可恢复', | |||||
| content: '是否确认删除?', | |||||
| onOk: () => { | |||||
| deleteAutoML(record); | |||||
| }, | |||||
| }); | |||||
| }; | |||||
| // 创建、编辑、复制自动机器学习 | |||||
| const createAutoML = (record?: AutoMLData, isCopy: boolean = false) => { | |||||
| setCacheState({ | |||||
| pagination, | |||||
| searchText, | |||||
| }); | |||||
| if (record) { | |||||
| if (isCopy) { | |||||
| navigate(`copy/${record.id}`); | |||||
| } else { | |||||
| navigate(`edit/${record.id}`); | |||||
| } | |||||
| } else { | |||||
| navigate(`create`); | |||||
| } | |||||
| }; | |||||
| // 查看自动机器学习详情 | |||||
| const gotoDetail = (record: AutoMLData) => { | |||||
| setCacheState({ | |||||
| pagination, | |||||
| searchText, | |||||
| }); | |||||
| navigate(`info/${record.id}`); | |||||
| }; | |||||
| // 启动自动机器学习 | |||||
| const startAutoML = async (record: AutoMLData) => { | |||||
| const request = config.runRecordReq; | |||||
| const [res] = await to(request(record.id)); | |||||
| if (res) { | |||||
| message.success('运行成功'); | |||||
| setExpandedRowKeys([record.id]); | |||||
| refreshExperimentList(); | |||||
| refreshExperimentIns(record.id); | |||||
| } | |||||
| }; | |||||
| // --------------------------- 实验实例 --------------------------- | |||||
| // 获取实验实例列表 | |||||
| const getExperimentInsList = async (recordId: number, page: number) => { | |||||
| const params = { | |||||
| [config.idProperty]: recordId, | |||||
| page: page, | |||||
| size: 5, | |||||
| }; | |||||
| const request = config.getInsListReq; | |||||
| const [res] = await to(request(params)); | |||||
| if (res && res.data) { | |||||
| const { content = [], totalElements = 0 } = res.data; | |||||
| try { | |||||
| if (page === 0) { | |||||
| setExperimentInsList(content); | |||||
| } else { | |||||
| setExperimentInsList((prev) => [...prev, ...content]); | |||||
| } | |||||
| setExperimentInsTotal(totalElements); | |||||
| } catch (error) { | |||||
| console.error('JSON parse error: ', error); | |||||
| } | |||||
| } | |||||
| }; | |||||
| // 展开实例 | |||||
| const handleExpandChange = (expanded: boolean, record: AutoMLData) => { | |||||
| setExperimentInsList([]); | |||||
| if (expanded) { | |||||
| setExpandedRowKeys([record.id]); | |||||
| getExperimentInsList(record.id, 0); | |||||
| } else { | |||||
| setExpandedRowKeys([]); | |||||
| } | |||||
| }; | |||||
| // 跳转到实验实例详情 | |||||
| const gotoInstanceInfo = (autoML: AutoMLData, record: ExperimentInstanceData) => { | |||||
| navigate(`instance/${autoML.id}/${record.id}`); | |||||
| }; | |||||
| // 刷新实验实例列表 | |||||
| const refreshExperimentIns = (experimentId: number) => { | |||||
| getExperimentInsList(experimentId, 0); | |||||
| }; | |||||
| // 加载更多实验实例 | |||||
| const loadMoreExperimentIns = () => { | |||||
| const page = Math.round(experimentInsList.length / 5); | |||||
| const recordId = expandedRowKeys[0]; | |||||
| getExperimentInsList(recordId, page); | |||||
| }; | |||||
| // 实验实例终止 | |||||
| const handleInstanceTerminate = async (experimentIns: ExperimentInstanceData) => { | |||||
| // 刷新实验列表 | |||||
| refreshExperimentList(); | |||||
| setExperimentInsList((prevList) => { | |||||
| return prevList.map((item) => { | |||||
| if (item.id === experimentIns.id) { | |||||
| return { | |||||
| ...item, | |||||
| status: ExperimentStatus.Terminated, | |||||
| }; | |||||
| } | |||||
| return item; | |||||
| }); | |||||
| }); | |||||
| }; | |||||
| // 刷新实验列表状态, | |||||
| // 目前是直接刷新实验列表,后续需要优化,只刷新状态 | |||||
| const refreshExperimentList = () => { | |||||
| getAutoMLList(); | |||||
| }; | |||||
| // --------------------------- Table --------------------------- | |||||
| // 分页切换 | |||||
| const handleTableChange: TableProps<AutoMLData>['onChange'] = ( | |||||
| pagination, | |||||
| _filters, | |||||
| _sorter, | |||||
| { action }, | |||||
| ) => { | |||||
| if (action === 'paginate') { | |||||
| setPagination(pagination); | |||||
| } | |||||
| }; | |||||
| const columns: TableProps<AutoMLData>['columns'] = [ | |||||
| { | |||||
| title: '实验名称', | |||||
| dataIndex: config.nameProperty, | |||||
| key: 'ml_name', | |||||
| width: '16%', | |||||
| render: tableCellRender(false, TableCellValueType.Link, { | |||||
| onClick: gotoDetail, | |||||
| }), | |||||
| }, | |||||
| { | |||||
| title: '实验描述', | |||||
| dataIndex: config.descProperty, | |||||
| key: 'ml_description', | |||||
| render: tableCellRender(true), | |||||
| ellipsis: { showTitle: false }, | |||||
| }, | |||||
| { | |||||
| title: '创建时间', | |||||
| dataIndex: 'update_time', | |||||
| key: 'update_time', | |||||
| width: '20%', | |||||
| render: tableCellRender(true, TableCellValueType.Date), | |||||
| ellipsis: { showTitle: false }, | |||||
| }, | |||||
| { | |||||
| title: '最近五次运行状态', | |||||
| dataIndex: 'status_list', | |||||
| key: 'status_list', | |||||
| width: 200, | |||||
| render: (text) => { | |||||
| const newText: string[] = text && text.replace(/\s+/g, '').split(','); | |||||
| return ( | |||||
| <> | |||||
| {newText && newText.length > 0 | |||||
| ? newText.map((item, index) => { | |||||
| return ( | |||||
| <Tooltip | |||||
| key={index} | |||||
| placement="top" | |||||
| title={experimentStatusInfo[item as ExperimentStatus].label} | |||||
| > | |||||
| <img | |||||
| style={{ width: '17px', marginRight: '6px' }} | |||||
| src={experimentStatusInfo[item as ExperimentStatus].icon} | |||||
| draggable={false} | |||||
| alt="" | |||||
| /> | |||||
| </Tooltip> | |||||
| ); | |||||
| }) | |||||
| : null} | |||||
| </> | |||||
| ); | |||||
| }, | |||||
| }, | |||||
| { | |||||
| title: '操作', | |||||
| dataIndex: 'operation', | |||||
| width: 360, | |||||
| key: 'operation', | |||||
| render: (_: any, record: AutoMLData) => ( | |||||
| <div> | |||||
| <Button | |||||
| type="link" | |||||
| size="small" | |||||
| key="start" | |||||
| icon={<KFIcon type="icon-yunhang" />} | |||||
| onClick={() => startAutoML(record)} | |||||
| > | |||||
| 运行 | |||||
| </Button> | |||||
| <Button | |||||
| type="link" | |||||
| size="small" | |||||
| key="edit" | |||||
| icon={<KFIcon type="icon-bianji" />} | |||||
| onClick={() => createAutoML(record, false)} | |||||
| > | |||||
| 编辑 | |||||
| </Button> | |||||
| <Button | |||||
| type="link" | |||||
| size="small" | |||||
| key="copy" | |||||
| icon={<KFIcon type="icon-fuzhi" />} | |||||
| onClick={() => createAutoML(record, true)} | |||||
| > | |||||
| 复制 | |||||
| </Button> | |||||
| <ConfigProvider | |||||
| theme={{ | |||||
| token: { | |||||
| colorLink: themes['warningColor'], | |||||
| }, | |||||
| }} | |||||
| > | |||||
| <Button | |||||
| type="link" | |||||
| size="small" | |||||
| key="remove" | |||||
| icon={<KFIcon type="icon-shanchu" />} | |||||
| onClick={() => handleAutoMLDelete(record)} | |||||
| > | |||||
| 删除 | |||||
| </Button> | |||||
| </ConfigProvider> | |||||
| </div> | |||||
| ), | |||||
| }, | |||||
| ]; | |||||
| return ( | |||||
| <div className={styles['experiment-list']}> | |||||
| <PageTitle title={config.title + '列表'}></PageTitle> | |||||
| <div className={styles['experiment-list__content']}> | |||||
| <div className={styles['experiment-list__content__filter']}> | |||||
| <Input.Search | |||||
| placeholder="按实验名称筛选" | |||||
| onSearch={onSearch} | |||||
| onChange={(e) => setInputText(e.target.value)} | |||||
| style={{ width: 300 }} | |||||
| value={inputText} | |||||
| allowClear | |||||
| /> | |||||
| <Button | |||||
| style={{ marginLeft: '20px' }} | |||||
| type="default" | |||||
| onClick={() => createAutoML()} | |||||
| icon={<KFIcon type="icon-xinjian2" />} | |||||
| > | |||||
| 新建实验 | |||||
| </Button> | |||||
| </div> | |||||
| <div | |||||
| className={classNames('vertical-scroll-table', styles['experiment-list__content__table'])} | |||||
| > | |||||
| <Table | |||||
| dataSource={tableData} | |||||
| columns={columns} | |||||
| scroll={{ y: 'calc(100% - 55px)' }} | |||||
| pagination={{ | |||||
| ...pagination, | |||||
| total: total, | |||||
| showSizeChanger: true, | |||||
| showQuickJumper: true, | |||||
| showTotal: () => `共${total}条`, | |||||
| }} | |||||
| onChange={handleTableChange} | |||||
| expandable={{ | |||||
| expandedRowRender: (record) => ( | |||||
| <ExperimentInstance | |||||
| type={type} | |||||
| experimentInsList={experimentInsList} | |||||
| experimentInsTotal={experimentInsTotal} | |||||
| onClickInstance={(item) => gotoInstanceInfo(record, item)} | |||||
| onRemove={() => { | |||||
| refreshExperimentIns(record.id); | |||||
| refreshExperimentList(); | |||||
| }} | |||||
| onTerminate={handleInstanceTerminate} | |||||
| onLoadMore={() => loadMoreExperimentIns()} | |||||
| ></ExperimentInstance> | |||||
| ), | |||||
| onExpand: (e, a) => { | |||||
| handleExpandChange(e, a); | |||||
| }, | |||||
| expandedRowKeys: expandedRowKeys, | |||||
| rowExpandable: () => true, | |||||
| }} | |||||
| rowKey="id" | |||||
| /> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default ExperimentList; | |||||
| @@ -197,7 +197,7 @@ function CodeConfigList() { | |||||
| title="暂无数据" | title="暂无数据" | ||||
| content={'很抱歉,没有搜索到您想要的内容\n建议刷新试试'} | content={'很抱歉,没有搜索到您想要的内容\n建议刷新试试'} | ||||
| hasFooter={true} | hasFooter={true} | ||||
| onRefresh={getDataList} | |||||
| onButtonClick={getDataList} | |||||
| /> | /> | ||||
| )} | )} | ||||
| </div> | </div> | ||||
| @@ -33,10 +33,7 @@ function CategoryItem({ resourceType, item, isSelected, onClick }: CategoryItemP | |||||
| alt="" | alt="" | ||||
| draggable={false} | draggable={false} | ||||
| /> | /> | ||||
| <Typography.Text | |||||
| ellipsis={{ tooltip: item.name }} | |||||
| style={{ color: 'inherit', marginTop: '4px' }} | |||||
| > | |||||
| <Typography.Text ellipsis={{ tooltip: item.name }} style={{ marginTop: '4px' }}> | |||||
| {item.name} | {item.name} | ||||
| </Typography.Text> | </Typography.Text> | ||||
| </div> | </div> | ||||
| @@ -1,17 +1,8 @@ | |||||
| import BasicTableInfo, { BasicInfoData } from '@/components/BasicTableInfo'; | import BasicTableInfo, { BasicInfoData } from '@/components/BasicTableInfo'; | ||||
| import SubAreaTitle from '@/components/SubAreaTitle'; | import SubAreaTitle from '@/components/SubAreaTitle'; | ||||
| import { ResourceInfoTabKeys } from '@/pages/Dataset/components/ResourceInfo'; | |||||
| import { | |||||
| DataSource, | |||||
| DatasetData, | |||||
| ModelData, | |||||
| ProjectDependency, | |||||
| ResourceType, | |||||
| TrainTask, | |||||
| resourceConfig, | |||||
| } from '@/pages/Dataset/config'; | |||||
| import { DatasetData, ModelData, ResourceType, resourceConfig } from '@/pages/Dataset/config'; | |||||
| import ModelMetrics from '@/pages/Model/components/ModelMetrics'; | import ModelMetrics from '@/pages/Model/components/ModelMetrics'; | ||||
| import { getGitUrl } from '@/utils'; | |||||
| import { formatCodeConfig, formatDatasets, formatSource, formatTrainTask } from '@/utils/format'; | |||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| @@ -24,103 +15,45 @@ type ResourceIntroProps = { | |||||
| version?: string; | version?: string; | ||||
| }; | }; | ||||
| export const formatDataset = (datasets?: DatasetData[]) => { | |||||
| if (!datasets || datasets.length === 0) { | |||||
| return undefined; | |||||
| } | |||||
| return datasets.map((item) => ({ | |||||
| value: item.name, | |||||
| url: `${origin}/dataset/dataset/info/${item.id}?tab=${ResourceInfoTabKeys.Version}&version=${item.version}&name=${item.name}&owner=${item.owner}&identifier=${item.identifier}`, | |||||
| })); | |||||
| }; | |||||
| export const getProjectUrl = (project?: ProjectDependency) => { | |||||
| if (!project || !project.url || !project.branch) { | |||||
| return undefined; | |||||
| } | |||||
| const { url, branch } = project; | |||||
| return getGitUrl(url, branch); | |||||
| }; | |||||
| export const formatProject = (project?: ProjectDependency) => { | |||||
| if (!project) { | |||||
| return undefined; | |||||
| } | |||||
| return { | |||||
| value: project.name, | |||||
| url: getProjectUrl(project), | |||||
| }; | |||||
| }; | |||||
| export const formatTrainTask = (task?: TrainTask) => { | |||||
| if (!task) { | |||||
| return undefined; | |||||
| } | |||||
| return { | |||||
| value: task.name, | |||||
| url: `${origin}/pipeline/experiment/instance/${task.workflow_id}/${task.ins_id}`, | |||||
| }; | |||||
| }; | |||||
| export const formatSource = (source?: string) => { | |||||
| if (source === DataSource.Create) { | |||||
| return '用户上传'; | |||||
| } else if (source === DataSource.HandExport) { | |||||
| return '手动导入'; | |||||
| } else if (source === DataSource.AtuoExport) { | |||||
| return '实验自动导入'; | |||||
| } | |||||
| return source; | |||||
| }; | |||||
| const getDatasetDatas = (data: DatasetData): BasicInfoData[] => [ | const getDatasetDatas = (data: DatasetData): BasicInfoData[] => [ | ||||
| { | { | ||||
| label: '数据集名称', | label: '数据集名称', | ||||
| value: data.name, | value: data.name, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '版本', | label: '版本', | ||||
| value: data.version, | value: data.version, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '创建人', | label: '创建人', | ||||
| value: data.create_by, | value: data.create_by, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '更新时间', | label: '更新时间', | ||||
| value: data.update_time, | value: data.update_time, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '数据来源', | label: '数据来源', | ||||
| value: data.dataset_source, | value: data.dataset_source, | ||||
| format: formatSource, | format: formatSource, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '训练任务', | label: '训练任务', | ||||
| value: data.train_task, | value: data.train_task, | ||||
| format: formatTrainTask, | format: formatTrainTask, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '处理代码', | label: '处理代码', | ||||
| value: data.processing_code, | value: data.processing_code, | ||||
| format: formatProject, | |||||
| ellipsis: true, | |||||
| format: formatCodeConfig, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '数据集分类', | label: '数据集分类', | ||||
| value: data.data_type, | value: data.data_type, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '研究方向', | label: '研究方向', | ||||
| value: data.data_tag, | value: data.data_tag, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| ]; | ]; | ||||
| @@ -153,19 +86,19 @@ const getModelDatas = (data: ModelData): BasicInfoData[] => [ | |||||
| { | { | ||||
| label: '训练代码', | label: '训练代码', | ||||
| value: data.project_depency, | value: data.project_depency, | ||||
| format: formatProject, | |||||
| format: formatCodeConfig, | |||||
| ellipsis: true, | ellipsis: true, | ||||
| }, | }, | ||||
| { | { | ||||
| label: '训练数据集', | label: '训练数据集', | ||||
| value: data.train_datasets, | value: data.train_datasets, | ||||
| format: formatDataset, | |||||
| format: formatDatasets, | |||||
| ellipsis: true, | ellipsis: true, | ||||
| }, | }, | ||||
| { | { | ||||
| label: '测试数据集', | label: '测试数据集', | ||||
| value: data.test_datasets, | value: data.test_datasets, | ||||
| format: formatDataset, | |||||
| format: formatDatasets, | |||||
| ellipsis: true, | ellipsis: true, | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -226,7 +226,7 @@ function ResourceList( | |||||
| title="暂无数据" | title="暂无数据" | ||||
| content={'很抱歉,没有搜索到您想要的内容\n建议刷新试试'} | content={'很抱歉,没有搜索到您想要的内容\n建议刷新试试'} | ||||
| hasFooter={true} | hasFooter={true} | ||||
| onRefresh={getDataList} | |||||
| onButtonClick={getDataList} | |||||
| /> | /> | ||||
| )} | )} | ||||
| </div> | </div> | ||||
| @@ -3,6 +3,7 @@ | |||||
| .title(@color, @background) { | .title(@color, @background) { | ||||
| width: 100%; | width: 100%; | ||||
| margin-bottom: 20px; | margin-bottom: 20px; | ||||
| padding: 0 15px; | |||||
| color: @color; | color: @color; | ||||
| font-weight: 500; | font-weight: 500; | ||||
| font-size: @font-size; | font-size: @font-size; | ||||
| @@ -8,11 +8,11 @@ import { | |||||
| resourceConfig, | resourceConfig, | ||||
| } from '@/pages/Dataset/config'; | } from '@/pages/Dataset/config'; | ||||
| import { isEmpty } from '@/utils'; | import { isEmpty } from '@/utils'; | ||||
| import { formatSource } from '@/utils/format'; | |||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import { Typography, type ModalProps } from 'antd'; | import { Typography, type ModalProps } from 'antd'; | ||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import { useEffect, useMemo, useState } from 'react'; | import { useEffect, useMemo, useState } from 'react'; | ||||
| import { formatSource } from '../ResourceIntro'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| type CompareData = { | type CompareData = { | ||||
| @@ -47,10 +47,10 @@ const formatProject = (project?: ProjectDependency) => { | |||||
| if (!project) { | if (!project) { | ||||
| return undefined; | return undefined; | ||||
| } | } | ||||
| return project.name; | |||||
| return `${project.name}:${project.branch}`; | |||||
| }; | }; | ||||
| export const formatTrainTask = (task?: TrainTask) => { | |||||
| const formatTrainTask = (task?: TrainTask) => { | |||||
| if (!task) { | if (!task) { | ||||
| return undefined; | return undefined; | ||||
| } | } | ||||
| @@ -203,7 +203,7 @@ function VersionCompareModal({ | |||||
| [styles['version-compare__left__text--different']]: isDifferent(key), | [styles['version-compare__left__text--different']]: isDifferent(key), | ||||
| })} | })} | ||||
| > | > | ||||
| <Typography.Text ellipsis={{ tooltip: text }} style={{ color: 'inherit' }}> | |||||
| <Typography.Text ellipsis={{ tooltip: text }}> | |||||
| {isEmpty(text) ? '--' : text} | {isEmpty(text) ? '--' : text} | ||||
| </Typography.Text> | </Typography.Text> | ||||
| </div> | </div> | ||||
| @@ -221,7 +221,7 @@ function VersionCompareModal({ | |||||
| [styles['version-compare__right__text--different']]: isDifferent(key), | [styles['version-compare__right__text--different']]: isDifferent(key), | ||||
| })} | })} | ||||
| > | > | ||||
| <Typography.Text ellipsis={{ tooltip: text }} style={{ color: 'inherit' }}> | |||||
| <Typography.Text ellipsis={{ tooltip: text }}> | |||||
| {isEmpty(text) ? '--' : text} | {isEmpty(text) ? '--' : text} | ||||
| </Typography.Text> | </Typography.Text> | ||||
| </div> | </div> | ||||
| @@ -49,9 +49,7 @@ function VersionSelectorModal({ versions, onOk, ...rest }: VersionSelectorModalP | |||||
| onClick={() => handleClick(item.name)} | onClick={() => handleClick(item.name)} | ||||
| > | > | ||||
| <img src={require('@/assets/img/dataset-version.png')} alt="" draggable={false} /> | <img src={require('@/assets/img/dataset-version.png')} alt="" draggable={false} /> | ||||
| <Typography.Text ellipsis={{ tooltip: item.name }} style={{ color: 'inherit' }}> | |||||
| {item.name} | |||||
| </Typography.Text> | |||||
| <Typography.Text ellipsis={{ tooltip: item.name }}>{item.name}</Typography.Text> | |||||
| </div> | </div> | ||||
| ); | ); | ||||
| })} | })} | ||||
| @@ -36,13 +36,13 @@ enum ComputingResourceType { | |||||
| const EditorRadioItems: KFRadioItem[] = [ | const EditorRadioItems: KFRadioItem[] = [ | ||||
| { | { | ||||
| key: ComputingResourceType.GPU, | |||||
| title: '英伟达GPU', | title: '英伟达GPU', | ||||
| value: ComputingResourceType.GPU, | |||||
| icon: <KFIcon type="icon-jiyugongwangjingxiang" />, | icon: <KFIcon type="icon-jiyugongwangjingxiang" />, | ||||
| }, | }, | ||||
| { | { | ||||
| key: ComputingResourceType.NPU, | |||||
| title: '昇腾NPU', | title: '昇腾NPU', | ||||
| value: ComputingResourceType.NPU, | |||||
| icon: <KFIcon type="icon-bendishangchuan" />, | icon: <KFIcon type="icon-bendishangchuan" />, | ||||
| }, | }, | ||||
| ]; | ]; | ||||
| @@ -0,0 +1,55 @@ | |||||
| .create-hyperparameter { | |||||
| height: 100%; | |||||
| &__content { | |||||
| height: calc(100% - 60px); | |||||
| margin-top: 10px; | |||||
| padding: 30px 30px 10px; | |||||
| overflow: auto; | |||||
| color: @text-color; | |||||
| font-size: @font-size-content; | |||||
| background-color: white; | |||||
| border-radius: 10px; | |||||
| &__type { | |||||
| color: @text-color; | |||||
| font-size: @font-size-input-lg; | |||||
| } | |||||
| :global { | |||||
| .ant-input-number { | |||||
| width: 100%; | |||||
| } | |||||
| .ant-form-item { | |||||
| margin-bottom: 20px; | |||||
| } | |||||
| .image-url { | |||||
| margin-top: -15px; | |||||
| .ant-form-item-label > label::after { | |||||
| content: ''; | |||||
| } | |||||
| } | |||||
| .ant-btn-variant-text:disabled { | |||||
| color: @text-disabled-color; | |||||
| } | |||||
| .ant-btn-variant-text { | |||||
| color: #565658; | |||||
| } | |||||
| .ant-btn.ant-btn-icon-only .anticon { | |||||
| font-size: 20px; | |||||
| } | |||||
| .anticon-question-circle { | |||||
| margin-top: -12px; | |||||
| margin-left: 1px !important; | |||||
| color: @text-color-tertiary !important; | |||||
| font-size: 12px !important; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,167 @@ | |||||
| /* | |||||
| * @Author: 赵伟 | |||||
| * @Date: 2024-04-16 13:58:08 | |||||
| * @Description: 创建实验 | |||||
| */ | |||||
| import PageTitle from '@/components/PageTitle'; | |||||
| import { addRayReq, getRayInfoReq, updateRayReq } from '@/services/hyperParameter'; | |||||
| import { safeInvoke } from '@/utils/functional'; | |||||
| import { to } from '@/utils/promise'; | |||||
| import { useLocation, useNavigate, useParams } from '@umijs/max'; | |||||
| import { App, Button, Form } from 'antd'; | |||||
| import { useEffect } from 'react'; | |||||
| import BasicConfig from '../components/CreateForm/BasicConfig'; | |||||
| import ExecuteConfig from '../components/CreateForm/ExecuteConfig'; | |||||
| import { getReqParamName } from '../components/CreateForm/utils'; | |||||
| import { FormData, HyperParameterData } from '../types'; | |||||
| import styles from './index.less'; | |||||
| function CreateHyperParameter() { | |||||
| const navigate = useNavigate(); | |||||
| const [form] = Form.useForm(); | |||||
| const { message } = App.useApp(); | |||||
| const params = useParams(); | |||||
| const id = safeInvoke(Number)(params.id); | |||||
| const { pathname } = useLocation(); | |||||
| const isCopy = pathname.includes('copy'); | |||||
| useEffect(() => { | |||||
| // 编辑,复制 | |||||
| if (id && !Number.isNaN(id)) { | |||||
| getHyperParameterInfo(id); | |||||
| } | |||||
| }, [id]); | |||||
| // 获取服务详情 | |||||
| const getHyperParameterInfo = async (id: number) => { | |||||
| const [res] = await to(getRayInfoReq({ id })); | |||||
| if (res && res.data) { | |||||
| const info: HyperParameterData = res.data; | |||||
| const { name: name_str, parameters, points_to_evaluate, ...rest } = info; | |||||
| const name = isCopy ? `${name_str}-copy` : name_str; | |||||
| if (parameters && Array.isArray(parameters)) { | |||||
| parameters.forEach((item) => { | |||||
| const paramName = getReqParamName(item.type); | |||||
| item.range = item[paramName]; | |||||
| item[paramName] = undefined; | |||||
| }); | |||||
| } | |||||
| const formData = { | |||||
| ...rest, | |||||
| name, | |||||
| parameters, | |||||
| points_to_evaluate: points_to_evaluate ?? [undefined], | |||||
| }; | |||||
| form.setFieldsValue(formData); | |||||
| } | |||||
| }; | |||||
| // 创建、更新、复制实验 | |||||
| const createExperiment = async (formData: FormData) => { | |||||
| // 按后台接口要求,修改参数表单数据结构,将 "value" 参数改为 "bounds"/"values"/"value" | |||||
| const formParameters = formData['parameters']; | |||||
| const parameters = formParameters.map((item) => { | |||||
| const paramName = getReqParamName(item.type); | |||||
| const range = item.range; | |||||
| return { | |||||
| ...item, | |||||
| [paramName]: range, | |||||
| range: undefined, | |||||
| }; | |||||
| }); | |||||
| // 根据后台要求,修改表单数据 | |||||
| const object = { | |||||
| ...formData, | |||||
| parameters: parameters, | |||||
| }; | |||||
| const params = | |||||
| id && !isCopy | |||||
| ? { | |||||
| id: id, | |||||
| ...object, | |||||
| } | |||||
| : object; | |||||
| const request = id && !isCopy ? updateRayReq : addRayReq; | |||||
| const [res] = await to(request(params)); | |||||
| if (res) { | |||||
| message.success('操作成功'); | |||||
| navigate(-1); | |||||
| } | |||||
| }; | |||||
| // 提交 | |||||
| const handleSubmit = (values: FormData) => { | |||||
| createExperiment(values); | |||||
| }; | |||||
| // 取消 | |||||
| const cancel = () => { | |||||
| navigate(-1); | |||||
| }; | |||||
| let buttonText = '新建'; | |||||
| let title = '新建实验'; | |||||
| if (id) { | |||||
| if (isCopy) { | |||||
| title = '复制实验'; | |||||
| buttonText = '确定'; | |||||
| } else { | |||||
| title = '编辑实验'; | |||||
| buttonText = '更新'; | |||||
| } | |||||
| } | |||||
| return ( | |||||
| <div className={styles['create-hyperparameter']}> | |||||
| <PageTitle title={title}></PageTitle> | |||||
| <div className={styles['create-hyperparameter__content']}> | |||||
| <div> | |||||
| <Form | |||||
| name="create-hyperparameter" | |||||
| labelCol={{ flex: '160px' }} | |||||
| labelAlign="left" | |||||
| form={form} | |||||
| onFinish={handleSubmit} | |||||
| size="large" | |||||
| autoComplete="off" | |||||
| scrollToFirstError | |||||
| initialValues={{ | |||||
| mode: 'max', | |||||
| parameters: [ | |||||
| { | |||||
| name: '', | |||||
| }, | |||||
| ], | |||||
| points_to_evaluate: [undefined], | |||||
| }} | |||||
| > | |||||
| <BasicConfig /> | |||||
| <ExecuteConfig /> | |||||
| <Form.Item wrapperCol={{ offset: 0, span: 16 }} style={{ marginTop: '40px' }}> | |||||
| <Button type="primary" htmlType="submit"> | |||||
| {buttonText} | |||||
| </Button> | |||||
| <Button | |||||
| type="default" | |||||
| htmlType="button" | |||||
| onClick={cancel} | |||||
| style={{ marginLeft: '20px' }} | |||||
| > | |||||
| 取消 | |||||
| </Button> | |||||
| </Form.Item> | |||||
| </Form> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default CreateHyperParameter; | |||||
| @@ -0,0 +1,40 @@ | |||||
| .auto-ml-info { | |||||
| position: relative; | |||||
| height: 100%; | |||||
| &__tabs { | |||||
| height: 50px; | |||||
| padding-left: 25px; | |||||
| background-image: url(@/assets/img/page-title-bg.png); | |||||
| background-repeat: no-repeat; | |||||
| background-position: top center; | |||||
| background-size: 100% 100%; | |||||
| } | |||||
| &__content { | |||||
| height: calc(100% - 60px); | |||||
| margin-top: 10px; | |||||
| } | |||||
| &__tips { | |||||
| position: absolute; | |||||
| top: 11px; | |||||
| left: 256px; | |||||
| padding: 3px 12px; | |||||
| color: #565658; | |||||
| font-size: @font-size-content; | |||||
| background: .addAlpha(@primary-color, 0.09) []; | |||||
| border-radius: 4px; | |||||
| &::before { | |||||
| position: absolute; | |||||
| top: 10px; | |||||
| left: -6px; | |||||
| width: 0; | |||||
| height: 0; | |||||
| border-top: 4px solid transparent; | |||||
| border-right: 6px solid .addAlpha(@primary-color, 0.09) []; | |||||
| border-bottom: 4px solid transparent; | |||||
| content: ''; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,47 @@ | |||||
| /* | |||||
| * @Author: 赵伟 | |||||
| * @Date: 2024-04-16 13:58:08 | |||||
| * @Description: 自主机器学习详情 | |||||
| */ | |||||
| import PageTitle from '@/components/PageTitle'; | |||||
| import { getRayInfoReq } from '@/services/hyperParameter'; | |||||
| import { safeInvoke } from '@/utils/functional'; | |||||
| import { to } from '@/utils/promise'; | |||||
| import { useParams } from '@umijs/max'; | |||||
| import { useEffect, useState } from 'react'; | |||||
| import HyperParameterBasic from '../components/HyperParameterBasic'; | |||||
| import { HyperParameterData } from '../types'; | |||||
| import styles from './index.less'; | |||||
| function HyperparameterInfo() { | |||||
| const params = useParams(); | |||||
| const hyperparameterId = safeInvoke(Number)(params.id); | |||||
| const [hyperparameterInfo, setHyperparameterInfo] = useState<HyperParameterData | undefined>( | |||||
| undefined, | |||||
| ); | |||||
| useEffect(() => { | |||||
| if (hyperparameterId) { | |||||
| getHyperparameterInfo(); | |||||
| } | |||||
| }, []); | |||||
| // 获取自动机器学习详情 | |||||
| const getHyperparameterInfo = async () => { | |||||
| const [res] = await to(getRayInfoReq({ id: hyperparameterId })); | |||||
| if (res && res.data) { | |||||
| setHyperparameterInfo(res.data); | |||||
| } | |||||
| }; | |||||
| return ( | |||||
| <div className={styles['auto-ml-info']}> | |||||
| <PageTitle title="实验详情"></PageTitle> | |||||
| <div className={styles['auto-ml-info__content']}> | |||||
| <HyperParameterBasic info={hyperparameterInfo} /> | |||||
| </div> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default HyperparameterInfo; | |||||