Browse Source

Merge pull request '添加 Storybook' (#170) from zw-storybook into dev-zw

pull/171/head
cp3hnu 11 months ago
parent
commit
6b5bb6aae5
100 changed files with 3219 additions and 339 deletions
  1. +2
    -0
      .gitignore
  2. +2
    -7
      react-ui/.gitignore
  3. +1
    -0
      react-ui/.nvmrc
  4. +16
    -0
      react-ui/.storybook/babel-plugin-auto-css-modules.js
  5. +117
    -0
      react-ui/.storybook/main.ts
  6. +19
    -0
      react-ui/.storybook/mock/umijs.mock.tsx
  7. +96
    -0
      react-ui/.storybook/preview.tsx
  8. +19
    -0
      react-ui/.storybook/storybook.css
  9. +26
    -0
      react-ui/.storybook/tsconfig.json
  10. +20
    -0
      react-ui/.storybook/typings.d.ts
  11. +27
    -2
      react-ui/package.json
  12. +307
    -0
      react-ui/public/mockServiceWorker.js
  13. +2
    -3
      react-ui/src/app.tsx
  14. +26
    -4
      react-ui/src/components/BasicInfo/BasicInfoItem.tsx
  15. +16
    -9
      react-ui/src/components/BasicInfo/BasicInfoItemValue.tsx
  16. +26
    -2
      react-ui/src/components/BasicInfo/index.less
  17. +34
    -4
      react-ui/src/components/BasicInfo/index.tsx
  18. +8
    -1
      react-ui/src/components/BasicTableInfo/index.less
  19. +8
    -9
      react-ui/src/components/BasicTableInfo/index.tsx
  20. +0
    -0
      react-ui/src/components/CodeConfigItem/index.less
  21. +0
    -0
      react-ui/src/components/CodeConfigItem/index.tsx
  22. +2
    -1
      react-ui/src/components/CodeSelect/index.tsx
  23. +49
    -0
      react-ui/src/components/CodeSelectorModal/index.less
  24. +11
    -7
      react-ui/src/components/CodeSelectorModal/index.tsx
  25. +41
    -0
      react-ui/src/components/ConfigInfo/index.tsx
  26. +3
    -0
      react-ui/src/components/DisabledInput/index.tsx
  27. +13
    -5
      react-ui/src/components/FullScreenFrame/index.tsx
  28. +10
    -6
      react-ui/src/components/IFramePage/index.tsx
  29. +2
    -1
      react-ui/src/components/InfoGroup/InfoGroupTitle.less
  30. +7
    -1
      react-ui/src/components/InfoGroup/InfoGroupTitle.tsx
  31. +12
    -3
      react-ui/src/components/InfoGroup/index.tsx
  32. +1
    -1
      react-ui/src/components/KFEmpty/index.less
  33. +16
    -6
      react-ui/src/components/KFEmpty/index.tsx
  34. +8
    -2
      react-ui/src/components/KFIcon/index.tsx
  35. +0
    -0
      react-ui/src/components/KFModal/KFModalTitle.less
  36. +5
    -1
      react-ui/src/components/KFModal/KFModalTitle.tsx
  37. +6
    -3
      react-ui/src/components/KFModal/index.tsx
  38. +12
    -4
      react-ui/src/components/KFRadio/index.tsx
  39. +11
    -5
      react-ui/src/components/KFSpin/index.tsx
  40. +0
    -19
      react-ui/src/components/LabelValue/index.less
  41. +0
    -20
      react-ui/src/components/LabelValue/index.tsx
  42. +1
    -2
      react-ui/src/components/MenuIconSelector/index.less
  43. +3
    -0
      react-ui/src/components/MenuIconSelector/index.tsx
  44. +8
    -1
      react-ui/src/components/PageTitle/index.tsx
  45. +17
    -1
      react-ui/src/components/ParameterInput/index.tsx
  46. +5
    -3
      react-ui/src/components/ResourceSelect/index.tsx
  47. +20
    -4
      react-ui/src/components/ResourceSelectorModal/index.tsx
  48. +2
    -5
      react-ui/src/components/RightContent/index.tsx
  49. +9
    -2
      react-ui/src/components/SubAreaTitle/index.tsx
  50. +1
    -1
      react-ui/src/global.less
  51. +1
    -1
      react-ui/src/pages/404.tsx
  52. +5
    -38
      react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx
  53. +0
    -20
      react-ui/src/pages/AutoML/components/ConfigInfo/index.less
  54. +0
    -27
      react-ui/src/pages/AutoML/components/ConfigInfo/index.tsx
  55. +1
    -1
      react-ui/src/pages/CodeConfig/List/index.tsx
  56. +0
    -9
      react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx
  57. +1
    -1
      react-ui/src/pages/Dataset/components/ResourceList/index.tsx
  58. +2
    -2
      react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx
  59. +4
    -22
      react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx
  60. +2
    -2
      react-ui/src/pages/Mirror/Create/index.tsx
  61. +6
    -1
      react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx
  62. +2
    -17
      react-ui/src/pages/ModelDeployment/components/VersionBasicInfo/index.tsx
  63. +0
    -50
      react-ui/src/pages/Pipeline/components/CodeSelectorModal/index.less
  64. +1
    -1
      react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx
  65. +0
    -0
      react-ui/src/pages/Workspace/components/RobotFrame/index.less
  66. +0
    -0
      react-ui/src/pages/Workspace/components/RobotFrame/index.tsx
  67. +1
    -1
      react-ui/src/pages/Workspace/index.tsx
  68. +1
    -1
      react-ui/src/pages/missingPage.jsx
  69. +102
    -0
      react-ui/src/stories/BasicInfo.stories.tsx
  70. +32
    -0
      react-ui/src/stories/BasicTableInfo.stories.tsx
  71. +56
    -0
      react-ui/src/stories/CodeSelect.stories.tsx
  72. +90
    -0
      react-ui/src/stories/CodeSelectorModal.stories.tsx
  73. +37
    -0
      react-ui/src/stories/Config.stories.tsx
  74. +32
    -0
      react-ui/src/stories/FullScreenFrame.stories.tsx
  75. +31
    -0
      react-ui/src/stories/IFramePage.stories.tsx
  76. +31
    -0
      react-ui/src/stories/InfoGroup.stories.tsx
  77. +53
    -0
      react-ui/src/stories/KFEmpty.stories.tsx
  78. +41
    -0
      react-ui/src/stories/KFIcon.stories.tsx
  79. +93
    -0
      react-ui/src/stories/KFModal.stories.tsx
  80. +52
    -0
      react-ui/src/stories/KFRadio.stories.tsx
  81. +35
    -0
      react-ui/src/stories/KFSpin.stories.tsx
  82. +67
    -0
      react-ui/src/stories/MenuIconSelector.stories.tsx
  83. +30
    -0
      react-ui/src/stories/PageTitle.stories.tsx
  84. +108
    -0
      react-ui/src/stories/ParameterInput.stories.tsx
  85. +135
    -0
      react-ui/src/stories/ResourceSelect.stories.tsx
  86. +23
    -0
      react-ui/src/stories/ResourceSelectorModal.mdx
  87. +217
    -0
      react-ui/src/stories/ResourceSelectorModal.stories.tsx
  88. +32
    -0
      react-ui/src/stories/SubAreaTitle.stories.tsx
  89. +199
    -0
      react-ui/src/stories/docs/Less.mdx
  90. +53
    -0
      react-ui/src/stories/example/Button.stories.ts
  91. +37
    -0
      react-ui/src/stories/example/Button.tsx
  92. +364
    -0
      react-ui/src/stories/example/Configure.mdx
  93. +33
    -0
      react-ui/src/stories/example/Header.stories.ts
  94. +56
    -0
      react-ui/src/stories/example/Header.tsx
  95. +32
    -0
      react-ui/src/stories/example/Page.stories.ts
  96. +73
    -0
      react-ui/src/stories/example/Page.tsx
  97. BIN
      react-ui/src/stories/example/assets/accessibility.png
  98. +1
    -0
      react-ui/src/stories/example/assets/accessibility.svg
  99. BIN
      react-ui/src/stories/example/assets/addon-library.png
  100. BIN
      react-ui/src/stories/example/assets/assets.png

+ 2
- 0
.gitignore View File

@@ -58,3 +58,5 @@ mvnw


# web # web
**/node_modules **/node_modules

*storybook.log

+ 2
- 7
react-ui/.gitignore View File

@@ -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

+ 1
- 0
react-ui/.nvmrc View File

@@ -0,0 +1 @@
v18.16.0

+ 16
- 0
react-ui/.storybook/babel-plugin-auto-css-modules.js View File

@@ -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";
}
}
},
},
};
};

