| @@ -58,3 +58,5 @@ mvnw | |||||
| # web | # web | ||||
| **/node_modules | **/node_modules | ||||
| *storybook.log | |||||
| @@ -41,10 +41,5 @@ screenshot | |||||
| build | build | ||||
| pnpm-lock.yaml | pnpm-lock.yaml | ||||
| /src/services/codeConfig/index.js | |||||
| /src/pages/CodeConfig/components/AddCodeConfigModal/index.less | |||||
| /src/pages/CodeConfig/List/index.less | |||||
| /src/pages/Dataset/components/ResourceItem/index.less | |||||
| /src/pages/CodeConfig/components/AddCodeConfigModal/index.tsx | |||||
| /src/pages/CodeConfig/components/CodeConfigItem/index.tsx | |||||
| /src/pages/Dataset/components/ResourceItem/index.tsx | |||||
| *storybook.log | |||||
| @@ -0,0 +1 @@ | |||||
| v18.16.0 | |||||
| @@ -0,0 +1,16 @@ | |||||
| export default function(babel) { | |||||
| const { types: t } = babel; | |||||
| return { | |||||
| visitor: { | |||||
| ImportDeclaration(path) { | |||||
| const source = path.node.source.value; | |||||
| // console.log("zzzz", source); | |||||
| if (source.endsWith('.less')) { | |||||
| if (path.node.specifiers.length > 0) { | |||||
| path.node.source.value += "?modules"; | |||||
| } | |||||
| } | |||||
| }, | |||||
| }, | |||||
| }; | |||||
| }; | |||||
| @@ -0,0 +1,117 @@ | |||||
| import type { StorybookConfig } from '@storybook/react-webpack5'; | |||||
| import path from 'path'; | |||||
| import webpack from 'webpack'; | |||||
| const config: StorybookConfig = { | |||||
| stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], | |||||
| addons: [ | |||||
| // '@storybook/addon-webpack5-compiler-swc', | |||||
| '@storybook/addon-webpack5-compiler-babel', | |||||
| '@storybook/addon-onboarding', | |||||
| '@storybook/addon-essentials', | |||||
| '@chromatic-com/storybook', | |||||
| '@storybook/addon-interactions', | |||||
| ], | |||||
| framework: { | |||||
| name: '@storybook/react-webpack5', | |||||
| options: {}, | |||||
| }, | |||||
| staticDirs: ['../public'], | |||||
| docs: { | |||||
| defaultName: 'Documentation', | |||||
| }, | |||||
| webpackFinal: async (config) => { | |||||
| if (config.resolve) { | |||||
| config.resolve.alias = { | |||||
| ...config.resolve.alias, | |||||
| '@': path.resolve(__dirname, '../src'), | |||||
| '@umijs/max$': path.resolve(__dirname, './mock/umijs.mock.tsx'), | |||||
| }; | |||||
| } | |||||
| if (config.module && config.module.rules) { | |||||
| config.module.rules.push( | |||||
| { | |||||
| test: /\.less$/, | |||||
| oneOf: [ | |||||
| { | |||||
| resourceQuery: /modules/, | |||||
| use: [ | |||||
| 'style-loader', | |||||
| { | |||||
| loader: 'css-loader', | |||||
| options: { | |||||
| importLoaders: 1, | |||||
| import: true, | |||||
| esModule: true, | |||||
| modules: { | |||||
| localIdentName: '[local]___[hash:base64:5]', | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| { | |||||
| loader: 'less-loader', | |||||
| options: { | |||||
| lessOptions: { | |||||
| javascriptEnabled: true, // 如果需要支持 Ant Design 的 Less 变量,开启此项 | |||||
| modifyVars: { | |||||
| hack: 'true; @import "@/styles/theme.less";', | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| ], | |||||
| include: path.resolve(__dirname, '../src'), // 限制范围,避免处理 node_modules | |||||
| }, | |||||
| { | |||||
| use: [ | |||||
| 'style-loader', | |||||
| 'css-loader', | |||||
| { | |||||
| loader: 'less-loader', | |||||
| options: { | |||||
| lessOptions: { | |||||
| javascriptEnabled: true, // 如果需要支持 Ant Design 的 Less 变量,开启此项 | |||||
| modifyVars: { | |||||
| hack: 'true; @import "@/styles/theme.less";', | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| ], | |||||
| include: path.resolve(__dirname, '../src'), // 限制范围,避免处理 node_modules | |||||
| }, | |||||
| ], | |||||
| }, | |||||
| { | |||||
| test: /\.(tsx?|jsx?)$/, | |||||
| loader: 'ts-loader', | |||||
| options: { | |||||
| transpileOnly: true, | |||||
| }, | |||||
| include: [ | |||||
| path.resolve(__dirname, '../src'), // 限制范围,避免处理 node_modules | |||||
| path.resolve(__dirname, './'), | |||||
| ], | |||||
| }, | |||||
| ); | |||||
| } | |||||
| if (config.plugins) { | |||||
| config.plugins.push( | |||||
| new webpack.ProvidePlugin({ | |||||
| React: 'react', // 全局注入 React | |||||
| }), | |||||
| ); | |||||
| } | |||||
| return config; | |||||
| }, | |||||
| babel: async (config: any) => { | |||||
| if (!config.plugins) { | |||||
| config.plugins = []; | |||||
| } | |||||
| config.plugins.push(path.resolve(__dirname, './babel-plugin-auto-css-modules.js')); | |||||
| return config; | |||||
| }, | |||||
| }; | |||||
| export default config; | |||||
| @@ -0,0 +1,19 @@ | |||||
| export const Link = ({ to, children, ...props }: any) => ( | |||||
| <a href={typeof to === 'string' ? to : '#'} {...props}> | |||||
| {children} | |||||
| </a> | |||||
| ); | |||||
| export const request = (url: string, options: any) => { | |||||
| return fetch(url, options) | |||||
| .then((res) => { | |||||
| if (!res.ok) { | |||||
| throw new Error(res.statusText); | |||||
| } | |||||
| return res; | |||||
| }) | |||||
| .then((res) => res.json()); | |||||
| }; | |||||
| export { useNavigate, useParams, useSearchParams } from 'react-router-dom'; | |||||
| export const history = window.history; | |||||
| @@ -0,0 +1,96 @@ | |||||
| 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: { | |||||
| matchers: { | |||||
| color: /(background|color)$/i, | |||||
| date: /Date$/i, | |||||
| }, | |||||
| }, | |||||
| options: { | |||||
| storySort: { | |||||
| method: 'alphabetical', | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| decorators: [ | |||||
| (Story) => ( | |||||
| <ConfigProvider | |||||
| locale={zhCN} | |||||
| theme={{ | |||||
| cssVar: true, | |||||
| token: { | |||||
| colorPrimary: themes['primaryColor'], | |||||
| colorSuccess: themes['successColor'], | |||||
| colorError: themes['errorColor'], | |||||
| colorWarning: themes['warningColor'], | |||||
| colorLink: themes['primaryColor'], | |||||
| colorText: themes['textColor'], | |||||
| controlHeightLG: 46, | |||||
| }, | |||||
| components: { | |||||
| Button: { | |||||
| defaultBg: 'rgba(22, 100, 255, 0.06)', | |||||
| defaultBorderColor: 'rgba(22, 100, 255, 0.11)', | |||||
| defaultColor: themes['textColor'], | |||||
| defaultHoverBg: 'rgba(22, 100, 255, 0.06)', | |||||
| defaultHoverBorderColor: 'rgba(22, 100, 255, 0.5)', | |||||
| defaultHoverColor: '#3F7FFF ', | |||||
| defaultActiveBg: 'rgba(22, 100, 255, 0.12)', | |||||
| defaultActiveBorderColor: 'rgba(22, 100, 255, 0.75)', | |||||
| defaultActiveColor: themes['primaryColor'], | |||||
| contentFontSize: parseInt(themes['fontSize']), | |||||
| }, | |||||
| Input: { | |||||
| inputFontSize: parseInt(themes['fontSizeInput']), | |||||
| inputFontSizeLG: parseInt(themes['fontSizeInputLg']), | |||||
| paddingBlockLG: 10, | |||||
| }, | |||||
| Select: { | |||||
| singleItemHeightLG: 46, | |||||
| optionSelectedColor: themes['primaryColor'], | |||||
| }, | |||||
| Table: { | |||||
| headerBg: 'rgba(242, 244, 247, 0.36)', | |||||
| headerBorderRadius: 4, | |||||
| rowSelectedBg: 'rgba(22, 100, 255, 0.05)', | |||||
| }, | |||||
| Tabs: { | |||||
| titleFontSize: 16, | |||||
| }, | |||||
| Form: { | |||||
| labelColor: 'rgba(29, 29, 32, 0.8);', | |||||
| }, | |||||
| Breadcrumb: { | |||||
| iconFontSize: parseInt(themes['fontSize']), | |||||
| linkColor: 'rgba(29, 29, 32, 0.7)', | |||||
| separatorColor: 'rgba(29, 29, 32, 0.7)', | |||||
| }, | |||||
| }, | |||||
| }} | |||||
| > | |||||
| <App message={{ maxCount: 3 }}> | |||||
| <Story /> | |||||
| </App> | |||||
| </ConfigProvider> | |||||
| ), | |||||
| ], | |||||
| loaders: [mswLoader], // 👈 Add the MSW loader to all stories | |||||
| }; | |||||
| export default preview; | |||||
| @@ -0,0 +1,19 @@ | |||||
| html, | |||||
| body, | |||||
| #root { | |||||
| min-width: unset; | |||||
| height: 100%; | |||||
| margin: 0; | |||||
| padding: 0; | |||||
| overflow-y: visible; | |||||
| } | |||||
| .ant-input-search-large .ant-input-affix-wrapper, .ant-input-search-large .ant-input-search-button { | |||||
| height: 46px; | |||||
| } | |||||
| *, | |||||
| *::before, | |||||
| *::after { | |||||
| box-sizing: border-box; | |||||
| } | |||||
| @@ -0,0 +1,26 @@ | |||||
| { | |||||
| "compilerOptions": { | |||||
| "target": "esnext", // 指定ECMAScript目标版本 | |||||
| "lib": ["dom", "dom.iterable", "esnext"], // 要包含在编译中的库文件列表 | |||||
| "allowJs": true, // 允许编译JavaScript文件 | |||||
| "skipLibCheck": true, // 跳过所有声明文件的类型检查 | |||||
| "esModuleInterop": true, // 禁用命名空间导入(import * as fs from "fs"),并启用CJS/AMD/UMD样式的导入(import fs from "fs") | |||||
| "allowSyntheticDefaultImports": true, // 允许从没有默认导出的模块进行默认导入 | |||||
| "strict": true, // 启用所有严格类型检查选项 | |||||
| "forceConsistentCasingInFileNames": false, // 允许对同一文件的引用使用不一致的大小写 | |||||
| "module": "esnext", // 指定模块代码生成 | |||||
| "moduleResolution": "bundler", // 使用bundlers样式解析模块 | |||||
| "isolatedModules": true, // 无条件地为未解析的文件发出导入 | |||||
| "resolveJsonModule": true, // 包含.json扩展名的模块 | |||||
| "noEmit": true, // 不发出输出(即不编译代码,只进行类型检查) | |||||
| "jsx": "react-jsx", // 在.tsx文件中支持JSX | |||||
| "sourceMap": true, // 生成相应的.map文件 | |||||
| "declaration": true, // 生成相应的.d.ts文件 | |||||
| "noUnusedLocals": true, // 报告未使用的局部变量错误 | |||||
| "noUnusedParameters": true, // 报告未使用的参数错误 | |||||
| "incremental": true, // 通过读写磁盘上的文件来启用增量编译 | |||||
| "noFallthroughCasesInSwitch": true, // 报告switch语句中的fallthrough案例错误 | |||||
| "strictNullChecks": true, // 启用严格的null检查 | |||||
| "baseUrl": "./" | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,20 @@ | |||||
| declare module 'slash2'; | |||||
| declare module '*.css'; | |||||
| declare module '*.less'; | |||||
| declare module '*.scss'; | |||||
| declare module '*.sass'; | |||||
| declare module '*.svg'; | |||||
| declare module '*.png'; | |||||
| declare module '*.jpg'; | |||||
| declare module '*.jpeg'; | |||||
| declare module '*.gif'; | |||||
| declare module '*.bmp'; | |||||
| declare module '*.tiff'; | |||||
| declare module 'omit.js'; | |||||
| declare module 'numeral'; | |||||
| declare module '@antv/data-set'; | |||||
| declare module 'mockjs'; | |||||
| declare module 'react-fittext'; | |||||
| declare module 'bizcharts-plugin-slider'; | |||||
| declare const REACT_APP_ENV: 'test' | 'dev' | 'pre' | false; | |||||
| @@ -39,7 +39,9 @@ | |||||
| "test": "jest", | "test": "jest", | ||||
| "test:coverage": "npm run jest -- --coverage", | "test:coverage": "npm run jest -- --coverage", | ||||
| "test:update": "npm run jest -- -u", | "test:update": "npm run jest -- -u", | ||||
| "tsc": "tsc --noEmit" | |||||
| "tsc": "tsc --noEmit", | |||||
| "storybook": "storybook dev -p 6006", | |||||
| "build-storybook": "storybook build" | |||||
| }, | }, | ||||
| "lint-staged": { | "lint-staged": { | ||||
| "**/*.{js,jsx,ts,tsx}": "npm run lint-staged:js", | "**/*.{js,jsx,ts,tsx}": "npm run lint-staged:js", | ||||
| @@ -83,6 +85,17 @@ | |||||
| }, | }, | ||||
| "devDependencies": { | "devDependencies": { | ||||
| "@ant-design/pro-cli": "^3.1.0", | "@ant-design/pro-cli": "^3.1.0", | ||||
| "@chromatic-com/storybook": "~3.2.4", | |||||
| "@storybook/addon-essentials": "~8.5.3", | |||||
| "@storybook/addon-interactions": "~8.5.3", | |||||
| "@storybook/addon-onboarding": "~8.5.3", | |||||
| "@storybook/addon-styling-webpack": "~1.0.1", | |||||
| "@storybook/addon-webpack5-compiler-babel": "~3.0.5", | |||||
| "@storybook/addon-webpack5-compiler-swc": "~2.0.0", | |||||
| "@storybook/blocks": "~8.5.3", | |||||
| "@storybook/react": "~8.5.3", | |||||
| "@storybook/react-webpack5": "~8.5.3", | |||||
| "@storybook/test": "~8.5.3", | |||||
| "@testing-library/react": "^14.0.0", | "@testing-library/react": "^14.0.0", | ||||
| "@types/antd": "^1.0.0", | "@types/antd": "^1.0.0", | ||||
| "@types/express": "^4.17.14", | "@types/express": "^4.17.14", | ||||
| @@ -96,15 +109,22 @@ | |||||
| "@umijs/max": "^4.0.66", | "@umijs/max": "^4.0.66", | ||||
| "cross-env": "^7.0.3", | "cross-env": "^7.0.3", | ||||
| "eslint": "^8.39.0", | "eslint": "^8.39.0", | ||||
| "eslint-plugin-storybook": "~0.11.2", | |||||
| "express": "^4.18.2", | "express": "^4.18.2", | ||||
| "gh-pages": "^5.0.0", | "gh-pages": "^5.0.0", | ||||
| "husky": "^8.0.3", | "husky": "^8.0.3", | ||||
| "jest": "^29.5.0", | "jest": "^29.5.0", | ||||
| "jest-environment-jsdom": "^29.5.0", | "jest-environment-jsdom": "^29.5.0", | ||||
| "less": "~4.2.2", | |||||
| "less-loader": "~12.2.0", | |||||
| "lint-staged": "^13.2.0", | "lint-staged": "^13.2.0", | ||||
| "mockjs": "^1.1.0", | "mockjs": "^1.1.0", | ||||
| "msw": "~2.7.0", | |||||
| "msw-storybook-addon": "~2.0.4", | |||||
| "prettier": "^2.8.1", | "prettier": "^2.8.1", | ||||
| "storybook": "~8.5.3", | |||||
| "swagger-ui-dist": "^4.18.2", | "swagger-ui-dist": "^4.18.2", | ||||
| "ts-loader": "~9.5.2", | |||||
| "ts-node": "^10.9.1", | "ts-node": "^10.9.1", | ||||
| "typescript": "^5.0.4", | "typescript": "^5.0.4", | ||||
| "umi-presets-pro": "^2.0.0" | "umi-presets-pro": "^2.0.0" | ||||
| @@ -140,5 +160,10 @@ | |||||
| "CNAME", | "CNAME", | ||||
| "create-umi" | "create-umi" | ||||
| ] | ] | ||||
| }, | |||||
| "msw": { | |||||
| "workerDirectory": [ | |||||
| "public" | |||||
| ] | |||||
| } | } | ||||
| } | |||||
| } | |||||
| @@ -0,0 +1,307 @@ | |||||
| /* eslint-disable */ | |||||
| /* tslint:disable */ | |||||
| /** | |||||
| * Mock Service Worker. | |||||
| * @see https://github.com/mswjs/msw | |||||
| * - Please do NOT modify this file. | |||||
| * - Please do NOT serve this file on production. | |||||
| */ | |||||
| const PACKAGE_VERSION = '2.7.0' | |||||
| const INTEGRITY_CHECKSUM = '00729d72e3b82faf54ca8b9621dbb96f' | |||||
| const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') | |||||
| const activeClientIds = new Set() | |||||
| self.addEventListener('install', function () { | |||||
| self.skipWaiting() | |||||
| }) | |||||
| self.addEventListener('activate', function (event) { | |||||
| event.waitUntil(self.clients.claim()) | |||||
| }) | |||||
| self.addEventListener('message', async function (event) { | |||||
| const clientId = event.source.id | |||||
| if (!clientId || !self.clients) { | |||||
| return | |||||
| } | |||||
| const client = await self.clients.get(clientId) | |||||
| if (!client) { | |||||
| return | |||||
| } | |||||
| const allClients = await self.clients.matchAll({ | |||||
| type: 'window', | |||||
| }) | |||||
| switch (event.data) { | |||||
| case 'KEEPALIVE_REQUEST': { | |||||
| sendToClient(client, { | |||||
| type: 'KEEPALIVE_RESPONSE', | |||||
| }) | |||||
| break | |||||
| } | |||||
| case 'INTEGRITY_CHECK_REQUEST': { | |||||
| sendToClient(client, { | |||||
| type: 'INTEGRITY_CHECK_RESPONSE', | |||||
| payload: { | |||||
| packageVersion: PACKAGE_VERSION, | |||||
| checksum: INTEGRITY_CHECKSUM, | |||||
| }, | |||||
| }) | |||||
| break | |||||
| } | |||||
| case 'MOCK_ACTIVATE': { | |||||
| activeClientIds.add(clientId) | |||||
| sendToClient(client, { | |||||
| type: 'MOCKING_ENABLED', | |||||
| payload: { | |||||
| client: { | |||||
| id: client.id, | |||||
| frameType: client.frameType, | |||||
| }, | |||||
| }, | |||||
| }) | |||||
| break | |||||
| } | |||||
| case 'MOCK_DEACTIVATE': { | |||||
| activeClientIds.delete(clientId) | |||||
| break | |||||
| } | |||||
| case 'CLIENT_CLOSED': { | |||||
| activeClientIds.delete(clientId) | |||||
| const remainingClients = allClients.filter((client) => { | |||||
| return client.id !== clientId | |||||
| }) | |||||
| // Unregister itself when there are no more clients | |||||
| if (remainingClients.length === 0) { | |||||
| self.registration.unregister() | |||||
| } | |||||
| break | |||||
| } | |||||
| } | |||||
| }) | |||||
| self.addEventListener('fetch', function (event) { | |||||
| const { request } = event | |||||
| // Bypass navigation requests. | |||||
| if (request.mode === 'navigate') { | |||||
| return | |||||
| } | |||||
| // Opening the DevTools triggers the "only-if-cached" request | |||||
| // that cannot be handled by the worker. Bypass such requests. | |||||
| if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { | |||||
| return | |||||
| } | |||||
| // Bypass all requests when there are no active clients. | |||||
| // Prevents the self-unregistered worked from handling requests | |||||
| // after it's been deleted (still remains active until the next reload). | |||||
| if (activeClientIds.size === 0) { | |||||
| return | |||||
| } | |||||
| // Generate unique request ID. | |||||
| const requestId = crypto.randomUUID() | |||||
| event.respondWith(handleRequest(event, requestId)) | |||||
| }) | |||||
| async function handleRequest(event, requestId) { | |||||
| const client = await resolveMainClient(event) | |||||
| const response = await getResponse(event, client, requestId) | |||||
| // Send back the response clone for the "response:*" life-cycle events. | |||||
| // Ensure MSW is active and ready to handle the message, otherwise | |||||
| // this message will pend indefinitely. | |||||
| if (client && activeClientIds.has(client.id)) { | |||||
| ;(async function () { | |||||
| const responseClone = response.clone() | |||||
| sendToClient( | |||||
| client, | |||||
| { | |||||
| type: 'RESPONSE', | |||||
| payload: { | |||||
| requestId, | |||||
| isMockedResponse: IS_MOCKED_RESPONSE in response, | |||||
| type: responseClone.type, | |||||
| status: responseClone.status, | |||||
| statusText: responseClone.statusText, | |||||
| body: responseClone.body, | |||||
| headers: Object.fromEntries(responseClone.headers.entries()), | |||||
| }, | |||||
| }, | |||||
| [responseClone.body], | |||||
| ) | |||||
| })() | |||||
| } | |||||
| return response | |||||
| } | |||||
| // Resolve the main client for the given event. | |||||
| // Client that issues a request doesn't necessarily equal the client | |||||
| // that registered the worker. It's with the latter the worker should | |||||
| // communicate with during the response resolving phase. | |||||
| async function resolveMainClient(event) { | |||||
| const client = await self.clients.get(event.clientId) | |||||
| if (activeClientIds.has(event.clientId)) { | |||||
| return client | |||||
| } | |||||
| if (client?.frameType === 'top-level') { | |||||
| return client | |||||
| } | |||||
| const allClients = await self.clients.matchAll({ | |||||
| type: 'window', | |||||
| }) | |||||
| return allClients | |||||
| .filter((client) => { | |||||
| // Get only those clients that are currently visible. | |||||
| return client.visibilityState === 'visible' | |||||
| }) | |||||
| .find((client) => { | |||||
| // Find the client ID that's recorded in the | |||||
| // set of clients that have registered the worker. | |||||
| return activeClientIds.has(client.id) | |||||
| }) | |||||
| } | |||||
| async function getResponse(event, client, requestId) { | |||||
| const { request } = event | |||||
| // Clone the request because it might've been already used | |||||
| // (i.e. its body has been read and sent to the client). | |||||
| const requestClone = request.clone() | |||||
| function passthrough() { | |||||
| // Cast the request headers to a new Headers instance | |||||
| // so the headers can be manipulated with. | |||||
| const headers = new Headers(requestClone.headers) | |||||
| // Remove the "accept" header value that marked this request as passthrough. | |||||
| // This prevents request alteration and also keeps it compliant with the | |||||
| // user-defined CORS policies. | |||||
| const acceptHeader = headers.get('accept') | |||||
| if (acceptHeader) { | |||||
| const values = acceptHeader.split(',').map((value) => value.trim()) | |||||
| const filteredValues = values.filter( | |||||
| (value) => value !== 'msw/passthrough', | |||||
| ) | |||||
| if (filteredValues.length > 0) { | |||||
| headers.set('accept', filteredValues.join(', ')) | |||||
| } else { | |||||
| headers.delete('accept') | |||||
| } | |||||
| } | |||||
| return fetch(requestClone, { headers }) | |||||
| } | |||||
| // Bypass mocking when the client is not active. | |||||
| if (!client) { | |||||
| return passthrough() | |||||
| } | |||||
| // Bypass initial page load requests (i.e. static assets). | |||||
| // The absence of the immediate/parent client in the map of the active clients | |||||
| // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet | |||||
| // and is not ready to handle requests. | |||||
| if (!activeClientIds.has(client.id)) { | |||||
| return passthrough() | |||||
| } | |||||
| // Notify the client that a request has been intercepted. | |||||
| const requestBuffer = await request.arrayBuffer() | |||||
| const clientMessage = await sendToClient( | |||||
| client, | |||||
| { | |||||
| type: 'REQUEST', | |||||
| payload: { | |||||
| id: requestId, | |||||
| url: request.url, | |||||
| mode: request.mode, | |||||
| method: request.method, | |||||
| headers: Object.fromEntries(request.headers.entries()), | |||||
| cache: request.cache, | |||||
| credentials: request.credentials, | |||||
| destination: request.destination, | |||||
| integrity: request.integrity, | |||||
| redirect: request.redirect, | |||||
| referrer: request.referrer, | |||||
| referrerPolicy: request.referrerPolicy, | |||||
| body: requestBuffer, | |||||
| keepalive: request.keepalive, | |||||
| }, | |||||
| }, | |||||
| [requestBuffer], | |||||
| ) | |||||
| switch (clientMessage.type) { | |||||
| case 'MOCK_RESPONSE': { | |||||
| return respondWithMock(clientMessage.data) | |||||
| } | |||||
| case 'PASSTHROUGH': { | |||||
| return passthrough() | |||||
| } | |||||
| } | |||||
| return passthrough() | |||||
| } | |||||
| function sendToClient(client, message, transferrables = []) { | |||||
| return new Promise((resolve, reject) => { | |||||
| const channel = new MessageChannel() | |||||
| channel.port1.onmessage = (event) => { | |||||
| if (event.data && event.data.error) { | |||||
| return reject(event.data.error) | |||||
| } | |||||
| resolve(event.data) | |||||
| } | |||||
| client.postMessage( | |||||
| message, | |||||
| [channel.port2].concat(transferrables.filter(Boolean)), | |||||
| ) | |||||
| }) | |||||
| } | |||||
| async function respondWithMock(response) { | |||||
| // Setting response status code to 0 is a no-op. | |||||
| // However, when responding with a "Response.error()", the produced Response | |||||
| // instance will have status code set to 0. Since it's not possible to create | |||||
| // a Response instance with status code 0, handle that use-case separately. | |||||
| if (response.status === 0) { | |||||
| return Response.error() | |||||
| } | |||||
| const mockedResponse = new Response(response.body, response) | |||||
| Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { | |||||
| value: true, | |||||
| enumerable: true, | |||||
| }) | |||||
| return mockedResponse | |||||
| } | |||||
| @@ -7,7 +7,6 @@ import defaultSettings from '../config/defaultSettings'; | |||||
| import '../public/fonts/font.css'; | import '../public/fonts/font.css'; | ||||
| import { getAccessToken } from './access'; | import { getAccessToken } from './access'; | ||||
| import './dayjsConfig'; | import './dayjsConfig'; | ||||
| import './global.less'; | |||||
| import { removeAllPageCacheState } from './hooks/pageCacheState'; | import { removeAllPageCacheState } from './hooks/pageCacheState'; | ||||
| import { | import { | ||||
| getRemoteMenu, | getRemoteMenu, | ||||
| @@ -41,7 +40,7 @@ export async function getInitialState(): Promise<GlobalInitialState> { | |||||
| roleNames: response.user.roles, | roleNames: response.user.roles, | ||||
| } as API.CurrentUser; | } as API.CurrentUser; | ||||
| } catch (error) { | } catch (error) { | ||||
| console.error('1111', error); | |||||
| console.error('getInitialState', error); | |||||
| gotoLoginPage(); | gotoLoginPage(); | ||||
| } | } | ||||
| return undefined; | return undefined; | ||||
| @@ -215,7 +214,7 @@ export const antd: RuntimeAntdConfig = (memo) => { | |||||
| defaultColor: themes['textColor'], | defaultColor: themes['textColor'], | ||||
| defaultHoverBg: 'rgba(22, 100, 255, 0.06)', | defaultHoverBg: 'rgba(22, 100, 255, 0.06)', | ||||
| defaultHoverBorderColor: 'rgba(22, 100, 255, 0.5)', | defaultHoverBorderColor: 'rgba(22, 100, 255, 0.5)', | ||||
| defaultHoverColor: '#3F7FFF ', | |||||
| defaultHoverColor: '#3F7FFF', | |||||
| defaultActiveBg: 'rgba(22, 100, 255, 0.12)', | defaultActiveBg: 'rgba(22, 100, 255, 0.12)', | ||||
| defaultActiveBorderColor: 'rgba(22, 100, 255, 0.75)', | defaultActiveBorderColor: 'rgba(22, 100, 255, 0.75)', | ||||
| defaultActiveColor: themes['primaryColor'], | defaultActiveColor: themes['primaryColor'], | ||||
| @@ -4,23 +4,37 @@ | |||||
| * @Description: 用于 BasicInfo 和 BasicTableInfo 组件的子组件 | * @Description: 用于 BasicInfo 和 BasicTableInfo 组件的子组件 | ||||
| */ | */ | ||||
| import { Typography } from 'antd'; | |||||
| import React from 'react'; | import React from 'react'; | ||||
| import BasicInfoItemValue from './BasicInfoItemValue'; | import BasicInfoItemValue from './BasicInfoItemValue'; | ||||
| import { type BasicInfoData, type BasicInfoLink } from './types'; | import { type BasicInfoData, type BasicInfoLink } from './types'; | ||||
| type BasicInfoItemProps = { | type BasicInfoItemProps = { | ||||
| /** 基础信息 */ | |||||
| data: BasicInfoData; | data: BasicInfoData; | ||||
| /** 标题宽度 */ | |||||
| labelWidth: number; | labelWidth: number; | ||||
| /** 自定义类名前缀 */ | |||||
| classPrefix: string; | classPrefix: string; | ||||
| /** 标题是否显示省略号 */ | |||||
| labelEllipsis?: boolean; | |||||
| /** 标签对齐方式 */ | |||||
| labelAlign?: 'start' | 'end' | 'justify'; | |||||
| }; | }; | ||||
| function BasicInfoItem({ data, labelWidth, classPrefix }: BasicInfoItemProps) { | |||||
| function BasicInfoItem({ | |||||
| data, | |||||
| labelWidth, | |||||
| classPrefix, | |||||
| labelEllipsis = true, | |||||
| labelAlign = 'start', | |||||
| }: BasicInfoItemProps) { | |||||
| const { label, value, format, ellipsis } = data; | const { label, value, format, ellipsis } = data; | ||||
| const formatValue = format ? format(value) : value; | const formatValue = format ? format(value) : value; | ||||
| const myClassName = `${classPrefix}__item`; | const myClassName = `${classPrefix}__item`; | ||||
| let valueComponent = undefined; | let valueComponent = undefined; | ||||
| if (React.isValidElement(formatValue)) { | if (React.isValidElement(formatValue)) { | ||||
| valueComponent = formatValue; | |||||
| valueComponent = <div className={`${myClassName}__node`}>{formatValue}</div>; | |||||
| } else if (Array.isArray(formatValue)) { | } else if (Array.isArray(formatValue)) { | ||||
| valueComponent = ( | valueComponent = ( | ||||
| <div className={`${myClassName}__value-container`}> | <div className={`${myClassName}__value-container`}> | ||||
| @@ -53,8 +67,16 @@ function BasicInfoItem({ data, labelWidth, classPrefix }: BasicInfoItemProps) { | |||||
| } | } | ||||
| return ( | return ( | ||||
| <div className={myClassName} key={label}> | <div className={myClassName} key={label}> | ||||
| <div className={`${myClassName}__label`} style={{ width: labelWidth }}> | |||||
| {label} | |||||
| <div | |||||
| className={`${myClassName}__label`} | |||||
| style={{ width: labelWidth, textAlign: labelAlign, textAlignLast: labelAlign }} | |||||
| > | |||||
| <Typography.Text | |||||
| ellipsis={labelEllipsis !== false ? { tooltip: label } : false} | |||||
| style={{ width: labelAlign === 'justify' ? '100%' : 'auto' }} | |||||
| > | |||||
| {label} | |||||
| </Typography.Text> | |||||
| </div> | </div> | ||||
| {valueComponent} | {valueComponent} | ||||
| </div> | </div> | ||||
| @@ -4,23 +4,30 @@ | |||||
| * @Description: 用于 BasicInfoItem 的组件 | * @Description: 用于 BasicInfoItem 的组件 | ||||
| */ | */ | ||||
| import { isEmpty } from '@/utils'; | |||||
| import { Link } from '@umijs/max'; | import { Link } from '@umijs/max'; | ||||
| import { Typography } from 'antd'; | import { Typography } from 'antd'; | ||||
| import React from 'react'; | |||||
| type BasicInfoItemValueProps = { | type BasicInfoItemValueProps = { | ||||
| /** 值是否显示省略号 */ | |||||
| ellipsis?: boolean; | ellipsis?: boolean; | ||||
| /** 自定义类名前缀 */ | |||||
| classPrefix: string; | classPrefix: string; | ||||
| value: string | React.ReactNode; | |||||
| /** 值 */ | |||||
| value?: string; | |||||
| /** 内部链接 */ | |||||
| link?: string; | link?: string; | ||||
| /** 外部链接 */ | |||||
| url?: string; | url?: string; | ||||
| }; | }; | ||||
| function BasicInfoItemValue({ value, link, url, ellipsis, classPrefix }: BasicInfoItemValueProps) { | |||||
| if (React.isValidElement(value)) { | |||||
| return value; | |||||
| } | |||||
| function BasicInfoItemValue({ | |||||
| value, | |||||
| link, | |||||
| url, | |||||
| classPrefix, | |||||
| ellipsis = true, | |||||
| }: BasicInfoItemValueProps) { | |||||
| const myClassName = `${classPrefix}__item__value`; | const myClassName = `${classPrefix}__item__value`; | ||||
| let component = undefined; | let component = undefined; | ||||
| if (url && value) { | if (url && value) { | ||||
| @@ -36,12 +43,12 @@ function BasicInfoItemValue({ value, link, url, ellipsis, classPrefix }: BasicIn | |||||
| </Link> | </Link> | ||||
| ); | ); | ||||
| } else { | } else { | ||||
| component = <span className={`${myClassName}__text`}>{value ?? '--'}</span>; | |||||
| component = <span className={`${myClassName}__text`}>{!isEmpty(value) ? value : '--'}</span>; | |||||
| } | } | ||||
| return ( | return ( | ||||
| <div className={myClassName}> | <div className={myClassName}> | ||||
| <Typography.Text ellipsis={ellipsis ? { tooltip: value } : false}> | |||||
| <Typography.Text ellipsis={ellipsis !== false ? { tooltip: value } : false}> | |||||
| {component} | {component} | ||||
| </Typography.Text> | </Typography.Text> | ||||
| </div> | </div> | ||||
| @@ -17,8 +17,6 @@ | |||||
| color: @text-color-secondary; | color: @text-color-secondary; | ||||
| font-size: @font-size-content; | font-size: @font-size-content; | ||||
| line-height: 1.6; | line-height: 1.6; | ||||
| text-align: justify; | |||||
| text-align-last: justify; | |||||
| &::after { | &::after { | ||||
| position: absolute; | position: absolute; | ||||
| @@ -31,10 +29,12 @@ | |||||
| flex: 1; | flex: 1; | ||||
| flex-direction: column; | flex-direction: column; | ||||
| gap: 5px 0; | gap: 5px 0; | ||||
| min-width: 0; | |||||
| } | } | ||||
| &__value { | &__value { | ||||
| flex: 1; | flex: 1; | ||||
| min-width: 0; | |||||
| margin-left: 16px; | margin-left: 16px; | ||||
| font-size: @font-size-content; | font-size: @font-size-content; | ||||
| line-height: 1.6; | line-height: 1.6; | ||||
| @@ -49,5 +49,29 @@ | |||||
| text-underline-offset: 3px; | text-underline-offset: 3px; | ||||
| } | } | ||||
| } | } | ||||
| &__node { | |||||
| flex: 1; | |||||
| min-width: 0; | |||||
| margin-left: 16px; | |||||
| font-size: @font-size-content; | |||||
| line-height: 1.6; | |||||
| word-break: break-all; | |||||
| } | |||||
| } | |||||
| } | |||||
| .kf-basic-info--three-columns { | |||||
| width: 100%; | |||||
| .kf-basic-info__item { | |||||
| width: calc((100% - 80px) / 3); | |||||
| &__label { | |||||
| font-size: @font-size; | |||||
| } | |||||
| &__value { | |||||
| font-size: @font-size; | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -5,22 +5,52 @@ import './index.less'; | |||||
| import type { BasicInfoData, BasicInfoLink } from './types'; | import type { BasicInfoData, BasicInfoLink } from './types'; | ||||
| export type { BasicInfoData, BasicInfoLink }; | export type { BasicInfoData, BasicInfoLink }; | ||||
| type BasicInfoProps = { | |||||
| export type BasicInfoProps = { | |||||
| /** 基础信息 */ | |||||
| datas: BasicInfoData[]; | datas: BasicInfoData[]; | ||||
| /** 标题宽度 */ | |||||
| labelWidth: number; | |||||
| /** 标题是否显示省略号 */ | |||||
| labelEllipsis?: boolean; | |||||
| /** 是否一行三列 */ | |||||
| threeColumns?: boolean; | |||||
| /** 标签对齐方式 */ | |||||
| labelAlign?: 'start' | 'end' | 'justify'; | |||||
| /** 自定义类名 */ | |||||
| className?: string; | className?: string; | ||||
| /** 自定义样式 */ | |||||
| style?: React.CSSProperties; | style?: React.CSSProperties; | ||||
| labelWidth: number; | |||||
| }; | }; | ||||
| export default function BasicInfo({ datas, className, style, labelWidth }: BasicInfoProps) { | |||||
| /** | |||||
| * 基础信息展示组件,用于展示基础信息,支持一行两列或一行三列,支持数据格式化 | |||||
| */ | |||||
| export default function BasicInfo({ | |||||
| datas, | |||||
| className, | |||||
| style, | |||||
| labelWidth, | |||||
| labelEllipsis = true, | |||||
| threeColumns = false, | |||||
| labelAlign = 'start', | |||||
| }: BasicInfoProps) { | |||||
| return ( | return ( | ||||
| <div className={classNames('kf-basic-info', className)} style={style}> | |||||
| <div | |||||
| className={classNames( | |||||
| 'kf-basic-info', | |||||
| { 'kf-basic-info--three-columns': threeColumns }, | |||||
| className, | |||||
| )} | |||||
| style={style} | |||||
| > | |||||
| {datas.map((item) => ( | {datas.map((item) => ( | ||||
| <BasicInfoItem | <BasicInfoItem | ||||
| key={item.label} | key={item.label} | ||||
| data={item} | data={item} | ||||
| labelWidth={labelWidth} | labelWidth={labelWidth} | ||||
| classPrefix="kf-basic-info" | classPrefix="kf-basic-info" | ||||
| labelEllipsis={labelEllipsis} | |||||
| labelAlign={labelAlign} | |||||
| /> | /> | ||||
| ))} | ))} | ||||
| </div> | </div> | ||||
| @@ -34,7 +34,6 @@ | |||||
| &__value { | &__value { | ||||
| flex: 1; | flex: 1; | ||||
| min-width: 0; | min-width: 0; | ||||
| margin: 0 !important; | |||||
| padding: 12px 20px 4px; | padding: 12px 20px 4px; | ||||
| font-size: @font-size; | font-size: @font-size; | ||||
| word-break: break-all; | word-break: break-all; | ||||
| @@ -56,5 +55,13 @@ | |||||
| text-underline-offset: 3px; | text-underline-offset: 3px; | ||||
| } | } | ||||
| } | } | ||||
| &__node { | |||||
| flex: 1; | |||||
| min-width: 0; | |||||
| padding: 12px 20px; | |||||
| font-size: @font-size; | |||||
| word-break: break-all; | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -1,29 +1,27 @@ | |||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import { BasicInfoProps } from '../BasicInfo'; | |||||
| import BasicInfoItem from '../BasicInfo/BasicInfoItem'; | import BasicInfoItem from '../BasicInfo/BasicInfoItem'; | ||||
| import { type BasicInfoData, type BasicInfoLink } from '../BasicInfo/types'; | import { type BasicInfoData, type BasicInfoLink } from '../BasicInfo/types'; | ||||
| import './index.less'; | import './index.less'; | ||||
| export type { BasicInfoData, BasicInfoLink }; | export type { BasicInfoData, BasicInfoLink }; | ||||
| type BasicTableInfoProps = { | |||||
| datas: BasicInfoData[]; | |||||
| className?: string; | |||||
| style?: React.CSSProperties; | |||||
| labelWidth: number; | |||||
| }; | |||||
| /** | |||||
| * 表格基础信息展示组件,用于展示基础信息,一行四列,支持数据格式化 | |||||
| */ | |||||
| export default function BasicTableInfo({ | export default function BasicTableInfo({ | ||||
| datas, | datas, | ||||
| className, | className, | ||||
| style, | style, | ||||
| labelWidth, | labelWidth, | ||||
| }: BasicTableInfoProps) { | |||||
| labelEllipsis, | |||||
| }: BasicInfoProps) { | |||||
| const remainder = datas.length % 4; | const remainder = datas.length % 4; | ||||
| const array = []; | const array = []; | ||||
| if (remainder > 0) { | if (remainder > 0) { | ||||
| for (let i = 0; i < 4 - remainder; i++) { | for (let i = 0; i < 4 - remainder; i++) { | ||||
| array.push({ | array.push({ | ||||
| label: '', | label: '', | ||||
| value: '', | |||||
| value: false, // 用于区分是否是空数据,不能是空字符串、null、undefined | |||||
| }); | }); | ||||
| } | } | ||||
| } | } | ||||
| @@ -36,6 +34,7 @@ export default function BasicTableInfo({ | |||||
| key={`${item.label}-${index}`} | key={`${item.label}-${index}`} | ||||
| data={item} | data={item} | ||||
| labelWidth={labelWidth} | labelWidth={labelWidth} | ||||
| labelEllipsis={labelEllipsis} | |||||
| classPrefix="kf-basic-table-info" | classPrefix="kf-basic-table-info" | ||||
| /> | /> | ||||
| ))} | ))} | ||||
| @@ -4,8 +4,8 @@ | |||||
| * @Description: 流水线选择代码配置表单 | * @Description: 流水线选择代码配置表单 | ||||
| */ | */ | ||||
| import CodeSelectorModal from '@/components/CodeSelectorModal'; | |||||
| import KFIcon from '@/components/KFIcon'; | import KFIcon from '@/components/KFIcon'; | ||||
| import CodeSelectorModal from '@/pages/Pipeline/components/CodeSelectorModal'; | |||||
| import { openAntdModal } from '@/utils/modal'; | import { openAntdModal } from '@/utils/modal'; | ||||
| import { Button } from 'antd'; | import { Button } from 'antd'; | ||||
| import ParameterInput, { type ParameterInputProps } from '../ParameterInput'; | import ParameterInput, { type ParameterInputProps } from '../ParameterInput'; | ||||
| @@ -15,6 +15,7 @@ export { requiredValidator, type ParameterInputObject } from '../ParameterInput' | |||||
| type CodeSelectProps = ParameterInputProps; | type CodeSelectProps = ParameterInputProps; | ||||
| /** 代码配置选择表单组件 */ | |||||
| function CodeSelect({ value, onChange, disabled, ...rest }: CodeSelectProps) { | function CodeSelect({ value, onChange, disabled, ...rest }: CodeSelectProps) { | ||||
| const selectResource = () => { | const selectResource = () => { | ||||
| const { close } = openAntdModal(CodeSelectorModal, { | const { close } = openAntdModal(CodeSelectorModal, { | ||||
| @@ -0,0 +1,49 @@ | |||||
| .kf-code-selector-modal { | |||||
| width: 100%; | |||||
| height: 100%; | |||||
| &__search { | |||||
| width: 100%; | |||||
| } | |||||
| &__content { | |||||
| display: flex; | |||||
| flex-direction: row; | |||||
| flex-wrap: wrap; | |||||
| gap: 10px; | |||||
| width: 100%; | |||||
| max-height: 50vh; | |||||
| margin-top: 24px; | |||||
| margin-bottom: 30px; | |||||
| overflow-x: hidden; | |||||
| overflow-y: auto; | |||||
| } | |||||
| &__empty { | |||||
| padding-top: 40px; | |||||
| } | |||||
| // 覆盖 antd 样式 | |||||
| .ant-input-affix-wrapper { | |||||
| border-radius: 23px !important; | |||||
| .ant-input-prefix { | |||||
| margin-inline-end: 12px; | |||||
| } | |||||
| .ant-input-suffix { | |||||
| margin-inline-end: 12px; | |||||
| } | |||||
| .ant-input-clear-icon { | |||||
| font-size: 16px; | |||||
| } | |||||
| } | |||||
| .ant-input-group-addon { | |||||
| display: none; | |||||
| } | |||||
| .ant-pagination { | |||||
| .ant-select-single { | |||||
| height: 32px !important; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -4,16 +4,16 @@ | |||||
| * @Description: 选择代码 | * @Description: 选择代码 | ||||
| */ | */ | ||||
| import KFIcon from '@/components/KFIcon'; | |||||
| import KFModal from '@/components/KFModal'; | import KFModal from '@/components/KFModal'; | ||||
| import { type CodeConfigData } from '@/pages/CodeConfig/List'; | import { type CodeConfigData } from '@/pages/CodeConfig/List'; | ||||
| import { getCodeConfigListReq } from '@/services/codeConfig'; | import { getCodeConfigListReq } from '@/services/codeConfig'; | ||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import { Icon } from '@umijs/max'; | |||||
| import type { ModalProps, PaginationProps } from 'antd'; | import type { ModalProps, PaginationProps } from 'antd'; | ||||
| import { Empty, Input, Pagination } from 'antd'; | import { Empty, Input, Pagination } from 'antd'; | ||||
| import { useEffect, useState } from 'react'; | import { useEffect, useState } from 'react'; | ||||
| import CodeConfigItem from '../CodeConfigItem'; | import CodeConfigItem from '../CodeConfigItem'; | ||||
| import styles from './index.less'; | |||||
| import './index.less'; | |||||
| export { type CodeConfigData }; | export { type CodeConfigData }; | ||||
| @@ -21,6 +21,7 @@ export interface CodeSelectorModalProps extends Omit<ModalProps, 'onOk'> { | |||||
| onOk?: (params: CodeConfigData | undefined) => void; | onOk?: (params: CodeConfigData | undefined) => void; | ||||
| } | } | ||||
| /** 选择代码配置的弹窗,推荐使用函数的方式打开 */ | |||||
| function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { | function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { | ||||
| const [dataList, setDataList] = useState<CodeConfigData[]>([]); | const [dataList, setDataList] = useState<CodeConfigData[]>([]); | ||||
| const [total, setTotal] = useState(0); | const [total, setTotal] = useState(0); | ||||
| @@ -79,9 +80,9 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { | |||||
| footer={null} | footer={null} | ||||
| destroyOnClose | destroyOnClose | ||||
| > | > | ||||
| <div className={styles['code-selector']}> | |||||
| <div className="kf-code-selector-modal"> | |||||
| <Input.Search | <Input.Search | ||||
| className={styles['code-selector__search']} | |||||
| className="kf-code-selector-modal__search" | |||||
| placeholder="按代码仓库名称筛选" | placeholder="按代码仓库名称筛选" | ||||
| allowClear | allowClear | ||||
| onSearch={handleSearch} | onSearch={handleSearch} | ||||
| @@ -90,12 +91,15 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { | |||||
| suffix={null} | suffix={null} | ||||
| value={inputText} | value={inputText} | ||||
| prefix={ | prefix={ | ||||
| <Icon icon="local:magnifying-glass" style={{ marginLeft: '10px', marginTop: '2px' }} /> | |||||
| <KFIcon type="icon-sousuo" color="rgba(22,100,255,0.4" style={{ marginLeft: '10px' }} /> | |||||
| } | } | ||||
| // prefix={ | |||||
| // <Icon icon="local:magnifying-glass" style={{ marginLeft: '10px', marginTop: '2px' }} /> | |||||
| // } | |||||
| /> | /> | ||||
| {dataList?.length !== 0 ? ( | {dataList?.length !== 0 ? ( | ||||
| <> | <> | ||||
| <div className={styles['code-selector__content']}> | |||||
| <div className="kf-code-selector-modal__content"> | |||||
| {dataList?.map((item) => ( | {dataList?.map((item) => ( | ||||
| <CodeConfigItem item={item} key={item.id} onClick={handleClick} /> | <CodeConfigItem item={item} key={item.id} onClick={handleClick} /> | ||||
| ))} | ))} | ||||
| @@ -112,7 +116,7 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { | |||||
| /> | /> | ||||
| </> | </> | ||||
| ) : ( | ) : ( | ||||
| <div className={styles['code-selector__empty']}> | |||||
| <div className="kf-code-selector-modal__empty"> | |||||
| <Empty image={Empty.PRESENTED_IMAGE_SIMPLE}></Empty> | <Empty image={Empty.PRESENTED_IMAGE_SIMPLE}></Empty> | ||||
| </div> | </div> | ||||
| )} | )} | ||||
| @@ -0,0 +1,41 @@ | |||||
| import BasicInfo, { type BasicInfoData, type BasicInfoProps } from '@/components/BasicInfo'; | |||||
| import InfoGroup from '@/components/InfoGroup'; | |||||
| import classNames from 'classnames'; | |||||
| export type { BasicInfoData }; | |||||
| interface ConfigInfoProps extends BasicInfoProps { | |||||
| /** 标题 */ | |||||
| title: string; | |||||
| /** 子元素 */ | |||||
| children?: React.ReactNode; | |||||
| } | |||||
| /** 详情基本信息块,目前主要用于主动机器学习、超参数寻优、自主学习详情中 */ | |||||
| function ConfigInfo({ | |||||
| title, | |||||
| datas, | |||||
| labelWidth, | |||||
| labelAlign = 'start', | |||||
| labelEllipsis = true, | |||||
| threeColumns = true, | |||||
| className, | |||||
| style, | |||||
| children, | |||||
| }: ConfigInfoProps) { | |||||
| return ( | |||||
| <InfoGroup title={title} className={classNames('kf-config-info', className)} style={style}> | |||||
| <div className={'kf-config-info__content'}> | |||||
| <BasicInfo | |||||
| datas={datas} | |||||
| labelWidth={labelWidth} | |||||
| labelAlign={labelAlign} | |||||
| labelEllipsis={labelEllipsis} | |||||
| threeColumns={threeColumns} | |||||
| /> | |||||
| {children} | |||||
| </div> | |||||
| </InfoGroup> | |||||
| ); | |||||
| } | |||||
| export default ConfigInfo; | |||||
| @@ -6,6 +6,9 @@ type DisabledInputProps = { | |||||
| valuePropName?: string; | valuePropName?: string; | ||||
| }; | }; | ||||
| /** | |||||
| * 模拟禁用的输入框,但是完全显示内容 | |||||
| */ | |||||
| function DisabledInput({ value, valuePropName }: DisabledInputProps) { | function DisabledInput({ value, valuePropName }: DisabledInputProps) { | ||||
| const data = valuePropName ? value[valuePropName] : value; | const data = valuePropName ? value[valuePropName] : value; | ||||
| return ( | return ( | ||||
| @@ -2,22 +2,30 @@ import classNames from 'classnames'; | |||||
| import './index.less'; | import './index.less'; | ||||
| type FullScreenFrameProps = { | type FullScreenFrameProps = { | ||||
| /** URL */ | |||||
| url: string; | url: string; | ||||
| /** 自定义类名 */ | |||||
| className?: string; | className?: string; | ||||
| /** 自定义样式 */ | |||||
| style?: React.CSSProperties; | style?: React.CSSProperties; | ||||
| onload?: (e?: React.SyntheticEvent<HTMLIFrameElement, Event>) => void; | |||||
| onerror?: (e?: React.SyntheticEvent<HTMLIFrameElement, Event>) => void; | |||||
| /** 加载完成回调 */ | |||||
| onLoad?: (e?: React.SyntheticEvent<HTMLIFrameElement, Event>) => void; | |||||
| /** 加载失败回调 */ | |||||
| onError?: (e?: React.SyntheticEvent<HTMLIFrameElement, Event>) => void; | |||||
| }; | }; | ||||
| function FullScreenFrame({ url, className, style, onload, onerror }: FullScreenFrameProps) { | |||||
| /** | |||||
| * 全屏 iframe,IFramePage 组件的子组件,开发中应该使用 IFramePage | |||||
| */ | |||||
| function FullScreenFrame({ url, className, style, onLoad, onError }: FullScreenFrameProps) { | |||||
| return ( | return ( | ||||
| <div className={classNames('kf-full-screen-frame', className ?? '')} style={style}> | <div className={classNames('kf-full-screen-frame', className ?? '')} style={style}> | ||||
| {url && ( | {url && ( | ||||
| <iframe | <iframe | ||||
| src={url} | src={url} | ||||
| className="kf-full-screen-frame__iframe" | className="kf-full-screen-frame__iframe" | ||||
| onLoad={onload} | |||||
| onError={onerror} | |||||
| onLoad={onLoad} | |||||
| onError={onError} | |||||
| ></iframe> | ></iframe> | ||||
| )} | )} | ||||
| </div> | </div> | ||||
| @@ -12,32 +12,36 @@ export enum IframePageType { | |||||
| DatasetAnnotation = 'DatasetAnnotation', // 数据标注 | DatasetAnnotation = 'DatasetAnnotation', // 数据标注 | ||||
| AppDevelopment = 'AppDevelopment', // 应用开发 | AppDevelopment = 'AppDevelopment', // 应用开发 | ||||
| DevEnv = 'DevEnv', // 开发环境 | DevEnv = 'DevEnv', // 开发环境 | ||||
| GitLink = 'GitLink', | |||||
| GitLink = 'GitLink', // git link | |||||
| } | } | ||||
| const getRequestAPI = (type: IframePageType): (() => Promise<any>) => { | const getRequestAPI = (type: IframePageType): (() => Promise<any>) => { | ||||
| switch (type) { | switch (type) { | ||||
| case IframePageType.DatasetAnnotation: | |||||
| case IframePageType.DatasetAnnotation: // 数据标注 | |||||
| return () => Promise.resolve({ code: 200, data: 'http://172.20.32.181:18888/oauth/login' }); //getLabelStudioUrl; | return () => Promise.resolve({ code: 200, data: 'http://172.20.32.181:18888/oauth/login' }); //getLabelStudioUrl; | ||||
| case IframePageType.AppDevelopment: | |||||
| case IframePageType.AppDevelopment: // 应用开发 | |||||
| return () => Promise.resolve({ code: 200, data: 'http://172.20.32.185:30080/' }); | return () => Promise.resolve({ code: 200, data: 'http://172.20.32.185:30080/' }); | ||||
| case IframePageType.DevEnv: | |||||
| case IframePageType.DevEnv: // 开发环境 | |||||
| return () => | return () => | ||||
| Promise.resolve({ | Promise.resolve({ | ||||
| code: 200, | code: 200, | ||||
| data: SessionStorage.getItem(SessionStorage.editorUrlKey) || '', | data: SessionStorage.getItem(SessionStorage.editorUrlKey) || '', | ||||
| }); | }); | ||||
| case IframePageType.GitLink: | |||||
| case IframePageType.GitLink: // git link | |||||
| return () => Promise.resolve({ code: 200, data: 'http://172.20.32.201:4000' }); | return () => Promise.resolve({ code: 200, data: 'http://172.20.32.201:4000' }); | ||||
| } | } | ||||
| }; | }; | ||||
| type IframePageProps = { | type IframePageProps = { | ||||
| /** 子系统 */ | |||||
| type: IframePageType; | type: IframePageType; | ||||
| /** 自定义样式类名 */ | |||||
| className?: string; | className?: string; | ||||
| /** 自定义样式 */ | |||||
| style?: React.CSSProperties; | style?: React.CSSProperties; | ||||
| }; | }; | ||||
| /** 系统内嵌 iframe,目前系统有数据标注、应用开发、开发环境、GitLink 四个子系统,使用时可以添加其他子系统 */ | |||||
| function IframePage({ type, className, style }: IframePageProps) { | function IframePage({ type, className, style }: IframePageProps) { | ||||
| const [iframeUrl, setIframeUrl] = useState(''); | const [iframeUrl, setIframeUrl] = useState(''); | ||||
| const [loading, setLoading] = useState(false); | const [loading, setLoading] = useState(false); | ||||
| @@ -66,7 +70,7 @@ function IframePage({ type, className, style }: IframePageProps) { | |||||
| return ( | return ( | ||||
| <div className={classNames('kf-iframe-page', className)} style={style}> | <div className={classNames('kf-iframe-page', className)} style={style}> | ||||
| {loading && createPortal(<KFSpin size="large" />, document.body)} | {loading && createPortal(<KFSpin size="large" />, document.body)} | ||||
| <FullScreenFrame url={iframeUrl} onload={hideLoading} onerror={hideLoading} /> | |||||
| <FullScreenFrame url={iframeUrl} onLoad={hideLoading} onError={hideLoading} /> | |||||
| </div> | </div> | ||||
| ); | ); | ||||
| } | } | ||||
| @@ -1,7 +1,7 @@ | |||||
| .kf-info-group-title { | .kf-info-group-title { | ||||
| width: 100%; | width: 100%; | ||||
| height: 56px; | height: 56px; | ||||
| padding-left: @content-padding; | |||||
| padding: 0 @content-padding; | |||||
| background: linear-gradient( | background: linear-gradient( | ||||
| 179.03deg, | 179.03deg, | ||||
| rgba(199, 223, 255, 0.12) 0%, | rgba(199, 223, 255, 0.12) 0%, | ||||
| @@ -21,6 +21,7 @@ | |||||
| color: @text-color; | color: @text-color; | ||||
| font-weight: 500; | font-weight: 500; | ||||
| font-size: @font-size-title; | font-size: @font-size-title; | ||||
| .singleLine(); | |||||
| &::after { | &::after { | ||||
| position: absolute; | position: absolute; | ||||
| @@ -1,13 +1,19 @@ | |||||
| import { Flex } from 'antd'; | import { Flex } from 'antd'; | ||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import './index.less'; | |||||
| import './InfoGroupTitle.less'; | |||||
| type InfoGroupTitleProps = { | type InfoGroupTitleProps = { | ||||
| /** 标题 */ | |||||
| title: string; | title: string; | ||||
| /** 自定义类名 */ | |||||
| className?: string; | className?: string; | ||||
| /** 自定义样式 */ | |||||
| style?: React.CSSProperties; | style?: React.CSSProperties; | ||||
| }; | }; | ||||
| /** | |||||
| * 信息组标题 | |||||
| */ | |||||
| function InfoGroupTitle({ title, style, className }: InfoGroupTitleProps) { | function InfoGroupTitle({ title, style, className }: InfoGroupTitleProps) { | ||||
| return ( | return ( | ||||
| <Flex align="center" className={classNames('kf-info-group-title', className)} style={style}> | <Flex align="center" className={classNames('kf-info-group-title', className)} style={style}> | ||||
| @@ -1,16 +1,25 @@ | |||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import InfoGroupTitle from '../InfoGroupTitle'; | |||||
| import InfoGroupTitle from './InfoGroupTitle'; | |||||
| import './index.less'; | import './index.less'; | ||||
| type InfoGroupProps = { | type InfoGroupProps = { | ||||
| /** 标题 */ | |||||
| title: string; | title: string; | ||||
| height?: string | number; // 如果要纵向滚动,需要设置高度 | |||||
| width?: string | number; // 如果要横向滚动,需要设置宽度 | |||||
| /** 高度, 如果要纵向滚动,需要设置高度 */ | |||||
| height?: string | number; | |||||
| /** 宽度, 如果要横向滚动,需要设置宽度 */ | |||||
| width?: string | number; | |||||
| /** 自定义类名 */ | |||||
| className?: string; | className?: string; | ||||
| /** 自定义样式 */ | |||||
| style?: React.CSSProperties; | style?: React.CSSProperties; | ||||
| /** 子元素 */ | |||||
| children?: React.ReactNode; | children?: React.ReactNode; | ||||
| }; | }; | ||||
| /** | |||||
| * 信息组,用于展示基本信息,支持横向、纵向滚动。自动机器学习、超参数寻优都是使用这个组件 | |||||
| */ | |||||
| function InfoGroup({ title, height, width, className, style, children }: InfoGroupProps) { | function InfoGroup({ title, height, width, className, style, children }: InfoGroupProps) { | ||||
| const contentStyle: React.CSSProperties = {}; | const contentStyle: React.CSSProperties = {}; | ||||
| if (height) { | if (height) { | ||||
| @@ -33,7 +33,7 @@ | |||||
| margin-top: 20px; | margin-top: 20px; | ||||
| margin-bottom: 30px; | margin-bottom: 30px; | ||||
| &__back-btn { | |||||
| &__button { | |||||
| height: 32px; | height: 32px; | ||||
| } | } | ||||
| } | } | ||||
| @@ -9,15 +9,24 @@ export enum EmptyType { | |||||
| } | } | ||||
| type EmptyProps = { | type EmptyProps = { | ||||
| className?: string; | |||||
| style?: React.CSSProperties; | |||||
| /** 类型 */ | |||||
| type: EmptyType; | type: EmptyType; | ||||
| /** 标题 */ | |||||
| title?: string; | title?: string; | ||||
| /** 内容 */ | |||||
| content?: string; | content?: string; | ||||
| /** 是否有页脚,如果有默认是一个按钮 */ | |||||
| hasFooter?: boolean; | hasFooter?: boolean; | ||||
| footer?: () => React.ReactNode; | |||||
| /** 按钮标题,默认是"刷新" */ | |||||
| buttonTitle?: string; | buttonTitle?: string; | ||||
| onRefresh?: () => void; | |||||
| /** 按钮点击回调 */ | |||||
| onButtonClick?: () => void; | |||||
| /** 自定义页脚内容 */ | |||||
| footer?: () => React.ReactNode; | |||||
| /** 自定义类名 */ | |||||
| className?: string; | |||||
| /** 自定义样式 */ | |||||
| style?: React.CSSProperties; | |||||
| }; | }; | ||||
| function getEmptyImage(type: EmptyType) { | function getEmptyImage(type: EmptyType) { | ||||
| @@ -31,6 +40,7 @@ function getEmptyImage(type: EmptyType) { | |||||
| } | } | ||||
| } | } | ||||
| /** 空状态 */ | |||||
| function KFEmpty({ | function KFEmpty({ | ||||
| className, | className, | ||||
| style, | style, | ||||
| @@ -40,7 +50,7 @@ function KFEmpty({ | |||||
| hasFooter = true, | hasFooter = true, | ||||
| footer, | footer, | ||||
| buttonTitle = '刷新', | buttonTitle = '刷新', | ||||
| onRefresh, | |||||
| onButtonClick, | |||||
| }: EmptyProps) { | }: EmptyProps) { | ||||
| const image = getEmptyImage(type); | const image = getEmptyImage(type); | ||||
| @@ -54,7 +64,7 @@ function KFEmpty({ | |||||
| {footer ? ( | {footer ? ( | ||||
| footer() | footer() | ||||
| ) : ( | ) : ( | ||||
| <Button className="kf-empty__footer__back-btn" type="primary" onClick={onRefresh}> | |||||
| <Button className="kf-empty__footer__button" type="primary" onClick={onButtonClick}> | |||||
| {buttonTitle} | {buttonTitle} | ||||
| </Button> | </Button> | ||||
| )} | )} | ||||
| @@ -14,14 +14,20 @@ const Icon = createFromIconfontCN({ | |||||
| type IconFontProps = Parameters<typeof Icon>[0]; | type IconFontProps = Parameters<typeof Icon>[0]; | ||||
| interface KFIconProps extends IconFontProps { | interface KFIconProps extends IconFontProps { | ||||
| /** 图标 */ | |||||
| type: string; | type: string; | ||||
| /** 字体大小 */ | |||||
| font?: number; | font?: number; | ||||
| /** 字体颜色 */ | |||||
| color?: string; | color?: string; | ||||
| style?: React.CSSProperties; | |||||
| /** 自定义类名 */ | |||||
| className?: string; | className?: string; | ||||
| /** 自定义样式 */ | |||||
| style?: React.CSSProperties; | |||||
| } | } | ||||
| function KFIcon({ type, font = 15, color = '', style = {}, className, ...rest }: KFIconProps) { | |||||
| /** 封装 iconfont 图标 */ | |||||
| function KFIcon({ type, font = 15, color, className, style, ...rest }: KFIconProps) { | |||||
| const iconStyle = { | const iconStyle = { | ||||
| ...style, | ...style, | ||||
| fontSize: font, | fontSize: font, | ||||
| @@ -6,12 +6,16 @@ | |||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import React from 'react'; | import React from 'react'; | ||||
| import './index.less'; | |||||
| import './KFModalTitle.less'; | |||||
| type ModalTitleProps = { | type ModalTitleProps = { | ||||
| /** 标题 */ | |||||
| title: React.ReactNode; | title: React.ReactNode; | ||||
| /** 图片 */ | |||||
| image?: string; | image?: string; | ||||
| /** 自定义样式 */ | |||||
| style?: React.CSSProperties; | style?: React.CSSProperties; | ||||
| /** 自定义类名 */ | |||||
| className?: string; | className?: string; | ||||
| }; | }; | ||||
| @@ -4,19 +4,22 @@ | |||||
| * @Description: 自定义 Modal | * @Description: 自定义 Modal | ||||
| */ | */ | ||||
| import ModalTitle from '@/components/ModalTitle'; | |||||
| import { Modal, type ModalProps } from 'antd'; | import { Modal, type ModalProps } from 'antd'; | ||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import KFModalTitle from './KFModalTitle'; | |||||
| import './index.less'; | import './index.less'; | ||||
| export interface KFModalProps extends ModalProps { | export interface KFModalProps extends ModalProps { | ||||
| /** 标题图片 */ | |||||
| image?: string; | image?: string; | ||||
| } | } | ||||
| /** 自定义 Modal,应用中的业务 Modal 应该使用它进行封装,推荐使用函数的方式打开 */ | |||||
| function KFModal({ | function KFModal({ | ||||
| title, | title, | ||||
| image, | image, | ||||
| children, | children, | ||||
| className = '', | |||||
| className, | |||||
| centered, | centered, | ||||
| maskClosable, | maskClosable, | ||||
| ...rest | ...rest | ||||
| @@ -27,7 +30,7 @@ function KFModal({ | |||||
| {...rest} | {...rest} | ||||
| centered={centered ?? true} | centered={centered ?? true} | ||||
| maskClosable={maskClosable ?? false} | maskClosable={maskClosable ?? false} | ||||
| title={<ModalTitle title={title} image={image}></ModalTitle>} | |||||
| title={<KFModalTitle title={title} image={image} />} | |||||
| > | > | ||||
| {children} | {children} | ||||
| </Modal> | </Modal> | ||||
| @@ -8,32 +8,40 @@ import classNames from 'classnames'; | |||||
| import './index.less'; | import './index.less'; | ||||
| export type KFRadioItem = { | export type KFRadioItem = { | ||||
| key: string; | |||||
| title: string; | title: string; | ||||
| value: string; | |||||
| icon?: React.ReactNode; | icon?: React.ReactNode; | ||||
| }; | }; | ||||
| type KFRadioProps = { | type KFRadioProps = { | ||||
| /** 选项 */ | |||||
| items: KFRadioItem[]; | items: KFRadioItem[]; | ||||
| /** 当前选中项 */ | |||||
| value?: string; | value?: string; | ||||
| /** 自定义样式 */ | |||||
| style?: React.CSSProperties; | style?: React.CSSProperties; | ||||
| /** 自定义类名 */ | |||||
| className?: string; | className?: string; | ||||
| /** 选中回调 */ | |||||
| onChange?: (value: string) => void; | onChange?: (value: string) => void; | ||||
| }; | }; | ||||
| /** | |||||
| * 自定义 Radio | |||||
| */ | |||||
| function KFRadio({ items, value, style, className, onChange }: KFRadioProps) { | function KFRadio({ items, value, style, className, onChange }: KFRadioProps) { | ||||
| return ( | return ( | ||||
| <span className={classNames('kf-radio', className)} style={style}> | <span className={classNames('kf-radio', className)} style={style}> | ||||
| {items.map((item) => { | {items.map((item) => { | ||||
| return ( | return ( | ||||
| <span | <span | ||||
| key={item.key} | |||||
| key={item.value} | |||||
| className={ | className={ | ||||
| value === item.key | |||||
| value === item.value | |||||
| ? classNames('kf-radio__item', 'kf-radio__item--active') | ? classNames('kf-radio__item', 'kf-radio__item--active') | ||||
| : 'kf-radio__item' | : 'kf-radio__item' | ||||
| } | } | ||||
| onClick={() => onChange?.(item.key)} | |||||
| onClick={() => onChange?.(item.value)} | |||||
| > | > | ||||
| {item.icon} | {item.icon} | ||||
| <span style={{ marginLeft: '5px' }}>{item.title}</span> | <span style={{ marginLeft: '5px' }}>{item.title}</span> | ||||
| @@ -5,13 +5,19 @@ | |||||
| */ | */ | ||||
| import { Spin, SpinProps } from 'antd'; | import { Spin, SpinProps } from 'antd'; | ||||
| import styles from './index.less'; | |||||
| import './index.less'; | |||||
| function KFSpin(props: SpinProps) { | |||||
| interface KFSpinProps extends SpinProps { | |||||
| /** 加载文本 */ | |||||
| label: string; | |||||
| } | |||||
| /** 自定义 Spin */ | |||||
| function KFSpin({ label = '加载中', ...rest }: KFSpinProps) { | |||||
| return ( | return ( | ||||
| <div className={styles['kf-spin']}> | |||||
| <Spin {...props} /> | |||||
| <div className={styles['kf-spin__label']}>加载中</div> | |||||
| <div className={'kf-spin'}> | |||||
| <Spin {...rest} /> | |||||
| <div className={'kf-spin__label'}>{label}</div> | |||||
| </div> | </div> | ||||
| ); | ); | ||||
| } | } | ||||
| @@ -1,19 +0,0 @@ | |||||
| .kf-label-value { | |||||
| display: flex; | |||||
| align-items: flex-start; | |||||
| font-size: 16px; | |||||
| line-height: 1.6; | |||||
| &__label { | |||||
| flex: none; | |||||
| width: 80px; | |||||
| color: @text-color-secondary; | |||||
| } | |||||
| &__value { | |||||
| flex: 1; | |||||
| color: @text-color; | |||||
| white-space: pre-line; | |||||
| word-break: break-all; | |||||
| } | |||||
| } | |||||
| @@ -1,20 +0,0 @@ | |||||
| import classNames from 'classnames'; | |||||
| import './index.less'; | |||||
| type labelValueProps = { | |||||
| label: string; | |||||
| value?: any; | |||||
| className?: string; | |||||
| style?: React.CSSProperties; | |||||
| }; | |||||
| function LabelValue({ label, value, className, style }: labelValueProps) { | |||||
| return ( | |||||
| <div className={classNames('kf-label-value', className)} style={style}> | |||||
| <div className="kf-label-value__label">{label}</div> | |||||
| <div className="kf-label-value__value">{value ?? '--'}</div> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default LabelValue; | |||||
| @@ -1,5 +1,4 @@ | |||||
| .menu-icon-selector { | .menu-icon-selector { | ||||
| // grid 布局,每行显示 8 个图标 | |||||
| display: grid; | display: grid; | ||||
| grid-template-columns: repeat(4, 80px); | grid-template-columns: repeat(4, 80px); | ||||
| gap: 20px; | gap: 20px; | ||||
| @@ -10,7 +9,7 @@ | |||||
| display: flex; | display: flex; | ||||
| align-items: center; | align-items: center; | ||||
| justify-content: center; | justify-content: center; | ||||
| width: 80x; | |||||
| width: 80px; | |||||
| height: 80px; | height: 80px; | ||||
| border: 1px solid transparent; | border: 1px solid transparent; | ||||
| border-radius: 4px; | border-radius: 4px; | ||||
| @@ -12,7 +12,9 @@ import { useEffect, useState } from 'react'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| interface MenuIconSelectorProps extends Omit<ModalProps, 'onOk'> { | interface MenuIconSelectorProps extends Omit<ModalProps, 'onOk'> { | ||||
| /** 选中的图标 */ | |||||
| selectedIcon?: string; | selectedIcon?: string; | ||||
| /** 选择回调 */ | |||||
| onOk: (param: string) => void; | onOk: (param: string) => void; | ||||
| } | } | ||||
| @@ -21,6 +23,7 @@ type IconObject = { | |||||
| font_class: string; | font_class: string; | ||||
| }; | }; | ||||
| /** 菜单图标选择器 */ | |||||
| function MenuIconSelector({ open, selectedIcon, onOk, ...rest }: MenuIconSelectorProps) { | function MenuIconSelector({ open, selectedIcon, onOk, ...rest }: MenuIconSelectorProps) { | ||||
| const [icons, setIcons] = useState<IconObject[]>([]); | const [icons, setIcons] = useState<IconObject[]>([]); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| @@ -8,10 +8,17 @@ import React from 'react'; | |||||
| import './index.less'; | import './index.less'; | ||||
| type PageTitleProps = { | type PageTitleProps = { | ||||
| title: string; | |||||
| /** 标题 */ | |||||
| title: React.ReactNode; | |||||
| /** 自定义类名 */ | |||||
| className?: string; | className?: string; | ||||
| /** 自定义样式 */ | |||||
| style?: React.CSSProperties; | style?: React.CSSProperties; | ||||
| }; | }; | ||||
| /** | |||||
| * 页面标题 | |||||
| */ | |||||
| function PageTitle({ title, style, className = '' }: PageTitleProps) { | function PageTitle({ title, style, className = '' }: PageTitleProps) { | ||||
| return ( | return ( | ||||
| <div className={classNames('kf-page-title', className)} style={style}> | <div className={classNames('kf-page-title', className)} style={style}> | ||||
| @@ -1,7 +1,7 @@ | |||||
| /* | /* | ||||
| * @Author: 赵伟 | * @Author: 赵伟 | ||||
| * @Date: 2024-04-16 08:42:57 | * @Date: 2024-04-16 08:42:57 | ||||
| * @Description: 参数输入组件,支持手动输入,选择全局参数,选择数据集/模型/镜像 | |||||
| * @Description: 参数输入表单组件,支持手动输入,也支持选择全局参数 | |||||
| */ | */ | ||||
| import { CommonTabKeys } from '@/enums'; | import { CommonTabKeys } from '@/enums'; | ||||
| @@ -26,18 +26,34 @@ export type ParameterInputObject = { | |||||
| export type ParameterInputValue = ParameterInputObject | string; | export type ParameterInputValue = ParameterInputObject | string; | ||||
| export interface ParameterInputProps { | export interface ParameterInputProps { | ||||
| /** 值,可以是字符串,也可以是 ParameterInputObject 对象 */ | |||||
| value?: ParameterInputValue; | value?: ParameterInputValue; | ||||
| /** | |||||
| * 值变化时的回调 | |||||
| * @param value 值,可以是字符串,也可以是 ParameterInputObject 对象 | |||||
| */ | |||||
| onChange?: (value?: ParameterInputValue) => void; | onChange?: (value?: ParameterInputValue) => void; | ||||
| /** 点击时的回调 */ | |||||
| onClick?: () => void; | onClick?: () => void; | ||||
| /** 删除时的回调 */ | |||||
| onRemove?: () => void; | onRemove?: () => void; | ||||
| /** 是否可以手动输入 */ | |||||
| canInput?: boolean; | canInput?: boolean; | ||||
| /** 是否是文本框 */ | |||||
| textArea?: boolean; | textArea?: boolean; | ||||
| /** 占位符 */ | |||||
| placeholder?: string; | placeholder?: string; | ||||
| /** 是否允许清除 */ | |||||
| allowClear?: boolean; | allowClear?: boolean; | ||||
| /** 自定义类名 */ | |||||
| className?: string; | className?: string; | ||||
| /** 自定义样式 */ | |||||
| style?: React.CSSProperties; | style?: React.CSSProperties; | ||||
| /** 大小 */ | |||||
| size?: 'middle' | 'small' | 'large'; | size?: 'middle' | 'small' | 'large'; | ||||
| /** 是否禁用 */ | |||||
| disabled?: boolean; | disabled?: boolean; | ||||
| /** 元素 id */ | |||||
| id?: string; | id?: string; | ||||
| } | } | ||||
| @@ -20,15 +20,17 @@ import './index.less'; | |||||
| export { requiredValidator, type ParameterInputObject } from '../ParameterInput'; | export { requiredValidator, type ParameterInputObject } from '../ParameterInput'; | ||||
| export { ResourceSelectorType, selectorTypeConfig, type ResourceSelectorResponse }; | export { ResourceSelectorType, selectorTypeConfig, type ResourceSelectorResponse }; | ||||
| type ResourceSelectProps = { | |||||
| interface ResourceSelectProps extends ParameterInputProps { | |||||
| /** 类型,数据集、模型、镜像 */ | |||||
| type: ResourceSelectorType; | type: ResourceSelectorType; | ||||
| } & ParameterInputProps; | |||||
| } | |||||
| // 获取选择数据集、模型后面按钮 icon | |||||
| // 获取选择数据集、模型、镜像后面按钮 icon | |||||
| const getSelectBtnIcon = (type: ResourceSelectorType) => { | const getSelectBtnIcon = (type: ResourceSelectorType) => { | ||||
| return <KFIcon type={selectorTypeConfig[type].buttonIcon} font={16} />; | return <KFIcon type={selectorTypeConfig[type].buttonIcon} font={16} />; | ||||
| }; | }; | ||||
| /** 数据集、模型、镜像选择表单组件 */ | |||||
| function ResourceSelect({ type, value, onChange, disabled, ...rest }: ResourceSelectProps) { | function ResourceSelect({ type, value, onChange, disabled, ...rest }: ResourceSelectProps) { | ||||
| const [selectedResource, setSelectedResource] = useState<ResourceSelectorResponse | undefined>( | const [selectedResource, setSelectedResource] = useState<ResourceSelectorResponse | undefined>( | ||||
| undefined, | undefined, | ||||
| @@ -4,11 +4,11 @@ | |||||
| * @Description: 选择数据集、模型、镜像 | * @Description: 选择数据集、模型、镜像 | ||||
| */ | */ | ||||
| import KFIcon from '@/components/KFIcon'; | |||||
| import KFModal from '@/components/KFModal'; | import KFModal from '@/components/KFModal'; | ||||
| import { CommonTabKeys } from '@/enums'; | import { CommonTabKeys } from '@/enums'; | ||||
| import { ResourceFileData } from '@/pages/Dataset/config'; | import { ResourceFileData } from '@/pages/Dataset/config'; | ||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import { Icon } from '@umijs/max'; | |||||
| import type { GetRef, ModalProps, TreeDataNode, TreeProps } from 'antd'; | import type { GetRef, ModalProps, TreeDataNode, TreeProps } from 'antd'; | ||||
| import { Input, Tabs, Tree } from 'antd'; | import { Input, Tabs, Tree } from 'antd'; | ||||
| import React, { useEffect, useMemo, useRef, useState } from 'react'; | import React, { useEffect, useMemo, useRef, useState } from 'react'; | ||||
| @@ -16,7 +16,7 @@ import { ResourceSelectorType, selectorTypeConfig } from './config'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| export { ResourceSelectorType, selectorTypeConfig }; | export { ResourceSelectorType, selectorTypeConfig }; | ||||
| // 选择数据集\模型\镜像的返回类型 | |||||
| // 选择数据集、模型、镜像的返回类型 | |||||
| export type ResourceSelectorResponse = { | export type ResourceSelectorResponse = { | ||||
| activeTab: CommonTabKeys; // 是我的还是公开的 | activeTab: CommonTabKeys; // 是我的还是公开的 | ||||
| id: string; // 数据集\模型\镜像 id | id: string; // 数据集\模型\镜像 id | ||||
| @@ -28,10 +28,18 @@ export type ResourceSelectorResponse = { | |||||
| }; | }; | ||||
| export interface ResourceSelectorModalProps extends Omit<ModalProps, 'onOk'> { | export interface ResourceSelectorModalProps extends Omit<ModalProps, 'onOk'> { | ||||
| type: ResourceSelectorType; // 数据集\模型\镜像 | |||||
| /** 类型,数据集、模型、镜像 */ | |||||
| type: ResourceSelectorType; | |||||
| /** 默认展开的节点 */ | |||||
| defaultExpandedKeys?: React.Key[]; | defaultExpandedKeys?: React.Key[]; | ||||
| /** 默认展开的节点 */ | |||||
| defaultCheckedKeys?: React.Key[]; | defaultCheckedKeys?: React.Key[]; | ||||
| /** 默认激活的 Tab */ | |||||
| defaultActiveTab?: CommonTabKeys; | defaultActiveTab?: CommonTabKeys; | ||||
| /** | |||||
| * 确认回调 | |||||
| * @param params 选择的数据 | |||||
| */ | |||||
| onOk?: (params: ResourceSelectorResponse | undefined) => void; | onOk?: (params: ResourceSelectorResponse | undefined) => void; | ||||
| } | } | ||||
| @@ -61,6 +69,7 @@ const getIdAndVersion = (versionKey: string) => { | |||||
| }; | }; | ||||
| }; | }; | ||||
| /** 选择数据集、模型、镜像的弹框,推荐使用函数的方式打开 */ | |||||
| function ResourceSelectorModal({ | function ResourceSelectorModal({ | ||||
| type, | type, | ||||
| defaultExpandedKeys = [], | defaultExpandedKeys = [], | ||||
| @@ -267,7 +276,14 @@ function ResourceSelectorModal({ | |||||
| variant="borderless" | variant="borderless" | ||||
| value={searchText} | value={searchText} | ||||
| onChange={(e) => setSearchText(e.target.value)} | onChange={(e) => setSearchText(e.target.value)} | ||||
| prefix={<Icon icon="local:magnifying-glass" style={{ height: '15px' }} />} | |||||
| prefix={ | |||||
| <KFIcon | |||||
| type="icon-sousuo" | |||||
| color="rgba(22,100,255,0.4)" | |||||
| style={{ height: '15px' }} | |||||
| /> | |||||
| } | |||||
| // prefix={<Icon icon="local:magnifying-glass" style={{ height: '15px' }} />} | |||||
| /> | /> | ||||
| <Tree | <Tree | ||||
| ref={treeRef} | ref={treeRef} | ||||
| @@ -1,9 +1,8 @@ | |||||
| import { useModel } from '@umijs/max'; | |||||
| import React from 'react'; | |||||
| // import KFBreadcrumb from '../KFBreadcrumb'; | |||||
| import KFIcon from '@/components/KFIcon'; | import KFIcon from '@/components/KFIcon'; | ||||
| import { ProBreadcrumb } from '@ant-design/pro-components'; | import { ProBreadcrumb } from '@ant-design/pro-components'; | ||||
| import { useModel } from '@umijs/max'; | |||||
| import { Button } from 'antd'; | import { Button } from 'antd'; | ||||
| import React from 'react'; | |||||
| import Avatar from './AvatarDropdown'; | import Avatar from './AvatarDropdown'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| // import { SelectLang } from '@umijs/max'; | // import { SelectLang } from '@umijs/max'; | ||||
| @@ -44,8 +43,6 @@ const GlobalHeaderRight: React.FC = () => { | |||||
| <ProBreadcrumb></ProBreadcrumb> | <ProBreadcrumb></ProBreadcrumb> | ||||
| {/* <KFBreadcrumb /> */} | |||||
| <Avatar menu={true} /> | <Avatar menu={true} /> | ||||
| {/* <SelectLang className={actionClassName} /> */} | {/* <SelectLang className={actionClassName} /> */} | ||||
| </div> | </div> | ||||
| @@ -8,13 +8,20 @@ import classNames from 'classnames'; | |||||
| import './index.less'; | import './index.less'; | ||||
| type SubAreaTitleProps = { | type SubAreaTitleProps = { | ||||
| /** 标题 */ | |||||
| title: string; | title: string; | ||||
| /** 图片 */ | |||||
| image?: string; | image?: string; | ||||
| style?: React.CSSProperties; | |||||
| /** 自定义类名 */ | |||||
| className?: string; | className?: string; | ||||
| /** 自定义样式 */ | |||||
| style?: React.CSSProperties; | |||||
| }; | }; | ||||
| function SubAreaTitle({ title, image, style, className }: SubAreaTitleProps) { | |||||
| /** | |||||
| * 表单或者详情页的分区标题 | |||||
| */ | |||||
| function SubAreaTitle({ title, image, className, style }: SubAreaTitleProps) { | |||||
| return ( | return ( | ||||
| <div className={classNames('kf-subarea-title', className)} style={style}> | <div className={classNames('kf-subarea-title', className)} style={style}> | ||||
| {image && ( | {image && ( | ||||
| @@ -5,7 +5,7 @@ body, | |||||
| height: 100%; | height: 100%; | ||||
| margin: 0; | margin: 0; | ||||
| padding: 0; | padding: 0; | ||||
| overflow-y: hidden; | |||||
| overflow-y: visible; | |||||
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, | ||||
| 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', | 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', | ||||
| 'Noto Color Emoji'; | 'Noto Color Emoji'; | ||||
| @@ -12,7 +12,7 @@ const NoFoundPage = () => { | |||||
| content={'很抱歉,您访问的页面地址有误,\n或者该页面不存在。'} | content={'很抱歉,您访问的页面地址有误,\n或者该页面不存在。'} | ||||
| hasFooter={true} | hasFooter={true} | ||||
| buttonTitle="返回首页" | buttonTitle="返回首页" | ||||
| onRefresh={() => navigate('/')} | |||||
| onButtonClick={() => navigate('/')} | |||||
| ></KFEmpty> | ></KFEmpty> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -1,3 +1,4 @@ | |||||
| import ConfigInfo, { type BasicInfoData } from '@/components/ConfigInfo'; | |||||
| import { AutoMLTaskType, autoMLEnsembleClassOptions, autoMLTaskTypeOptions } from '@/enums'; | import { AutoMLTaskType, autoMLEnsembleClassOptions, autoMLTaskTypeOptions } from '@/enums'; | ||||
| import { AutoMLData } from '@/pages/AutoML/types'; | import { AutoMLData } from '@/pages/AutoML/types'; | ||||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | import { experimentStatusInfo } from '@/pages/Experiment/status'; | ||||
| @@ -8,7 +9,6 @@ import { formatBoolean, formatDataset, formatDate, formatEnum } from '@/utils/fo | |||||
| import { Flex } from 'antd'; | import { Flex } from 'antd'; | ||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import { useMemo } from 'react'; | import { useMemo } from 'react'; | ||||
| import ConfigInfo, { type BasicInfoData } from '../ConfigInfo'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| // 格式化优化方向 | // 格式化优化方向 | ||||
| @@ -46,28 +46,23 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB | |||||
| { | { | ||||
| label: '实验名称', | label: '实验名称', | ||||
| value: info.ml_name, | value: info.ml_name, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '实验描述', | label: '实验描述', | ||||
| value: info.ml_description, | value: info.ml_description, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '创建人', | label: '创建人', | ||||
| value: info.create_by, | value: info.create_by, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '创建时间', | label: '创建时间', | ||||
| value: info.create_time, | value: info.create_time, | ||||
| ellipsis: true, | |||||
| format: formatDate, | format: formatDate, | ||||
| }, | }, | ||||
| { | { | ||||
| label: '更新时间', | label: '更新时间', | ||||
| value: info.update_time, | value: info.update_time, | ||||
| ellipsis: true, | |||||
| format: formatDate, | format: formatDate, | ||||
| }, | }, | ||||
| ]; | ]; | ||||
| @@ -81,18 +76,15 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB | |||||
| { | { | ||||
| label: '任务类型', | label: '任务类型', | ||||
| value: info.task_type, | value: info.task_type, | ||||
| ellipsis: true, | |||||
| format: formatEnum(autoMLTaskTypeOptions), | format: formatEnum(autoMLTaskTypeOptions), | ||||
| }, | }, | ||||
| { | { | ||||
| label: '特征预处理算法', | label: '特征预处理算法', | ||||
| value: info.include_feature_preprocessor, | value: info.include_feature_preprocessor, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '排除的特征预处理算法', | label: '排除的特征预处理算法', | ||||
| value: info.exclude_feature_preprocessor, | value: info.exclude_feature_preprocessor, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: info.task_type === AutoMLTaskType.Regression ? '回归算法' : '分类算法', | label: info.task_type === AutoMLTaskType.Regression ? '回归算法' : '分类算法', | ||||
| @@ -100,7 +92,6 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB | |||||
| info.task_type === AutoMLTaskType.Regression | info.task_type === AutoMLTaskType.Regression | ||||
| ? info.include_regressor | ? info.include_regressor | ||||
| : info.include_classifier, | : info.include_classifier, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: info.task_type === AutoMLTaskType.Regression ? '排除的回归算法' : '排除的分类算法', | label: info.task_type === AutoMLTaskType.Regression ? '排除的回归算法' : '排除的分类算法', | ||||
| @@ -108,91 +99,73 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB | |||||
| info.task_type === AutoMLTaskType.Regression | info.task_type === AutoMLTaskType.Regression | ||||
| ? info.exclude_regressor | ? info.exclude_regressor | ||||
| : info.exclude_classifier, | : info.exclude_classifier, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '集成方式', | label: '集成方式', | ||||
| value: info.ensemble_class, | value: info.ensemble_class, | ||||
| ellipsis: true, | |||||
| format: formatEnum(autoMLEnsembleClassOptions), | format: formatEnum(autoMLEnsembleClassOptions), | ||||
| }, | }, | ||||
| { | { | ||||
| label: '集成模型数量', | label: '集成模型数量', | ||||
| value: info.ensemble_size, | value: info.ensemble_size, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '集成最佳模型数量', | label: '集成最佳模型数量', | ||||
| value: info.ensemble_nbest, | value: info.ensemble_nbest, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '最大数量', | label: '最大数量', | ||||
| value: info.max_models_on_disc, | value: info.max_models_on_disc, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '内存限制(MB)', | label: '内存限制(MB)', | ||||
| value: info.memory_limit, | value: info.memory_limit, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '单次时间限制(秒)', | label: '单次时间限制(秒)', | ||||
| value: info.per_run_time_limit, | value: info.per_run_time_limit, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '搜索时间限制(秒)', | label: '搜索时间限制(秒)', | ||||
| value: info.time_left_for_this_task, | value: info.time_left_for_this_task, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '重采样策略', | label: '重采样策略', | ||||
| value: info.resampling_strategy, | value: info.resampling_strategy, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '交叉验证折数', | label: '交叉验证折数', | ||||
| value: info.folds, | value: info.folds, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '是否打乱', | label: '是否打乱', | ||||
| value: info.shuffle, | value: info.shuffle, | ||||
| ellipsis: true, | |||||
| format: formatBoolean, | format: formatBoolean, | ||||
| }, | }, | ||||
| { | { | ||||
| label: '训练集比率', | label: '训练集比率', | ||||
| value: info.train_size, | value: info.train_size, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '测试集比率', | label: '测试集比率', | ||||
| value: info.test_size, | value: info.test_size, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '计算指标', | label: '计算指标', | ||||
| value: info.scoring_functions, | value: info.scoring_functions, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '随机种子', | label: '随机种子', | ||||
| value: info.seed, | value: info.seed, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '数据集', | label: '数据集', | ||||
| value: info.dataset, | value: info.dataset, | ||||
| ellipsis: true, | |||||
| format: formatDataset, | format: formatDataset, | ||||
| }, | }, | ||||
| { | { | ||||
| label: '预测目标列', | label: '预测目标列', | ||||
| value: info.target_columns, | value: info.target_columns, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| ]; | ]; | ||||
| }, [info]); | }, [info]); | ||||
| @@ -205,18 +178,15 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB | |||||
| { | { | ||||
| label: '指标名称', | label: '指标名称', | ||||
| value: info.metric_name, | value: info.metric_name, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '优化方向', | label: '优化方向', | ||||
| value: info.greater_is_better, | value: info.greater_is_better, | ||||
| ellipsis: true, | |||||
| format: formatOptimizeMode, | format: formatOptimizeMode, | ||||
| }, | }, | ||||
| { | { | ||||
| label: '指标权重', | label: '指标权重', | ||||
| value: info.metrics, | value: info.metrics, | ||||
| ellipsis: true, | |||||
| format: formatMetricsWeight, | format: formatMetricsWeight, | ||||
| }, | }, | ||||
| ]; | ]; | ||||
| @@ -231,12 +201,10 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB | |||||
| { | { | ||||
| label: '启动时间', | label: '启动时间', | ||||
| value: formatDate(runStatus.startedAt), | value: formatDate(runStatus.startedAt), | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '执行时长', | label: '执行时长', | ||||
| value: elapsedTime(runStatus.startedAt, runStatus.finishedAt), | value: elapsedTime(runStatus.startedAt, runStatus.finishedAt), | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '状态', | label: '状态', | ||||
| @@ -259,7 +227,6 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB | |||||
| </div> | </div> | ||||
| </Flex> | </Flex> | ||||
| ), | ), | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| ]; | ]; | ||||
| }, [runStatus]); | }, [runStatus]); | ||||
| @@ -269,7 +236,7 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB | |||||
| {isInstance && runStatus && ( | {isInstance && runStatus && ( | ||||
| <ConfigInfo | <ConfigInfo | ||||
| title="运行信息" | title="运行信息" | ||||
| data={instanceDatas} | |||||
| datas={instanceDatas} | |||||
| labelWidth={70} | labelWidth={70} | ||||
| style={{ marginBottom: '20px' }} | style={{ marginBottom: '20px' }} | ||||
| /> | /> | ||||
| @@ -277,18 +244,18 @@ function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLB | |||||
| {!isInstance && ( | {!isInstance && ( | ||||
| <ConfigInfo | <ConfigInfo | ||||
| title="基本信息" | title="基本信息" | ||||
| data={basicDatas} | |||||
| datas={basicDatas} | |||||
| labelWidth={70} | labelWidth={70} | ||||
| style={{ marginBottom: '20px' }} | style={{ marginBottom: '20px' }} | ||||
| /> | /> | ||||
| )} | )} | ||||
| <ConfigInfo | <ConfigInfo | ||||
| title="配置信息" | title="配置信息" | ||||
| data={configDatas} | |||||
| datas={configDatas} | |||||
| labelWidth={150} | labelWidth={150} | ||||
| style={{ marginBottom: '20px' }} | style={{ marginBottom: '20px' }} | ||||
| /> | /> | ||||
| <ConfigInfo title="优化指标" data={metricsData} labelWidth={70} /> | |||||
| <ConfigInfo title="优化指标" datas={metricsData} labelWidth={70} /> | |||||
| </div> | </div> | ||||
| ); | ); | ||||
| } | } | ||||
| @@ -1,20 +0,0 @@ | |||||
| .config-info { | |||||
| :global { | |||||
| .kf-basic-info { | |||||
| width: 100%; | |||||
| &__item { | |||||
| width: calc((100% - 80px) / 3); | |||||
| &__label { | |||||
| font-size: @font-size; | |||||
| text-align: left; | |||||
| text-align-last: left; | |||||
| } | |||||
| &__value { | |||||
| min-width: 0; | |||||
| font-size: @font-size; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -1,27 +0,0 @@ | |||||
| import BasicInfo, { type BasicInfoData } from '@/components/BasicInfo'; | |||||
| import InfoGroup from '@/components/InfoGroup'; | |||||
| import classNames from 'classnames'; | |||||
| import styles from './index.less'; | |||||
| export type { BasicInfoData }; | |||||
| type ConfigInfoProps = { | |||||
| title: string; | |||||
| data: BasicInfoData[]; | |||||
| labelWidth: number; | |||||
| className?: string; | |||||
| style?: React.CSSProperties; | |||||
| children?: React.ReactNode; | |||||
| }; | |||||
| function ConfigInfo({ title, data, labelWidth, className, style, children }: ConfigInfoProps) { | |||||
| return ( | |||||
| <InfoGroup title={title} className={classNames(styles['config-info'], className)} style={style}> | |||||
| <div className={styles['config-info__content']}> | |||||
| <BasicInfo datas={data} labelWidth={labelWidth} /> | |||||
| {children} | |||||
| </div> | |||||
| </InfoGroup> | |||||
| ); | |||||
| } | |||||
| export default ConfigInfo; | |||||
| @@ -197,7 +197,7 @@ function CodeConfigList() { | |||||
| title="暂无数据" | title="暂无数据" | ||||
| content={'很抱歉,没有搜索到您想要的内容\n建议刷新试试'} | content={'很抱歉,没有搜索到您想要的内容\n建议刷新试试'} | ||||
| hasFooter={true} | hasFooter={true} | ||||
| onRefresh={getDataList} | |||||
| onButtonClick={getDataList} | |||||
| /> | /> | ||||
| )} | )} | ||||
| </div> | </div> | ||||
| @@ -19,50 +19,41 @@ const getDatasetDatas = (data: DatasetData): BasicInfoData[] => [ | |||||
| { | { | ||||
| label: '数据集名称', | label: '数据集名称', | ||||
| value: data.name, | value: data.name, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '版本', | label: '版本', | ||||
| value: data.version, | value: data.version, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '创建人', | label: '创建人', | ||||
| value: data.create_by, | value: data.create_by, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '更新时间', | label: '更新时间', | ||||
| value: data.update_time, | value: data.update_time, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '数据来源', | label: '数据来源', | ||||
| value: data.dataset_source, | value: data.dataset_source, | ||||
| format: formatSource, | format: formatSource, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '训练任务', | label: '训练任务', | ||||
| value: data.train_task, | value: data.train_task, | ||||
| format: formatTrainTask, | format: formatTrainTask, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '处理代码', | label: '处理代码', | ||||
| value: data.processing_code, | value: data.processing_code, | ||||
| format: formatCodeConfig, | format: formatCodeConfig, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '数据集分类', | label: '数据集分类', | ||||
| value: data.data_type, | value: data.data_type, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '研究方向', | label: '研究方向', | ||||
| value: data.data_tag, | value: data.data_tag, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| ]; | ]; | ||||
| @@ -226,7 +226,7 @@ function ResourceList( | |||||
| title="暂无数据" | title="暂无数据" | ||||
| content={'很抱歉,没有搜索到您想要的内容\n建议刷新试试'} | content={'很抱歉,没有搜索到您想要的内容\n建议刷新试试'} | ||||
| hasFooter={true} | hasFooter={true} | ||||
| onRefresh={getDataList} | |||||
| onButtonClick={getDataList} | |||||
| /> | /> | ||||
| )} | )} | ||||
| </div> | </div> | ||||
| @@ -36,13 +36,13 @@ enum ComputingResourceType { | |||||
| const EditorRadioItems: KFRadioItem[] = [ | const EditorRadioItems: KFRadioItem[] = [ | ||||
| { | { | ||||
| key: ComputingResourceType.GPU, | |||||
| title: '英伟达GPU', | title: '英伟达GPU', | ||||
| value: ComputingResourceType.GPU, | |||||
| icon: <KFIcon type="icon-jiyugongwangjingxiang" />, | icon: <KFIcon type="icon-jiyugongwangjingxiang" />, | ||||
| }, | }, | ||||
| { | { | ||||
| key: ComputingResourceType.NPU, | |||||
| title: '昇腾NPU', | title: '昇腾NPU', | ||||
| value: ComputingResourceType.NPU, | |||||
| icon: <KFIcon type="icon-bendishangchuan" />, | icon: <KFIcon type="icon-bendishangchuan" />, | ||||
| }, | }, | ||||
| ]; | ]; | ||||
| @@ -1,6 +1,6 @@ | |||||
| import ConfigInfo, { type BasicInfoData } from '@/components/ConfigInfo'; | |||||
| import { hyperParameterOptimizedMode } from '@/enums'; | import { hyperParameterOptimizedMode } from '@/enums'; | ||||
| import { useComputingResource } from '@/hooks/resource'; | import { useComputingResource } from '@/hooks/resource'; | ||||
| import ConfigInfo, { type BasicInfoData } from '@/pages/AutoML/components/ConfigInfo'; | |||||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | import { experimentStatusInfo } from '@/pages/Experiment/status'; | ||||
| import { | import { | ||||
| schedulerAlgorithms, | schedulerAlgorithms, | ||||
| @@ -61,28 +61,23 @@ function HyperParameterBasic({ | |||||
| { | { | ||||
| label: '实验名称', | label: '实验名称', | ||||
| value: info.name, | value: info.name, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '实验描述', | label: '实验描述', | ||||
| value: info.description, | value: info.description, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '创建人', | label: '创建人', | ||||
| value: info.create_by, | value: info.create_by, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '创建时间', | label: '创建时间', | ||||
| value: info.create_time, | value: info.create_time, | ||||
| ellipsis: true, | |||||
| format: formatDate, | format: formatDate, | ||||
| }, | }, | ||||
| { | { | ||||
| label: '更新时间', | label: '更新时间', | ||||
| value: info.update_time, | value: info.update_time, | ||||
| ellipsis: true, | |||||
| format: formatDate, | format: formatDate, | ||||
| }, | }, | ||||
| ]; | ]; | ||||
| @@ -96,75 +91,62 @@ function HyperParameterBasic({ | |||||
| { | { | ||||
| label: '代码', | label: '代码', | ||||
| value: info.code, | value: info.code, | ||||
| ellipsis: true, | |||||
| format: formatCodeConfig, | format: formatCodeConfig, | ||||
| }, | }, | ||||
| { | { | ||||
| label: '主函数代码文件', | label: '主函数代码文件', | ||||
| value: info.main_py, | value: info.main_py, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '镜像', | label: '镜像', | ||||
| value: info.image, | value: info.image, | ||||
| format: formatMirror, | format: formatMirror, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '数据集', | label: '数据集', | ||||
| value: info.dataset, | value: info.dataset, | ||||
| ellipsis: true, | |||||
| format: formatDataset, | format: formatDataset, | ||||
| }, | }, | ||||
| { | { | ||||
| label: '模型', | label: '模型', | ||||
| value: info.model, | value: info.model, | ||||
| ellipsis: true, | |||||
| format: formatModel, | format: formatModel, | ||||
| }, | }, | ||||
| { | { | ||||
| label: '总实验次数', | label: '总实验次数', | ||||
| value: info.num_samples, | value: info.num_samples, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '搜索算法', | label: '搜索算法', | ||||
| value: info.search_alg, | value: info.search_alg, | ||||
| format: formatEnum(searchAlgorithms), | format: formatEnum(searchAlgorithms), | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '调度算法', | label: '调度算法', | ||||
| value: info.scheduler, | value: info.scheduler, | ||||
| format: formatEnum(schedulerAlgorithms), | format: formatEnum(schedulerAlgorithms), | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '单次试验最大时间', | label: '单次试验最大时间', | ||||
| value: info.max_t, | value: info.max_t, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '最小试验数', | label: '最小试验数', | ||||
| value: info.min_samples_required, | value: info.min_samples_required, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '优化方向', | label: '优化方向', | ||||
| value: info.mode, | value: info.mode, | ||||
| ellipsis: true, | |||||
| format: formatOptimizeMode, | format: formatOptimizeMode, | ||||
| }, | }, | ||||
| { | { | ||||
| label: '指标', | label: '指标', | ||||
| value: info.metric, | value: info.metric, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '资源规格', | label: '资源规格', | ||||
| value: info.resource, | value: info.resource, | ||||
| format: formatResource, | format: formatResource, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| ]; | ]; | ||||
| }, [info]); | }, [info]); | ||||
| @@ -216,7 +198,7 @@ function HyperParameterBasic({ | |||||
| {isInstance && runStatus && ( | {isInstance && runStatus && ( | ||||
| <ConfigInfo | <ConfigInfo | ||||
| title="运行信息" | title="运行信息" | ||||
| data={instanceDatas} | |||||
| datas={instanceDatas} | |||||
| labelWidth={70} | labelWidth={70} | ||||
| style={{ marginBottom: '20px' }} | style={{ marginBottom: '20px' }} | ||||
| /> | /> | ||||
| @@ -224,14 +206,14 @@ function HyperParameterBasic({ | |||||
| {!isInstance && ( | {!isInstance && ( | ||||
| <ConfigInfo | <ConfigInfo | ||||
| title="基本信息" | title="基本信息" | ||||
| data={basicDatas} | |||||
| datas={basicDatas} | |||||
| labelWidth={70} | labelWidth={70} | ||||
| style={{ marginBottom: '20px' }} | style={{ marginBottom: '20px' }} | ||||
| /> | /> | ||||
| )} | )} | ||||
| <ConfigInfo | <ConfigInfo | ||||
| title="配置信息" | title="配置信息" | ||||
| data={configDatas} | |||||
| datas={configDatas} | |||||
| labelWidth={120} | labelWidth={120} | ||||
| style={{ marginBottom: '20px' }} | style={{ marginBottom: '20px' }} | ||||
| > | > | ||||
| @@ -30,13 +30,13 @@ type FormData = { | |||||
| const mirrorRadioItems: KFRadioItem[] = [ | const mirrorRadioItems: KFRadioItem[] = [ | ||||
| { | { | ||||
| key: CommonTabKeys.Public, | |||||
| title: '基于公网镜像', | title: '基于公网镜像', | ||||
| value: CommonTabKeys.Public, | |||||
| icon: <KFIcon type="icon-jiyugongwangjingxiang" />, | icon: <KFIcon type="icon-jiyugongwangjingxiang" />, | ||||
| }, | }, | ||||
| { | { | ||||
| key: CommonTabKeys.Private, | |||||
| title: '本地上传', | title: '本地上传', | ||||
| value: CommonTabKeys.Private, | |||||
| icon: <KFIcon type="icon-bendishangchuan" />, | icon: <KFIcon type="icon-bendishangchuan" />, | ||||
| }, | }, | ||||
| ]; | ]; | ||||
| @@ -401,7 +401,12 @@ function ServiceInfo() { | |||||
| image={require('@/assets/img/mirror-basic.png')} | image={require('@/assets/img/mirror-basic.png')} | ||||
| style={{ marginBottom: '26px', flex: 'none' }} | style={{ marginBottom: '26px', flex: 'none' }} | ||||
| ></SubAreaTitle> | ></SubAreaTitle> | ||||
| <BasicInfo datas={basicInfo} labelWidth={66} style={{ flex: 'none' }}></BasicInfo> | |||||
| <BasicInfo | |||||
| datas={basicInfo} | |||||
| labelWidth={66} | |||||
| labelAlign="justify" | |||||
| style={{ flex: 'none' }} | |||||
| ></BasicInfo> | |||||
| <SubAreaTitle | <SubAreaTitle | ||||
| title="服务版本" | title="服务版本" | ||||
| image={require('@/assets/img/service-version.png')} | image={require('@/assets/img/service-version.png')} | ||||
| @@ -18,7 +18,7 @@ const formatStatus = (status?: ServiceRunStatus) => { | |||||
| } | } | ||||
| return ( | return ( | ||||
| <Flex align="center" style={{ marginLeft: '16px', fontSize: '16px', lineHeight: 1.6 }}> | |||||
| <Flex align="center" style={{ fontSize: '16px', lineHeight: 1.6 }}> | |||||
| {ModelDeployStatusCell(status)} | {ModelDeployStatusCell(status)} | ||||
| </Flex> | </Flex> | ||||
| ); | ); | ||||
| @@ -51,84 +51,69 @@ function VersionBasicInfo({ info }: BasicInfoProps) { | |||||
| { | { | ||||
| label: '服务名称', | label: '服务名称', | ||||
| value: info?.service_name, | value: info?.service_name, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '版本名称', | label: '版本名称', | ||||
| value: info?.version, | value: info?.version, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '代码配置', | label: '代码配置', | ||||
| value: info?.code_config, | value: info?.code_config, | ||||
| format: formatCodeConfig, | format: formatCodeConfig, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '镜像', | label: '镜像', | ||||
| value: info?.image, | value: info?.image, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '状态', | label: '状态', | ||||
| value: info?.run_state, | value: info?.run_state, | ||||
| format: formatStatus, | format: formatStatus, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '模型', | label: '模型', | ||||
| value: info?.model, | value: info?.model, | ||||
| format: formatModel, | format: formatModel, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '资源规格', | label: '资源规格', | ||||
| value: info?.resource, | value: info?.resource, | ||||
| format: formatResource, | format: formatResource, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '挂载路径', | label: '挂载路径', | ||||
| value: info?.mount_path, | value: info?.mount_path, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: 'API URL', | label: 'API URL', | ||||
| value: info?.url, | value: info?.url, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '副本数量', | label: '副本数量', | ||||
| value: info?.replicas, | value: info?.replicas, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '创建时间', | label: '创建时间', | ||||
| value: info?.create_time, | value: info?.create_time, | ||||
| format: formatDate, | format: formatDate, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '更新时间', | label: '更新时间', | ||||
| value: info?.update_time, | value: info?.update_time, | ||||
| format: formatDate, | format: formatDate, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '环境变量', | label: '环境变量', | ||||
| value: info?.env_variables, | value: info?.env_variables, | ||||
| format: formatEnvText, | format: formatEnvText, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '描述', | label: '描述', | ||||
| value: info?.description, | value: info?.description, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| ]; | ]; | ||||
| return <BasicInfo datas={datas} labelWidth={66}></BasicInfo>; | |||||
| return <BasicInfo datas={datas} labelWidth={66} labelAlign="justify"></BasicInfo>; | |||||
| } | } | ||||
| export default VersionBasicInfo; | export default VersionBasicInfo; | ||||
| @@ -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; | |||||
| } | |||||
| } | |||||
| @@ -1,3 +1,4 @@ | |||||
| import CodeSelectorModal from '@/components/CodeSelectorModal'; | |||||
| import KFIcon from '@/components/KFIcon'; | import KFIcon from '@/components/KFIcon'; | ||||
| import ParameterInput, { requiredValidator } from '@/components/ParameterInput'; | import ParameterInput, { requiredValidator } from '@/components/ParameterInput'; | ||||
| import ParameterSelect from '@/components/ParameterSelect'; | import ParameterSelect from '@/components/ParameterSelect'; | ||||
| @@ -21,7 +22,6 @@ import { INode } from '@antv/g6'; | |||||
| import { Button, Drawer, Form, Input, MenuProps, Select } from 'antd'; | import { Button, Drawer, Form, Input, MenuProps, Select } from 'antd'; | ||||
| import { NamePath } from 'antd/es/form/interface'; | import { NamePath } from 'antd/es/form/interface'; | ||||
| import { forwardRef, useImperativeHandle, useState } from 'react'; | import { forwardRef, useImperativeHandle, useState } from 'react'; | ||||
| import CodeSelectorModal from '../CodeSelectorModal'; | |||||
| import PropsLabel from '../PropsLabel'; | import PropsLabel from '../PropsLabel'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| const { TextArea } = Input; | const { TextArea } = Input; | ||||
| @@ -1,4 +1,3 @@ | |||||
| import RobotFrame from '@/components/RobotFrame'; | |||||
| import { useDraggable } from '@/hooks/draggable'; | import { useDraggable } from '@/hooks/draggable'; | ||||
| import { getWorkspaceOverviewReq } from '@/services/workspace'; | import { getWorkspaceOverviewReq } from '@/services/workspace'; | ||||
| import { ExperimentInstance } from '@/types'; | import { ExperimentInstance } from '@/types'; | ||||
| @@ -9,6 +8,7 @@ import AssetsManagement from './components/AssetsManagement'; | |||||
| import ExperimentChart, { type ExperimentStatistics } from './components/ExperimentChart'; | import ExperimentChart, { type ExperimentStatistics } from './components/ExperimentChart'; | ||||
| import ExperitableTable from './components/ExperimentTable'; | import ExperitableTable from './components/ExperimentTable'; | ||||
| import QuickStart from './components/QuickStart'; | import QuickStart from './components/QuickStart'; | ||||
| import RobotFrame from './components/RobotFrame'; | |||||
| import TotalStatistics from './components/TotalStatistics'; | import TotalStatistics from './components/TotalStatistics'; | ||||
| import UserSpace from './components/UserSpace'; | import UserSpace from './components/UserSpace'; | ||||
| import WorkspaceIntro from './components/WorkspaceIntro'; | import WorkspaceIntro from './components/WorkspaceIntro'; | ||||
| @@ -12,7 +12,7 @@ const MissingPage = () => { | |||||
| content={'很抱歉,您访问的正在开发中,\n请耐心等待。'} | content={'很抱歉,您访问的正在开发中,\n请耐心等待。'} | ||||
| hasFooter={true} | hasFooter={true} | ||||
| buttonTitle="返回首页" | buttonTitle="返回首页" | ||||
| onRefresh={() => navigate('/')} | |||||
| onButtonClick={() => navigate('/')} | |||||
| ></KFEmpty> | ></KFEmpty> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -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<typeof BasicInfo>; | |||||
| export default meta; | |||||
| type Story = StoryObj<typeof meta>; | |||||
| // 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: ( | |||||
| <Button type="primary" style={{ height: 26 }}> | |||||
| click | |||||
| </Button> | |||||
| ), | |||||
| }, | |||||
| ], | |||||
| labelWidth: 80, | |||||
| labelAlign: 'justify', | |||||
| }, | |||||
| }; | |||||
| /** 一行三列 */ | |||||
| export const ThreeColumn: Story = { | |||||
| args: { | |||||
| ...Primary.args, | |||||
| labelAlign: 'start', | |||||
| threeColumns: true, | |||||
| }, | |||||
| }; | |||||
| @@ -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<typeof BasicTableInfo>; | |||||
| export default meta; | |||||
| type Story = StoryObj<typeof meta>; | |||||
| // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args | |||||
| export const Primary: Story = { | |||||
| args: { | |||||
| ...BasicInfoStories.Primary.args, | |||||
| labelWidth: 100, | |||||
| }, | |||||
| }; | |||||
| @@ -0,0 +1,56 @@ | |||||
| import CodeSelect from '@/components/CodeSelect'; | |||||
| 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<typeof CodeSelect>; | |||||
| export default meta; | |||||
| type Story = StoryObj<typeof meta>; | |||||
| // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args | |||||
| export const Primary: Story = { | |||||
| render: ({ onChange }) => { | |||||
| return ( | |||||
| <Form name="code-select-form" size="large"> | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item label="代码配置" name="code_config"> | |||||
| <CodeSelect | |||||
| placeholder="请选择代码配置" | |||||
| canInput={false} | |||||
| size="large" | |||||
| onChange={onChange} | |||||
| /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| </Form> | |||||
| ); | |||||
| }, | |||||
| }; | |||||
| @@ -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<typeof CodeSelectorModal>; | |||||
| export default meta; | |||||
| type Story = StoryObj<typeof meta>; | |||||
| // 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 ( | |||||
| <> | |||||
| <Button type="primary" onClick={onClick}> | |||||
| 选择代码配置 | |||||
| </Button> | |||||
| <CodeSelectorModal {...args} open={open} onOk={handleOk} onCancel={handleCancel} /> | |||||
| </> | |||||
| ); | |||||
| }, | |||||
| }; | |||||
| /** 通过 `openAntdModal` 函数打开 */ | |||||
| export const OpenByFunction: Story = { | |||||
| render: function Render(args) { | |||||
| const handleClick = () => { | |||||
| const { close } = openAntdModal(CodeSelectorModal, { | |||||
| onOk: (res) => { | |||||
| const { onOk } = args; | |||||
| onOk?.(res); | |||||
| close(); | |||||
| }, | |||||
| }); | |||||
| }; | |||||
| return ( | |||||
| <Button type="primary" onClick={handleClick}> | |||||
| 以函数的方式打开 | |||||
| </Button> | |||||
| ); | |||||
| }, | |||||
| }; | |||||
| @@ -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<typeof ConfigInfo>; | |||||
| export default meta; | |||||
| type Story = StoryObj<typeof meta>; | |||||
| // 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: <div>I am a child element </div>, | |||||
| }, | |||||
| }; | |||||
| @@ -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<typeof FullScreenFrame>; | |||||
| export default meta; | |||||
| type Story = StoryObj<typeof meta>; | |||||
| // 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' }, | |||||
| }, | |||||
| }; | |||||
| @@ -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<typeof IFramePage>; | |||||
| export default meta; | |||||
| type Story = StoryObj<typeof meta>; | |||||
| // 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' }, | |||||
| }, | |||||
| }; | |||||
| @@ -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<typeof InfoGroup>; | |||||
| export default meta; | |||||
| type Story = StoryObj<typeof meta>; | |||||
| // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args | |||||
| export const Primary: Story = { | |||||
| args: { | |||||
| title: '基本信息', | |||||
| children: <div>I am a child element </div>, | |||||
| }, | |||||
| }; | |||||
| @@ -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: 'select', 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<typeof KFEmpty>; | |||||
| export default meta; | |||||
| type Story = StoryObj<typeof meta>; | |||||
| // 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: '返回首页', | |||||
| }, | |||||
| }; | |||||
| @@ -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<typeof KFIcon>; | |||||
| export default meta; | |||||
| type Story = StoryObj<typeof meta>; | |||||
| // 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 ( | |||||
| <Button icon={<KFIcon {...args} />} type="primary"> | |||||
| 下载 | |||||
| </Button> | |||||
| ); | |||||
| }, | |||||
| }; | |||||
| @@ -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<typeof KFModal>; | |||||
| export default meta; | |||||
| type Story = StoryObj<typeof meta>; | |||||
| // 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 ( | |||||
| <> | |||||
| <Button type="primary" onClick={onClick}> | |||||
| 打开 KFModal | |||||
| </Button> | |||||
| <KFModal {...args} open={open} onOk={handleOk} onCancel={handleCancel} /> | |||||
| </> | |||||
| ); | |||||
| }, | |||||
| }; | |||||
| /** 通过 `openAntdModal` 函数打开 */ | |||||
| export const OpenByFunction: Story = { | |||||
| render: function Render() { | |||||
| const handleClick = () => { | |||||
| const { close } = openAntdModal(KFModal, { | |||||
| title: '创建实验', | |||||
| image: CreateExperiment, | |||||
| children: '这是一个模态框', | |||||
| onOk: () => { | |||||
| close(); | |||||
| }, | |||||
| }); | |||||
| }; | |||||
| return ( | |||||
| <Button type="primary" onClick={handleClick}> | |||||
| 以函数的方式打开 | |||||
| </Button> | |||||
| ); | |||||
| }, | |||||
| }; | |||||
| @@ -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<typeof KFRadio>; | |||||
| export default meta; | |||||
| type Story = StoryObj<typeof meta>; | |||||
| // 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: <KFIcon type="icon-jiyugongwangjingxiang" />, | |||||
| }, | |||||
| { | |||||
| title: '昇腾NPU', | |||||
| value: 'NPU', | |||||
| icon: <KFIcon type="icon-bendishangchuan" />, | |||||
| }, | |||||
| ], | |||||
| value: 'GPU', | |||||
| }, | |||||
| render: function Render(args) { | |||||
| const [{ value }, updateArgs] = useArgs(); | |||||
| function onChange(value: string) { | |||||
| updateArgs({ value: value }); | |||||
| } | |||||
| return <KFRadio {...args} value={value} onChange={onChange} />; | |||||
| }, | |||||
| }; | |||||
| @@ -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) => ( | |||||
| <div style={{ height: '200px' }}> | |||||
| <Story /> | |||||
| </div> | |||||
| ), | |||||
| ], | |||||
| // 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<typeof KFSpin>; | |||||
| export default meta; | |||||
| type Story = StoryObj<typeof meta>; | |||||
| // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args | |||||
| export const Primary: Story = { | |||||
| args: {}, | |||||
| }; | |||||
| @@ -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<typeof MenuIconSelector>; | |||||
| export default meta; | |||||
| type Story = StoryObj<typeof meta>; | |||||
| // 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 ( | |||||
| <> | |||||
| <Button type="primary" onClick={onClick}> | |||||
| 打开 MenuIconSelector | |||||
| </Button> | |||||
| <MenuIconSelector | |||||
| {...args} | |||||
| open={open} | |||||
| selectedIcon={selectedIcon} | |||||
| onOk={handleOk} | |||||
| onCancel={handleCancel} | |||||
| /> | |||||
| </> | |||||
| ); | |||||
| }, | |||||
| }; | |||||
| @@ -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<typeof PageTitle>; | |||||
| export default meta; | |||||
| type Story = StoryObj<typeof meta>; | |||||
| // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args | |||||
| export const Primary: Story = { | |||||
| args: { | |||||
| title: '数据集列表', | |||||
| }, | |||||
| }; | |||||
| @@ -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<typeof ParameterInput>; | |||||
| export default meta; | |||||
| type Story = StoryObj<typeof meta>; | |||||
| // 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<ParameterInputValue | undefined>(''); | |||||
| const onClick = () => { | |||||
| setValue({ | |||||
| value: 'storybook', | |||||
| showValue: 'storybook', | |||||
| fromSelect: true, | |||||
| }); | |||||
| }; | |||||
| return ( | |||||
| <> | |||||
| <ParameterInput | |||||
| {...args} | |||||
| value={value} | |||||
| onChange={(value) => setValue(value)} | |||||
| ></ParameterInput> | |||||
| <Button type="primary" style={{ display: 'block', marginTop: 10 }} onClick={onClick}> | |||||
| 模拟从全局参数选择 | |||||
| </Button> | |||||
| </> | |||||
| ); | |||||
| }, | |||||
| }; | |||||
| @@ -0,0 +1,135 @@ | |||||
| import ResourceSelect, { | |||||
| requiredValidator, | |||||
| ResourceSelectorType, | |||||
| } from '@/components/ResourceSelect'; | |||||
| 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: { | |||||
| // 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<typeof ResourceSelect>; | |||||
| export default meta; | |||||
| type Story = StoryObj<typeof meta>; | |||||
| // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args | |||||
| export const Primary: Story = { | |||||
| args: { | |||||
| type: ResourceSelectorType.Dataset, | |||||
| }, | |||||
| render: ({ onChange }) => { | |||||
| return ( | |||||
| <Form | |||||
| name="resource-select-form" | |||||
| labelCol={{ flex: '80px' }} | |||||
| labelAlign="left" | |||||
| size="large" | |||||
| autoComplete="off" | |||||
| > | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item label="数据集" name="dataset"> | |||||
| <ResourceSelect | |||||
| type={ResourceSelectorType.Dataset} | |||||
| placeholder="请选择" | |||||
| canInput={false} | |||||
| size="large" | |||||
| onChange={onChange} | |||||
| /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item | |||||
| label="模型" | |||||
| name="model" | |||||
| rules={[ | |||||
| { | |||||
| validator: requiredValidator, | |||||
| message: '请选择镜像', | |||||
| }, | |||||
| ]} | |||||
| required | |||||
| > | |||||
| <ResourceSelect | |||||
| type={ResourceSelectorType.Model} | |||||
| placeholder="请选择" | |||||
| canInput={false} | |||||
| size="large" | |||||
| onChange={onChange} | |||||
| /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item label="镜像" name="image"> | |||||
| <ResourceSelect | |||||
| type={ResourceSelectorType.Mirror} | |||||
| placeholder="请选择" | |||||
| canInput={false} | |||||
| size="large" | |||||
| onChange={onChange} | |||||
| /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| </Form> | |||||
| ); | |||||
| }, | |||||
| }; | |||||
| @@ -0,0 +1,23 @@ | |||||
| import { Meta, Canvas } from '@storybook/blocks'; | |||||
| import * as ResourceSelectorModalStories from "./ResourceSelectorModal.stories" | |||||
| <Meta of={ResourceSelectorModalStories} name="Usage" /> | |||||
| # Usage | |||||
| 推荐通过 `openAntdModal` 函数打开 `ResourceSelectorModal`,打开 -> 处理 -> 关闭,整套代码在同一个地方 | |||||
| ```ts | |||||
| const handleClick = () => { | |||||
| const { close } = openAntdModal(ResourceSelectorModal, { | |||||
| type: ResourceSelectorType.Dataset, | |||||
| onOk: (res) => { | |||||
| // 处理逻辑 | |||||
| close(); | |||||
| }, | |||||
| }); | |||||
| ``` | |||||
| <Canvas of={ResourceSelectorModalStories.OpenByFunction} /> | |||||
| @@ -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<typeof ResourceSelectorModal>; | |||||
| export default meta; | |||||
| type Story = StoryObj<typeof meta>; | |||||
| // 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 ( | |||||
| <> | |||||
| <Button type="primary" onClick={onClick}> | |||||
| 选择数据集 | |||||
| </Button> | |||||
| <ResourceSelectorModal {...args} open={open} onOk={handleOk} onCancel={handleCancel} /> | |||||
| </> | |||||
| ); | |||||
| }, | |||||
| }; | |||||
| 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 ( | |||||
| <> | |||||
| <Button type="primary" onClick={onClick}> | |||||
| 选择模型 | |||||
| </Button> | |||||
| <ResourceSelectorModal {...args} open={open} onOk={handleOk} onCancel={handleCancel} /> | |||||
| </> | |||||
| ); | |||||
| }, | |||||
| }; | |||||
| 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 ( | |||||
| <> | |||||
| <Button type="primary" onClick={onClick}> | |||||
| 选择镜像 | |||||
| </Button> | |||||
| <ResourceSelectorModal {...args} open={open} onOk={handleOk} onCancel={handleCancel} /> | |||||
| </> | |||||
| ); | |||||
| }, | |||||
| }; | |||||
| /** 通过 `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 ( | |||||
| <Button type="primary" onClick={handleClick}> | |||||
| 以函数的方式打开 | |||||
| </Button> | |||||
| ); | |||||
| }, | |||||
| }; | |||||
| @@ -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<typeof SubAreaTitle>; | |||||
| export default meta; | |||||
| type Story = StoryObj<typeof meta>; | |||||
| // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args | |||||
| export const Primary: Story = { | |||||
| args: { | |||||
| title: '基本信息', | |||||
| image: MirrorBasic, | |||||
| }, | |||||
| }; | |||||
| @@ -0,0 +1,199 @@ | |||||
| import { Meta, Controls } from '@storybook/blocks'; | |||||
| <Meta title="Documentation/Less" /> | |||||
| # 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 ( | |||||
| <div className={styles['code-config-item']}> | |||||
| <Flex justify="space-between" align="center" style={{ marginBottom: '15px' }}> | |||||
| <Typography.Paragraph | |||||
| className={styles['code-config-item__name']} | |||||
| ellipsis={{ tooltip: item.code_repo_name }} | |||||
| > | |||||
| {item.code_repo_name} | |||||
| </Typography.Paragraph> | |||||
| <div | |||||
| className={classNames( | |||||
| styles['code-config-item__tag'], | |||||
| item.code_repo_vis === AvailableRange.Public | |||||
| ? styles['code-config-item__tag--public'] | |||||
| : styles['code-config-item__tag--private'], | |||||
| )} | |||||
| > | |||||
| {item.code_repo_vis === AvailableRange.Public ? '公开' : '私有'} | |||||
| </div> | |||||
| </Flex> | |||||
| <Typography.Paragraph | |||||
| className={styles['code-config-item__url']} | |||||
| ellipsis={{ rows: 2, tooltip: item.git_url }} | |||||
| > | |||||
| {item.git_url} | |||||
| </Typography.Paragraph> | |||||
| <div className={styles['code-config-item__branch']}>{item.git_branch}</div> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| ``` | |||||
| ### 一些建议 | |||||
| 如果你陷入嵌套地狱,比如 | |||||
| ```tsx | |||||
| function Component() { | |||||
| return ( | |||||
| <div className="component"> | |||||
| <div className="component__element1"> | |||||
| <div className="component__element1__element2"> | |||||
| <div className="component__element1__element2__element3"> | |||||
| <div className="component__element1__element2__element3__element4"> | |||||
| // 等等 | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| ``` | |||||
| 说明你需要拆分组件了 | |||||
| ```tsx | |||||
| function Component1() { | |||||
| return ( | |||||
| <div className="component1"> | |||||
| <div className="component1__element1"> | |||||
| </div> | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| function Component() { | |||||
| return ( | |||||
| <div className="component"> | |||||
| <div className="component__element1"> | |||||
| <Component1></Component1> | |||||
| </div> | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| ``` | |||||
| 既减少了类名的嵌套,又减少了HTML的嵌套,使代码逻辑更加清晰,易于理解与维护 | |||||
| @@ -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<typeof Button>; | |||||
| export default meta; | |||||
| type Story = StoryObj<typeof meta>; | |||||
| // 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', | |||||
| }, | |||||
| }; | |||||
| @@ -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 ( | |||||
| <button | |||||
| type="button" | |||||
| className={['storybook-button', `storybook-button--${size}`, mode].join(' ')} | |||||
| style={{ backgroundColor }} | |||||
| {...props} | |||||
| > | |||||
| {label} | |||||
| </button> | |||||
| ); | |||||
| }; | |||||
| @@ -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 = () => <svg | |||||
| viewBox="0 0 14 14" | |||||
| width="8px" | |||||
| height="14px" | |||||
| style={{ | |||||
| marginLeft: '4px', | |||||
| display: 'inline-block', | |||||
| shapeRendering: 'inherit', | |||||
| verticalAlign: 'middle', | |||||
| fill: 'currentColor', | |||||
| 'path fill': 'currentColor' | |||||
| }} | |||||
| > | |||||
| <path d="m11.1 7.35-5.5 5.5a.5.5 0 0 1-.7-.7L10.04 7 4.9 1.85a.5.5 0 1 1 .7-.7l5.5 5.5c.2.2.2.5 0 .7Z" /> | |||||
| </svg> | |||||
| <Meta title="Configure your project" /> | |||||
| <div className="sb-container"> | |||||
| <div className='sb-section-title'> | |||||
| # 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. | |||||
| </div> | |||||
| <div className="sb-section"> | |||||
| <div className="sb-section-item"> | |||||
| <img | |||||
| src={Styling} | |||||
| alt="A wall of logos representing different styling technologies" | |||||
| /> | |||||
| <h4 className="sb-section-item-heading">Add styling and CSS</h4> | |||||
| <p className="sb-section-item-paragraph">Like with web applications, there are many ways to include CSS within Storybook. Learn more about setting up styling within Storybook.</p> | |||||
| <a | |||||
| href="https://storybook.js.org/docs/configure/styling-and-css/?renderer=react" | |||||
| target="_blank" | |||||
| >Learn more<RightArrow /></a> | |||||
| </div> | |||||
| <div className="sb-section-item"> | |||||
| <img | |||||
| src={Context} | |||||
| alt="An abstraction representing the composition of data for a component" | |||||
| /> | |||||
| <h4 className="sb-section-item-heading">Provide context and mocking</h4> | |||||
| <p className="sb-section-item-paragraph">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.</p> | |||||
| <a | |||||
| href="https://storybook.js.org/docs/writing-stories/decorators/?renderer=react#context-for-mocking" | |||||
| target="_blank" | |||||
| >Learn more<RightArrow /></a> | |||||
| </div> | |||||
| <div className="sb-section-item"> | |||||
| <img src={Assets} alt="A representation of typography and image assets" /> | |||||
| <div> | |||||
| <h4 className="sb-section-item-heading">Load assets and resources</h4> | |||||
| <p className="sb-section-item-paragraph">To link static files (like fonts) to your projects and stories, use the | |||||
| `staticDirs` configuration option to specify folders to load when | |||||
| starting Storybook.</p> | |||||
| <a | |||||
| href="https://storybook.js.org/docs/configure/images-and-assets/?renderer=react" | |||||
| target="_blank" | |||||
| >Learn more<RightArrow /></a> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| <div className="sb-container"> | |||||
| <div className='sb-section-title'> | |||||
| # 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. | |||||
| </div> | |||||
| <div className="sb-section"> | |||||
| <div className="sb-features-grid"> | |||||
| <div className="sb-grid-item"> | |||||
| <img src={Docs} alt="A screenshot showing the autodocs tag being set, pointing a docs page being generated" /> | |||||
| <h4 className="sb-section-item-heading">Autodocs</h4> | |||||
| <p className="sb-section-item-paragraph">Auto-generate living, | |||||
| interactive reference documentation from your components and stories.</p> | |||||
| <a | |||||
| href="https://storybook.js.org/docs/writing-docs/autodocs/?renderer=react" | |||||
| target="_blank" | |||||
| >Learn more<RightArrow /></a> | |||||
| </div> | |||||
| <div className="sb-grid-item"> | |||||
| <img src={Share} alt="A browser window showing a Storybook being published to a chromatic.com URL" /> | |||||
| <h4 className="sb-section-item-heading">Publish to Chromatic</h4> | |||||
| <p className="sb-section-item-paragraph">Publish your Storybook to review and collaborate with your entire team.</p> | |||||
| <a | |||||
| href="https://storybook.js.org/docs/sharing/publish-storybook/?renderer=react#publish-storybook-with-chromatic" | |||||
| target="_blank" | |||||
| >Learn more<RightArrow /></a> | |||||
| </div> | |||||
| <div className="sb-grid-item"> | |||||
| <img src={FigmaPlugin} alt="Windows showing the Storybook plugin in Figma" /> | |||||
| <h4 className="sb-section-item-heading">Figma Plugin</h4> | |||||
| <p className="sb-section-item-paragraph">Embed your stories into Figma to cross-reference the design and live | |||||
| implementation in one place.</p> | |||||
| <a | |||||
| href="https://storybook.js.org/docs/sharing/design-integrations/?renderer=react#embed-storybook-in-figma-with-the-plugin" | |||||
| target="_blank" | |||||
| >Learn more<RightArrow /></a> | |||||
| </div> | |||||
| <div className="sb-grid-item"> | |||||
| <img src={Testing} alt="Screenshot of tests passing and failing" /> | |||||
| <h4 className="sb-section-item-heading">Testing</h4> | |||||
| <p className="sb-section-item-paragraph">Use stories to test a component in all its variations, no matter how | |||||
| complex.</p> | |||||
| <a | |||||
| href="https://storybook.js.org/docs/writing-tests/?renderer=react" | |||||
| target="_blank" | |||||
| >Learn more<RightArrow /></a> | |||||
| </div> | |||||
| <div className="sb-grid-item"> | |||||
| <img src={Accessibility} alt="Screenshot of accessibility tests passing and failing" /> | |||||
| <h4 className="sb-section-item-heading">Accessibility</h4> | |||||
| <p className="sb-section-item-paragraph">Automatically test your components for a11y issues as you develop.</p> | |||||
| <a | |||||
| href="https://storybook.js.org/docs/writing-tests/accessibility-testing/?renderer=react" | |||||
| target="_blank" | |||||
| >Learn more<RightArrow /></a> | |||||
| </div> | |||||
| <div className="sb-grid-item"> | |||||
| <img src={Theming} alt="Screenshot of Storybook in light and dark mode" /> | |||||
| <h4 className="sb-section-item-heading">Theming</h4> | |||||
| <p className="sb-section-item-paragraph">Theme Storybook's UI to personalize it to your project.</p> | |||||
| <a | |||||
| href="https://storybook.js.org/docs/configure/theming/?renderer=react" | |||||
| target="_blank" | |||||
| >Learn more<RightArrow /></a> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| <div className='sb-addon'> | |||||
| <div className='sb-addon-text'> | |||||
| <h4>Addons</h4> | |||||
| <p className="sb-section-item-paragraph">Integrate your tools with Storybook to connect workflows.</p> | |||||
| <a | |||||
| href="https://storybook.js.org/addons/" | |||||
| target="_blank" | |||||
| >Discover all addons<RightArrow /></a> | |||||
| </div> | |||||
| <div className='sb-addon-img'> | |||||
| <img src={AddonLibrary} alt="Integrate your tools with Storybook to connect workflows." /> | |||||
| </div> | |||||
| </div> | |||||
| <div className="sb-section sb-socials"> | |||||
| <div className="sb-section-item"> | |||||
| <img src={Github} alt="Github logo" className="sb-explore-image"/> | |||||
| Join our contributors building the future of UI development. | |||||
| <a | |||||
| href="https://github.com/storybookjs/storybook" | |||||
| target="_blank" | |||||
| >Star on GitHub<RightArrow /></a> | |||||
| </div> | |||||
| <div className="sb-section-item"> | |||||
| <img src={Discord} alt="Discord logo" className="sb-explore-image"/> | |||||
| <div> | |||||
| Get support and chat with frontend developers. | |||||
| <a | |||||
| href="https://discord.gg/storybook" | |||||
| target="_blank" | |||||
| >Join Discord server<RightArrow /></a> | |||||
| </div> | |||||
| </div> | |||||
| <div className="sb-section-item"> | |||||
| <img src={Youtube} alt="Youtube logo" className="sb-explore-image"/> | |||||
| <div> | |||||
| Watch tutorials, feature previews and interviews. | |||||
| <a | |||||
| href="https://www.youtube.com/@chromaticui" | |||||
| target="_blank" | |||||
| >Watch on YouTube<RightArrow /></a> | |||||
| </div> | |||||
| </div> | |||||
| <div className="sb-section-item"> | |||||
| <img src={Tutorials} alt="A book" className="sb-explore-image"/> | |||||
| <p>Follow guided walkthroughs on for key workflows.</p> | |||||
| <a | |||||
| href="https://storybook.js.org/tutorials/" | |||||
| target="_blank" | |||||
| >Discover tutorials<RightArrow /></a> | |||||
| </div> | |||||
| </div> | |||||
| <style> | |||||
| {` | |||||
| .sb-container { | |||||
| margin-bottom: 48px; | |||||
| } | |||||
| .sb-section { | |||||
| width: 100%; | |||||
| display: flex; | |||||
| flex-direction: row; | |||||
| gap: 20px; | |||||
| } | |||||
| img { | |||||
| object-fit: cover; | |||||
| } | |||||
| .sb-section-title { | |||||
| margin-bottom: 32px; | |||||
| } | |||||
| .sb-section a:not(h1 a, h2 a, h3 a) { | |||||
| font-size: 14px; | |||||
| } | |||||
| .sb-section-item, .sb-grid-item { | |||||
| flex: 1; | |||||
| display: flex; | |||||
| flex-direction: column; | |||||
| } | |||||
| .sb-section-item-heading { | |||||
| padding-top: 20px !important; | |||||
| padding-bottom: 5px !important; | |||||
| margin: 0 !important; | |||||
| } | |||||
| .sb-section-item-paragraph { | |||||
| margin: 0; | |||||
| padding-bottom: 10px; | |||||
| } | |||||
| .sb-chevron { | |||||
| margin-left: 5px; | |||||
| } | |||||
| .sb-features-grid { | |||||
| display: grid; | |||||
| grid-template-columns: repeat(2, 1fr); | |||||
| grid-gap: 32px 20px; | |||||
| } | |||||
| .sb-socials { | |||||
| display: grid; | |||||
| grid-template-columns: repeat(4, 1fr); | |||||
| } | |||||
| .sb-socials p { | |||||
| margin-bottom: 10px; | |||||
| } | |||||
| .sb-explore-image { | |||||
| max-height: 32px; | |||||
| align-self: flex-start; | |||||
| } | |||||
| .sb-addon { | |||||
| width: 100%; | |||||
| display: flex; | |||||
| align-items: center; | |||||
| position: relative; | |||||
| background-color: #EEF3F8; | |||||
| border-radius: 5px; | |||||
| border: 1px solid rgba(0, 0, 0, 0.05); | |||||
| background: #EEF3F8; | |||||
| height: 180px; | |||||
| margin-bottom: 48px; | |||||
| overflow: hidden; | |||||
| } | |||||
| .sb-addon-text { | |||||
| padding-left: 48px; | |||||
| max-width: 240px; | |||||
| } | |||||
| .sb-addon-text h4 { | |||||
| padding-top: 0px; | |||||
| } | |||||
| .sb-addon-img { | |||||
| position: absolute; | |||||
| left: 345px; | |||||
| top: 0; | |||||
| height: 100%; | |||||
| width: 200%; | |||||
| overflow: hidden; | |||||
| } | |||||
| .sb-addon-img img { | |||||
| width: 650px; | |||||
| transform: rotate(-15deg); | |||||
| margin-left: 40px; | |||||
| margin-top: -72px; | |||||
| box-shadow: 0 0 1px rgba(255, 255, 255, 0); | |||||
| backface-visibility: hidden; | |||||
| } | |||||
| @media screen and (max-width: 800px) { | |||||
| .sb-addon-img { | |||||
| left: 300px; | |||||
| } | |||||
| } | |||||
| @media screen and (max-width: 600px) { | |||||
| .sb-section { | |||||
| flex-direction: column; | |||||
| } | |||||
| .sb-features-grid { | |||||
| grid-template-columns: repeat(1, 1fr); | |||||
| } | |||||
| .sb-socials { | |||||
| grid-template-columns: repeat(2, 1fr); | |||||
| } | |||||
| .sb-addon { | |||||
| height: 280px; | |||||
| align-items: flex-start; | |||||
| padding-top: 32px; | |||||
| overflow: hidden; | |||||
| } | |||||
| .sb-addon-text { | |||||
| padding-left: 24px; | |||||
| } | |||||
| .sb-addon-img { | |||||
| right: 0; | |||||
| left: 0; | |||||
| top: 130px; | |||||
| bottom: 0; | |||||
| overflow: hidden; | |||||
| height: auto; | |||||
| width: 124%; | |||||
| } | |||||
| .sb-addon-img img { | |||||
| width: 1200px; | |||||
| transform: rotate(-12deg); | |||||
| margin-left: 0; | |||||
| margin-top: 48px; | |||||
| margin-bottom: -40px; | |||||
| margin-left: -24px; | |||||
| } | |||||
| } | |||||
| `} | |||||
| </style> | |||||
| @@ -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<typeof Header>; | |||||
| export default meta; | |||||
| type Story = StoryObj<typeof meta>; | |||||
| export const LoggedIn: Story = { | |||||
| args: { | |||||
| user: { | |||||
| name: 'Jane Doe', | |||||
| }, | |||||
| }, | |||||
| }; | |||||
| export const LoggedOut: Story = {}; | |||||
| @@ -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) => ( | |||||
| <header> | |||||
| <div className="storybook-header"> | |||||
| <div> | |||||
| <svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"> | |||||
| <g fill="none" fillRule="evenodd"> | |||||
| <path | |||||
| d="M10 0h12a10 10 0 0110 10v12a10 10 0 01-10 10H10A10 10 0 010 22V10A10 10 0 0110 0z" | |||||
| fill="#FFF" | |||||
| /> | |||||
| <path | |||||
| d="M5.3 10.6l10.4 6v11.1l-10.4-6v-11zm11.4-6.2l9.7 5.5-9.7 5.6V4.4z" | |||||
| fill="#555AB9" | |||||
| /> | |||||
| <path | |||||
| d="M27.2 10.6v11.2l-10.5 6V16.5l10.5-6zM15.7 4.4v11L6 10l9.7-5.5z" | |||||
| fill="#91BAF8" | |||||
| /> | |||||
| </g> | |||||
| </svg> | |||||
| <h1>Acme</h1> | |||||
| </div> | |||||
| <div> | |||||
| {user ? ( | |||||
| <> | |||||
| <span className="welcome"> | |||||
| Welcome, <b>{user.name}</b>! | |||||
| </span> | |||||
| <Button size="small" onClick={onLogout} label="Log out" /> | |||||
| </> | |||||
| ) : ( | |||||
| <> | |||||
| <Button size="small" onClick={onLogin} label="Log in" /> | |||||
| <Button primary size="small" onClick={onCreateAccount} label="Sign up" /> | |||||
| </> | |||||
| )} | |||||
| </div> | |||||
| </div> | |||||
| </header> | |||||
| ); | |||||
| @@ -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<typeof Page>; | |||||
| export default meta; | |||||
| type Story = StoryObj<typeof meta>; | |||||
| 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(); | |||||
| }, | |||||
| }; | |||||
| @@ -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<User>(); | |||||
| return ( | |||||
| <article> | |||||
| <Header | |||||
| user={user} | |||||
| onLogin={() => setUser({ name: 'Jane Doe' })} | |||||
| onLogout={() => setUser(undefined)} | |||||
| onCreateAccount={() => setUser({ name: 'Jane Doe' })} | |||||
| /> | |||||
| <section className="storybook-page"> | |||||
| <h2>Pages in Storybook</h2> | |||||
| <p> | |||||
| We recommend building UIs with a{' '} | |||||
| <a href="https://componentdriven.org" target="_blank" rel="noopener noreferrer"> | |||||
| <strong>component-driven</strong> | |||||
| </a>{' '} | |||||
| process starting with atomic components and ending with pages. | |||||
| </p> | |||||
| <p> | |||||
| 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: | |||||
| </p> | |||||
| <ul> | |||||
| <li> | |||||
| Use a higher-level connected component. Storybook helps you compose such data from the | |||||
| "args" of child component stories | |||||
| </li> | |||||
| <li> | |||||
| Assemble data in the page component from your services. You can mock these services out | |||||
| using Storybook. | |||||
| </li> | |||||
| </ul> | |||||
| <p> | |||||
| Get a guided tutorial on component-driven development at{' '} | |||||
| <a href="https://storybook.js.org/tutorials/" target="_blank" rel="noopener noreferrer"> | |||||
| Storybook tutorials | |||||
| </a> | |||||
| . Read more in the{' '} | |||||
| <a href="https://storybook.js.org/docs" target="_blank" rel="noopener noreferrer"> | |||||
| docs | |||||
| </a> | |||||
| . | |||||
| </p> | |||||
| <div className="tip-wrapper"> | |||||
| <span className="tip">Tip</span> Adjust the width of the canvas with the{' '} | |||||
| <svg width="10" height="10" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg"> | |||||
| <g fill="none" fillRule="evenodd"> | |||||
| <path | |||||
| d="M1.5 5.2h4.8c.3 0 .5.2.5.4v5.1c-.1.2-.3.3-.4.3H1.4a.5.5 0 01-.5-.4V5.7c0-.3.2-.5.5-.5zm0-2.1h6.9c.3 0 .5.2.5.4v7a.5.5 0 01-1 0V4H1.5a.5.5 0 010-1zm0-2.1h9c.3 0 .5.2.5.4v9.1a.5.5 0 01-1 0V2H1.5a.5.5 0 010-1zm4.3 5.2H2V10h3.8V6.2z" | |||||
| id="a" | |||||
| fill="#999" | |||||
| /> | |||||
| </g> | |||||
| </svg> | |||||
| Viewports addon in the toolbar | |||||
| </div> | |||||
| </section> | |||||
| </article> | |||||
| ); | |||||
| }; | |||||
| @@ -0,0 +1 @@ | |||||
| <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="none" viewBox="0 0 48 48"><title>Accessibility</title><circle cx="24.334" cy="24" r="24" fill="#A849FF" fill-opacity=".3"/><path fill="#A470D5" fill-rule="evenodd" d="M27.8609 11.585C27.8609 9.59506 26.2497 7.99023 24.2519 7.99023C22.254 7.99023 20.6429 9.65925 20.6429 11.585C20.6429 13.575 22.254 15.1799 24.2519 15.1799C26.2497 15.1799 27.8609 13.575 27.8609 11.585ZM21.8922 22.6473C21.8467 23.9096 21.7901 25.4788 21.5897 26.2771C20.9853 29.0462 17.7348 36.3314 17.3325 37.2275C17.1891 37.4923 17.1077 37.7955 17.1077 38.1178C17.1077 39.1519 17.946 39.9902 18.9802 39.9902C19.6587 39.9902 20.253 39.6293 20.5814 39.0889L20.6429 38.9874L24.2841 31.22C24.2841 31.22 27.5529 37.9214 27.9238 38.6591C28.2948 39.3967 28.8709 39.9902 29.7168 39.9902C30.751 39.9902 31.5893 39.1519 31.5893 38.1178C31.5893 37.7951 31.3639 37.2265 31.3639 37.2265C30.9581 36.3258 27.698 29.0452 27.0938 26.2771C26.8975 25.4948 26.847 23.9722 26.8056 22.7236C26.7927 22.333 26.7806 21.9693 26.7653 21.6634C26.7008 21.214 27.0231 20.8289 27.4097 20.7005L35.3366 18.3253C36.3033 18.0685 36.8834 16.9773 36.6256 16.0144C36.3678 15.0515 35.2722 14.4737 34.3055 14.7305C34.3055 14.7305 26.8619 17.1057 24.2841 17.1057C21.7062 17.1057 14.456 14.7947 14.456 14.7947C13.4893 14.5379 12.3937 14.9873 12.0715 15.9502C11.7493 16.9131 12.3293 18.0044 13.3604 18.3253L21.2873 20.7005C21.674 20.8289 21.9318 21.214 21.9318 21.6634C21.9174 21.9493 21.9053 22.2857 21.8922 22.6473Z" clip-rule="evenodd"/></svg> | |||||