diff --git a/.gitignore b/.gitignore index 21a12484..5510490a 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,5 @@ mvnw # web **/node_modules + +*storybook.log diff --git a/k8s/template-yaml/rolebindings.yaml b/k8s/template-yaml/rolebindings.yaml new file mode 100644 index 00000000..54c2e13d --- /dev/null +++ b/k8s/template-yaml/rolebindings.yaml @@ -0,0 +1,68 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: custom-workflow + namespace: argo +rules: + - apiGroups: + - argoproj.io + resources: + - workflows + verbs: + - create + - get + - list + - watch + - update + - patch + - delete + - apiGroups: + - "" + resources: + - pods + - services + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - pods/exec + verbs: + - create + - get + - list + - watch + - update + - patch + - delete + - apiGroups: + - "apps" + resources: + - deployments + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +--- + +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: custom-workflow-default +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: custom-workflow +subjects: + - kind: ServiceAccount + name: default diff --git a/react-ui/.gitignore b/react-ui/.gitignore index 889039c2..9d2581cd 100644 --- a/react-ui/.gitignore +++ b/react-ui/.gitignore @@ -41,10 +41,5 @@ screenshot build 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 diff --git a/react-ui/.nvmrc b/react-ui/.nvmrc new file mode 100644 index 00000000..8ddbc0c6 --- /dev/null +++ b/react-ui/.nvmrc @@ -0,0 +1 @@ +v18.16.0 diff --git a/react-ui/.storybook/babel-plugin-auto-css-modules.js b/react-ui/.storybook/babel-plugin-auto-css-modules.js new file mode 100644 index 00000000..9c7709ff --- /dev/null +++ b/react-ui/.storybook/babel-plugin-auto-css-modules.js @@ -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"; + } + } + }, + }, + }; +}; \ No newline at end of file diff --git a/react-ui/.storybook/main.ts b/react-ui/.storybook/main.ts new file mode 100644 index 00000000..820a0eeb --- /dev/null +++ b/react-ui/.storybook/main.ts @@ -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; diff --git a/react-ui/.storybook/mock/umijs.mock.tsx b/react-ui/.storybook/mock/umijs.mock.tsx new file mode 100644 index 00000000..ae8a7646 --- /dev/null +++ b/react-ui/.storybook/mock/umijs.mock.tsx @@ -0,0 +1,19 @@ +export const Link = ({ to, children, ...props }: any) => ( + + {children} + +); + +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; diff --git a/react-ui/.storybook/preview.tsx b/react-ui/.storybook/preview.tsx new file mode 100644 index 00000000..61e82aaa --- /dev/null +++ b/react-ui/.storybook/preview.tsx @@ -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) => ( + + + + + + ), + ], + loaders: [mswLoader], // 👈 Add the MSW loader to all stories +}; + +export default preview; diff --git a/react-ui/.storybook/storybook.css b/react-ui/.storybook/storybook.css new file mode 100644 index 00000000..6c592a3c --- /dev/null +++ b/react-ui/.storybook/storybook.css @@ -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; +} diff --git a/react-ui/.storybook/tsconfig.json b/react-ui/.storybook/tsconfig.json new file mode 100644 index 00000000..601d7708 --- /dev/null +++ b/react-ui/.storybook/tsconfig.json @@ -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": "./" + } +} diff --git a/react-ui/.storybook/typings.d.ts b/react-ui/.storybook/typings.d.ts new file mode 100644 index 00000000..742f70c6 --- /dev/null +++ b/react-ui/.storybook/typings.d.ts @@ -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; diff --git a/react-ui/config/config.ts b/react-ui/config/config.ts index f64c8c77..04ab330d 100644 --- a/react-ui/config/config.ts +++ b/react-ui/config/config.ts @@ -75,7 +75,7 @@ export default defineConfig({ * @name layout 插件 * @doc https://umijs.org/docs/max/layout-menu */ - title: '复杂智能软件', + title: '智能材料科研平台', layout: { ...defaultSettings, }, diff --git a/react-ui/config/defaultSettings.ts b/react-ui/config/defaultSettings.ts index 306c89db..c4c59d2f 100644 --- a/react-ui/config/defaultSettings.ts +++ b/react-ui/config/defaultSettings.ts @@ -17,7 +17,7 @@ const Settings: ProLayoutProps & { fixSiderbar: false, splitMenus: false, colorWeak: false, - title: '复杂智能软件', + title: '智能材料科研平台', pwa: true, token: { // 参见ts声明,demo 见文档,通过token 修改样式 diff --git a/react-ui/config/routes.ts b/react-ui/config/routes.ts index eaddb001..e9363f91 100644 --- a/react-ui/config/routes.ts +++ b/react-ui/config/routes.ts @@ -44,7 +44,7 @@ export default [ { name: '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', + }, + ], + }, ], }, { diff --git a/react-ui/package.json b/react-ui/package.json index dc5be1c5..2b2cfd4b 100644 --- a/react-ui/package.json +++ b/react-ui/package.json @@ -8,6 +8,7 @@ "build": "max build", "deploy": "npm run build && npm run gh-pages", "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-prod:build": "docker-compose -f ./docker/docker-compose.yml build", "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: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", + "storybook": "storybook dev -p 6006", + "storybook-build": "storybook build", + "storybook-docs": "storybook dev --docs", + "storybook-docs-build": "storybook build --docs", "test": "jest", "test:coverage": "npm run jest -- --coverage", "test:update": "npm run jest -- -u", @@ -83,6 +88,17 @@ }, "devDependencies": { "@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", "@types/antd": "^1.0.0", "@types/express": "^4.17.14", @@ -96,15 +112,22 @@ "@umijs/max": "^4.0.66", "cross-env": "^7.0.3", "eslint": "^8.39.0", + "eslint-plugin-storybook": "~0.11.2", "express": "^4.18.2", "gh-pages": "^5.0.0", "husky": "^8.0.3", "jest": "^29.5.0", "jest-environment-jsdom": "^29.5.0", + "less": "~4.2.2", + "less-loader": "~12.2.0", "lint-staged": "^13.2.0", "mockjs": "^1.1.0", + "msw": "~2.7.0", + "msw-storybook-addon": "~2.0.4", "prettier": "^2.8.1", + "storybook": "~8.5.3", "swagger-ui-dist": "^4.18.2", + "ts-loader": "~9.5.2", "ts-node": "^10.9.1", "typescript": "^5.0.4", "umi-presets-pro": "^2.0.0" @@ -140,5 +163,10 @@ "CNAME", "create-umi" ] + }, + "msw": { + "workerDirectory": [ + "public" + ] } } diff --git a/react-ui/public/favicon-cc.ico b/react-ui/public/favicon-cc.ico new file mode 100644 index 00000000..4d544cb1 Binary files /dev/null and b/react-ui/public/favicon-cc.ico differ diff --git a/react-ui/public/favicon-cl.ico b/react-ui/public/favicon-cl.ico deleted file mode 100644 index 408b8a23..00000000 Binary files a/react-ui/public/favicon-cl.ico and /dev/null differ diff --git a/react-ui/public/favicon.ico b/react-ui/public/favicon.ico index 4d544cb1..408b8a23 100644 Binary files a/react-ui/public/favicon.ico and b/react-ui/public/favicon.ico differ diff --git a/react-ui/public/mockServiceWorker.js b/react-ui/public/mockServiceWorker.js new file mode 100644 index 00000000..ec47a9a5 --- /dev/null +++ b/react-ui/public/mockServiceWorker.js @@ -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 +} diff --git a/react-ui/src/app.tsx b/react-ui/src/app.tsx index d08ca129..65b4440a 100644 --- a/react-ui/src/app.tsx +++ b/react-ui/src/app.tsx @@ -7,7 +7,6 @@ import defaultSettings from '../config/defaultSettings'; import '../public/fonts/font.css'; import { getAccessToken } from './access'; import './dayjsConfig'; -import './global.less'; import { removeAllPageCacheState } from './hooks/pageCacheState'; import { getRemoteMenu, @@ -41,7 +40,7 @@ export async function getInitialState(): Promise { roleNames: response.user.roles, } as API.CurrentUser; } catch (error) { - console.error('1111', error); + console.error('getInitialState', error); gotoLoginPage(); } return undefined; @@ -215,7 +214,7 @@ export const antd: RuntimeAntdConfig = (memo) => { defaultColor: themes['textColor'], defaultHoverBg: 'rgba(22, 100, 255, 0.06)', defaultHoverBorderColor: 'rgba(22, 100, 255, 0.5)', - defaultHoverColor: '#3F7FFF ', + defaultHoverColor: '#3F7FFF', defaultActiveBg: 'rgba(22, 100, 255, 0.12)', defaultActiveBorderColor: 'rgba(22, 100, 255, 0.75)', defaultActiveColor: themes['primaryColor'], @@ -228,6 +227,7 @@ export const antd: RuntimeAntdConfig = (memo) => { }; memo.theme.components.Select = { singleItemHeightLG: 46, + optionSelectedColor: themes['primaryColor'], }; memo.theme.components.Table = { headerBg: 'rgba(242, 244, 247, 0.36)', diff --git a/react-ui/src/assets/img/logo-cc.png b/react-ui/src/assets/img/logo-cc.png new file mode 100644 index 00000000..cae91fe5 Binary files /dev/null and b/react-ui/src/assets/img/logo-cc.png differ diff --git a/react-ui/src/assets/img/logo-cl.png b/react-ui/src/assets/img/logo-cl.png deleted file mode 100644 index e2fbcfe5..00000000 Binary files a/react-ui/src/assets/img/logo-cl.png and /dev/null differ diff --git a/react-ui/src/assets/img/logo.png b/react-ui/src/assets/img/logo.png index cae91fe5..e2fbcfe5 100644 Binary files a/react-ui/src/assets/img/logo.png and b/react-ui/src/assets/img/logo.png differ diff --git a/react-ui/src/assets/img/popover-bg.png b/react-ui/src/assets/img/popover-bg.png new file mode 100644 index 00000000..d783c637 Binary files /dev/null and b/react-ui/src/assets/img/popover-bg.png differ diff --git a/react-ui/src/components/BasicInfo/BasicInfoItem.tsx b/react-ui/src/components/BasicInfo/BasicInfoItem.tsx new file mode 100644 index 00000000..86e63891 --- /dev/null +++ b/react-ui/src/components/BasicInfo/BasicInfoItem.tsx @@ -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 =
{formatValue}
; + } else if (Array.isArray(formatValue)) { + valueComponent = ( +
+ {formatValue.map((item: BasicInfoLink) => ( + + ))} +
+ ); + } else if (typeof formatValue === 'object' && formatValue) { + valueComponent = ( + + ); + } else { + valueComponent = ( + + ); + } + return ( +
+
+ + {label} + +
+ {valueComponent} +
+ ); +} + +export default BasicInfoItem; diff --git a/react-ui/src/components/BasicInfo/BasicInfoItemValue.tsx b/react-ui/src/components/BasicInfo/BasicInfoItemValue.tsx new file mode 100644 index 00000000..c5a993e4 --- /dev/null +++ b/react-ui/src/components/BasicInfo/BasicInfoItemValue.tsx @@ -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 = ( + + {value} + + ); + } else if (link && value) { + component = ( + + {value} + + ); + } else { + component = {!isEmpty(value) ? value : '--'}; + } + + return ( +
+ + {component} + +
+ ); +} + +export default BasicInfoItemValue; diff --git a/react-ui/src/components/BasicInfo/components.tsx b/react-ui/src/components/BasicInfo/components.tsx deleted file mode 100644 index b8932a25..00000000 --- a/react-ui/src/components/BasicInfo/components.tsx +++ /dev/null @@ -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 = ( -
- {formatValue.map((item: BasicInfoLink) => ( - - ))} -
- ); - } else if (React.isValidElement(formatValue)) { - // 这个判断必须在下面的判断之前 - valueComponent = ( - - ); - } else if (typeof formatValue === 'object' && formatValue) { - valueComponent = ( - - ); - } else { - valueComponent = ( - - ); - } - return ( -
-
- {label} -
- {valueComponent} -
- ); -} - -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 = ( - - {value} - - ); - } else if (link && value) { - component = ( - - {value} - - ); - } else if (React.isValidElement(value)) { - return value; - } else { - component = {value ?? '--'}; - } - - return ( -
- - {component} - -
- ); -} diff --git a/react-ui/src/components/BasicInfo/format.ts b/react-ui/src/components/BasicInfo/format.ts deleted file mode 100644 index 0dae2422..00000000 --- a/react-ui/src/components/BasicInfo/format.ts +++ /dev/null @@ -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 : '--'; - }; -}; diff --git a/react-ui/src/components/BasicInfo/index.less b/react-ui/src/components/BasicInfo/index.less index e4570868..ced2d2ba 100644 --- a/react-ui/src/components/BasicInfo/index.less +++ b/react-ui/src/components/BasicInfo/index.less @@ -17,8 +17,6 @@ color: @text-color-secondary; font-size: @font-size-content; line-height: 1.6; - text-align: justify; - text-align-last: justify; &::after { position: absolute; @@ -31,10 +29,12 @@ flex: 1; flex-direction: column; gap: 5px 0; + min-width: 0; } &__value { flex: 1; + min-width: 0; margin-left: 16px; font-size: @font-size-content; line-height: 1.6; @@ -49,5 +49,29 @@ 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; + } } } diff --git a/react-ui/src/components/BasicInfo/index.tsx b/react-ui/src/components/BasicInfo/index.tsx index 1336d0b6..11eacfde 100644 --- a/react-ui/src/components/BasicInfo/index.tsx +++ b/react-ui/src/components/BasicInfo/index.tsx @@ -1,27 +1,56 @@ import classNames from 'classnames'; import React from 'react'; -import { BasicInfoItem } from './components'; +import BasicInfoItem from './BasicInfoItem'; import './index.less'; import type { BasicInfoData, BasicInfoLink } from './types'; -export * from './format'; export type { BasicInfoData, BasicInfoLink }; -type BasicInfoProps = { +export type BasicInfoProps = { + /** 基础信息 */ datas: BasicInfoData[]; + /** 标题宽度 */ + labelWidth: number; + /** 标题是否显示省略号 */ + labelEllipsis?: boolean; + /** 是否一行三列 */ + threeColumns?: boolean; + /** 标签对齐方式 */ + labelAlign?: 'start' | 'end' | 'justify'; + /** 自定义类名 */ className?: string; + /** 自定义样式 */ 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 ( -
+
{datas.map((item) => ( ))}
diff --git a/react-ui/src/components/BasicInfo/types.ts b/react-ui/src/components/BasicInfo/types.ts index a7c10ba0..be2ac774 100644 --- a/react-ui/src/components/BasicInfo/types.ts +++ b/react-ui/src/components/BasicInfo/types.ts @@ -3,12 +3,12 @@ export type BasicInfoData = { label: string; value?: any; ellipsis?: boolean; - format?: (_value?: any) => string | BasicInfoLink | BasicInfoLink[] | undefined; + format?: (_value?: any) => string | React.ReactNode | BasicInfoLink | BasicInfoLink[] | undefined; }; // 值为链接的类型 export type BasicInfoLink = { - value: string; + value?: string; link?: string; url?: string; }; diff --git a/react-ui/src/components/BasicTableInfo/index.less b/react-ui/src/components/BasicTableInfo/index.less index 850af79c..479fe332 100644 --- a/react-ui/src/components/BasicTableInfo/index.less +++ b/react-ui/src/components/BasicTableInfo/index.less @@ -34,7 +34,6 @@ &__value { flex: 1; min-width: 0; - margin: 0 !important; padding: 12px 20px 4px; font-size: @font-size; word-break: break-all; @@ -56,5 +55,13 @@ text-underline-offset: 3px; } } + + &__node { + flex: 1; + min-width: 0; + padding: 12px 20px; + font-size: @font-size; + word-break: break-all; + } } } diff --git a/react-ui/src/components/BasicTableInfo/index.tsx b/react-ui/src/components/BasicTableInfo/index.tsx index 571c4b5b..68c350bd 100644 --- a/react-ui/src/components/BasicTableInfo/index.tsx +++ b/react-ui/src/components/BasicTableInfo/index.tsx @@ -1,30 +1,27 @@ 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 './index.less'; -export * from '../BasicInfo/format'; export type { BasicInfoData, BasicInfoLink }; -type BasicTableInfoProps = { - datas: BasicInfoData[]; - className?: string; - style?: React.CSSProperties; - labelWidth: number; -}; - +/** + * 表格基础信息展示组件,用于展示基础信息,一行四列,支持数据格式化 + */ export default function BasicTableInfo({ datas, className, style, labelWidth, -}: BasicTableInfoProps) { + labelEllipsis, +}: BasicInfoProps) { const remainder = datas.length % 4; const array = []; if (remainder > 0) { for (let i = 0; i < 4 - remainder; i++) { array.push({ label: '', - value: '', + value: false, // 用于区分是否是空数据,不能是空字符串、null、undefined }); } } @@ -37,6 +34,7 @@ export default function BasicTableInfo({ key={`${item.label}-${index}`} data={item} labelWidth={labelWidth} + labelEllipsis={labelEllipsis} classPrefix="kf-basic-table-info" /> ))} diff --git a/react-ui/src/pages/Pipeline/components/CodeConfigItem/index.less b/react-ui/src/components/CodeConfigItem/index.less similarity index 100% rename from react-ui/src/pages/Pipeline/components/CodeConfigItem/index.less rename to react-ui/src/components/CodeConfigItem/index.less diff --git a/react-ui/src/pages/Pipeline/components/CodeConfigItem/index.tsx b/react-ui/src/components/CodeConfigItem/index.tsx similarity index 100% rename from react-ui/src/pages/Pipeline/components/CodeConfigItem/index.tsx rename to react-ui/src/components/CodeConfigItem/index.tsx diff --git a/react-ui/src/components/CodeSelect/index.tsx b/react-ui/src/components/CodeSelect/index.tsx index 79401b25..d2afbb94 100644 --- a/react-ui/src/components/CodeSelect/index.tsx +++ b/react-ui/src/components/CodeSelect/index.tsx @@ -4,18 +4,32 @@ * @Description: 流水线选择代码配置表单 */ +import CodeSelectorModal from '@/components/CodeSelectorModal'; import KFIcon from '@/components/KFIcon'; -import CodeSelectorModal from '@/pages/Pipeline/components/CodeSelectorModal'; import { openAntdModal } from '@/utils/modal'; import { Button } from 'antd'; +import classNames from 'classnames'; import ParameterInput, { type ParameterInputProps } from '../ParameterInput'; import './index.less'; -export { requiredValidator, type ParameterInputObject } from '../ParameterInput'; +export { + requiredValidator, + type ParameterInputObject, + type ParameterInputValue, +} from '../ParameterInput'; type CodeSelectProps = ParameterInputProps; -function CodeSelect({ value, onChange, disabled, ...rest }: CodeSelectProps) { +/** 代码配置选择表单组件 */ +function CodeSelect({ + value, + size, + disabled, + className, + style, + onChange, + ...rest +}: CodeSelectProps) { const selectResource = () => { const { close } = openAntdModal(CodeSelectorModal, { onOk: (res) => { @@ -46,9 +60,10 @@ function CodeSelect({ value, onChange, disabled, ...rest }: CodeSelectProps) { }; return ( -
+
)} diff --git a/react-ui/src/components/KFIcon/index.tsx b/react-ui/src/components/KFIcon/index.tsx index d84257a7..38d3644c 100644 --- a/react-ui/src/components/KFIcon/index.tsx +++ b/react-ui/src/components/KFIcon/index.tsx @@ -14,14 +14,20 @@ const Icon = createFromIconfontCN({ type IconFontProps = Parameters[0]; interface KFIconProps extends IconFontProps { + /** 图标 */ type: string; + /** 字体大小 */ font?: number; + /** 字体颜色 */ color?: string; - style?: React.CSSProperties; + /** 自定义类名 */ 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 = { ...style, fontSize: font, diff --git a/react-ui/src/components/ModalTitle/index.less b/react-ui/src/components/KFModal/KFModalTitle.less similarity index 100% rename from react-ui/src/components/ModalTitle/index.less rename to react-ui/src/components/KFModal/KFModalTitle.less diff --git a/react-ui/src/components/ModalTitle/index.tsx b/react-ui/src/components/KFModal/KFModalTitle.tsx similarity index 84% rename from react-ui/src/components/ModalTitle/index.tsx rename to react-ui/src/components/KFModal/KFModalTitle.tsx index 4c0179eb..d2ec3265 100644 --- a/react-ui/src/components/ModalTitle/index.tsx +++ b/react-ui/src/components/KFModal/KFModalTitle.tsx @@ -6,12 +6,16 @@ import classNames from 'classnames'; import React from 'react'; -import './index.less'; +import './KFModalTitle.less'; type ModalTitleProps = { + /** 标题 */ title: React.ReactNode; + /** 图片 */ image?: string; + /** 自定义样式 */ style?: React.CSSProperties; + /** 自定义类名 */ className?: string; }; diff --git a/react-ui/src/components/KFModal/index.tsx b/react-ui/src/components/KFModal/index.tsx index c073ab27..8156d6a9 100644 --- a/react-ui/src/components/KFModal/index.tsx +++ b/react-ui/src/components/KFModal/index.tsx @@ -4,19 +4,22 @@ * @Description: 自定义 Modal */ -import ModalTitle from '@/components/ModalTitle'; import { Modal, type ModalProps } from 'antd'; import classNames from 'classnames'; +import KFModalTitle from './KFModalTitle'; import './index.less'; export interface KFModalProps extends ModalProps { + /** 标题图片 */ image?: string; } + +/** 自定义 Modal,应用中的业务 Modal 应该使用它进行封装,推荐使用函数的方式打开 */ function KFModal({ title, image, children, - className = '', + className, centered, maskClosable, ...rest @@ -27,7 +30,7 @@ function KFModal({ {...rest} centered={centered ?? true} maskClosable={maskClosable ?? false} - title={} + title={} > {children} diff --git a/react-ui/src/components/KFRadio/index.tsx b/react-ui/src/components/KFRadio/index.tsx index 4bb4ccce..7191dae6 100644 --- a/react-ui/src/components/KFRadio/index.tsx +++ b/react-ui/src/components/KFRadio/index.tsx @@ -8,32 +8,40 @@ import classNames from 'classnames'; import './index.less'; export type KFRadioItem = { - key: string; title: string; + value: string; icon?: React.ReactNode; }; type KFRadioProps = { + /** 选项 */ items: KFRadioItem[]; + /** 当前选中项 */ value?: string; + /** 自定义样式 */ style?: React.CSSProperties; + /** 自定义类名 */ className?: string; + /** 选中回调 */ onChange?: (value: string) => void; }; +/** + * 自定义 Radio + */ function KFRadio({ items, value, style, className, onChange }: KFRadioProps) { return ( {items.map((item) => { return ( onChange?.(item.key)} + onClick={() => onChange?.(item.value)} > {item.icon} {item.title} diff --git a/react-ui/src/components/KFSpin/index.tsx b/react-ui/src/components/KFSpin/index.tsx index 519ab6ef..a3a6c3e5 100644 --- a/react-ui/src/components/KFSpin/index.tsx +++ b/react-ui/src/components/KFSpin/index.tsx @@ -5,13 +5,19 @@ */ 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 ( -
- -
加载中
+
+ +
{label}
); } diff --git a/react-ui/src/components/LabelValue/index.less b/react-ui/src/components/LabelValue/index.less deleted file mode 100644 index 5f1b9b0c..00000000 --- a/react-ui/src/components/LabelValue/index.less +++ /dev/null @@ -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; - } -} diff --git a/react-ui/src/components/LabelValue/index.tsx b/react-ui/src/components/LabelValue/index.tsx deleted file mode 100644 index 22b9b3eb..00000000 --- a/react-ui/src/components/LabelValue/index.tsx +++ /dev/null @@ -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 ( -
-
{label}
-
{value ?? '--'}
-
- ); -} - -export default LabelValue; diff --git a/react-ui/src/components/MenuIconSelector/index.less b/react-ui/src/components/MenuIconSelector/index.less index 77529762..5a64a8d3 100644 --- a/react-ui/src/components/MenuIconSelector/index.less +++ b/react-ui/src/components/MenuIconSelector/index.less @@ -1,5 +1,4 @@ .menu-icon-selector { - // grid 布局,每行显示 8 个图标 display: grid; grid-template-columns: repeat(4, 80px); gap: 20px; @@ -10,7 +9,7 @@ display: flex; align-items: center; justify-content: center; - width: 80x; + width: 80px; height: 80px; border: 1px solid transparent; border-radius: 4px; diff --git a/react-ui/src/components/MenuIconSelector/index.tsx b/react-ui/src/components/MenuIconSelector/index.tsx index dd57320e..fa38910b 100644 --- a/react-ui/src/components/MenuIconSelector/index.tsx +++ b/react-ui/src/components/MenuIconSelector/index.tsx @@ -12,7 +12,9 @@ import { useEffect, useState } from 'react'; import styles from './index.less'; interface MenuIconSelectorProps extends Omit { + /** 选中的图标 */ selectedIcon?: string; + /** 选择回调 */ onOk: (param: string) => void; } @@ -21,6 +23,7 @@ type IconObject = { font_class: string; }; +/** 菜单图标选择器 */ function MenuIconSelector({ open, selectedIcon, onOk, ...rest }: MenuIconSelectorProps) { const [icons, setIcons] = useState([]); useEffect(() => { diff --git a/react-ui/src/components/PageTitle/index.tsx b/react-ui/src/components/PageTitle/index.tsx index ca192454..2703e032 100644 --- a/react-ui/src/components/PageTitle/index.tsx +++ b/react-ui/src/components/PageTitle/index.tsx @@ -8,10 +8,17 @@ import React from 'react'; import './index.less'; type PageTitleProps = { - title: string; + /** 标题 */ + title: React.ReactNode; + /** 自定义类名 */ className?: string; + /** 自定义样式 */ style?: React.CSSProperties; }; + +/** + * 页面标题 + */ function PageTitle({ title, style, className = '' }: PageTitleProps) { return (
diff --git a/react-ui/src/components/ParameterInput/index.less b/react-ui/src/components/ParameterInput/index.less index 4b22d208..fff69eb1 100644 --- a/react-ui/src/components/ParameterInput/index.less +++ b/react-ui/src/components/ParameterInput/index.less @@ -39,7 +39,7 @@ &__placeholder { min-height: 22px; - color: rgba(0, 0, 0, 0.25); + color: @text-placeholder-color; font-size: @font-size-input; line-height: 1.5714285714285714; } @@ -49,18 +49,31 @@ padding: 10px 11px; 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; 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 { - 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 { - font-size: 12px; + font-size: 10px; } } diff --git a/react-ui/src/components/ParameterInput/index.tsx b/react-ui/src/components/ParameterInput/index.tsx index 94fdddde..47ef5d0e 100644 --- a/react-ui/src/components/ParameterInput/index.tsx +++ b/react-ui/src/components/ParameterInput/index.tsx @@ -1,21 +1,22 @@ /* * @Author: 赵伟 * @Date: 2024-04-16 08:42:57 - * @Description: 参数输入组件 + * @Description: 参数输入表单组件,支持手动输入,也支持选择全局参数 */ +import { CommonTabKeys } from '@/enums'; import { CloseOutlined } from '@ant-design/icons'; import { Form, Input } from 'antd'; import { RuleObject } from 'antd/es/form'; import classNames from 'classnames'; import './index.less'; -// 对象 +// 如果值是对象时的类型 export type ParameterInputObject = { value?: any; // 值 showValue?: any; // 显示值 fromSelect?: boolean; // 是否来自选择 - activeTab?: string; // 选择镜像、数据集、模型时,保存当前激活的tab + activeTab?: CommonTabKeys; // 选择镜像、数据集、模型时,保存当前激活的tab expandedKeys?: string[]; // 选择镜像、数据集、模型时,保存展开的keys checkedKeys?: string[]; // 选择镜像、数据集、模型时,保存选中的keys [key: string]: any; @@ -25,18 +26,34 @@ export type ParameterInputObject = { export type ParameterInputValue = ParameterInputObject | string; export interface ParameterInputProps { + /** 值,可以是字符串,也可以是 ParameterInputObject 对象 */ value?: ParameterInputValue; + /** + * 值变化时的回调 + * @param value 值,可以是字符串,也可以是 ParameterInputObject 对象 + */ onChange?: (value?: ParameterInputValue) => void; + /** 点击时的回调 */ onClick?: () => void; + /** 删除时的回调 */ onRemove?: () => void; + /** 是否可以手动输入 */ canInput?: boolean; + /** 是否是文本框 */ textArea?: boolean; + /** 占位符 */ placeholder?: string; + /** 是否允许清除 */ allowClear?: boolean; + /** 自定义类名 */ className?: string; + /** 自定义样式 */ style?: React.CSSProperties; + /** 大小 */ size?: 'middle' | 'small' | 'large'; + /** 是否禁用 */ disabled?: boolean; + /** 元素 id */ id?: string; } @@ -88,6 +105,7 @@ function ParameterInput({ className={classNames( 'parameter-input', { 'parameter-input--large': size === 'large' }, + { 'parameter-input--small': size === 'small' }, { [`parameter-input--${status}`]: status }, className, )} diff --git a/react-ui/src/components/ParameterSelect/index.tsx b/react-ui/src/components/ParameterSelect/index.tsx index ffaf8415..2c9f862f 100644 --- a/react-ui/src/components/ParameterSelect/index.tsx +++ b/react-ui/src/components/ParameterSelect/index.tsx @@ -1,7 +1,7 @@ /* * @Author: 赵伟 * @Date: 2024-04-16 08:42:57 - * @Description: 参数选择组件 + * @Description: 参数下拉选择组件,支持资源规格、数据集、模型、服务 */ import { PipelineNodeModelParameter } from '@/types'; diff --git a/react-ui/src/components/ResourceSelect/index.tsx b/react-ui/src/components/ResourceSelect/index.tsx index 5f2142d8..c4bef4bf 100644 --- a/react-ui/src/components/ResourceSelect/index.tsx +++ b/react-ui/src/components/ResourceSelect/index.tsx @@ -9,30 +9,71 @@ import ResourceSelectorModal, { ResourceSelectorResponse, ResourceSelectorType, selectorTypeConfig, -} from '@/pages/Pipeline/components/ResourceSelectorModal'; +} from '@/components/ResourceSelectorModal'; import { openAntdModal } from '@/utils/modal'; 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 './index.less'; -export { requiredValidator, type ParameterInputObject } from '../ParameterInput'; +export { + requiredValidator, + type ParameterInputObject, + type ParameterInputValue, +} from '../ParameterInput'; export { ResourceSelectorType, selectorTypeConfig, type ResourceSelectorResponse }; -type ResourceSelectProps = { +interface ResourceSelectProps extends ParameterInputProps { + /** 类型,数据集、模型、镜像 */ type: ResourceSelectorType; -} & ParameterInputProps; +} -// 获取选择数据集、模型后面按钮 icon +// 获取选择数据集、模型、镜像后面按钮 icon const getSelectBtnIcon = (type: ResourceSelectorType) => { return ; }; -function ResourceSelect({ type, value, onChange, disabled, ...rest }: ResourceSelectProps) { +/** 数据集、模型、镜像选择表单组件 */ +function ResourceSelect({ + type, + value, + size, + disabled, + className, + style, + onChange, + ...rest +}: ResourceSelectProps) { const [selectedResource, setSelectedResource] = useState( 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 resource = selectedResource; const { close } = openAntdModal(ResourceSelectorModal, { @@ -50,8 +91,10 @@ function ResourceSelect({ type, value, onChange, disabled, ...rest }: ResourceSe showValue: path, fromSelect: true, activeTab, - expandedKeys: [`${id}`], - checkedKeys: [`${id}-${version}`], + id, + name, + version, + path, }); } else { const jsonObj = { @@ -69,8 +112,6 @@ function ResourceSelect({ type, value, onChange, disabled, ...rest }: ResourceSe showValue, fromSelect: true, activeTab, - expandedKeys: [`${id}`], - checkedKeys: [`${id}-${version}`], ...jsonObj, }); } @@ -80,8 +121,6 @@ function ResourceSelect({ type, value, onChange, disabled, ...rest }: ResourceSe showValue: undefined, fromSelect: false, activeTab: undefined, - expandedKeys: [], - checkedKeys: [], }); } close(); @@ -90,18 +129,19 @@ function ResourceSelect({ type, value, onChange, disabled, ...rest }: ResourceSe }; return ( -
+
setSelectedResource(undefined)} onClick={selectResource} > diff --git a/react-ui/src/pages/AutoML/Info/index.tsx b/react-ui/src/pages/AutoML/Info/index.tsx index cc5247e2..0d0ec460 100644 --- a/react-ui/src/pages/AutoML/Info/index.tsx +++ b/react-ui/src/pages/AutoML/Info/index.tsx @@ -3,9 +3,7 @@ * @Date: 2024-04-16 13:58:08 * @Description: 自主机器学习详情 */ -import KFIcon from '@/components/KFIcon'; import PageTitle from '@/components/PageTitle'; -import { CommonTabKeys } from '@/enums'; import { getAutoMLInfoReq } from '@/services/autoML'; import { safeInvoke } from '@/utils/functional'; import { to } from '@/utils/promise'; @@ -16,24 +14,10 @@ import { AutoMLData } from '../types'; import styles from './index.less'; function AutoMLInfo() { - const [activeTab, setActiveTab] = useState(CommonTabKeys.Public); const params = useParams(); const autoMLId = safeInvoke(Number)(params.id); const [autoMLInfo, setAutoMLInfo] = useState(undefined); - const tabItems = [ - { - key: CommonTabKeys.Public, - label: '基本信息', - icon: , - }, - { - key: CommonTabKeys.Private, - label: 'Trial列表', - icon: , - }, - ]; - useEffect(() => { if (autoMLId) { getAutoMLInfo(); diff --git a/react-ui/src/pages/AutoML/List/index.tsx b/react-ui/src/pages/AutoML/List/index.tsx index 13e3dcbe..a4488e4d 100644 --- a/react-ui/src/pages/AutoML/List/index.tsx +++ b/react-ui/src/pages/AutoML/List/index.tsx @@ -3,419 +3,11 @@ * @Date: 2024-04-16 13:58:08 * @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([]); - const [total, setTotal] = useState(0); - const [experimentInsList, setExperimentInsList] = useState([]); - const [expandedRowKeys, setExpandedRowKeys] = useState([]); - const [experimentInsTotal, setExperimentInsTotal] = useState(0); - const [pagination, setPagination] = useState( - cacheState?.pagination ?? { - current: 1, - pageSize: 10, - }, - ); - - useEffect(() => { - getAutoMLList(); - }, [pagination, searchText]); - - // 获取自主机器学习列表 - const getAutoMLList = async () => { - const params: Record = { - 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['onChange'] = ( - pagination, - _filters, - _sorter, - { action }, - ) => { - if (action === 'paginate') { - setPagination(pagination); - } - }; - - const columns: TableProps['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 ( - - - - ); - }) - : null} - - ); - }, - }, - { - title: '操作', - dataIndex: 'operation', - width: 360, - key: 'operation', - render: (_: any, record: AutoMLData) => ( -
- - - - - - - -
- ), - }, - ]; - - return ( -
- -
-
- setInputText(e.target.value)} - style={{ width: 300 }} - value={inputText} - allowClear - /> - -
-
- `共${total}条`, - }} - onChange={handleTableChange} - expandable={{ - expandedRowRender: (record) => ( - gotoInstanceInfo(record, item)} - onRemove={() => { - refreshExperimentIns(record.id); - refreshExperimentList(); - }} - onTerminate={handleInstanceTerminate} - onLoadMore={() => loadMoreExperimentIns()} - > - ), - onExpand: (e, a) => { - handleExpandChange(e, a); - }, - expandedRowKeys: expandedRowKeys, - rowExpandable: () => true, - }} - rowKey="id" - /> - - - - ); +function AutoMLList() { + return ; } export default AutoMLList; diff --git a/react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx b/react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx index 854c6035..5f207b7d 100644 --- a/react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx +++ b/react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx @@ -1,28 +1,16 @@ +import ConfigInfo, { type BasicInfoData } from '@/components/ConfigInfo'; import { AutoMLTaskType, autoMLEnsembleClassOptions, autoMLTaskTypeOptions } from '@/enums'; import { AutoMLData } from '@/pages/AutoML/types'; import { experimentStatusInfo } from '@/pages/Experiment/status'; import { type NodeStatus } from '@/types'; import { parseJsonText } from '@/utils'; import { elapsedTime } from '@/utils/date'; +import { formatBoolean, formatDataset, formatDate, formatEnum } from '@/utils/format'; import { Flex } from 'antd'; import classNames from 'classnames'; import { useMemo } from 'react'; -import ConfigInfo, { - formatBoolean, - formatDate, - formatEnum, - type BasicInfoData, -} from '../ConfigInfo'; 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) => { return value ? '越大越好' : '越小越好'; @@ -58,28 +46,23 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB { label: '实验名称', value: info.ml_name, - ellipsis: true, }, { label: '实验描述', value: info.ml_description, - ellipsis: true, }, { label: '创建人', value: info.create_by, - ellipsis: true, }, { label: '创建时间', value: info.create_time, - ellipsis: true, format: formatDate, }, { label: '更新时间', value: info.update_time, - ellipsis: true, format: formatDate, }, ]; @@ -93,18 +76,15 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB { label: '任务类型', value: info.task_type, - ellipsis: true, format: formatEnum(autoMLTaskTypeOptions), }, { label: '特征预处理算法', value: info.include_feature_preprocessor, - ellipsis: true, }, { label: '排除的特征预处理算法', value: info.exclude_feature_preprocessor, - ellipsis: true, }, { label: info.task_type === AutoMLTaskType.Regression ? '回归算法' : '分类算法', @@ -112,7 +92,6 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB info.task_type === AutoMLTaskType.Regression ? info.include_regressor : info.include_classifier, - ellipsis: true, }, { label: info.task_type === AutoMLTaskType.Regression ? '排除的回归算法' : '排除的分类算法', @@ -120,91 +99,73 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB info.task_type === AutoMLTaskType.Regression ? info.exclude_regressor : info.exclude_classifier, - ellipsis: true, }, { label: '集成方式', value: info.ensemble_class, - ellipsis: true, format: formatEnum(autoMLEnsembleClassOptions), }, { label: '集成模型数量', value: info.ensemble_size, - ellipsis: true, }, { label: '集成最佳模型数量', value: info.ensemble_nbest, - ellipsis: true, }, { label: '最大数量', value: info.max_models_on_disc, - ellipsis: true, }, { label: '内存限制(MB)', value: info.memory_limit, - ellipsis: true, }, { label: '单次时间限制(秒)', value: info.per_run_time_limit, - ellipsis: true, }, { label: '搜索时间限制(秒)', value: info.time_left_for_this_task, - ellipsis: true, }, { label: '重采样策略', value: info.resampling_strategy, - ellipsis: true, }, { label: '交叉验证折数', value: info.folds, - ellipsis: true, }, { label: '是否打乱', value: info.shuffle, - ellipsis: true, format: formatBoolean, }, { label: '训练集比率', value: info.train_size, - ellipsis: true, }, { label: '测试集比率', value: info.test_size, - ellipsis: true, }, { label: '计算指标', value: info.scoring_functions, - ellipsis: true, }, { label: '随机种子', value: info.seed, - ellipsis: true, }, - { label: '数据集', value: info.dataset, - ellipsis: true, format: formatDataset, }, { label: '预测目标列', value: info.target_columns, - ellipsis: true, }, ]; }, [info]); @@ -217,18 +178,15 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB { label: '指标名称', value: info.metric_name, - ellipsis: true, }, { label: '优化方向', value: info.greater_is_better, - ellipsis: true, format: formatOptimizeMode, }, { label: '指标权重', value: info.metrics, - ellipsis: true, format: formatMetricsWeight, }, ]; @@ -243,12 +201,10 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB { label: '启动时间', value: formatDate(runStatus.startedAt), - ellipsis: true, }, { label: '执行时长', value: elapsedTime(runStatus.startedAt, runStatus.finishedAt), - ellipsis: true, }, { label: '状态', @@ -271,7 +227,6 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB ), - ellipsis: true, }, ]; }, [runStatus]); @@ -281,7 +236,7 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB {isInstance && runStatus && ( @@ -289,18 +244,18 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB {!isInstance && ( )} - + ); } diff --git a/react-ui/src/pages/AutoML/components/ConfigInfo/index.less b/react-ui/src/pages/AutoML/components/ConfigInfo/index.less deleted file mode 100644 index 33fb3314..00000000 --- a/react-ui/src/pages/AutoML/components/ConfigInfo/index.less +++ /dev/null @@ -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; - } - } - } - } -} diff --git a/react-ui/src/pages/AutoML/components/ConfigInfo/index.tsx b/react-ui/src/pages/AutoML/components/ConfigInfo/index.tsx deleted file mode 100644 index 10e042e4..00000000 --- a/react-ui/src/pages/AutoML/components/ConfigInfo/index.tsx +++ /dev/null @@ -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 ( - -
- -
-
- ); -} - -export default ConfigInfo; diff --git a/react-ui/src/pages/AutoML/components/CopyingText/index.tsx b/react-ui/src/pages/AutoML/components/CopyingText/index.tsx index b4c56f4e..586de40b 100644 --- a/react-ui/src/pages/AutoML/components/CopyingText/index.tsx +++ b/react-ui/src/pages/AutoML/components/CopyingText/index.tsx @@ -9,11 +9,7 @@ export type CopyingTextProps = { function CopyingText({ text }: CopyingTextProps) { return (
- + {text} - + + + + + + + +
+ ), + }, + ]; + + return ( +
+ +
+
+ setInputText(e.target.value)} + style={{ width: 300 }} + value={inputText} + allowClear + /> + +
+
+
`共${total}条`, + }} + onChange={handleTableChange} + expandable={{ + expandedRowRender: (record) => ( + gotoInstanceInfo(record, item)} + onRemove={() => { + refreshExperimentIns(record.id); + refreshExperimentList(); + }} + onTerminate={handleInstanceTerminate} + onLoadMore={() => loadMoreExperimentIns()} + > + ), + onExpand: (e, a) => { + handleExpandChange(e, a); + }, + expandedRowKeys: expandedRowKeys, + rowExpandable: () => true, + }} + rowKey="id" + /> + + + + ); +} + +export default ExperimentList; diff --git a/react-ui/src/pages/CodeConfig/List/index.tsx b/react-ui/src/pages/CodeConfig/List/index.tsx index 2efef04c..0c484e54 100644 --- a/react-ui/src/pages/CodeConfig/List/index.tsx +++ b/react-ui/src/pages/CodeConfig/List/index.tsx @@ -197,7 +197,7 @@ function CodeConfigList() { title="暂无数据" content={'很抱歉,没有搜索到您想要的内容\n建议刷新试试'} hasFooter={true} - onRefresh={getDataList} + onButtonClick={getDataList} /> )} diff --git a/react-ui/src/pages/Dataset/components/CategoryItem/index.tsx b/react-ui/src/pages/Dataset/components/CategoryItem/index.tsx index 08398e56..d308dd45 100644 --- a/react-ui/src/pages/Dataset/components/CategoryItem/index.tsx +++ b/react-ui/src/pages/Dataset/components/CategoryItem/index.tsx @@ -33,10 +33,7 @@ function CategoryItem({ resourceType, item, isSelected, onClick }: CategoryItemP alt="" draggable={false} /> - + {item.name} diff --git a/react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx b/react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx index b0bad9de..10d7d9d2 100644 --- a/react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx +++ b/react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx @@ -1,17 +1,8 @@ import BasicTableInfo, { BasicInfoData } from '@/components/BasicTableInfo'; 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 { getGitUrl } from '@/utils'; +import { formatCodeConfig, formatDatasets, formatSource, formatTrainTask } from '@/utils/format'; import classNames from 'classnames'; import styles from './index.less'; @@ -24,103 +15,45 @@ type ResourceIntroProps = { 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[] => [ { label: '数据集名称', value: data.name, - ellipsis: true, }, { label: '版本', value: data.version, - ellipsis: true, }, { label: '创建人', value: data.create_by, - ellipsis: true, }, { label: '更新时间', value: data.update_time, - ellipsis: true, }, { label: '数据来源', value: data.dataset_source, format: formatSource, - ellipsis: true, }, { label: '训练任务', value: data.train_task, format: formatTrainTask, - ellipsis: true, }, { label: '处理代码', value: data.processing_code, - format: formatProject, - ellipsis: true, + format: formatCodeConfig, }, { label: '数据集分类', value: data.data_type, - ellipsis: true, }, { label: '研究方向', value: data.data_tag, - ellipsis: true, }, ]; @@ -153,19 +86,19 @@ const getModelDatas = (data: ModelData): BasicInfoData[] => [ { label: '训练代码', value: data.project_depency, - format: formatProject, + format: formatCodeConfig, ellipsis: true, }, { label: '训练数据集', value: data.train_datasets, - format: formatDataset, + format: formatDatasets, ellipsis: true, }, { label: '测试数据集', value: data.test_datasets, - format: formatDataset, + format: formatDatasets, ellipsis: true, }, { diff --git a/react-ui/src/pages/Dataset/components/ResourceList/index.tsx b/react-ui/src/pages/Dataset/components/ResourceList/index.tsx index 6f2c7523..9577ad41 100644 --- a/react-ui/src/pages/Dataset/components/ResourceList/index.tsx +++ b/react-ui/src/pages/Dataset/components/ResourceList/index.tsx @@ -226,7 +226,7 @@ function ResourceList( title="暂无数据" content={'很抱歉,没有搜索到您想要的内容\n建议刷新试试'} hasFooter={true} - onRefresh={getDataList} + onButtonClick={getDataList} /> )} diff --git a/react-ui/src/pages/Dataset/components/VersionCompareModal/index.less b/react-ui/src/pages/Dataset/components/VersionCompareModal/index.less index 70de3494..f1935eb2 100644 --- a/react-ui/src/pages/Dataset/components/VersionCompareModal/index.less +++ b/react-ui/src/pages/Dataset/components/VersionCompareModal/index.less @@ -3,6 +3,7 @@ .title(@color, @background) { width: 100%; margin-bottom: 20px; + padding: 0 15px; color: @color; font-weight: 500; font-size: @font-size; diff --git a/react-ui/src/pages/Dataset/components/VersionCompareModal/index.tsx b/react-ui/src/pages/Dataset/components/VersionCompareModal/index.tsx index 2ee76e78..b3b6b2f1 100644 --- a/react-ui/src/pages/Dataset/components/VersionCompareModal/index.tsx +++ b/react-ui/src/pages/Dataset/components/VersionCompareModal/index.tsx @@ -8,11 +8,11 @@ import { resourceConfig, } from '@/pages/Dataset/config'; import { isEmpty } from '@/utils'; +import { formatSource } from '@/utils/format'; import { to } from '@/utils/promise'; import { Typography, type ModalProps } from 'antd'; import classNames from 'classnames'; import { useEffect, useMemo, useState } from 'react'; -import { formatSource } from '../ResourceIntro'; import styles from './index.less'; type CompareData = { @@ -47,10 +47,10 @@ const formatProject = (project?: ProjectDependency) => { if (!project) { return undefined; } - return project.name; + return `${project.name}:${project.branch}`; }; -export const formatTrainTask = (task?: TrainTask) => { +const formatTrainTask = (task?: TrainTask) => { if (!task) { return undefined; } @@ -203,7 +203,7 @@ function VersionCompareModal({ [styles['version-compare__left__text--different']]: isDifferent(key), })} > - + {isEmpty(text) ? '--' : text} @@ -221,7 +221,7 @@ function VersionCompareModal({ [styles['version-compare__right__text--different']]: isDifferent(key), })} > - + {isEmpty(text) ? '--' : text} diff --git a/react-ui/src/pages/Dataset/components/VersionSelectorModal/index.tsx b/react-ui/src/pages/Dataset/components/VersionSelectorModal/index.tsx index c58bf87e..b63d02fc 100644 --- a/react-ui/src/pages/Dataset/components/VersionSelectorModal/index.tsx +++ b/react-ui/src/pages/Dataset/components/VersionSelectorModal/index.tsx @@ -49,9 +49,7 @@ function VersionSelectorModal({ versions, onOk, ...rest }: VersionSelectorModalP onClick={() => handleClick(item.name)} > - - {item.name} - + {item.name} ); })} diff --git a/react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx b/react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx index 90ad4d15..1a0b9a18 100644 --- a/react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx +++ b/react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx @@ -36,13 +36,13 @@ enum ComputingResourceType { const EditorRadioItems: KFRadioItem[] = [ { - key: ComputingResourceType.GPU, title: '英伟达GPU', + value: ComputingResourceType.GPU, icon: , }, { - key: ComputingResourceType.NPU, title: '昇腾NPU', + value: ComputingResourceType.NPU, icon: , }, ]; diff --git a/react-ui/src/pages/HyperParameter/Create/index.less b/react-ui/src/pages/HyperParameter/Create/index.less new file mode 100644 index 00000000..145be0d1 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/Create/index.less @@ -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; + } + } + } +} diff --git a/react-ui/src/pages/HyperParameter/Create/index.tsx b/react-ui/src/pages/HyperParameter/Create/index.tsx new file mode 100644 index 00000000..79e45582 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/Create/index.tsx @@ -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 ( +
+ +
+
+
+ + + + + + + + +
+
+
+ ); +} + +export default CreateHyperParameter; diff --git a/react-ui/src/pages/HyperParameter/Info/index.less b/react-ui/src/pages/HyperParameter/Info/index.less new file mode 100644 index 00000000..e27756ef --- /dev/null +++ b/react-ui/src/pages/HyperParameter/Info/index.less @@ -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: ''; + } + } +} diff --git a/react-ui/src/pages/HyperParameter/Info/index.tsx b/react-ui/src/pages/HyperParameter/Info/index.tsx new file mode 100644 index 00000000..3d5c5c04 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/Info/index.tsx @@ -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( + undefined, + ); + + useEffect(() => { + if (hyperparameterId) { + getHyperparameterInfo(); + } + }, []); + + // 获取自动机器学习详情 + const getHyperparameterInfo = async () => { + const [res] = await to(getRayInfoReq({ id: hyperparameterId })); + if (res && res.data) { + setHyperparameterInfo(res.data); + } + }; + + return ( +
+ +
+ +
+
+ ); +} + +export default HyperparameterInfo; diff --git a/react-ui/src/pages/HyperParameter/Instance/index.less b/react-ui/src/pages/HyperParameter/Instance/index.less new file mode 100644 index 00000000..889faeb5 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/Instance/index.less @@ -0,0 +1,42 @@ +.auto-ml-instance { + height: 100%; + + &__tabs { + height: 100%; + :global { + .ant-tabs-nav-list { + width: 100%; + height: 50px; + padding-left: 15px; + background-image: url(@/assets/img/page-title-bg.png); + background-repeat: no-repeat; + background-position: top center; + background-size: 100% 100%; + } + + .ant-tabs-content-holder { + height: calc(100% - 50px); + .ant-tabs-content { + height: 100%; + .ant-tabs-tabpane { + height: 100%; + } + } + } + } + } + + &__basic { + height: calc(100% - 10px); + margin-top: 10px; + } + + &__log { + height: calc(100% - 10px); + margin-top: 10px; + padding: 20px calc(@content-padding - 8px); + overflow-y: visible; + background-color: white; + border-radius: 10px; + } +} diff --git a/react-ui/src/pages/HyperParameter/Instance/index.tsx b/react-ui/src/pages/HyperParameter/Instance/index.tsx new file mode 100644 index 00000000..8d29521b --- /dev/null +++ b/react-ui/src/pages/HyperParameter/Instance/index.tsx @@ -0,0 +1,215 @@ +import KFIcon from '@/components/KFIcon'; +import { AutoMLTaskType, ExperimentStatus } from '@/enums'; +import LogList from '@/pages/Experiment/components/LogList'; +import { getExperimentInsReq } from '@/services/autoML'; +import { NodeStatus } from '@/types'; +import { parseJsonText } from '@/utils'; +import { safeInvoke } from '@/utils/functional'; +import { to } from '@/utils/promise'; +import { useParams } from '@umijs/max'; +import { Tabs } from 'antd'; +import { useEffect, useRef, useState } from 'react'; +import ExperimentHistory from '../components/ExperimentHistory'; +import ExperimentResult from '../components/ExperimentResult'; +import HyperParameterBasic from '../components/HyperParameterBasic'; +import { AutoMLInstanceData, HyperParameterData } from '../types'; +import styles from './index.less'; + +enum TabKeys { + Params = 'params', + Log = 'log', + Result = 'result', + History = 'history', +} + +function AutoMLInstance() { + const [activeTab, setActiveTab] = useState(TabKeys.Params); + const [autoMLInfo, setAutoMLInfo] = useState(undefined); + const [instanceInfo, setInstanceInfo] = useState(undefined); + const params = useParams(); + // const autoMLId = safeInvoke(Number)(params.autoMLId); + const instanceId = safeInvoke(Number)(params.id); + const evtSourceRef = useRef(null); + + useEffect(() => { + if (instanceId) { + getExperimentInsInfo(false); + } + return () => { + closeSSE(); + }; + }, []); + + // 获取实验实例详情 + const getExperimentInsInfo = async (isStatusDetermined: boolean) => { + const [res] = await to(getExperimentInsReq(instanceId)); + if (res && res.data) { + const info = res.data as AutoMLInstanceData; + const { param, node_status, argo_ins_name, argo_ins_ns, status } = info; + // 解析配置参数 + const paramJson = parseJsonText(param); + if (paramJson) { + setAutoMLInfo(paramJson); + } + + // 这个接口返回的状态有延时,SSE 返回的状态是最新的 + // SSE 调用时,不需要解析 node_status, 也不要重新建立 SSE + if (isStatusDetermined) { + setInstanceInfo((prev) => ({ + ...info, + nodeStatus: prev!.nodeStatus, + })); + return; + } + + // 进行节点状态 + const nodeStatusJson = parseJsonText(node_status); + if (nodeStatusJson) { + Object.keys(nodeStatusJson).forEach((key) => { + if (key.startsWith('auto-ml')) { + const value = nodeStatusJson[key]; + info.nodeStatus = value; + } + }); + } + setInstanceInfo(info); + // 运行中或者等待中,开启 SSE + if (status === ExperimentStatus.Pending || status === ExperimentStatus.Running) { + setupSSE(argo_ins_name, argo_ins_ns); + } + } + }; + + const setupSSE = (name: string, namespace: string) => { + let { origin } = location; + if (process.env.NODE_ENV === 'development') { + origin = 'http://172.20.32.181:31213'; + } + const params = encodeURIComponent(`metadata.namespace=${namespace},metadata.name=${name}`); + const evtSource = new EventSource( + `${origin}/api/v1/realtimeStatus?listOptions.fieldSelector=${params}`, + { withCredentials: false }, + ); + evtSource.onmessage = (event) => { + const data = event?.data; + if (!data) { + return; + } + const dataJson = parseJsonText(data); + if (dataJson) { + const nodes = dataJson?.result?.object?.status?.nodes; + if (nodes) { + const statusData = Object.values(nodes).find((node: any) => + node.displayName.startsWith('auto-ml'), + ) as NodeStatus; + if (statusData) { + setInstanceInfo((prev) => ({ + ...prev!, + nodeStatus: statusData, + })); + + // 实验结束,关闭 SSE + if ( + statusData.phase !== ExperimentStatus.Pending && + statusData.phase !== ExperimentStatus.Running + ) { + closeSSE(); + getExperimentInsInfo(true); + } + } + } + } + }; + evtSource.onerror = (error) => { + console.error('SSE error: ', error); + }; + + evtSourceRef.current = evtSource; + }; + + const closeSSE = () => { + if (evtSourceRef.current) { + evtSourceRef.current.close(); + evtSourceRef.current = null; + } + }; + + const basicTabItems = [ + { + key: TabKeys.Params, + label: '基本信息', + icon: , + children: ( + + ), + }, + { + key: TabKeys.Log, + label: '日志', + icon: , + children: ( +
+ {instanceInfo && instanceInfo.nodeStatus && ( + + )} +
+ ), + }, + ]; + + const resultTabItems = [ + { + key: TabKeys.Result, + label: '实验结果', + icon: , + children: ( + + ), + }, + { + key: TabKeys.History, + label: 'Trial 列表', + icon: , + children: ( + + ), + }, + ]; + + const tabItems = + instanceInfo?.status === ExperimentStatus.Succeeded + ? [...basicTabItems, ...resultTabItems] + : basicTabItems; + + return ( +
+ +
+ ); +} + +export default AutoMLInstance; diff --git a/react-ui/src/pages/HyperParameter/List/index.tsx b/react-ui/src/pages/HyperParameter/List/index.tsx new file mode 100644 index 00000000..5ebfcde9 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/List/index.tsx @@ -0,0 +1,13 @@ +/* + * @Author: 赵伟 + * @Date: 2024-04-16 13:58:08 + * @Description: 超参数自动寻优 + */ + +import ExperimentList, { ExperimentListType } from '@/pages/AutoML/components/ExperimentList'; + +function HyperParameter() { + return ; +} + +export default HyperParameter; diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/BasicConfig.tsx b/react-ui/src/pages/HyperParameter/components/CreateForm/BasicConfig.tsx new file mode 100644 index 00000000..8829f12a --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/BasicConfig.tsx @@ -0,0 +1,54 @@ +import SubAreaTitle from '@/components/SubAreaTitle'; +import { Col, Form, Input, Row } from 'antd'; + +function BasicConfig() { + return ( + <> + + +
+ + + + + + + + + + + + + + ); +} + +export default BasicConfig; diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx b/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx new file mode 100644 index 00000000..1609328c --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx @@ -0,0 +1,608 @@ +import CodeSelect from '@/components/CodeSelect'; +import KFIcon from '@/components/KFIcon'; +import ResourceSelect, { + ResourceSelectorType, + requiredValidator, +} from '@/components/ResourceSelect'; +import SubAreaTitle from '@/components/SubAreaTitle'; +import { hyperParameterOptimizedModeOptions } from '@/enums'; +import { useComputingResource } from '@/hooks/resource'; +import { isEmpty } from '@/utils'; +import { modalConfirm } from '@/utils/ui'; +import { MinusCircleOutlined, PlusCircleOutlined, QuestionCircleOutlined } from '@ant-design/icons'; +import { + Button, + Col, + Flex, + Form, + Input, + InputNumber, + Radio, + Row, + Select, + Tooltip, + Typography, +} from 'antd'; +import { isEqual } from 'lodash'; +import PopParameterRange from './PopParameterRange'; +import styles from './index.less'; +import { + axParameterOptions, + parameterOptions, + schedulerAlgorithms, + searchAlgorithms, + type FormParameter, +} from './utils'; + +const parameterTooltip = `uniform(low, high) + 在 low 和 high 之间均匀采样浮点数 + + quniform(low, high, q) + 在 low 和 high 之间均匀采样浮点数,四舍五入到 q 的倍数 + + loguniform(low, high) + 在 low 和 high 之间均匀采样浮点数,对数空间采样 + + qloguniform(low, high, q) + 在 low 和 high 之间均匀采样浮点数,对数空间采样并四舍五入到 q 的倍数 + + randn(m, s) + 在均值为 m,方差为 s 的正态分布中进行随机浮点数抽样 + + qrandn(m, s, q) + 在均值为 m,方差为 s 的正态分布中进行随机浮点数抽样,四舍五入到 q 的倍数 + + randint(low, high) + 在 low(包括)到 high(不包括)之间均匀采样整数 + + qrandint(low, high, q) + 在 low(包括)到 high(不包括)之间均匀采样整数,四舍五入到 q 的倍数(包括 high) + + lograndint(low, high) + 在 low(包括)到 high(不包括)之间对数空间上均匀采样整数 + + qlograndint(low, high, q) + 在 low(包括)到 high(不包括)之间对数空间上均匀采样整数,并四舍五入到 q 的倍数 + + choice + 从指定的选项中采样一个选项 + + grid + 对选项进行网格搜索,每个值都将被采样 +`; + +const axParameterTooltip = `fixed + 固定取值 + + range(low, high) + 在 low 和 high 范围内采样取值 + + choice + 从指定的选项中采样一个选项 + `; + +function ExecuteConfig() { + const form = Form.useFormInstance(); + const searchAlgorithm = Form.useWatch('search_alg', form); + const paramsTypeOptions = searchAlgorithm === 'Ax' ? axParameterOptions : parameterOptions; + const paramsTypeTooltip = searchAlgorithm === 'Ax' ? axParameterTooltip : parameterTooltip; + const [resourceStandardList, filterResourceStandard] = useComputingResource(); + + const handleSearchAlgorithmChange = (value: string) => { + if ( + (value === 'Ax' && searchAlgorithm !== 'Ax') || + (value !== 'Ax' && searchAlgorithm === 'Ax') + ) { + form.setFieldValue('parameters', [{ name: '' }]); + } + }; + + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {({ getFieldValue }) => { + const schedulerAlgorithm = getFieldValue('scheduler'); + if (schedulerAlgorithm === 'ASHA' || schedulerAlgorithm === 'HyperBand') { + return ( + + + + + + + + ); + } else if (schedulerAlgorithm === 'MedianStopping') { + return ( + + + + + + + + ); + } + return null; + }} + + + + {(fields, { add, remove }) => ( + <> + + + + + +
+ +
参数名称
+
+ 参数类型 + + + +
+
取值范围
+
操作
+
+ + {fields.map(({ key, name, ...restField }, index) => ( + + { + if (!value) { + return Promise.reject(new Error('请输入参数名称')); + } + // 判断不能重名 + const list = form + .getFieldValue('parameters') + .filter( + (item: FormParameter | undefined) => + item !== undefined && item !== null, + ); + + const names = list.map((item: FormParameter) => item.name); + if (new Set(names).size !== names.length) { + return Promise.reject(new Error('名称不能重复')); + } + return Promise.resolve(); + }, + }, + ]} + > + + + + form.validateFields(['points_to_evaluate'])} + /> + + ))} +
+
+ + {index === fields.length - 1 && ( + + )} +
+ + ))} + + + + )} + + ); + }} + + + + + + + + + + + + + + + + + + + + + + + + + + {index === fields.length - 1 && ( + + )} + + + ))} + {fields.length === 0 && ( + + + + )} + + )} + + + ) : ( + + {formOptions.map((item, index) => { + return ( + <> + + + + {index !== formOptions.length - 1 && ( + + {index === 0 ? '-' : ' '} + + )} + + ); + })} + + )} + + + + + + ); +} + +export default ParameterRange; diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.less b/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.less new file mode 100644 index 00000000..92080c3e --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.less @@ -0,0 +1,83 @@ +.parameter-range { + border-radius: 18px; + box-shadow: 0px 3px 10px rgba(22, 100, 255, 0.15); + :global { + .ant-popover-content { + .ant-popover-inner { + width: 400px; + padding: 20px 20px 12px; + background-image: url(@/assets/img/popover-bg.png); + background-repeat: no-repeat; + background-position: top left; + background-size: 100% auto; + } + .ant-popconfirm-description { + margin-top: 20px; + } + + .ant-popconfirm-buttons { + display: none; + } + } + } + + &__input { + display: flex; + align-items: center; + width: 100%; + min-height: 46px; + padding: 10px 11px; + color: @text-color; + font-size: @font-size-input-lg; + line-height: 1.5; + background-color: white; + border: 1px solid #d9d9d9; + border-radius: 8px; + cursor: pointer; + + &__text { + flex: 1; + margin-right: 10px; + } + + &__icon { + flex: none; + } + + &:hover { + border-color: #4086ff; + } + + &:hover &__icon { + color: #4086ff; + } + + &&--disabled { + background-color: rgba(0, 0, 0, 0.04); + border-color: rgba(0, 0, 0, 0.04) !important; + cursor: not-allowed; + } + + &&--empty { + color: @text-placeholder-color; + } + + &&--disabled &__icon { + color: #aaaaaa !important; + } + } +} + +.parameter-range-title { + color: @text-color; + font-weight: 500; + font-size: @font-size-content; +} + +.parameter-range-title-icon { + color: @text-color-secondary; + + &:hover { + color: @text-color; + } +} diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.tsx b/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.tsx new file mode 100644 index 00000000..b5db36b9 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/PopParameterRange/index.tsx @@ -0,0 +1,105 @@ +import KFIcon from '@/components/KFIcon'; +import { isEmpty } from '@/utils'; +import { Flex, Popconfirm, Typography } from 'antd'; +import classNames from 'classnames'; +import { useEffect, useRef, useState } from 'react'; +import ParameterRange from '../ParameterRange'; +import { ParameterType } from '../utils'; +import styles from './index.less'; + +type ParameterRangeProps = { + type: ParameterType; + value?: any[]; + onChange?: (value: any[]) => void; +}; + +function PopParameterRange({ type, value, onChange }: ParameterRangeProps) { + const [open, setOpen] = useState(false); + const popconfirmRef = useRef(null); + const disabled = !type; + const jsonText = JSON.stringify(value); + + const handleClickOutside = (event: MouseEvent) => { + // 判断点击是否在 Popconfirm 内 + const popconfirmNode = document.getElementById('pop-parameter'); + if (popconfirmNode && !popconfirmNode.contains(event.target as Node)) { + setOpen(false); + } + }; + + useEffect(() => { + if (open) { + document.addEventListener('mousedown', handleClickOutside); + } else { + document.removeEventListener('mousedown', handleClickOutside); + } + // 清理事件监听器 + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [open]); + + const handleClick = () => { + if (!disabled) { + setOpen(true); + } + }; + + const handleCancel = () => { + setOpen(false); + }; + + const handleConfirm = (value: number[]) => { + onChange?.(value); + setOpen(false); + }; + + return ( +
+ } + disabled={disabled} + description={ + + } + overlayClassName={styles['parameter-range']} + icon={null} + open={open} + destroyTooltipOnHide + > +
+ + {jsonText ?? '请选择'} + + +
+
+
+ ); +} + +function PopconfirmTitle({ title, onClose }: { title: string; onClose: () => void }) { + return ( + + {title} + + + ); +} + +export default PopParameterRange; diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/index.less b/react-ui/src/pages/HyperParameter/components/CreateForm/index.less new file mode 100644 index 00000000..06bbd5b7 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/index.less @@ -0,0 +1,145 @@ +.metrics-weight { + margin-bottom: 20px; + + &:last-child { + margin-bottom: 0; + } +} + +.add-weight { + margin-bottom: 0 !important; + + // 增加样式权重 + & &__button { + border-color: .addAlpha(@primary-color, 0.5) []; + box-shadow: none !important; + &:hover { + border-style: solid; + } + } +} + +.hyper-parameter { + width: 83.33%; + margin-bottom: 20px; + border: 1px solid rgba(234, 234, 234, 0.8); + border-radius: 4px; + &__header { + height: 50px; + padding-left: 8px; + color: @text-color; + font-size: @font-size; + background: #f8f8f9; + border-radius: 4px 4px 0px 0px; + + &__name, + &__type, + &__space { + flex: 1; + min-width: 0; + margin-right: 15px; + + &::before { + display: inline-block; + color: @error-color; + font-size: 14px; + font-family: SimSun, sans-serif; + line-height: 1; + content: '*'; + margin-inline-end: 4px; + } + + :global { + .anticon-question-circle { + vertical-align: middle; + cursor: help; + } + } + } + + &__tooltip { + max-width: 600px; + :global { + .ant-tooltip-inner { + max-height: 400px; + overflow-y: auto; + white-space: pre-line; + + &::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.5); + } + } + } + } + + &__operation { + flex: none; + width: 100px; + } + } + &__body { + padding: 8px; + border-bottom: 1px solid rgba(234, 234, 234, 0.8); + + &:last-child { + border-bottom: none; + } + + &__name, + &__type, + &__space { + flex: 1; + min-width: 0; + margin-right: 15px; + margin-bottom: 0 !important; + } + + &__operation { + display: flex; + flex: none; + align-items: center; + width: 100px; + height: 46px; + } + } + + &__add { + display: flex; + align-items: center; + justify-content: center; + padding: 15px 0; + } +} + +.run-parameter { + width: calc(41.66% + 126px); + margin-bottom: 20px; + + &__body { + flex: 1; + margin-right: 10px; + padding: 20px 20px 0; + border: 1px dashed #e0e0e0; + border-radius: 8px; + + :global { + .ant-form-item-label { + label { + width: calc(100% - 10px); + } + } + } + } + + &__operation { + display: flex; + flex: none; + align-items: center; + width: 100px; + } + + &__error { + margin-top: -20px; + color: @error-color; + } +} diff --git a/react-ui/src/pages/HyperParameter/components/CreateForm/utils.ts b/react-ui/src/pages/HyperParameter/components/CreateForm/utils.ts new file mode 100644 index 00000000..558637e9 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/CreateForm/utils.ts @@ -0,0 +1,228 @@ +export enum ParameterType { + Uniform = 'uniform', + QUniform = 'quniform', + LogUniform = 'loguniform', + QLogUniform = 'qloguniform', + Randn = 'randn', + QRandn = 'qrandn', + RandInt = 'randint', + QRandInt = 'qrandint', + LogRandInt = 'lograndint', + QLogRandInt = 'qlograndint', + Choice = 'choice', + Grid = 'grid', + Range = 'range', + Fixed = 'fixed', +} + +export const parameterOptions = [ + 'uniform', + 'quniform', + 'loguniform', + 'qloguniform', + 'randn', + 'qrandn', + 'randint', + 'qrandint', + 'lograndint', + 'qlograndint', + 'choice', + 'grid', +].map((name) => ({ + label: name, + value: name, +})); + +export const axParameterOptions = ['fixed', 'range', 'choice'].map((name) => ({ + label: name, + value: name, +})); + +export const parameterTooltip: Record = { + [ParameterType.Uniform]: '在 low 和 high 之间均匀采样浮点数', + [ParameterType.QUniform]: '在 low 和 high 之间均匀采样浮点数,四舍五入到 q 的倍数', + [ParameterType.LogUniform]: '在 low 和 high 之间均匀采样浮点数,对数空间采样', + [ParameterType.QLogUniform]: + '在 low 和 high 之间均匀采样浮点数,对数空间采样并四舍五入到 q 的倍数', + [ParameterType.Randn]: '在均值为 m,方差为 s 的正态分布中进行随机浮点数抽样', + [ParameterType.QRandn]: + '在均值为 m,方差为 s 的正态分布中进行随机浮点数抽样,四舍五入到 q 的倍数', + [ParameterType.RandInt]: '在 low(包括)到 high(不包括)之间均匀采样整数', + [ParameterType.QRandInt]: + '在 low(包括)到 high(不包括)之间均匀采样整数,四舍五入到 q 的倍数(包括 high)', + [ParameterType.LogRandInt]: '在 low(包括)到 high(不包括)之间对数空间上均匀采样整数', + [ParameterType.QLogRandInt]: + '在 low(包括)到 high(不包括)之间对数空间上均匀采样整数,并四舍五入到 q 的倍数', + [ParameterType.Choice]: '从指定的选项中采样一个选项', + [ParameterType.Grid]: '对选项进行网格搜索,每个值都将被采样', + [ParameterType.Range]: '在 low 和 high 范围内采样取值', + [ParameterType.Fixed]: '固定取值', +}; + +export type ParameterData = { + label: string; + name: string; + value?: number; +}; + +// 参数表单数据 +export type FormParameter = { + name: string; // 参数名称 + type: ParameterType; // 参数类型 + range: any; // 参数值 + [key: string]: any; +}; + +export const getFormOptions = (type?: ParameterType, value?: number[]): ParameterData[] => { + const numbers = + value?.map((item) => { + const num = Number(item); + if (isNaN(num)) { + return undefined; + } + return num; + }) ?? []; + switch (type) { + case ParameterType.Uniform: + case ParameterType.LogUniform: + case ParameterType.RandInt: + case ParameterType.LogRandInt: + case ParameterType.Range: + return [ + { + name: 'low', + label: '最小值', + value: numbers?.[0], + }, + { + name: 'high', + label: '最大值', + value: numbers?.[1], + }, + ]; + case ParameterType.QUniform: + case ParameterType.QLogUniform: + case ParameterType.QRandInt: + case ParameterType.QLogRandInt: + return [ + { + name: 'low', + label: '最小值', + value: numbers?.[0], + }, + { + name: 'high', + label: '最大值', + value: numbers?.[1], + }, + { + name: 'q', + label: '间隔', + value: numbers?.[2], + }, + ]; + case ParameterType.Randn: + return [ + { + name: 'm', + label: '均值', + value: numbers?.[0], + }, + { + name: 's', + label: '方差', + value: numbers?.[1], + }, + ]; + case ParameterType.QRandn: + return [ + { + name: 'm', + label: '均值', + value: numbers?.[0], + }, + { + name: 's', + label: '方差', + value: numbers?.[1], + }, + { + name: 'q', + label: '间隔', + value: numbers?.[2], + }, + ]; + case ParameterType.Fixed: + return [ + { + name: 'value', + label: '值', + value: numbers?.[0], + }, + ]; + default: + return []; + } +}; + +export const getReqParamName = (type: ParameterType) => { + if (type === ParameterType.Fixed) { + return 'value'; + } else if (type === ParameterType.Choice || type === ParameterType.Grid) { + return 'values'; + } else { + return 'bounds'; + } +}; + +// 搜索算法 +export const searchAlgorithms = [ + { + label: 'HyperOpt(分布式异步超参数优化)', + value: 'HyperOpt', + }, + { + label: 'HEBO(异方差进化贝叶斯优化)', + value: 'HEBO', + }, + { + label: 'BayesOpt(贝叶斯优化)', + value: 'BayesOpt', + }, + { + label: 'Optuna', + value: 'Optuna', + }, + { + label: 'ZOOpt', + value: 'ZOOpt', + }, + { + label: 'Ax', + value: 'Ax', + }, +]; + +// 调度算法 +export const schedulerAlgorithms = [ + { + label: 'ASHA(异步连续减半)', + value: 'ASHA', + }, + { + label: 'HyperBand(HyperBand 早停算法)', + value: 'HyperBand', + }, + { + label: 'MedianStopping(中值停止规则)', + value: 'MedianStopping', + }, + { + label: 'PopulationBased(基于种群训练)', + value: 'PopulationBased', + }, + { + label: 'PB2(Population Based Bandits)', + value: 'PB2', + }, +]; diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less new file mode 100644 index 00000000..beac2a8a --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less @@ -0,0 +1,14 @@ +.experiment-history { + height: calc(100% - 10px); + margin-top: 10px; + &__content { + height: 100%; + padding: 20px @content-padding; + background-color: white; + border-radius: 10px; + + &__table { + height: 100%; + } + } +} diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx new file mode 100644 index 00000000..e95ccd42 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx @@ -0,0 +1,132 @@ +import { getFileReq } from '@/services/file'; +import { to } from '@/utils/promise'; +import tableCellRender from '@/utils/table'; +import { Table, type TableProps } from 'antd'; +import classNames from 'classnames'; +import { useEffect, useState } from 'react'; +import styles from './index.less'; + +type ExperimentHistoryProps = { + fileUrl?: string; + isClassification: boolean; +}; + +type TableData = { + id?: string; + accuracy?: number; + duration?: number; + train_loss?: number; + status?: string; + feature?: string; + althorithm?: string; +}; + +function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps) { + const [tableData, setTableData] = useState([]); + useEffect(() => { + if (fileUrl) { + getHistoryFile(); + } + }, [fileUrl]); + + // 获取实验运行历史记录 + const getHistoryFile = async () => { + const [res] = await to(getFileReq(fileUrl)); + if (res) { + const data: any[] = res.data; + const list: TableData[] = data.map((item) => { + return { + id: item[0]?.[0], + accuracy: item[1]?.[5]?.accuracy, + duration: item[1]?.[5]?.duration, + train_loss: item[1]?.[5]?.train_loss, + status: item[1]?.[2]?.['__enum__']?.split('.')?.[1], + }; + }); + list.forEach((item) => { + if (!item.id) return; + const config = (res as any).configs?.[item.id]; + item.feature = config?.['feature_preprocessor:__choice__']; + item.althorithm = isClassification + ? config?.['classifier:__choice__'] + : config?.['regressor:__choice__']; + }); + setTableData(list); + } + }; + + const columns: TableProps['columns'] = [ + { + title: 'ID', + dataIndex: 'id', + key: 'id', + width: 80, + render: tableCellRender(false), + }, + { + title: '准确率', + dataIndex: 'accuracy', + key: 'accuracy', + render: tableCellRender(true), + ellipsis: { showTitle: false }, + }, + { + title: '耗时', + dataIndex: 'duration', + key: 'duration', + render: tableCellRender(true), + ellipsis: { showTitle: false }, + }, + { + title: '训练损失', + dataIndex: 'train_loss', + key: 'train_loss', + render: tableCellRender(true), + ellipsis: { showTitle: false }, + }, + { + title: '特征处理', + dataIndex: 'feature', + key: 'feature', + render: tableCellRender(true), + ellipsis: { showTitle: false }, + }, + { + title: '算法', + dataIndex: 'althorithm', + key: 'althorithm', + render: tableCellRender(true), + ellipsis: { showTitle: false }, + }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + width: 120, + render: tableCellRender(false), + }, + ]; + + return ( +
+
+
+
+ + + + ); +} + +export default ExperimentHistory; diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.less b/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.less new file mode 100644 index 00000000..e69de29b diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx b/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx new file mode 100644 index 00000000..e69de29b diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less b/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less new file mode 100644 index 00000000..342817c3 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less @@ -0,0 +1,52 @@ +.experiment-result { + height: calc(100% - 10px); + margin-top: 10px; + padding: 20px @content-padding; + overflow-y: auto; + background-color: white; + border-radius: 10px; + + &__download { + padding-top: 16px; + padding-bottom: 16px; + + padding-left: @content-padding; + color: @text-color; + font-size: 13px; + background-color: #f8f8f9; + border-radius: 4px; + + &__btn { + display: block; + height: 36px; + margin-top: 15px; + font-size: 14px; + } + } + + &__text { + white-space: pre-wrap; + } + + &__images { + display: flex; + align-items: flex-start; + width: 100%; + overflow-x: auto; + + :global { + .ant-image { + margin-right: 20px; + + &:last-child { + margin-right: 0; + } + } + } + + &__item { + height: 248px; + border: 1px solid rgba(96, 107, 122, 0.3); + } + } +} diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx b/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx new file mode 100644 index 00000000..a826155d --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx @@ -0,0 +1,83 @@ +import InfoGroup from '@/components/InfoGroup'; +import { getFileReq } from '@/services/file'; +import { to } from '@/utils/promise'; +import { Button, Image } from 'antd'; +import { useEffect, useMemo, useState } from 'react'; +import styles from './index.less'; + +type ExperimentResultProps = { + fileUrl?: string; + imageUrl?: string; + modelPath?: string; +}; + +function ExperimentResult({ fileUrl, imageUrl, modelPath }: ExperimentResultProps) { + const [result, setResult] = useState(''); + + const images = useMemo(() => { + if (imageUrl) { + return imageUrl.split(',').map((item) => item.trim()); + } + return []; + }, [imageUrl]); + + useEffect(() => { + if (fileUrl) { + getResultFile(); + } + }, [fileUrl]); + + // 获取实验运行历史记录 + const getResultFile = async () => { + const [res] = await to(getFileReq(fileUrl)); + if (res) { + setResult(res as any as string); + } + }; + + return ( +
+ +
{result}
+
+ +
+ + console.log(`current index: ${current}, prev index: ${prev}`), + }} + > + {images.map((item) => ( + + ))} + +
+
+ {modelPath && ( +
+ 文件名 + save_model.joblib + +
+ )} +
+ ); +} + +export default ExperimentResult; diff --git a/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.less b/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.less new file mode 100644 index 00000000..f365aa66 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.less @@ -0,0 +1,13 @@ +.hyper-parameter-basic { + height: 100%; + padding: 20px @content-padding; + overflow-y: auto; + background-color: white; + border-radius: 10px; + + :global { + .kf-basic-info__item__value__text { + white-space: pre; + } + } +} diff --git a/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx b/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx new file mode 100644 index 00000000..817d8418 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx @@ -0,0 +1,226 @@ +import ConfigInfo, { type BasicInfoData } from '@/components/ConfigInfo'; +import { hyperParameterOptimizedMode } from '@/enums'; +import { useComputingResource } from '@/hooks/resource'; +import { experimentStatusInfo } from '@/pages/Experiment/status'; +import { + schedulerAlgorithms, + searchAlgorithms, +} from '@/pages/HyperParameter/components/CreateForm/utils'; +import { HyperParameterData } from '@/pages/HyperParameter/types'; +import { type NodeStatus } from '@/types'; +import { elapsedTime } from '@/utils/date'; +import { + formatCodeConfig, + formatDataset, + formatDate, + formatEnum, + formatMirror, + formatModel, +} from '@/utils/format'; +import { Flex } from 'antd'; +import classNames from 'classnames'; +import { useMemo } from 'react'; +import ParameterInfo from '../ParameterInfo'; +import styles from './index.less'; + +// 格式化优化方向 +const formatOptimizeMode = (value: string) => { + return value === hyperParameterOptimizedMode.Max ? '越大越好' : '越小越好'; +}; + +type HyperParameterBasicProps = { + info?: HyperParameterData; + className?: string; + isInstance?: boolean; + runStatus?: NodeStatus; +}; + +function HyperParameterBasic({ + info, + className, + runStatus, + isInstance = false, +}: HyperParameterBasicProps) { + const getResourceDescription = useComputingResource()[2]; + + // 格式化资源规格 + const formatResource = (resource?: string) => { + if (!resource) { + return undefined; + } + + return getResourceDescription(resource); + }; + + const basicDatas: BasicInfoData[] = useMemo(() => { + if (!info) { + return []; + } + + return [ + { + label: '实验名称', + value: info.name, + }, + { + label: '实验描述', + value: info.description, + }, + { + label: '创建人', + value: info.create_by, + }, + { + label: '创建时间', + value: info.create_time, + format: formatDate, + }, + { + label: '更新时间', + value: info.update_time, + format: formatDate, + }, + ]; + }, [info]); + + const configDatas: BasicInfoData[] = useMemo(() => { + if (!info) { + return []; + } + return [ + { + label: '代码', + value: info.code, + format: formatCodeConfig, + }, + { + label: '主函数代码文件', + value: info.main_py, + }, + { + label: '镜像', + value: info.image, + format: formatMirror, + }, + { + label: '数据集', + value: info.dataset, + format: formatDataset, + }, + { + label: '模型', + value: info.model, + format: formatModel, + }, + { + label: '总实验次数', + value: info.num_samples, + }, + { + label: '搜索算法', + value: info.search_alg, + format: formatEnum(searchAlgorithms), + }, + { + label: '调度算法', + value: info.scheduler, + format: formatEnum(schedulerAlgorithms), + }, + { + label: '单次试验最大时间', + value: info.max_t, + }, + { + label: '最小试验数', + value: info.min_samples_required, + }, + { + label: '优化方向', + value: info.mode, + format: formatOptimizeMode, + }, + { + label: '指标', + value: info.metric, + }, + { + label: '资源规格', + value: info.resource, + format: formatResource, + }, + ]; + }, [info]); + + const instanceDatas = useMemo(() => { + if (!runStatus) { + return []; + } + + return [ + { + label: '启动时间', + value: formatDate(runStatus.startedAt), + ellipsis: true, + }, + { + label: '执行时长', + value: elapsedTime(runStatus.startedAt, runStatus.finishedAt), + ellipsis: true, + }, + { + label: '状态', + value: ( + + +
+ {experimentStatusInfo[runStatus?.phase]?.label} +
+
+ ), + ellipsis: true, + }, + ]; + }, [runStatus]); + + return ( +
+ {isInstance && runStatus && ( + + )} + {!isInstance && ( + + )} + + {info && } + +
+ ); +} + +export default HyperParameterBasic; diff --git a/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.less b/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.less new file mode 100644 index 00000000..81d6fd56 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.less @@ -0,0 +1,7 @@ +.parameter-info { + &__title { + margin: 20px 0; + color: @text-color-secondary; + font-size: @font-size; + } +} diff --git a/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.tsx b/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.tsx new file mode 100644 index 00000000..0e5ced40 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/ParameterInfo/index.tsx @@ -0,0 +1,106 @@ +import { + getReqParamName, + type FormParameter, +} from '@/pages/HyperParameter/components/CreateForm/utils'; +import { HyperParameterData } from '@/pages/HyperParameter/types'; +import tableCellRender, { TableCellValueType } from '@/utils/table'; +import { Table, Tooltip, type TableProps } from 'antd'; +import { useMemo } from 'react'; +import styles from './index.less'; + +type ParameterInfoProps = { + info: HyperParameterData; +}; + +function ParameterInfo({ info }: ParameterInfoProps) { + const parameters = useMemo(() => { + if (!info.parameters) { + return []; + } + return info.parameters.map((item) => { + const paramName = getReqParamName(item.type); + const range = item[paramName]; + return { + ...item, + range, + }; + }); + }, [info]); + + const runParameters = useMemo(() => { + if (!info.points_to_evaluate) { + return []; + } + return info.points_to_evaluate.map((item, index) => ({ + ...item, + id: index, + })); + }, [info]); + + const columns: TableProps['columns'] = [ + { + title: '参数名称', + dataIndex: 'name', + key: 'type', + width: '40%', + render: tableCellRender(true), + ellipsis: { showTitle: false }, + }, + { + title: '参数类型', + dataIndex: 'type', + key: 'type', + width: '20%', + render: tableCellRender(true), + ellipsis: { showTitle: false }, + }, + { + title: '取值范围', + dataIndex: 'range', + key: 'range', + width: '40%', + render: tableCellRender(true, TableCellValueType.Custom, { + format: (value) => { + return JSON.stringify(value); + }, + }), + ellipsis: { showTitle: false }, + }, + ]; + + const runColumns: TableProps>['columns'] = + runParameters.length > 0 + ? parameters.map(({ name }) => { + return { + title: ( + + {name} + + ), + dataIndex: name, + key: name, + width: 150, + render: tableCellRender(true), + ellipsis: { showTitle: false }, + }; + }) + : []; + + return ( +
+
参数
+
+
手动运行参数
+
+ + ); +} + +export default ParameterInfo; diff --git a/react-ui/src/pages/HyperParameter/types.ts b/react-ui/src/pages/HyperParameter/types.ts new file mode 100644 index 00000000..1e610f20 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/types.ts @@ -0,0 +1,64 @@ +import { type ParameterInputObject } from '@/components/ResourceSelect'; +import { type NodeStatus } from '@/types'; +import { type FormParameter } from './components/CreateForm/utils'; + +// 操作类型 +export enum OperationType { + Create = 'Create', // 创建 + Update = 'Update', // 更新 +} + +// 表单数据 +export type FormData = { + name: string; // 实验名称 + description: string; // 实验描述 + code: ParameterInputObject; // 代码 + dataset: ParameterInputObject; // 数据集 + model: ParameterInputObject; // 模型 + image: ParameterInputObject; // 镜像 + main_py: string; // 主函数代码文件 + metric: string; // 指标 + mode: string; // 优化方向 + search_alg?: string; // 搜索算法 + scheduler?: string; // 调度算法 + num_samples: number; // 总实验次数 + max_t: number; // 单次试验最大时间 + min_samples_required: number; // 计算中位数的最小试验数 + resource: string; // 资源规格 + parameters: FormParameter[]; + points_to_evaluate: { [key: string]: any }[]; +}; + +export type HyperParameterData = { + id: number; + progress: number; + run_state: string; + state: number; + create_by?: string; + create_time?: string; + update_by?: string; + update_time?: string; + status_list: string; // 最近五次运行状态 +} & FormData; + +// 自动机器学习实验实例 +export type AutoMLInstanceData = { + id: number; + auto_ml_id: number; + result_path: string; + model_path: string; + img_path: string; + run_history_path: string; + state: number; + status: string; + node_status: string; + node_result: string; + param: string; + source: string | null; + argo_ins_name: string; + argo_ins_ns: string; + create_time: string; + update_time: string; + finish_time: string; + nodeStatus?: NodeStatus; +}; diff --git a/react-ui/src/pages/Mirror/Create/index.tsx b/react-ui/src/pages/Mirror/Create/index.tsx index c4e89ce2..01a22031 100644 --- a/react-ui/src/pages/Mirror/Create/index.tsx +++ b/react-ui/src/pages/Mirror/Create/index.tsx @@ -30,13 +30,13 @@ type FormData = { const mirrorRadioItems: KFRadioItem[] = [ { - key: CommonTabKeys.Public, title: '基于公网镜像', + value: CommonTabKeys.Public, icon: , }, { - key: CommonTabKeys.Private, title: '本地上传', + value: CommonTabKeys.Private, icon: , }, ]; diff --git a/react-ui/src/pages/ModelDeployment/CreateVersion/index.less b/react-ui/src/pages/ModelDeployment/CreateVersion/index.less index 0460788f..bf7f7f9d 100644 --- a/react-ui/src/pages/ModelDeployment/CreateVersion/index.less +++ b/react-ui/src/pages/ModelDeployment/CreateVersion/index.less @@ -30,7 +30,7 @@ } .ant-btn-variant-text:disabled { - color: rgba(0, 0, 0, 0.25); + color: @text-disabled-color; } .ant-btn-variant-text { diff --git a/react-ui/src/pages/ModelDeployment/CreateVersion/index.tsx b/react-ui/src/pages/ModelDeployment/CreateVersion/index.tsx index b8de0bcf..38171380 100644 --- a/react-ui/src/pages/ModelDeployment/CreateVersion/index.tsx +++ b/react-ui/src/pages/ModelDeployment/CreateVersion/index.tsx @@ -432,7 +432,14 @@ function CreateServiceVersion() { {(fields, { add, remove }) => ( <> {fields.map(({ key, name, ...restField }, index) => ( - + @@ -519,7 +527,7 @@ function CreateServiceVersion() { - + diff --git a/react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx b/react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx index 9cd4ebfb..b4cfb84a 100644 --- a/react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx +++ b/react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx @@ -401,7 +401,12 @@ function ServiceInfo() { image={require('@/assets/img/mirror-basic.png')} style={{ marginBottom: '26px', flex: 'none' }} > - + - +
diff --git a/react-ui/src/pages/ModelDeployment/components/BasicInfo/index.tsx b/react-ui/src/pages/ModelDeployment/components/BasicInfo/index.tsx deleted file mode 100644 index 32497e13..00000000 --- a/react-ui/src/pages/ModelDeployment/components/BasicInfo/index.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import LabelValue from '@/components/LabelValue'; -import { useComputingResource } from '@/hooks/resource'; -import { ServiceVersionData } from '@/pages/ModelDeployment/types'; -import { getGitUrl } from '@/utils'; -import { formatDate } from '@/utils/date'; -import { Link } from '@umijs/max'; -import { Col, Row } from 'antd'; -import ServiceRunStatusCell from '../ModelDeployStatusCell'; -import styles from './index.less'; - -type BasicInfoProps = { - info?: ServiceVersionData; -}; - -function BasicInfo({ info }: BasicInfoProps) { - const getResourceDescription = useComputingResource()[2]; - - // 格式化环境变量 - const formatEnvText = () => { - if (!info?.env_variables || Object.keys(info.env_variables).length === 0) { - return '--'; - } - const env = info.env_variables; - return Object.entries(env) - .map(([key, value]) => `${key}: ${value}`) - .join('\n'); - }; - - const formatCodeConfig = () => { - if (info && info.code_config && info.code_config.code_path) { - const { code_path, branch } = info.code_config; - const url = getGitUrl(code_path, branch); - return ( - - {info?.code_config?.show_value} - - ); - } - return '--'; - }; - - const formatResource = () => { - if (info && info.resource) { - return getResourceDescription(info.resource); - } - return undefined; - }; - - const formatModel = () => { - if (info && info.model) { - const model = info.model; - const path = `/dataset/model/info/${model.id}?version=${model.version}&name=${model.name}&owner=${model.owner}&identifier=${model.identifier}`; - return {info?.model?.show_value}; - } - return undefined; - }; - - return ( -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -} - -export default BasicInfo; diff --git a/react-ui/src/pages/ModelDeployment/components/BasicInfo/index.less b/react-ui/src/pages/ModelDeployment/components/VersionBasicInfo/index.less similarity index 100% rename from react-ui/src/pages/ModelDeployment/components/BasicInfo/index.less rename to react-ui/src/pages/ModelDeployment/components/VersionBasicInfo/index.less diff --git a/react-ui/src/pages/ModelDeployment/components/VersionBasicInfo/index.tsx b/react-ui/src/pages/ModelDeployment/components/VersionBasicInfo/index.tsx new file mode 100644 index 00000000..4eacca04 --- /dev/null +++ b/react-ui/src/pages/ModelDeployment/components/VersionBasicInfo/index.tsx @@ -0,0 +1,119 @@ +import BasicInfo, { type BasicInfoData } from '@/components/BasicInfo'; +import { ServiceRunStatus } from '@/enums'; +import { useComputingResource } from '@/hooks/resource'; +import { ServiceVersionData } from '@/pages/ModelDeployment/types'; +import { formatDate } from '@/utils/date'; +import { formatCodeConfig, formatModel } from '@/utils/format'; +import { Flex } from 'antd'; +import ModelDeployStatusCell from '../ModelDeployStatusCell'; + +type BasicInfoProps = { + info?: ServiceVersionData; +}; + +// 格式化状态 +const formatStatus = (status?: ServiceRunStatus) => { + if (!status) { + return undefined; + } + + return ( + + {ModelDeployStatusCell(status)} + + ); +}; + +// 格式化环境变量 +const formatEnvText = (env?: Record) => { + if (!env || Object.keys(env).length === 0) { + return undefined; + } + + return Object.entries(env).map(([key, value]) => ({ + value: `${key}: ${value}`, + })); +}; + +function VersionBasicInfo({ info }: BasicInfoProps) { + const getResourceDescription = useComputingResource()[2]; + + // 格式化资源规格 + const formatResource = (resource?: string) => { + if (!resource) { + return undefined; + } + + return getResourceDescription(resource); + }; + + const datas: BasicInfoData[] = [ + { + label: '服务名称', + value: info?.service_name, + }, + { + label: '版本名称', + value: info?.version, + }, + { + label: '代码配置', + value: info?.code_config, + format: formatCodeConfig, + }, + { + label: '镜像', + value: info?.image, + }, + { + label: '状态', + value: info?.run_state, + format: formatStatus, + }, + { + label: '模型', + value: info?.model, + format: formatModel, + }, + { + label: '资源规格', + value: info?.resource, + format: formatResource, + }, + { + label: '挂载路径', + value: info?.mount_path, + }, + { + label: 'API URL', + value: info?.url, + }, + { + label: '副本数量', + value: info?.replicas, + }, + { + label: '创建时间', + value: info?.create_time, + format: formatDate, + }, + { + label: '更新时间', + value: info?.update_time, + format: formatDate, + }, + { + label: '环境变量', + value: info?.env_variables, + format: formatEnvText, + }, + { + label: '描述', + value: info?.description, + }, + ]; + + return ; +} + +export default VersionBasicInfo; diff --git a/react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.less b/react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.less index 70de3494..f1935eb2 100644 --- a/react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.less +++ b/react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.less @@ -3,6 +3,7 @@ .title(@color, @background) { width: 100%; margin-bottom: 20px; + padding: 0 15px; color: @color; font-weight: 500; font-size: @font-size; diff --git a/react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.tsx b/react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.tsx index da88b2de..b6562237 100644 --- a/react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.tsx +++ b/react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.tsx @@ -165,7 +165,7 @@ function VersionCompareModal({ version1, version2, ...rest }: VersionCompareModa [styles['version-compare__left__text--different']]: isDifferent(key), })} > - + {isEmpty(text) ? '--' : text} @@ -183,7 +183,7 @@ function VersionCompareModal({ version1, version2, ...rest }: VersionCompareModa [styles['version-compare__right__text--different']]: isDifferent(key), })} > - + {isEmpty(text) ? '--' : text} diff --git a/react-ui/src/pages/Pipeline/components/CodeSelectorModal/index.less b/react-ui/src/pages/Pipeline/components/CodeSelectorModal/index.less deleted file mode 100644 index cb77da6d..00000000 --- a/react-ui/src/pages/Pipeline/components/CodeSelectorModal/index.less +++ /dev/null @@ -1,50 +0,0 @@ -.code-selector { - width: 100%; - height: 100%; - - &__search { - width: 100%; - } - - :global { - .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; - } - } - } - - &__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; - } -} diff --git a/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx b/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx index a7740e7b..de041c72 100644 --- a/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx +++ b/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx @@ -1,6 +1,11 @@ +import CodeSelectorModal from '@/components/CodeSelectorModal'; import KFIcon from '@/components/KFIcon'; import ParameterInput, { requiredValidator } from '@/components/ParameterInput'; import ParameterSelect from '@/components/ParameterSelect'; +import ResourceSelectorModal, { + ResourceSelectorType, + selectorTypeConfig, +} from '@/components/ResourceSelectorModal'; import SubAreaTitle from '@/components/SubAreaTitle'; import { CommonTabKeys } from '@/enums'; import { useComputingResource } from '@/hooks/resource'; @@ -17,12 +22,7 @@ import { INode } from '@antv/g6'; import { Button, Drawer, Form, Input, MenuProps, Select } from 'antd'; import { NamePath } from 'antd/es/form/interface'; import { forwardRef, useImperativeHandle, useState } from 'react'; -import CodeSelectorModal from '../CodeSelectorModal'; import PropsLabel from '../PropsLabel'; -import ResourceSelectorModal, { - ResourceSelectorType, - selectorTypeConfig, -} from '../ResourceSelectorModal'; import styles from './index.less'; const { TextArea } = Input; diff --git a/react-ui/src/pages/User/Login/login.tsx b/react-ui/src/pages/User/Login/login.tsx index c22f8be9..9ef18148 100644 --- a/react-ui/src/pages/User/Login/login.tsx +++ b/react-ui/src/pages/User/Login/login.tsx @@ -119,10 +119,10 @@ const Login = () => { draggable={false} alt="" /> - 复杂智能软件 + 智能材料科研平台
- 复杂智能软件 + 智能材料科研平台 {
欢迎登录 - 复杂智能软件 + 智能材料科研平台
账号登录
diff --git a/react-ui/src/components/RobotFrame/index.less b/react-ui/src/pages/Workspace/components/RobotFrame/index.less similarity index 100% rename from react-ui/src/components/RobotFrame/index.less rename to react-ui/src/pages/Workspace/components/RobotFrame/index.less diff --git a/react-ui/src/components/RobotFrame/index.tsx b/react-ui/src/pages/Workspace/components/RobotFrame/index.tsx similarity index 100% rename from react-ui/src/components/RobotFrame/index.tsx rename to react-ui/src/pages/Workspace/components/RobotFrame/index.tsx diff --git a/react-ui/src/pages/Workspace/components/WorkspaceIntro/index.less b/react-ui/src/pages/Workspace/components/WorkspaceIntro/index.less index 59a0cb5b..26a67688 100644 --- a/react-ui/src/pages/Workspace/components/WorkspaceIntro/index.less +++ b/react-ui/src/pages/Workspace/components/WorkspaceIntro/index.less @@ -37,7 +37,6 @@ } &__icon { - // width: 363px; height: 176px; } } diff --git a/react-ui/src/pages/Workspace/components/WorkspaceIntro/index.tsx b/react-ui/src/pages/Workspace/components/WorkspaceIntro/index.tsx index f3543705..81ab8f54 100644 --- a/react-ui/src/pages/Workspace/components/WorkspaceIntro/index.tsx +++ b/react-ui/src/pages/Workspace/components/WorkspaceIntro/index.tsx @@ -1,14 +1,16 @@ +import { Button } from 'antd'; import styles from './index.less'; function WorkspaceIntro() { return (
-
复杂智能软件
+
自主实验平台
- 复杂智能软件平台构建一套完整的版本迭代升级机制、开发与运行态版本依赖关系分析,以及整合开发部署和持续优化的一体化流程,涵盖数据管理、模型建模、服务开发和系统运行等关键环节,以实现高效、稳定的软件生命周期管理。 + 材料领域的自主实验系统是一种用于材料研究和开发的技术平台,它旨在提供实验数据收集、分析和可视化等功能, + 以支持材料工程师、科学家和研究人员在材料设计、性能评估和工艺优化方面的工作
- {/*
+
-
*/} +
{ content={'很抱歉,您访问的正在开发中,\n请耐心等待。'} hasFooter={true} buttonTitle="返回首页" - onRefresh={() => navigate('/')} + onButtonClick={() => navigate('/')} > ); }; diff --git a/react-ui/src/services/hyperParameter/index.js b/react-ui/src/services/hyperParameter/index.js new file mode 100644 index 00000000..c97e617d --- /dev/null +++ b/react-ui/src/services/hyperParameter/index.js @@ -0,0 +1,93 @@ +/* + * @Author: 赵伟 + * @Date: 2024-11-18 10:18:27 + * @Description: 超参数自动寻优请求 + */ + +import { request } from '@umijs/max'; + + +// 分页查询超参数自动寻优 +export function getRayListReq(params) { + return request(`/api/mmp/ray`, { + method: 'GET', + params, + }); +} + +// 查询超参数自动寻优详情 +export function getRayInfoReq(params) { + return request(`/api/mmp/ray/getRayDetail`, { + method: 'GET', + params, + }); +} + +// 新增超参数自动寻优 +export function addRayReq(data) { + return request(`/api/mmp/ray`, { + method: 'POST', + data, + }); +} + +// 编辑超参数自动寻优 +export function updateRayReq(data) { + return request(`/api/mmp/ray`, { + method: 'PUT', + data, + }); +} + +// 删除超参数自动寻优 +export function deleteRayReq(id) { + return request(`/api/mmp/ray/${id}`, { + method: 'DELETE', + }); +} + +// 运行超参数自动寻优 +export function runRayReq(id) { + return request(`/api/mmp/ray/run/${id}`, { + method: 'POST', + }); +} + +// ----------------------- 实验实例 ----------------------- +// 获取实验实例列表 +export function getRayInsListReq(params) { + return request(`/api/mmp/rayIns`, { + method: 'GET', + params, + }); +} + +// 查询实验实例详情 +export function getRayInsReq(id) { + return request(`/api/mmp/rayIns/${id}`, { + method: 'GET', + }); +} + +// 停止实验实例 +export function stopRayInsReq(id) { + return request(`/api/mmp/rayIns/${id}`, { + method: 'PUT', + }); +} + +// 删除实验实例 +export function deleteRayInsReq(id) { + return request(`/api/mmp/rayIns/${id}`, { + method: 'DELETE', + }); +} + +// 批量删除实验实例 +export function batchDeleteRayInsReq(data) { + return request(`/api/mmp/rayIns/batchDelete`, { + method: 'DELETE', + data + }); +} + diff --git a/react-ui/src/stories/BasicInfo.stories.tsx b/react-ui/src/stories/BasicInfo.stories.tsx new file mode 100644 index 00000000..80a0337c --- /dev/null +++ b/react-ui/src/stories/BasicInfo.stories.tsx @@ -0,0 +1,102 @@ +import BasicInfo from '@/components/BasicInfo'; +import { formatDate } from '@/utils/date'; +import { formatList } from '@/utils/format'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Button } from 'antd'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/BasicInfo', + component: BasicInfo, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + // args: { onClick: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +/** 一行两列 */ +export const Primary: Story = { + args: { + datas: [ + { label: '服务名称', value: '手写体识别' }, + { + label: '无数据', + value: '', + }, + { + label: '外部链接', + value: 'https://www.baidu.com/', + format: (value: string) => { + return { + value: '百度', + url: value, + }; + }, + }, + { + label: '内部链接', + value: 'https://www.baidu.com/', + format: () => { + return { + value: '实验', + link: '/pipeline/experiment/instance/1/1', + }; + }, + }, + { label: '日期', value: new Date(), format: formatDate }, + { label: '数组', value: ['a', 'b', 'c'], format: formatList }, + { + label: '带省略号', + value: '这是一个很长的字符串这是一个很长的字符串这是一个很长的字符串这是一个很长的字符串', + }, + { + label: '多行', + value: [ + { + label: '服务名称', + value: '手写体识别', + }, + { + label: '服务名称', + value: '人脸识别', + }, + ], + format: (value: any) => + value.map((item: any) => ({ + value: item.label + ':' + item.value, + })), + }, + { + label: '自定义组件', + value: ( + + ), + }, + ], + labelWidth: 80, + labelAlign: 'justify', + }, +}; + +/** 一行三列 */ +export const ThreeColumn: Story = { + args: { + ...Primary.args, + labelAlign: 'start', + threeColumns: true, + }, +}; diff --git a/react-ui/src/stories/BasicTableInfo.stories.tsx b/react-ui/src/stories/BasicTableInfo.stories.tsx new file mode 100644 index 00000000..82581f6a --- /dev/null +++ b/react-ui/src/stories/BasicTableInfo.stories.tsx @@ -0,0 +1,32 @@ +import BasicTableInfo from '@/components/BasicTableInfo'; +import type { Meta, StoryObj } from '@storybook/react'; +import * as BasicInfoStories from './BasicInfo.stories'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/BasicTableInfo', + component: BasicTableInfo, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + // args: { onClick: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + ...BasicInfoStories.Primary.args, + labelWidth: 100, + }, +}; diff --git a/react-ui/src/stories/CodeSelect.stories.tsx b/react-ui/src/stories/CodeSelect.stories.tsx new file mode 100644 index 00000000..b5520c96 --- /dev/null +++ b/react-ui/src/stories/CodeSelect.stories.tsx @@ -0,0 +1,75 @@ +import CodeSelect, { type ParameterInputValue } from '@/components/CodeSelect'; +import { useArgs } from '@storybook/preview-api'; +import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; +import { Col, Form, Row } from 'antd'; +import { http, HttpResponse } from 'msw'; +import { codeListData } from './mockData'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/CodeSelect', + component: CodeSelect, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + msw: { + handlers: [ + http.get('/api/mmp/codeConfig', () => { + return HttpResponse.json(codeListData); + }), + ], + }, + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + args: { onChange: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + canInput: false, + textArea: false, + size: 'large', + style: { width: 400 }, + }, + render: function Render(args) { + const [{ value }, updateArgs] = useArgs(); + function handleChange(value?: ParameterInputValue) { + updateArgs({ value: value }); + args.onChange?.(value); + } + + return ; + }, +}; + +export const InForm: Story = { + render: ({ onChange }) => { + return ( +
+ +
+ + + + + + + ); + }, +}; diff --git a/react-ui/src/stories/CodeSelectorModal.stories.tsx b/react-ui/src/stories/CodeSelectorModal.stories.tsx new file mode 100644 index 00000000..59026ec6 --- /dev/null +++ b/react-ui/src/stories/CodeSelectorModal.stories.tsx @@ -0,0 +1,90 @@ +import CodeSelectorModal from '@/components/CodeSelectorModal'; +import { openAntdModal } from '@/utils/modal'; +import { useArgs } from '@storybook/preview-api'; +import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; +import { Button } from 'antd'; +import { http, HttpResponse } from 'msw'; +import { codeListData } from './mockData'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/CodeSelectorModal', + component: CodeSelectorModal, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + layout: 'centered', + msw: { + handlers: [ + http.get('/api/mmp/codeConfig', () => { + return HttpResponse.json(codeListData); + }), + ], + }, + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + open: { + description: '对话框是否可见', + }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + args: { onCancel: fn(), onOk: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + open: false, + }, + render: function Render({ onOk, onCancel, ...args }) { + const [{ open }, updateArgs] = useArgs(); + function onClick() { + updateArgs({ open: true }); + } + function handleOk(res: any) { + updateArgs({ open: false }); + onOk?.(res); + } + + function handleCancel() { + updateArgs({ open: false }); + onCancel?.(); + } + + return ( + <> + + + + ); + }, +}; + +/** 通过 `openAntdModal` 函数打开 */ +export const OpenByFunction: Story = { + render: function Render(args) { + const handleClick = () => { + const { close } = openAntdModal(CodeSelectorModal, { + onOk: (res) => { + const { onOk } = args; + onOk?.(res); + close(); + }, + }); + }; + return ( + + ); + }, +}; diff --git a/react-ui/src/stories/Config.stories.tsx b/react-ui/src/stories/Config.stories.tsx new file mode 100644 index 00000000..0287d718 --- /dev/null +++ b/react-ui/src/stories/Config.stories.tsx @@ -0,0 +1,37 @@ +import ConfigInfo from '@/components/ConfigInfo'; +import type { Meta, StoryObj } from '@storybook/react'; +import * as BasicInfoStories from './BasicInfo.stories'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/ConfigInfo', + component: ConfigInfo, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + // args: { onClick: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + title: '基本信息', + datas: BasicInfoStories.Primary.args.datas, + labelAlign: 'start', + labelEllipsis: true, + threeColumns: true, + labelWidth: 80, + children:
I am a child element
, + }, +}; diff --git a/react-ui/src/stories/FullScreenFrame.stories.tsx b/react-ui/src/stories/FullScreenFrame.stories.tsx new file mode 100644 index 00000000..fa334ed1 --- /dev/null +++ b/react-ui/src/stories/FullScreenFrame.stories.tsx @@ -0,0 +1,32 @@ +import FullScreenFrame from '@/components/FullScreenFrame'; +import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/FullScreenFrame', + component: FullScreenFrame, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + args: { onLoad: fn(), onError: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + url: 'https://www.hao123.com/', + style: { height: '500px' }, + }, +}; diff --git a/react-ui/src/stories/IFramePage.stories.tsx b/react-ui/src/stories/IFramePage.stories.tsx new file mode 100644 index 00000000..3d1714e8 --- /dev/null +++ b/react-ui/src/stories/IFramePage.stories.tsx @@ -0,0 +1,31 @@ +import IFramePage, { IframePageType } from '@/components/IFramePage'; +import type { Meta, StoryObj } from '@storybook/react'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/IFramePage', + component: IFramePage, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + type: { control: 'select', options: Object.values(IframePageType) }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + // args: { onClick: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + type: IframePageType.GitLink, + style: { height: '500px' }, + }, +}; diff --git a/react-ui/src/stories/InfoGroup.stories.tsx b/react-ui/src/stories/InfoGroup.stories.tsx new file mode 100644 index 00000000..f3a6def8 --- /dev/null +++ b/react-ui/src/stories/InfoGroup.stories.tsx @@ -0,0 +1,31 @@ +import InfoGroup from '@/components/InfoGroup'; +import type { Meta, StoryObj } from '@storybook/react'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/InfoGroup', + component: InfoGroup, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + // args: { onClick: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + title: '基本信息', + children:
I am a child element
, + }, +}; diff --git a/react-ui/src/stories/KFEmpty.stories.tsx b/react-ui/src/stories/KFEmpty.stories.tsx new file mode 100644 index 00000000..fef1d5bf --- /dev/null +++ b/react-ui/src/stories/KFEmpty.stories.tsx @@ -0,0 +1,53 @@ +import KFEmpty, { EmptyType } from '@/components/KFEmpty'; +import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/KFEmpty', + component: KFEmpty, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + type: { control: 'radio', options: Object.values(EmptyType) }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + args: { onButtonClick: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Developing: Story = { + args: { + type: EmptyType.Developing, + title: '敬请期待~', + content: '很抱歉,您访问的正在开发中,\n请耐心等待。', + hasFooter: true, + buttonTitle: '返回首页', + }, +}; + +export const NoData: Story = { + args: { + type: EmptyType.NoData, + title: '暂无数据', + content: '很抱歉,没有搜索到您想要的内容\n建议刷新试试', + hasFooter: true, + }, +}; + +export const NotFound: Story = { + args: { + type: EmptyType.NotFound, + title: '404', + content: '很抱歉,您访问的页面地址有误,\n或者该页面不存在。', + hasFooter: true, + buttonTitle: '返回首页', + }, +}; diff --git a/react-ui/src/stories/KFIcon.stories.tsx b/react-ui/src/stories/KFIcon.stories.tsx new file mode 100644 index 00000000..44cb274e --- /dev/null +++ b/react-ui/src/stories/KFIcon.stories.tsx @@ -0,0 +1,41 @@ +import KFIcon from '@/components/KFIcon'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Button } from 'antd'; +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/KFIcon', + component: KFIcon, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: {}, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + args: {}, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + type: 'icon-xiazai', + }, +}; + +export const InButton: Story = { + args: { + type: 'icon-xiazai', + }, + render: function Render(args) { + return ( + + ); + }, +}; diff --git a/react-ui/src/stories/KFModal.stories.tsx b/react-ui/src/stories/KFModal.stories.tsx new file mode 100644 index 00000000..763aaf31 --- /dev/null +++ b/react-ui/src/stories/KFModal.stories.tsx @@ -0,0 +1,93 @@ +import CreateExperiment from '@/assets/img/create-experiment.png'; +import KFModal from '@/components/KFModal'; +import { openAntdModal } from '@/utils/modal'; +import { useArgs } from '@storybook/preview-api'; +import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; +import { Button } from 'antd'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/KFModal', + component: KFModal, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + title: { + description: '标题', + }, + open: { + description: '对话框是否可见', + }, + children: { + description: '子元素', + type: 'string', + }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + args: { onCancel: fn(), onOk: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + title: '创建实验', + image: CreateExperiment, + open: false, + children: '这是一个模态框', + }, + render: function Render({ onOk, onCancel, ...args }) { + const [{ open }, updateArgs] = useArgs(); + function onClick() { + updateArgs({ open: true }); + } + function handleOk() { + updateArgs({ open: false }); + onOk?.(); + } + + function handleCancel() { + updateArgs({ open: false }); + onCancel?.(); + } + + return ( + <> + + + + ); + }, +}; + +/** 通过 `openAntdModal` 函数打开 */ +export const OpenByFunction: Story = { + render: function Render() { + const handleClick = () => { + const { close } = openAntdModal(KFModal, { + title: '创建实验', + image: CreateExperiment, + children: '这是一个模态框', + onOk: () => { + close(); + }, + }); + }; + return ( + + ); + }, +}; diff --git a/react-ui/src/stories/KFRadio.stories.tsx b/react-ui/src/stories/KFRadio.stories.tsx new file mode 100644 index 00000000..c48c16e9 --- /dev/null +++ b/react-ui/src/stories/KFRadio.stories.tsx @@ -0,0 +1,52 @@ +import KFIcon from '@/components/KFIcon'; +import KFRadio from '@/components/KFRadio'; +import { useArgs } from '@storybook/preview-api'; +import type { Meta, StoryObj } from '@storybook/react'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/KFRadio', + component: KFRadio, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + // args: { onClick: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + items: [ + { + title: '英伟达GPU', + value: 'GPU', + icon: , + }, + { + title: '昇腾NPU', + value: 'NPU', + icon: , + }, + ], + value: 'GPU', + }, + render: function Render(args) { + const [{ value }, updateArgs] = useArgs(); + function onChange(value: string) { + updateArgs({ value: value }); + } + + return ; + }, +}; diff --git a/react-ui/src/stories/KFSpin.stories.tsx b/react-ui/src/stories/KFSpin.stories.tsx new file mode 100644 index 00000000..7f3185d2 --- /dev/null +++ b/react-ui/src/stories/KFSpin.stories.tsx @@ -0,0 +1,35 @@ +import KFSpin from '@/components/KFSpin'; +import type { Meta, StoryObj } from '@storybook/react'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/KFSpin', + component: KFSpin, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + // args: { onClick: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: {}, +}; diff --git a/react-ui/src/stories/MenuIconSelector.stories.tsx b/react-ui/src/stories/MenuIconSelector.stories.tsx new file mode 100644 index 00000000..f02fb77c --- /dev/null +++ b/react-ui/src/stories/MenuIconSelector.stories.tsx @@ -0,0 +1,67 @@ +import MenuIconSelector from '@/components/MenuIconSelector'; +import { useArgs } from '@storybook/preview-api'; +import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; +import { Button } from 'antd'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/MenuIconSelector', + component: MenuIconSelector, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + open: { + control: 'boolean', + description: '对话框是否可见', + }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + args: { onCancel: fn(), onOk: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + selectedIcon: 'manual-icon', + open: false, + }, + render: function Render({ onOk, onCancel, ...args }) { + const [{ open, selectedIcon }, updateArgs] = useArgs(); + function onClick() { + updateArgs({ open: true }); + } + function handleOk(value: string) { + updateArgs({ selectedIcon: value, open: false }); + onOk?.(value); + } + + function handleCancel() { + updateArgs({ open: false }); + onCancel?.(); + } + + return ( + <> + + + + ); + }, +}; diff --git a/react-ui/src/stories/PageTitle.stories.tsx b/react-ui/src/stories/PageTitle.stories.tsx new file mode 100644 index 00000000..216591b1 --- /dev/null +++ b/react-ui/src/stories/PageTitle.stories.tsx @@ -0,0 +1,30 @@ +import PageTitle from '@/components/PageTitle'; +import type { Meta, StoryObj } from '@storybook/react'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/PageTitle', + component: PageTitle, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + // args: { onClick: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + title: '数据集列表', + }, +}; diff --git a/react-ui/src/stories/ParameterInput.stories.tsx b/react-ui/src/stories/ParameterInput.stories.tsx new file mode 100644 index 00000000..9d9525e2 --- /dev/null +++ b/react-ui/src/stories/ParameterInput.stories.tsx @@ -0,0 +1,108 @@ +import ParameterInput, { ParameterInputValue } from '@/components/ParameterInput'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Button } from 'antd'; +import { useState } from 'react'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/ParameterInput', + component: ParameterInput, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + // args: { onClick: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Input: Story = { + args: { + placeholder: '请输入工作目录', + style: { width: 300 }, + canInput: true, + textArea: false, + allowClear: true, + size: 'large', + }, +}; + +export const Select: Story = { + args: { + placeholder: '请输入工作目录', + style: { width: 300 }, + value: 'storybook', + canInput: false, + size: 'large', + }, +}; + +export const SelectWithObjctValue: Story = { + args: { + placeholder: '请输入工作目录', + style: { width: 300 }, + value: { + value: 'storybook', + showValue: 'storybook', + fromSelect: true, + }, + canInput: true, + size: 'large', + }, +}; + +export const Disabled: Story = { + args: { + placeholder: '请输入工作目录', + style: { width: 300 }, + value: { + value: 'storybook', + showValue: 'storybook', + fromSelect: true, + }, + canInput: true, + size: 'large', + disabled: true, + }, +}; + +export const Application: Story = { + args: { + placeholder: '请输入工作目录', + style: { width: 300 }, + canInput: true, + size: 'large', + }, + render: function Render(args) { + const [value, setValue] = useState(''); + + const onClick = () => { + setValue({ + value: 'storybook', + showValue: 'storybook', + fromSelect: true, + }); + }; + return ( + <> + setValue(value)} + > + + + ); + }, +}; diff --git a/react-ui/src/stories/ResourceSelect.stories.tsx b/react-ui/src/stories/ResourceSelect.stories.tsx new file mode 100644 index 00000000..28c314e3 --- /dev/null +++ b/react-ui/src/stories/ResourceSelect.stories.tsx @@ -0,0 +1,156 @@ +import ResourceSelect, { + ParameterInputValue, + requiredValidator, + ResourceSelectorType, +} from '@/components/ResourceSelect'; +import { useArgs } from '@storybook/preview-api'; +import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; +import { Col, Form, Row } from 'antd'; +import { http, HttpResponse } from 'msw'; +import { + datasetDetailData, + datasetListData, + datasetVersionData, + mirrorListData, + mirrorVerionData, + modelDetailData, + modelListData, + modelVersionData, +} from './mockData'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/ResourceSelect', + component: ResourceSelect, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + msw: { + handlers: [ + http.get('/api/mmp/newdataset/queryDatasets', () => { + return HttpResponse.json(datasetListData); + }), + http.get('/api/mmp/newdataset/getVersionList', () => { + return HttpResponse.json(datasetVersionData); + }), + http.get('/api/mmp/newdataset/getDatasetDetail', () => { + return HttpResponse.json(datasetDetailData); + }), + http.get('/api/mmp/newmodel/queryModels', () => { + return HttpResponse.json(modelListData); + }), + http.get('/api/mmp/newmodel/getVersionList', () => { + return HttpResponse.json(modelVersionData); + }), + http.get('/api/mmp/newmodel/getModelDetail', () => { + return HttpResponse.json(modelDetailData); + }), + http.get('/api/mmp/image', () => { + return HttpResponse.json(mirrorListData); + }), + http.get('/api/mmp/imageVersion', () => { + return HttpResponse.json(mirrorVerionData); + }), + ], + }, + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + type: { control: 'radio', options: Object.values(ResourceSelectorType) }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + args: { onChange: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + type: ResourceSelectorType.Dataset, + canInput: false, + textArea: false, + size: 'large', + style: { width: 400 }, + }, + render: function Render(args) { + const [{ value }, updateArgs] = useArgs(); + function handleChange(value?: ParameterInputValue) { + updateArgs({ value: value }); + args.onChange?.(value); + } + + return ; + }, +}; + +export const InForm: Story = { + args: { + type: ResourceSelectorType.Dataset, + }, + render: ({ onChange }) => { + return ( +
+ +
+ + + + + + + + + + + + + + + + + + + + + ); + }, +}; diff --git a/react-ui/src/stories/ResourceSelectorModal.mdx b/react-ui/src/stories/ResourceSelectorModal.mdx new file mode 100644 index 00000000..28702123 --- /dev/null +++ b/react-ui/src/stories/ResourceSelectorModal.mdx @@ -0,0 +1,23 @@ +import { Meta, Canvas } from '@storybook/blocks'; +import * as ResourceSelectorModalStories from "./ResourceSelectorModal.stories" + + + +# Usage + +推荐通过 `openAntdModal` 函数打开 `ResourceSelectorModal`,打开 -> 处理 -> 关闭,整套代码在同一个地方 + +```ts +const handleClick = () => { + const { close } = openAntdModal(ResourceSelectorModal, { + type: ResourceSelectorType.Dataset, + onOk: (res) => { + // 处理逻辑 + close(); + }, +}); +``` + + + + diff --git a/react-ui/src/stories/ResourceSelectorModal.stories.tsx b/react-ui/src/stories/ResourceSelectorModal.stories.tsx new file mode 100644 index 00000000..9e72efd2 --- /dev/null +++ b/react-ui/src/stories/ResourceSelectorModal.stories.tsx @@ -0,0 +1,217 @@ +import ResourceSelectorModal, { ResourceSelectorType } from '@/components/ResourceSelectorModal'; +import { openAntdModal } from '@/utils/modal'; +import { useArgs } from '@storybook/preview-api'; +import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; +import { Button } from 'antd'; +import { http, HttpResponse } from 'msw'; +import { + datasetDetailData, + datasetListData, + datasetVersionData, + mirrorListData, + mirrorVerionData, + modelDetailData, + modelListData, + modelVersionData, +} from './mockData'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/ResourceSelectorModal', + component: ResourceSelectorModal, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + open: { + description: '对话框是否可见', + }, + type: { + control: 'select', + options: Object.values(ResourceSelectorType), + }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + args: { onCancel: fn(), onOk: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Dataset: Story = { + args: { + type: ResourceSelectorType.Dataset, + open: false, + }, + parameters: { + msw: { + handlers: [ + http.get('/api/mmp/newdataset/queryDatasets', () => { + return HttpResponse.json(datasetListData); + }), + http.get('/api/mmp/newdataset/getVersionList', () => { + return HttpResponse.json(datasetVersionData); + }), + http.get('/api/mmp/newdataset/getDatasetDetail', () => { + return HttpResponse.json(datasetDetailData); + }), + ], + }, + }, + render: function Render({ onOk, onCancel, ...args }) { + const [{ open }, updateArgs] = useArgs(); + function onClick() { + updateArgs({ open: true }); + } + function handleOk(res: any) { + updateArgs({ open: false }); + onOk?.(res); + } + + function handleCancel() { + updateArgs({ open: false }); + onCancel?.(); + } + + return ( + <> + + + + ); + }, +}; + +export const Model: Story = { + args: { + type: ResourceSelectorType.Model, + open: false, + }, + parameters: { + msw: { + handlers: [ + http.get('/api/mmp/newmodel/queryModels', () => { + return HttpResponse.json(modelListData); + }), + http.get('/api/mmp/newmodel/getVersionList', () => { + return HttpResponse.json(modelVersionData); + }), + http.get('/api/mmp/newmodel/getModelDetail', () => { + return HttpResponse.json(modelDetailData); + }), + ], + }, + }, + render: function Render({ onOk, onCancel, ...args }) { + const [{ open }, updateArgs] = useArgs(); + function onClick() { + updateArgs({ open: true }); + } + function handleOk(res: any) { + updateArgs({ open: false }); + onOk?.(res); + } + + function handleCancel() { + updateArgs({ open: false }); + onCancel?.(); + } + + return ( + <> + + + + ); + }, +}; + +export const Mirror: Story = { + args: { + type: ResourceSelectorType.Mirror, + open: false, + }, + parameters: { + msw: { + handlers: [ + http.get('/api/mmp/image', () => { + return HttpResponse.json(mirrorListData); + }), + http.get('/api/mmp/imageVersion', () => { + return HttpResponse.json(mirrorVerionData); + }), + ], + }, + }, + render: function Render({ onOk, onCancel, ...args }) { + const [{ open }, updateArgs] = useArgs(); + function onClick() { + updateArgs({ open: true }); + } + function handleOk(res: any) { + updateArgs({ open: false }); + onOk?.(res); + } + + function handleCancel() { + updateArgs({ open: false }); + onCancel?.(); + } + + return ( + <> + + + + ); + }, +}; + +/** 通过 `openAntdModal` 函数打开 */ +export const OpenByFunction: Story = { + args: { + type: ResourceSelectorType.Mirror, + }, + parameters: { + msw: { + handlers: [ + http.get('/api/mmp/image', () => { + return HttpResponse.json(mirrorListData); + }), + http.get('/api/mmp/imageVersion', () => { + return HttpResponse.json(mirrorVerionData); + }), + ], + }, + }, + render: function Render(args) { + const handleClick = () => { + const { close } = openAntdModal(ResourceSelectorModal, { + type: args.type, + onOk: (res) => { + const { onOk } = args; + onOk?.(res); + close(); + }, + }); + }; + return ( + + ); + }, +}; diff --git a/react-ui/src/stories/SubAreaTitle.stories.tsx b/react-ui/src/stories/SubAreaTitle.stories.tsx new file mode 100644 index 00000000..9fad7d71 --- /dev/null +++ b/react-ui/src/stories/SubAreaTitle.stories.tsx @@ -0,0 +1,32 @@ +import MirrorBasic from '@/assets/img/mirror-basic.png'; +import SubAreaTitle from '@/components/SubAreaTitle'; +import type { Meta, StoryObj } from '@storybook/react'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Components/SubAreaTitle', + component: SubAreaTitle, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + // layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + // backgroundColor: { control: 'color' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + // args: { onClick: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + title: '基本信息', + image: MirrorBasic, + }, +}; diff --git a/react-ui/src/stories/docs/Less.mdx b/react-ui/src/stories/docs/Less.mdx new file mode 100644 index 00000000..1b3a6632 --- /dev/null +++ b/react-ui/src/stories/docs/Less.mdx @@ -0,0 +1,199 @@ +import { Meta, Controls } from '@storybook/blocks'; + + + +# Less 规范 + +## Theme + +### 自定义主题 + +`src/styles/theme.less` 定义了 UI 主题颜色变量、Less 函数、Less 混合。在开发过程中使用这个文件的定义的变量、函数以及混合,通过 UmiJS 的配置,我们在 Less 文件不需要收到导入这个文件。 + +颜色变量还可以在 `js/ts/jsx/tsx` 里使用 + +```js +import themes from "@/styles/theme.less" + +const primaryColor = themes['primaryColor']; // #1664ff +``` + +### Ant Design 主题覆盖 + +Ant Design 可以[定制主题](https://ant-design.antgroup.com/docs/react/customize-theme-cn),Ant Design 是通过 [ConfigProvider](https://ant-design.antgroup.com/components/config-provider-cn) 组件进行主题定制,而 UmiJS 可以在[配置文件](https://umijs.org/docs/max/antd#%E6%9E%84%E5%BB%BA%E6%97%B6%E9%85%8D%E7%BD%AE)或者 [`app.ts`](https://umijs.org/docs/max/antd#%E8%BF%90%E8%A1%8C%E6%97%B6%E9%85%8D%E7%BD%AE) 里进行配置。我选择在 [`app.ts`](https://umijs.org/docs/max/antd#%E8%BF%90%E8%A1%8C%E6%97%B6%E9%85%8D%E7%BD%AE) 里进行配置,因为这里可以使用主题颜色变量。 + +```tsx +// 主题修改 +export const antd: RuntimeAntdConfig = (memo) => { + memo.theme ??= {}; + memo.theme.token = { + colorPrimary: themes['primaryColor'], + colorSuccess: themes['successColor'], + colorError: themes['errorColor'], + colorWarning: themes['warningColor'], + colorLink: themes['primaryColor'], + colorText: themes['textColor'], + controlHeightLG: 46, + }; + memo.theme.components ??= {}; + memo.theme.components.Tabs = {}; + memo.theme.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']), + }; + memo.theme.components.Input = { + inputFontSize: parseInt(themes['fontSizeInput']), + inputFontSizeLG: parseInt(themes['fontSizeInputLg']), + paddingBlockLG: 10, + }; + memo.theme.components.Select = { + singleItemHeightLG: 46, + optionSelectedColor: themes['primaryColor'], + }; + memo.theme.components.Table = { + headerBg: 'rgba(242, 244, 247, 0.36)', + headerBorderRadius: 4, + rowSelectedBg: 'rgba(22, 100, 255, 0.05)', + }; + memo.theme.components.Tabs = { + titleFontSize: 16, + }; + memo.theme.components.Form = { + labelColor: 'rgba(29, 29, 32, 0.8);', + }; + memo.theme.components.Breadcrumb = { + iconFontSize: parseInt(themes['fontSize']), + linkColor: 'rgba(29, 29, 32, 0.7)', + separatorColor: 'rgba(29, 29, 32, 0.7)', + }; + + memo.theme.cssVar = true; + + memo.appConfig = { + message: { + // 配置 message 最大显示数,超过限制时,最早的消息会被自动关闭 + maxCount: 3, + }, + }; + + return memo; +}; +``` + +覆盖 Ant Design 的默认样式,优先选择这种方式,如果没有相应的变量,才覆盖 Ant Design 的样式,在 `src/overrides.less` 文件里覆盖,请查看 UmiJS 关于[`global.less`](https://umijs.org/docs/guides/directory-structure#globalcsslesssassscss) 与 [`overrides.less`](https://umijs.org/docs/guides/directory-structure#overridescsslesssassscss) 的说明。 + +## BEM + +类名遵循 [BEM - Block, Element, Modifier](https://getbem.com/) 规范 + +### Block + +有意义的独立实体,Block 的类名由小写字母、数字和横线组成,比如 `model`、`form`、`paramneter-input` + +### Element + +块的一部分,Element 的类名由 `Block 的类名` + `双下划线(__)` + `Element 的名称` 组成,比如 `model__title`、`form__input`、`paramneter-input__content` + +### Modifier + +块或元素的变种,Modifier 的类名由 `Block 的类名` 或者 `Element 的类名` + `双横线(--)` + `Modifier 的名称` 组成,比如 `button--active`、`form--large` + +举个 🌰 + +```tsx +// @/components/CodeConfigItem/index.tsx + +import classNames from 'classnames'; +import styles from './index.less'; + +function CodeConfigItem({ item, onClick }: CodeConfigItemProps) { + return ( +
+ + + {item.code_repo_name} + +
+ {item.code_repo_vis === AvailableRange.Public ? '公开' : '私有'} +
+
+ + {item.git_url} + +
{item.git_branch}
+
+ ); +} + +``` + +### 一些建议 + +如果你陷入嵌套地狱,比如 + +```tsx +function Component() { + return ( +
+
+
+
+
+ // 等等 +
+
+
+
+
+ ) +} +``` + +说明你需要拆分组件了 + +```tsx +function Component1() { + return ( +
+
+
+
+ ) +} + +function Component() { + return ( +
+
+ +
+
+ ) +} +``` + +既减少了类名的嵌套,又减少了HTML的嵌套,使代码逻辑更加清晰,易于理解与维护 + + + diff --git a/react-ui/src/stories/example/Button.stories.ts b/react-ui/src/stories/example/Button.stories.ts new file mode 100644 index 00000000..2a05e01b --- /dev/null +++ b/react-ui/src/stories/example/Button.stories.ts @@ -0,0 +1,53 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; + +import { Button } from './Button'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Example/Button', + component: Button, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + backgroundColor: { control: 'color' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + args: { onClick: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + primary: true, + label: 'Button', + }, +}; + +export const Secondary: Story = { + args: { + label: 'Button', + }, +}; + +export const Large: Story = { + args: { + size: 'large', + label: 'Button', + }, +}; + +export const Small: Story = { + args: { + size: 'small', + label: 'Button', + }, +}; diff --git a/react-ui/src/stories/example/Button.tsx b/react-ui/src/stories/example/Button.tsx new file mode 100644 index 00000000..f35dafdc --- /dev/null +++ b/react-ui/src/stories/example/Button.tsx @@ -0,0 +1,37 @@ +import React from 'react'; + +import './button.css'; + +export interface ButtonProps { + /** Is this the principal call to action on the page? */ + primary?: boolean; + /** What background color to use */ + backgroundColor?: string; + /** How large should the button be? */ + size?: 'small' | 'medium' | 'large'; + /** Button contents */ + label: string; + /** Optional click handler */ + onClick?: () => void; +} + +/** Primary UI component for user interaction */ +export const Button = ({ + primary = false, + size = 'medium', + backgroundColor, + label, + ...props +}: ButtonProps) => { + const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary'; + return ( + + ); +}; diff --git a/react-ui/src/stories/example/Configure.mdx b/react-ui/src/stories/example/Configure.mdx new file mode 100644 index 00000000..6a537304 --- /dev/null +++ b/react-ui/src/stories/example/Configure.mdx @@ -0,0 +1,364 @@ +import { Meta } from "@storybook/blocks"; + +import Github from "./assets/github.svg"; +import Discord from "./assets/discord.svg"; +import Youtube from "./assets/youtube.svg"; +import Tutorials from "./assets/tutorials.svg"; +import Styling from "./assets/styling.png"; +import Context from "./assets/context.png"; +import Assets from "./assets/assets.png"; +import Docs from "./assets/docs.png"; +import Share from "./assets/share.png"; +import FigmaPlugin from "./assets/figma-plugin.png"; +import Testing from "./assets/testing.png"; +import Accessibility from "./assets/accessibility.png"; +import Theming from "./assets/theming.png"; +import AddonLibrary from "./assets/addon-library.png"; + +export const RightArrow = () => + + + + + +
+
+ # Configure your project + + Because Storybook works separately from your app, you'll need to configure it for your specific stack and setup. Below, explore guides for configuring Storybook with popular frameworks and tools. If you get stuck, learn how you can ask for help from our community. +
+
+
+ A wall of logos representing different styling technologies +

Add styling and CSS

+

Like with web applications, there are many ways to include CSS within Storybook. Learn more about setting up styling within Storybook.

+ Learn more +
+
+ An abstraction representing the composition of data for a component +

Provide context and mocking

+

Often when a story doesn't render, it's because your component is expecting a specific environment or context (like a theme provider) to be available.

+ Learn more +
+
+ A representation of typography and image assets +
+

Load assets and resources

+

To link static files (like fonts) to your projects and stories, use the + `staticDirs` configuration option to specify folders to load when + starting Storybook.

+ Learn more +
+
+
+
+
+
+ # Do more with Storybook + + Now that you know the basics, let's explore other parts of Storybook that will improve your experience. This list is just to get you started. You can customise Storybook in many ways to fit your needs. +
+ +
+
+
+ A screenshot showing the autodocs tag being set, pointing a docs page being generated +

Autodocs

+

Auto-generate living, + interactive reference documentation from your components and stories.

+ Learn more +
+
+ A browser window showing a Storybook being published to a chromatic.com URL +

Publish to Chromatic

+

Publish your Storybook to review and collaborate with your entire team.

+ Learn more +
+
+ Windows showing the Storybook plugin in Figma +

Figma Plugin

+

Embed your stories into Figma to cross-reference the design and live + implementation in one place.

+ Learn more +
+
+ Screenshot of tests passing and failing +

Testing

+

Use stories to test a component in all its variations, no matter how + complex.

+ Learn more +
+
+ Screenshot of accessibility tests passing and failing +

Accessibility

+

Automatically test your components for a11y issues as you develop.

+ Learn more +
+
+ Screenshot of Storybook in light and dark mode +

Theming

+

Theme Storybook's UI to personalize it to your project.

+ Learn more +
+
+
+
+
+
+

Addons

+

Integrate your tools with Storybook to connect workflows.

+ Discover all addons +
+
+ Integrate your tools with Storybook to connect workflows. +
+
+ +
+
+ Github logo + Join our contributors building the future of UI development. + + Star on GitHub +
+
+ Discord logo +
+ Get support and chat with frontend developers. + + Join Discord server +
+
+
+ Youtube logo +
+ Watch tutorials, feature previews and interviews. + + Watch on YouTube +
+
+
+ A book +

Follow guided walkthroughs on for key workflows.

+ + Discover tutorials +
+
+ + diff --git a/react-ui/src/stories/example/Header.stories.ts b/react-ui/src/stories/example/Header.stories.ts new file mode 100644 index 00000000..80c71d0f --- /dev/null +++ b/react-ui/src/stories/example/Header.stories.ts @@ -0,0 +1,33 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; + +import { Header } from './Header'; + +const meta = { + title: 'Example/Header', + component: Header, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + parameters: { + // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout + layout: 'fullscreen', + }, + args: { + onLogin: fn(), + onLogout: fn(), + onCreateAccount: fn(), + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const LoggedIn: Story = { + args: { + user: { + name: 'Jane Doe', + }, + }, +}; + +export const LoggedOut: Story = {}; diff --git a/react-ui/src/stories/example/Header.tsx b/react-ui/src/stories/example/Header.tsx new file mode 100644 index 00000000..1bf981a4 --- /dev/null +++ b/react-ui/src/stories/example/Header.tsx @@ -0,0 +1,56 @@ +import React from 'react'; + +import { Button } from './Button'; +import './header.css'; + +type User = { + name: string; +}; + +export interface HeaderProps { + user?: User; + onLogin?: () => void; + onLogout?: () => void; + onCreateAccount?: () => void; +} + +export const Header = ({ user, onLogin, onLogout, onCreateAccount }: HeaderProps) => ( +
+
+
+ + + + + + + +

Acme

+
+
+ {user ? ( + <> + + Welcome, {user.name}! + +
+
+
+); diff --git a/react-ui/src/stories/example/Page.stories.ts b/react-ui/src/stories/example/Page.stories.ts new file mode 100644 index 00000000..5d2c688a --- /dev/null +++ b/react-ui/src/stories/example/Page.stories.ts @@ -0,0 +1,32 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { expect, userEvent, within } from '@storybook/test'; + +import { Page } from './Page'; + +const meta = { + title: 'Example/Page', + component: Page, + parameters: { + // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout + layout: 'fullscreen', + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const LoggedOut: Story = {}; + +// More on component testing: https://storybook.js.org/docs/writing-tests/component-testing +export const LoggedIn: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const loginButton = canvas.getByRole('button', { name: /Log in/i }); + await expect(loginButton).toBeInTheDocument(); + await userEvent.click(loginButton); + await expect(loginButton).not.toBeInTheDocument(); + + const logoutButton = canvas.getByRole('button', { name: /Log out/i }); + await expect(logoutButton).toBeInTheDocument(); + }, +}; diff --git a/react-ui/src/stories/example/Page.tsx b/react-ui/src/stories/example/Page.tsx new file mode 100644 index 00000000..e1174830 --- /dev/null +++ b/react-ui/src/stories/example/Page.tsx @@ -0,0 +1,73 @@ +import React from 'react'; + +import { Header } from './Header'; +import './page.css'; + +type User = { + name: string; +}; + +export const Page: React.FC = () => { + const [user, setUser] = React.useState(); + + return ( +
+
setUser({ name: 'Jane Doe' })} + onLogout={() => setUser(undefined)} + onCreateAccount={() => setUser({ name: 'Jane Doe' })} + /> + +
+

Pages in Storybook

+

+ We recommend building UIs with a{' '} + + component-driven + {' '} + process starting with atomic components and ending with pages. +

+

+ Render pages with mock data. This makes it easy to build and review page states without + needing to navigate to them in your app. Here are some handy patterns for managing page + data in Storybook: +

+
    +
  • + Use a higher-level connected component. Storybook helps you compose such data from the + "args" of child component stories +
  • +
  • + Assemble data in the page component from your services. You can mock these services out + using Storybook. +
  • +
+

+ Get a guided tutorial on component-driven development at{' '} + + Storybook tutorials + + . Read more in the{' '} + + docs + + . +

+
+ Tip Adjust the width of the canvas with the{' '} + + + + + + Viewports addon in the toolbar +
+
+
+ ); +}; diff --git a/react-ui/src/stories/example/assets/accessibility.png b/react-ui/src/stories/example/assets/accessibility.png new file mode 100644 index 00000000..6ffe6fea Binary files /dev/null and b/react-ui/src/stories/example/assets/accessibility.png differ diff --git a/react-ui/src/stories/example/assets/accessibility.svg b/react-ui/src/stories/example/assets/accessibility.svg new file mode 100644 index 00000000..107e93f8 --- /dev/null +++ b/react-ui/src/stories/example/assets/accessibility.svg @@ -0,0 +1 @@ +Accessibility \ No newline at end of file diff --git a/react-ui/src/stories/example/assets/addon-library.png b/react-ui/src/stories/example/assets/addon-library.png new file mode 100644 index 00000000..95deb38a Binary files /dev/null and b/react-ui/src/stories/example/assets/addon-library.png differ diff --git a/react-ui/src/stories/example/assets/assets.png b/react-ui/src/stories/example/assets/assets.png new file mode 100644 index 00000000..cfba6817 Binary files /dev/null and b/react-ui/src/stories/example/assets/assets.png differ diff --git a/react-ui/src/stories/example/assets/avif-test-image.avif b/react-ui/src/stories/example/assets/avif-test-image.avif new file mode 100644 index 00000000..530709bc Binary files /dev/null and b/react-ui/src/stories/example/assets/avif-test-image.avif differ diff --git a/react-ui/src/stories/example/assets/context.png b/react-ui/src/stories/example/assets/context.png new file mode 100644 index 00000000..e5cd249a Binary files /dev/null and b/react-ui/src/stories/example/assets/context.png differ diff --git a/react-ui/src/stories/example/assets/discord.svg b/react-ui/src/stories/example/assets/discord.svg new file mode 100644 index 00000000..d638958b --- /dev/null +++ b/react-ui/src/stories/example/assets/discord.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/react-ui/src/stories/example/assets/docs.png b/react-ui/src/stories/example/assets/docs.png new file mode 100644 index 00000000..a749629d Binary files /dev/null and b/react-ui/src/stories/example/assets/docs.png differ diff --git a/react-ui/src/stories/example/assets/figma-plugin.png b/react-ui/src/stories/example/assets/figma-plugin.png new file mode 100644 index 00000000..8f79b08c Binary files /dev/null and b/react-ui/src/stories/example/assets/figma-plugin.png differ diff --git a/react-ui/src/stories/example/assets/github.svg b/react-ui/src/stories/example/assets/github.svg new file mode 100644 index 00000000..dc513528 --- /dev/null +++ b/react-ui/src/stories/example/assets/github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/react-ui/src/stories/example/assets/share.png b/react-ui/src/stories/example/assets/share.png new file mode 100644 index 00000000..8097a370 Binary files /dev/null and b/react-ui/src/stories/example/assets/share.png differ diff --git a/react-ui/src/stories/example/assets/styling.png b/react-ui/src/stories/example/assets/styling.png new file mode 100644 index 00000000..d341e826 Binary files /dev/null and b/react-ui/src/stories/example/assets/styling.png differ diff --git a/react-ui/src/stories/example/assets/testing.png b/react-ui/src/stories/example/assets/testing.png new file mode 100644 index 00000000..d4ac39a0 Binary files /dev/null and b/react-ui/src/stories/example/assets/testing.png differ diff --git a/react-ui/src/stories/example/assets/theming.png b/react-ui/src/stories/example/assets/theming.png new file mode 100644 index 00000000..1535eb9b Binary files /dev/null and b/react-ui/src/stories/example/assets/theming.png differ diff --git a/react-ui/src/stories/example/assets/tutorials.svg b/react-ui/src/stories/example/assets/tutorials.svg new file mode 100644 index 00000000..b492a9c6 --- /dev/null +++ b/react-ui/src/stories/example/assets/tutorials.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/react-ui/src/stories/example/assets/youtube.svg b/react-ui/src/stories/example/assets/youtube.svg new file mode 100644 index 00000000..a7515d7e --- /dev/null +++ b/react-ui/src/stories/example/assets/youtube.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/react-ui/src/stories/example/button.css b/react-ui/src/stories/example/button.css new file mode 100644 index 00000000..4e3620b0 --- /dev/null +++ b/react-ui/src/stories/example/button.css @@ -0,0 +1,30 @@ +.storybook-button { + display: inline-block; + cursor: pointer; + border: 0; + border-radius: 3em; + font-weight: 700; + line-height: 1; + font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; +} +.storybook-button--primary { + background-color: #555ab9; + color: white; +} +.storybook-button--secondary { + box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset; + background-color: transparent; + color: #333; +} +.storybook-button--small { + padding: 10px 16px; + font-size: 12px; +} +.storybook-button--medium { + padding: 11px 20px; + font-size: 14px; +} +.storybook-button--large { + padding: 12px 24px; + font-size: 16px; +} diff --git a/react-ui/src/stories/example/header.css b/react-ui/src/stories/example/header.css new file mode 100644 index 00000000..5efd46c2 --- /dev/null +++ b/react-ui/src/stories/example/header.css @@ -0,0 +1,32 @@ +.storybook-header { + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + padding: 15px 20px; + font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +.storybook-header svg { + display: inline-block; + vertical-align: top; +} + +.storybook-header h1 { + display: inline-block; + vertical-align: top; + margin: 6px 0 6px 10px; + font-weight: 700; + font-size: 20px; + line-height: 1; +} + +.storybook-header button + button { + margin-left: 10px; +} + +.storybook-header .welcome { + margin-right: 10px; + color: #333; + font-size: 14px; +} diff --git a/react-ui/src/stories/example/page.css b/react-ui/src/stories/example/page.css new file mode 100644 index 00000000..77c81d2d --- /dev/null +++ b/react-ui/src/stories/example/page.css @@ -0,0 +1,68 @@ +.storybook-page { + margin: 0 auto; + padding: 48px 20px; + max-width: 600px; + color: #333; + font-size: 14px; + line-height: 24px; + font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +.storybook-page h2 { + display: inline-block; + vertical-align: top; + margin: 0 0 4px; + font-weight: 700; + font-size: 32px; + line-height: 1; +} + +.storybook-page p { + margin: 1em 0; +} + +.storybook-page a { + color: inherit; +} + +.storybook-page ul { + margin: 1em 0; + padding-left: 30px; +} + +.storybook-page li { + margin-bottom: 8px; +} + +.storybook-page .tip { + display: inline-block; + vertical-align: top; + margin-right: 10px; + border-radius: 1em; + background: #e7fdd8; + padding: 4px 12px; + color: #357a14; + font-weight: 700; + font-size: 11px; + line-height: 12px; +} + +.storybook-page .tip-wrapper { + margin-top: 40px; + margin-bottom: 40px; + font-size: 13px; + line-height: 20px; +} + +.storybook-page .tip-wrapper svg { + display: inline-block; + vertical-align: top; + margin-top: 3px; + margin-right: 4px; + width: 12px; + height: 12px; +} + +.storybook-page .tip-wrapper svg path { + fill: #1ea7fd; +} diff --git a/react-ui/src/stories/mockData.ts b/react-ui/src/stories/mockData.ts new file mode 100644 index 00000000..3d910b06 --- /dev/null +++ b/react-ui/src/stories/mockData.ts @@ -0,0 +1,548 @@ +export const datasetListData = { + msg: '操作成功', + code: 200, + data: { + content: [ + { + name: '手写体识别训练测试数据集', + identifier: 'admin_dataset_20241213140429', + description: '手写体识别数据集', + is_public: false, + time_ago: '2个月前', + id: 1454047, + visits: 0, + create_by: '陈志航', + owner: 'chenzhihang', + }, + { + name: '手写体识别', + identifier: 'admin_dataset_20241213140020', + description: '手写体识别数据集', + is_public: false, + time_ago: '2个月前', + id: 1454046, + visits: 0, + create_by: '陈志航', + owner: 'chenzhihang', + }, + { + name: '生物活性分子数据集', + identifier: 'admin_dataset_20241211151411', + description: '生物活性分子数据集', + is_public: false, + time_ago: '2个月前', + id: 1454004, + visits: 0, + create_by: '陈志航', + owner: 'chenzhihang', + }, + { + name: '介电材料数据集', + identifier: 'admin_dataset_20241211151330', + description: '介电材料数据集', + is_public: false, + time_ago: '2个月前', + id: 1454003, + visits: 0, + create_by: '陈志航', + owner: 'chenzhihang', + }, + ], + pageable: { + sort: { + unsorted: true, + sorted: false, + empty: true, + }, + pageSize: 2000, + pageNumber: 0, + offset: 0, + unpaged: false, + paged: true, + }, + last: true, + totalElements: 4, + totalPages: 1, + sort: { + unsorted: true, + sorted: false, + empty: true, + }, + first: true, + number: 0, + numberOfElements: 4, + size: 2000, + empty: false, + }, +}; + +export const datasetVersionData = { + msg: '操作成功', + code: 200, + data: [ + { + name: 'v2', + http_url: 'https://cdn09022024.gitlink.org.cn/chenzhihang/admin_dataset_20241213140429.git', + zip_url: + 'https://www.gitlink.org.cn/api/chenzhihang/admin_dataset_20241213140429/archive/v2.zip', + tar_url: + 'https://www.gitlink.org.cn/api/chenzhihang/admin_dataset_20241213140429/archive/v2.tar.gz', + }, + { + name: 'v3', + http_url: 'https://cdn09022024.gitlink.org.cn/chenzhihang/admin_dataset_20241213140429.git', + zip_url: + 'https://www.gitlink.org.cn/api/chenzhihang/admin_dataset_20241213140429/archive/v3.zip', + tar_url: + 'https://www.gitlink.org.cn/api/chenzhihang/admin_dataset_20241213140429/archive/v3.tar.gz', + }, + { + name: 'v1', + http_url: 'https://cdn09022024.gitlink.org.cn/chenzhihang/admin_dataset_20241213140429.git', + zip_url: + 'https://www.gitlink.org.cn/api/chenzhihang/admin_dataset_20241213140429/archive/v1.zip', + tar_url: + 'https://www.gitlink.org.cn/api/chenzhihang/admin_dataset_20241213140429/archive/v1.tar.gz', + }, + ], +}; + +export const datasetDetailData = { + msg: '操作成功', + code: 200, + data: { + name: '生物活性分子材料数据集', + identifier: 'admin_dataset_20250217105241', + description: '生物活性分子材料数据集', + is_public: false, + data_type: '自然语言处理', + data_tag: '机器翻译', + version: 'v1', + dataset_version_vos: [ + { + url: '/home/resource/admin/datasets/1454953/admin_dataset_20250217105241/v1/dataset/BBBP.zip', + file_name: 'BBBP.zip', + file_size: '42.14 KB', + }, + ], + id: 1454953, + create_by: '樊帅', + version_desc: '生物活性分子材料数据集', + usage: + '
# 克隆数据集配置文件与存储参数到本地\ngit clone -b v1 https://gitlink.org.cn/fanshuai/admin_dataset_20250217105241.git\n# 远程拉取配置文件\ndvc pull\n
', + update_time: '2025-02-17 10:52:43', + owner: 'fanshuai', + dataset_source: 'add', + relative_paths: 'admin/datasets/1454953/admin_dataset_20250217105241/v1/dataset', + }, +}; + +export const modelListData = { + msg: '操作成功', + code: 200, + data: { + content: [ + { + id: 1454208, + name: '介电材料模型1', + create_by: '陈志航', + description: '介电材料模型1', + time_ago: '2个月前', + owner: 'chenzhihang', + identifier: 'admin_model_20241224095928', + is_public: false, + }, + { + id: 1454007, + name: '手写体识别部署模型', + create_by: '陈志航', + description: '手写体识别部署模型', + time_ago: '2个月前', + owner: 'chenzhihang', + identifier: 'admin_model_20241211151713', + is_public: false, + }, + { + id: 1454006, + name: '生物活性分子材料', + create_by: '陈志航', + description: '生物活性分子材料', + time_ago: '2个月前', + owner: 'chenzhihang', + identifier: 'admin_model_20241211151645', + is_public: false, + }, + { + id: 1454005, + name: '介电材料模型', + create_by: '陈志航', + description: '介电材料模型', + time_ago: '2个月前', + owner: 'chenzhihang', + identifier: 'admin_model_20241211151601', + is_public: false, + }, + ], + pageable: { + sort: { + unsorted: true, + sorted: false, + empty: true, + }, + pageSize: 2000, + pageNumber: 0, + offset: 0, + unpaged: false, + paged: true, + }, + last: true, + totalElements: 4, + totalPages: 1, + sort: { + unsorted: true, + sorted: false, + empty: true, + }, + first: true, + number: 0, + numberOfElements: 4, + size: 2000, + empty: false, + }, +}; + +export const modelVersionData = { + msg: '操作成功', + code: 200, + data: [ + { + name: 'v1', + http_url: 'https://cdn09022024.gitlink.org.cn/chenzhihang/admin_model_20241224095928.git', + zip_url: + 'https://www.gitlink.org.cn/api/chenzhihang/admin_model_20241224095928/archive/v1.zip', + tar_url: + 'https://www.gitlink.org.cn/api/chenzhihang/admin_model_20241224095928/archive/v1.tar.gz', + }, + { + name: 'v2', + http_url: 'https://cdn09022024.gitlink.org.cn/chenzhihang/admin_model_20241224095928.git', + zip_url: + 'https://www.gitlink.org.cn/api/chenzhihang/admin_model_20241224095928/archive/v1.zip', + tar_url: + 'https://www.gitlink.org.cn/api/chenzhihang/admin_model_20241224095928/archive/v1.tar.gz', + }, + ], +}; + +export const modelDetailData = { + msg: '操作成功', + code: 200, + data: { + id: 1454208, + name: '介电材料模型1', + version: 'v1', + version_desc: '介电材料模型1', + create_by: '陈志航', + create_time: '2024-12-24 09:59:31', + update_time: '2024-12-24 09:59:31', + model_size: '101.90 KB', + model_source: 'add', + model_tag: '图像分类', + model_type: 'PyTorch', + description: '介电材料模型1', + usage: + '
# 克隆模型配置文件与存储参数到本地\ngit clone -b v1 https://gitlink.org.cn/chenzhihang/admin_model_20241224095928.git\n# 远程拉取配置文件\ndvc pull\n
', + owner: 'chenzhihang', + identifier: 'admin_model_20241224095928', + is_public: false, + relative_paths: 'admin/model/1454208/admin_model_20241224095928/v1/model', + model_version_vos: [ + { + url: '/home/resource/admin/model/1454208/admin_model_20241224095928/v1/model/sklearn_svr_good.pkl', + file_name: 'sklearn_svr_good.pkl', + file_size: '101.90 KB', + }, + ], + }, +}; + +export const mirrorListData = { + code: 200, + msg: '操作成功', + data: { + content: [ + { + id: 42, + name: 'ccr.ccs.tencentyun.com/somunslotus/httpserver', + description: 'htttp服务器镜像', + image_type: 0, + create_by: 'admin', + create_time: '2024-04-30T14:44:37.000+08:00', + update_by: 'admin', + update_time: '2024-04-30T14:44:37.000+08:00', + state: 1, + version_count: 1, + }, + { + id: 44, + name: 'minio-test', + description: 'minio镜像', + image_type: 0, + create_by: 'admin', + create_time: '2024-05-08T15:19:00.000+08:00', + update_by: 'admin', + update_time: '2024-05-08T15:19:00.000+08:00', + state: 1, + version_count: 1, + }, + { + id: 46, + name: 'machine-learning/mnist-model-deploy', + description: 'mnist手写体识别部署镜像', + image_type: 0, + create_by: 'admin', + create_time: '2024-05-28T10:18:30.000+08:00', + update_by: 'admin', + update_time: '2024-05-28T10:18:30.000+08:00', + state: 1, + version_count: 2, + }, + { + id: 49, + name: 'go_httpsever', + description: 'golang httpserver镜像', + image_type: 0, + create_by: 'admin', + create_time: '2024-06-25T11:11:41.000+08:00', + update_by: 'admin', + update_time: '2024-06-25T11:11:41.000+08:00', + state: 1, + version_count: 1, + }, + { + id: 51, + name: 'pytorch2_python3_cuda_12', + description: 'pytorch2.1.2_python3.11_cuda_12', + image_type: 0, + create_by: 'admin', + create_time: '2024-10-14T16:16:35.000+08:00', + update_by: 'admin', + update_time: '2024-10-14T16:16:35.000+08:00', + state: 1, + version_count: 2, + }, + { + id: 53, + name: 'jupyterlab', + description: 'v1', + image_type: 0, + create_by: 'admin', + create_time: '2024-10-15T16:19:29.000+08:00', + update_by: 'admin', + update_time: '2024-10-15T16:19:29.000+08:00', + state: 1, + version_count: 1, + }, + { + id: 55, + name: 'machine-learning/ax-pytorch', + description: '自动机器学习Ax镜像,带pytorch', + image_type: 0, + create_by: 'admin', + create_time: '2024-12-13T11:25:37.000+08:00', + update_by: 'admin', + update_time: '2024-12-13T11:25:37.000+08:00', + state: 1, + version_count: 1, + }, + ], + pageable: { + sort: { + unsorted: true, + sorted: false, + empty: true, + }, + pageSize: 2000, + pageNumber: 0, + offset: 0, + unpaged: false, + paged: true, + }, + last: true, + totalElements: 7, + totalPages: 1, + sort: { + unsorted: true, + sorted: false, + empty: true, + }, + first: true, + number: 0, + numberOfElements: 7, + size: 2000, + empty: false, + }, +}; + +export const mirrorVerionData = { + code: 200, + msg: '操作成功', + data: { + content: [ + { + id: 54, + image_id: 42, + version: null, + url: '172.20.32.187/testlib/admin/ccr.ccs.tencentyun.com/somunslotus/httpserver:v1', + tag_name: 'v1', + file_size: '6.98 MB', + status: 'available', + create_by: 'admin', + create_time: '2024-04-30T14:44:37.000+08:00', + update_by: 'admin', + update_time: '2024-04-30T14:44:51.000+08:00', + state: 1, + host_ip: null, + }, + ], + pageable: { + sort: { + unsorted: true, + sorted: false, + empty: true, + }, + pageSize: 2000, + pageNumber: 0, + offset: 0, + unpaged: false, + paged: true, + }, + last: true, + totalElements: 1, + totalPages: 1, + sort: { + unsorted: true, + sorted: false, + empty: true, + }, + first: true, + number: 0, + numberOfElements: 1, + size: 2000, + empty: false, + }, +}; + +export const codeListData = { + code: 200, + msg: '操作成功', + data: { + content: [ + { + id: 2, + code_repo_name: '介电材料代码', + code_repo_vis: 1, + git_url: 'https://gitlink.org.cn/fuli/ML_for_Materials.git', + git_branch: 'master', + verify_mode: null, + git_user_name: null, + git_password: null, + ssh_key: null, + create_by: 'admin', + create_time: '2024-10-14T16:10:45.000+08:00', + update_by: 'admin', + update_time: '2024-10-14T16:10:45.000+08:00', + state: 1, + }, + { + id: 3, + code_repo_name: '生物活性材料代码', + code_repo_vis: 1, + git_url: 'https://gitlink.org.cn/zhaoyihan/test_mole_pre.git', + git_branch: 'parse_dataset', + verify_mode: null, + git_user_name: null, + git_password: null, + ssh_key: null, + create_by: 'admin', + create_time: '2024-10-16T08:41:39.000+08:00', + update_by: 'admin', + update_time: '2024-10-16T08:41:39.000+08:00', + state: 1, + }, + { + id: 4, + code_repo_name: '数据处理', + code_repo_vis: 1, + git_url: 'https://openi.pcl.ac.cn/somunslotus/somun202304241505581.git', + git_branch: 'train_ci_test', + verify_mode: null, + git_user_name: null, + git_password: null, + ssh_key: null, + create_by: 'admin', + create_time: '2024-10-16T14:51:18.000+08:00', + update_by: 'admin', + update_time: '2024-10-16T14:51:18.000+08:00', + state: 1, + }, + { + id: 5, + code_repo_name: '手写体识别部署', + code_repo_vis: 1, + git_url: 'https://gitlink.org.cn/somunslotus/mnist-inference.git', + git_branch: 'master', + verify_mode: null, + git_user_name: null, + git_password: null, + ssh_key: null, + create_by: 'admin', + create_time: '2024-10-16T16:36:43.000+08:00', + update_by: 'admin', + update_time: '2024-10-16T16:36:43.000+08:00', + state: 1, + }, + { + id: 7, + code_repo_name: '手写体识别训练', + code_repo_vis: 1, + git_url: 'https://openi.pcl.ac.cn/somunslotus/somun202304241505581.git', + git_branch: 'train_ci_test', + verify_mode: null, + git_user_name: null, + git_password: null, + ssh_key: null, + create_by: 'admin', + create_time: '2024-12-13T13:58:50.000+08:00', + update_by: 'admin', + update_time: '2024-12-13T13:58:50.000+08:00', + state: 1, + }, + ], + pageable: { + sort: { + unsorted: true, + sorted: false, + empty: true, + }, + pageSize: 20, + pageNumber: 0, + offset: 0, + unpaged: false, + paged: true, + }, + last: true, + totalElements: 5, + totalPages: 1, + sort: { + unsorted: true, + sorted: false, + empty: true, + }, + first: true, + number: 0, + numberOfElements: 5, + size: 20, + empty: false, + }, +}; diff --git a/react-ui/src/styles/theme.less b/react-ui/src/styles/theme.less index 758eec31..ff7813f9 100644 --- a/react-ui/src/styles/theme.less +++ b/react-ui/src/styles/theme.less @@ -12,6 +12,8 @@ @text-color: #1d1d20; @text-color-secondary: #575757; @text-color-tertiary: #8a8a8a; +@text-placeholder-color: rgba(0, 0, 0, 0.25); +@text-disabled-color: rgba(0, 0, 0, 0.25); @success-color: #6ac21d; @error-color: #c73131; @warning-color: #f98e1b; @@ -47,7 +49,7 @@ // padding @content-padding: 25px; -// 函数 +// 函数,hex 添加 alpha 值 .addAlpha(@color, @alpha) { @red: red(@color); @green: green(@color); @@ -56,6 +58,7 @@ } // 混合 +// 单行 .singleLine() { overflow: hidden; white-space: nowrap; @@ -63,6 +66,7 @@ word-break: break-all; } +// 多行 .multiLine(@line) { display: -webkit-box; overflow: hidden; diff --git a/react-ui/src/utils/constant.ts b/react-ui/src/utils/constant.ts new file mode 100644 index 00000000..4fe1ea9b --- /dev/null +++ b/react-ui/src/utils/constant.ts @@ -0,0 +1,3 @@ +export const xlCols = { span: 12 }; +export const xllCols = { span: 10 }; +export const formCols = { xl: xlCols, xxl: xllCols }; diff --git a/react-ui/src/utils/format.ts b/react-ui/src/utils/format.ts new file mode 100644 index 00000000..c540e441 --- /dev/null +++ b/react-ui/src/utils/format.ts @@ -0,0 +1,133 @@ +import { ResourceSelectorResponse } from '@/components/ResourceSelectorModal'; +import { ResourceInfoTabKeys } from '@/pages/Dataset/components/ResourceInfo'; +import { + DataSource, + DatasetData, + ModelData, + ProjectDependency, + TrainTask, +} from '@/pages/Dataset/config'; +import { getGitUrl } from '@/utils'; +// 格式化日期 +export { formatDate } from '@/utils/date'; + +type SelectedCodeConfig = { + code_path: string; + branch: string; + showValue?: string; // 前端使用的 + show_value?: string; // 后端使用的 +}; + +// 格式化数据集数组 +export const formatDatasets = (datasets?: DatasetData[]) => { + if (!datasets || datasets.length === 0) { + return undefined; + } + return datasets.map((item) => ({ + value: item.name, + link: `/dataset/dataset/info/${item.id}?tab=${ResourceInfoTabKeys.Introduction}&version=${item.version}&name=${item.name}&owner=${item.owner}&identifier=${item.identifier}`, + })); +}; + +// 格式化数据集 +export const formatDataset = (dataset?: DatasetData) => { + if (!dataset) { + return undefined; + } + return { + value: dataset.name, + link: `/dataset/dataset/info/${dataset.id}?tab=${ResourceInfoTabKeys.Introduction}&version=${dataset.version}&name=${dataset.name}&owner=${dataset.owner}&identifier=${dataset.identifier}`, + }; +}; + +// 格式化模型 +export const formatModel = (model: ModelData) => { + if (!model) { + return undefined; + } + return { + value: model.name, + link: `/dataset/model/info/${model.id}?tab=${ResourceInfoTabKeys.Introduction}&version=${model.version}&name=${model.name}&owner=${model.owner}&identifier=${model.identifier}`, + }; +}; + +// 格式化镜像 +export const formatMirror = (mirror: ResourceSelectorResponse) => { + if (!mirror) { + return undefined; + } + return mirror.path; +}; + +// 格式化代码配置 +export const formatCodeConfig = (project?: ProjectDependency | SelectedCodeConfig) => { + if (!project) { + return undefined; + } + // 创建表单,CodeSelect 组件返回,目前有流水线、模型部署、超参数自动寻优创建时选择了代码配置 + if ('code_path' in project) { + const { showValue, show_value, code_path, branch } = project; + return { + value: showValue || show_value, + url: getGitUrl(code_path, branch), + }; + } else { + // 数据集和模型的代码配置 + const { url, branch, name } = project; + return { + value: name, + url: getGitUrl(url, branch), + }; + } +}; + +// 格式化训练任务(实验实例) +export const formatTrainTask = (task?: TrainTask) => { + if (!task) { + return undefined; + } + return { + value: task.name, + url: `/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; +}; + +// 格式化字符串数组,以逗号分隔 +export const formatList = (value: string[] | null | undefined): string => { + if ( + value === undefined || + value === null || + Array.isArray(value) === false || + value.length === 0 + ) { + return '--'; + } + return value.join(','); +}; + +// 格式化布尔值 +export const formatBoolean = (value: boolean): string => { + return value ? '是' : '否'; +}; + +type FormatEnum = (value: string | number) => string; + +// 格式化枚举 +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 : '--'; + }; +}; diff --git a/react-ui/src/utils/table.tsx b/react-ui/src/utils/table.tsx index 0d4b1927..d3ec10d6 100644 --- a/react-ui/src/utils/table.tsx +++ b/react-ui/src/utils/table.tsx @@ -4,6 +4,7 @@ * @Description: Table cell 自定义 render */ +import { isEmpty } from '@/utils'; import { formatDate } from '@/utils/date'; import { Tooltip } from 'antd'; import dayjs from 'dayjs'; @@ -113,7 +114,7 @@ function renderCell( } function renderText(text: any | undefined | null) { - return {text ?? '--'}; + return {!isEmpty(text) ? text : '--'}; } function renderLink( diff --git a/react-ui/tsconfig.json b/react-ui/tsconfig.json index 0afa8788..55ce7f74 100644 --- a/react-ui/tsconfig.json +++ b/react-ui/tsconfig.json @@ -9,7 +9,7 @@ "strict": true, // 启用所有严格类型检查选项 "forceConsistentCasingInFileNames": false, // 允许对同一文件的引用使用不一致的大小写 "module": "esnext", // 指定模块代码生成 - "moduleResolution": "node", // 使用Node.js样式解析模块 + "moduleResolution": "bundler", // 使用bundlers样式解析模块 "isolatedModules": true, // 无条件地为未解析的文件发出导入 "resolveJsonModule": true, // 包含.json扩展名的模块 "noEmit": true, // 不发出输出(即不编译代码,只进行类型检查) diff --git a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/experiment/ExperimentController.java b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/experiment/ExperimentController.java index 7eba2b2c..1faec4d4 100644 --- a/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/experiment/ExperimentController.java +++ b/ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/experiment/ExperimentController.java @@ -102,7 +102,7 @@ public class ExperimentController extends BaseController { * @return 删除是否成功 */ @DeleteMapping("{id}") - @ApiOperation("删除流水线") + @ApiOperation("删除实验") public GenericsAjaxResult deleteById(@PathVariable("id") Integer id) throws Exception { return genericsSuccess(this.experimentService.removeById(id)); }