+ 117
- 0
react-ui/.storybook/main.ts View File

@@ -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;

+ 19
- 0
react-ui/.storybook/mock/umijs.mock.tsx View File

@@ -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;

+ 96
- 0
react-ui/.storybook/preview.tsx View File

@@ -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;

+ 19
- 0
react-ui/.storybook/storybook.css View File

@@ -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;
}

+ 26
- 0
react-ui/.storybook/tsconfig.json View File

@@ -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": "./"
}
}

+ 20
- 0
react-ui/.storybook/typings.d.ts View File

@@ -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;

+ 27
- 2
react-ui/package.json View File

@@ -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"
]
} }
}
}

+ 307
- 0
react-ui/public/mockServiceWorker.js View File

@@ -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
}

+ 2
- 3
react-ui/src/app.tsx View File

@@ -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'],


+ 26
- 4
react-ui/src/components/BasicInfo/BasicInfoItem.tsx View File

@@ -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>


+ 16
- 9
react-ui/src/components/BasicInfo/BasicInfoItemValue.tsx View File

@@ -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>


+ 26
- 2
react-ui/src/components/BasicInfo/index.less View File

@@ -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;
}
} }
} }

+ 34
- 4
react-ui/src/components/BasicInfo/index.tsx View File

@@ -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>


+ 8
- 1
react-ui/src/components/BasicTableInfo/index.less View File

@@ -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;
}
} }
} }

+ 8
- 9
react-ui/src/components/BasicTableInfo/index.tsx View File

@@ -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"
/> />
))} ))}


react-ui/src/pages/Pipeline/components/CodeConfigItem/index.less → react-ui/src/components/CodeConfigItem/index.less View File


react-ui/src/pages/Pipeline/components/CodeConfigItem/index.tsx → react-ui/src/components/CodeConfigItem/index.tsx View File


+ 2
- 1
react-ui/src/components/CodeSelect/index.tsx View File

@@ -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, {


+ 49
- 0
react-ui/src/components/CodeSelectorModal/index.less View File

@@ -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;
}
}
}

react-ui/src/pages/Pipeline/components/CodeSelectorModal/index.tsx → react-ui/src/components/CodeSelectorModal/index.tsx View File

@@ -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>
)} )}

+ 41
- 0
react-ui/src/components/ConfigInfo/index.tsx View File

@@ -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;

+ 3
- 0
react-ui/src/components/DisabledInput/index.tsx View File

@@ -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 (


+ 13
- 5
react-ui/src/components/FullScreenFrame/index.tsx View File

@@ -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>


+ 10
- 6
react-ui/src/components/IFramePage/index.tsx View File

@@ -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>
); );
} }


react-ui/src/components/InfoGroupTitle/index.less → react-ui/src/components/InfoGroup/InfoGroupTitle.less View File

@@ -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;

react-ui/src/components/InfoGroupTitle/index.tsx → react-ui/src/components/InfoGroup/InfoGroupTitle.tsx View File

@@ -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}>

+ 12
- 3
react-ui/src/components/InfoGroup/index.tsx View File

@@ -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) {


+ 1
- 1
react-ui/src/components/KFEmpty/index.less View File

@@ -33,7 +33,7 @@
margin-top: 20px; margin-top: 20px;
margin-bottom: 30px; margin-bottom: 30px;


&__back-btn {
&__button {
height: 32px; height: 32px;
} }
} }


+ 16
- 6
react-ui/src/components/KFEmpty/index.tsx View File

@@ -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>
)} )}


+ 8
- 2
react-ui/src/components/KFIcon/index.tsx View File

@@ -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,


react-ui/src/components/ModalTitle/index.less → react-ui/src/components/KFModal/KFModalTitle.less View File


react-ui/src/components/ModalTitle/index.tsx → react-ui/src/components/KFModal/KFModalTitle.tsx View File

@@ -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;
}; };



+ 6
- 3
react-ui/src/components/KFModal/index.tsx View File

@@ -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>


+ 12
- 4
react-ui/src/components/KFRadio/index.tsx View File

@@ -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>


+ 11
- 5
react-ui/src/components/KFSpin/index.tsx View File

@@ -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>
); );
} }


+ 0
- 19
react-ui/src/components/LabelValue/index.less View File

@@ -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;
}
}

+ 0
- 20
react-ui/src/components/LabelValue/index.tsx View File

@@ -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
- 2
react-ui/src/components/MenuIconSelector/index.less View File

@@ -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;


+ 3
- 0
react-ui/src/components/MenuIconSelector/index.tsx View File

@@ -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
- 1
react-ui/src/components/PageTitle/index.tsx View File

@@ -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}>


+ 17
- 1
react-ui/src/components/ParameterInput/index.tsx View File

@@ -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;
} }




+ 5
- 3
react-ui/src/components/ResourceSelect/index.tsx View File

@@ -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,


+ 20
- 4
react-ui/src/components/ResourceSelectorModal/index.tsx View File

@@ -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}


+ 2
- 5
react-ui/src/components/RightContent/index.tsx View File

@@ -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>


+ 9
- 2
react-ui/src/components/SubAreaTitle/index.tsx View File

@@ -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 && (


+ 1
- 1
react-ui/src/global.less View File

@@ -5,7 +5,7 @@ body,
height: 100%; height: 100%;
margin: 0; margin: 0;
padding: 0; padding: 0;
overflow-y: hidden;
overflow-y: visible;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji'; 'Noto Color Emoji';


+ 1
- 1
react-ui/src/pages/404.tsx View File

@@ -12,7 +12,7 @@ const NoFoundPage = () => {
content={'很抱歉,您访问的页面地址有误,\n或者该页面不存在。'} content={'很抱歉,您访问的页面地址有误,\n或者该页面不存在。'}
hasFooter={true} hasFooter={true}
buttonTitle="返回首页" buttonTitle="返回首页"
onRefresh={() => navigate('/')}
onButtonClick={() => navigate('/')}
></KFEmpty> ></KFEmpty>
); );
}; };


+ 5
- 38
react-ui/src/pages/AutoML/components/AutoMLBasic/index.tsx View File

@@ -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>
); );
} }


+ 0
- 20
react-ui/src/pages/AutoML/components/ConfigInfo/index.less View File

@@ -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;
}
}
}
}
}

+ 0
- 27
react-ui/src/pages/AutoML/components/ConfigInfo/index.tsx View File

@@ -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;

+ 1
- 1
react-ui/src/pages/CodeConfig/List/index.tsx View File

@@ -197,7 +197,7 @@ function CodeConfigList() {
title="暂无数据" title="暂无数据"
content={'很抱歉,没有搜索到您想要的内容\n建议刷新试试'} content={'很抱歉,没有搜索到您想要的内容\n建议刷新试试'}
hasFooter={true} hasFooter={true}
onRefresh={getDataList}
onButtonClick={getDataList}
/> />
)} )}
</div> </div>


+ 0
- 9
react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx View File

@@ -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,
}, },
]; ];




+ 1
- 1
react-ui/src/pages/Dataset/components/ResourceList/index.tsx View File

@@ -226,7 +226,7 @@ function ResourceList(
title="暂无数据" title="暂无数据"
content={'很抱歉,没有搜索到您想要的内容\n建议刷新试试'} content={'很抱歉,没有搜索到您想要的内容\n建议刷新试试'}
hasFooter={true} hasFooter={true}
onRefresh={getDataList}
onButtonClick={getDataList}
/> />
)} )}
</div> </div>


+ 2
- 2
react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx View File

@@ -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" />,
}, },
]; ];


+ 4
- 22
react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx View File

@@ -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' }}
> >


+ 2
- 2
react-ui/src/pages/Mirror/Create/index.tsx View File

@@ -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" />,
}, },
]; ];


+ 6
- 1
react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx View File

@@ -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')}


+ 2
- 17
react-ui/src/pages/ModelDeployment/components/VersionBasicInfo/index.tsx View File

@@ -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;

+ 0
- 50
react-ui/src/pages/Pipeline/components/CodeSelectorModal/index.less View File

@@ -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
- 1
react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx View File

@@ -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;


react-ui/src/components/RobotFrame/index.less → react-ui/src/pages/Workspace/components/RobotFrame/index.less View File


react-ui/src/components/RobotFrame/index.tsx → react-ui/src/pages/Workspace/components/RobotFrame/index.tsx View File


+ 1
- 1
react-ui/src/pages/Workspace/index.tsx View File

@@ -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';


+ 1
- 1
react-ui/src/pages/missingPage.jsx View File

@@ -12,7 +12,7 @@ const MissingPage = () => {
content={'很抱歉,您访问的正在开发中,\n请耐心等待。'} content={'很抱歉,您访问的正在开发中,\n请耐心等待。'}
hasFooter={true} hasFooter={true}
buttonTitle="返回首页" buttonTitle="返回首页"
onRefresh={() => navigate('/')}
onButtonClick={() => navigate('/')}
></KFEmpty> ></KFEmpty>
); );
}; };


+ 102
- 0
react-ui/src/stories/BasicInfo.stories.tsx View File

@@ -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,
},
};

+ 32
- 0
react-ui/src/stories/BasicTableInfo.stories.tsx View File

@@ -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,
},
};

+ 56
- 0
react-ui/src/stories/CodeSelect.stories.tsx View File

@@ -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>
);
},
};

+ 90
- 0
react-ui/src/stories/CodeSelectorModal.stories.tsx View File

@@ -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>
);
},
};

+ 37
- 0
react-ui/src/stories/Config.stories.tsx View File

@@ -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>,
},
};

+ 32
- 0
react-ui/src/stories/FullScreenFrame.stories.tsx View File

@@ -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' },
},
};

+ 31
- 0
react-ui/src/stories/IFramePage.stories.tsx View File

@@ -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' },
},
};

+ 31
- 0
react-ui/src/stories/InfoGroup.stories.tsx View File

@@ -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>,
},
};

+ 53
- 0
react-ui/src/stories/KFEmpty.stories.tsx View File

@@ -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: '返回首页',
},
};

+ 41
- 0
react-ui/src/stories/KFIcon.stories.tsx View File

@@ -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>
);
},
};

+ 93
- 0
react-ui/src/stories/KFModal.stories.tsx View File

@@ -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>
);
},
};

+ 52
- 0
react-ui/src/stories/KFRadio.stories.tsx View File

@@ -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} />;
},
};

+ 35
- 0
react-ui/src/stories/KFSpin.stories.tsx View File

@@ -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: {},
};

+ 67
- 0
react-ui/src/stories/MenuIconSelector.stories.tsx View File

@@ -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}
/>
</>
);
},
};

+ 30
- 0
react-ui/src/stories/PageTitle.stories.tsx View File

@@ -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: '数据集列表',
},
};

+ 108
- 0
react-ui/src/stories/ParameterInput.stories.tsx View File

@@ -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>
</>
);
},
};

+ 135
- 0
react-ui/src/stories/ResourceSelect.stories.tsx View File

@@ -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>
);
},
};

+ 23
- 0
react-ui/src/stories/ResourceSelectorModal.mdx View File

@@ -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} />

+ 217
- 0
react-ui/src/stories/ResourceSelectorModal.stories.tsx View File

@@ -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>
);
},
};

+ 32
- 0
react-ui/src/stories/SubAreaTitle.stories.tsx View File

@@ -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,
},
};

+ 199
- 0
react-ui/src/stories/docs/Less.mdx View File

@@ -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的嵌套,使代码逻辑更加清晰,易于理解与维护




+ 53
- 0
react-ui/src/stories/example/Button.stories.ts View File

@@ -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',
},
};

+ 37
- 0
react-ui/src/stories/example/Button.tsx View File

@@ -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>
);
};

+ 364
- 0
react-ui/src/stories/example/Configure.mdx View File

@@ -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>

+ 33
- 0
react-ui/src/stories/example/Header.stories.ts View File

@@ -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 = {};

+ 56
- 0
react-ui/src/stories/example/Header.tsx View File

@@ -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>
);

+ 32
- 0
react-ui/src/stories/example/Page.stories.ts View File

@@ -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();
},
};

+ 73
- 0
react-ui/src/stories/example/Page.tsx View File

@@ -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>
);
};

BIN
react-ui/src/stories/example/assets/accessibility.png View File

Before After
Width: 890  |  Height: 520  |  Size: 42 kB

+ 1
- 0
react-ui/src/stories/example/assets/accessibility.svg View File

@@ -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>

BIN
react-ui/src/stories/example/assets/addon-library.png View File

Before After
Width: 4720  |  Height: 2520  |  Size: 467 kB

BIN
react-ui/src/stories/example/assets/assets.png View File

Before After
Width: 580  |  Height: 260  |  Size: 3.9 kB

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save