初始化3.0.0版本

This commit is contained in:
zengqiao
2022-08-18 17:04:05 +08:00
parent 462303fca0
commit 51832385b1
2446 changed files with 93177 additions and 127211 deletions

5
km-console/.eslintignore Normal file
View File

@@ -0,0 +1,5 @@
test/
scripts/
build/
public/
types/

33
km-console/.eslintrc.js Normal file
View File

@@ -0,0 +1,33 @@
module.exports = {
env: {
browser: true,
node: true,
es2021: true,
},
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended', // 如果同时使用了eslint和prettier发生冲突了会关闭掉与prettier有冲突的规则也就是使用prettier认为对的规则
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 12,
sourceType: 'module',
},
plugins: [
'react',
'react-hooks',
'@typescript-eslint',
'prettier', // eslint 会使用pretter的规则对代码格式化
],
rules: {
'react-hooks/rules-of-hooks': 'error', // Checks rules of Hooks
'@typescript-eslint/no-var-requires': 0,
'prettier/prettier': 2, // 这项配置 对于不符合prettier规范的写法eslint会提示报错
'no-console': 1,
},
};

14
km-console/.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
node_modules
dist/
pub/
build/
.sass-cache/
.DS_Store
.idea/
.vscode/
coverage
versions/
debug.log
package-lock.json
yarn.lock
target

10
km-console/.prettierrc.js Normal file
View File

@@ -0,0 +1,10 @@
module.exports = {
"printWidth": 140, // 指定代码长度,超出换行
"tabWidth": 2, // tab 键的宽度
"semi": true, // 结尾加上分号
"trailingComma": "es5", // 确保对象的最后一个属性后有逗号
singleQuote: true,
"bracketSpacing": true, // 大括号有空格 { name: 'rose' }
"insertPragma": false, // 是否在格式化的文件顶部插入Pragma标记以表明该文件被prettier格式化过了
"htmlWhitespaceSensitivity": "ignore", // html文件的空格敏感度控制空格是否影响布局
}

3
km-console/CHANGELOG.md Normal file
View File

@@ -0,0 +1,3 @@

19
km-console/README.md Normal file
View File

@@ -0,0 +1,19 @@
## 安装项目依赖
* 安装lerna
```
npm i -g lerna
```
## 启动项目
```
npm run start
```
### 环境信息
http://localhost:port
## 构建项目
```
npm run build
```

View File

@@ -0,0 +1 @@
module.exports = {extends: ['@commitlint/config-conventional']}

5
km-console/d1.js Normal file
View File

@@ -0,0 +1,5 @@
const d1Config = require('./d1.json');
d1Config.appConfig.webpackChain = function (config) {
// config.devServer.port(10000);
};
module.exports = d1Config;

28
km-console/d1.json Normal file
View File

@@ -0,0 +1,28 @@
{
"appConfig": {
"appName": "KnowStreaming-FE",
"ident": "",
"port": "8000",
"webpackCustom": "",
"webpackChain": "",
"entry": [
{
"title": "",
"name": "",
"src": ""
}
],
"layout": "layout-clusters-fe",
"packages": [
"config-manager-fe",
"layout-clusters-fe"
]
},
"entrust": true,
"localBuilderVersion": true,
"extensions": [],
"preset": "@didi/d1-preset-opensource",
"builderType": "lerna",
"generatorType": "",
"mockDir": "mock"
}

6
km-console/lerna.json Normal file
View File

@@ -0,0 +1,6 @@
{
"packages": [
"packages/*"
],
"version": "0.0.0"
}

43
km-console/package.json Normal file
View File

@@ -0,0 +1,43 @@
{
"name": "root",
"private": true,
"devDependencies": {
"@commitlint/cli": "^13.1.0",
"@commitlint/config-conventional": "^13.1.0",
"@typescript-eslint/eslint-plugin": "4.13.0",
"@typescript-eslint/parser": "4.13.0",
"babel-eslint": "10.1.0",
"commitizen": "^4.2.4",
"conventional-changelog-cli": "^2.1.1",
"cz-conventional-changelog-zh": "0.0.2",
"eslint": "7.30.0",
"eslint-config-prettier": "8.3.0",
"eslint-plugin-jsx-a11y": "6.4.1",
"eslint-plugin-prettier": "3.4.0",
"eslint-plugin-react": "7.22.0",
"eslint-plugin-react-hooks": "^4.2.0",
"husky": "4.3.7",
"lerna": "^4.0.0",
"lint-staged": "10.5.3",
"prettier": "2.3.2"
},
"scripts": {
"i": "npm install && lerna bootstrap",
"clean": "rm -rf node_modules package-lock.json packages/*/node_modules packages/*/package-lock.json",
"start": "sh ./tool/start.sh",
"build": "sh ./tool/build.sh",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0 && git add CHANGELOG.md",
"cm": "git add . && cz"
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog-zh"
}
},
"lint-staged": {
"*.{js,tsx}": "eslint"
},
"dependencies": {
"react-cron-antd": "^1.1.2"
}
}

View File

@@ -0,0 +1,5 @@
test/
scripts/
build/
public/
types/

View File

@@ -0,0 +1,13 @@
node_modules
dist/
pub/
build/
.sass-cache/
.DS_Store
.idea/
.vscode/
coverage
versions/
debug.log
package-lock.json
yarn.lock

View File

@@ -0,0 +1,11 @@
# `logi-fe`
> TODO: description
## Usage
### 启动
* npm i @didi/d1-cli -g
* d1 start
### 常见问题

View File

@@ -0,0 +1,37 @@
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const ProgressBarPlugin = require('progress-bar-webpack-plugin');
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
const StatsPlugin = require('stats-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const TerserJSPlugin = require('terser-webpack-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const HappyPack = require('happypack');
const os = require('os');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
var cwd = process.cwd();
const path = require('path');
const isProd = process.env.NODE_ENV === 'production';
const babelOptions = {
cacheDirectory: true,
babelrc: false,
presets: [require.resolve('@babel/preset-env'), require.resolve('@babel/preset-typescript'), require.resolve('@babel/preset-react')],
plugins: [
new HtmlWebpackPlugin({
meta: {
manifest: 'manifest.json',
},
template: './src/index.html',
inject: 'body',
}),
],
output: {
library: pkgJson.ident,
libraryTarget: 'amd',
},
entry: {
[pkgJson.ident]: ['./src/index.tsx'],
},
};

View File

@@ -0,0 +1,200 @@
/* eslint-disable */
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const ProgressBarPlugin = require('progress-bar-webpack-plugin');
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
const StatsPlugin = require('stats-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const TerserJSPlugin = require('terser-webpack-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const HappyPack = require('happypack');
const os = require('os');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
const theme = require('./theme');
var cwd = process.cwd();
const path = require('path');
const isProd = process.env.NODE_ENV === 'production';
// const publicPath = isProd ? '//img-ys011.didistatic.com/static/bp_fe_daily/bigdata_cloud_KnowStreaming_FE/gn/' : '/';
const publicPath = '/';
const babelOptions = {
cacheDirectory: true,
babelrc: false,
presets: [require.resolve('@babel/preset-env'), require.resolve('@babel/preset-typescript'), require.resolve('@babel/preset-react')],
plugins: [
[require.resolve('@babel/plugin-proposal-decorators'), { legacy: true }],
[require.resolve('@babel/plugin-proposal-class-properties'), { loose: true }],
[require.resolve('@babel/plugin-proposal-private-methods'), { loose: true }],
require.resolve('@babel/plugin-proposal-export-default-from'),
require.resolve('@babel/plugin-proposal-export-namespace-from'),
require.resolve('@babel/plugin-proposal-object-rest-spread'),
require.resolve('@babel/plugin-transform-runtime'),
require.resolve('@babel/plugin-proposal-optional-chaining'), //
require.resolve('@babel/plugin-proposal-nullish-coalescing-operator'), // 解决 ?? 无法转义问题
require.resolve('@babel/plugin-proposal-numeric-separator'), // 转义 1_000_000
!isProd && require.resolve('react-refresh/babel'),
]
.filter(Boolean)
.concat([
[
'babel-plugin-import',
{
libraryName: 'antd',
style: true,
},
],
'@babel/plugin-transform-object-assign',
]),
};
module.exports = () => {
const manifestName = `manifest.json`;
const jsFileName = isProd ? '[name]-[chunkhash].js' : '[name].js';
const cssFileName = isProd ? '[name]-[chunkhash].css' : '[name].css';
const plugins = [
// !isProd && new HardSourceWebpackPlugin(),
new ProgressBarPlugin(),
new CaseSensitivePathsPlugin(),
new MiniCssExtractPlugin({
filename: cssFileName,
}),
new StatsPlugin(manifestName, {
chunkModules: false,
source: true,
chunks: false,
modules: false,
assets: true,
children: false,
exclude: [/node_modules/],
}),
new HappyPack({
id: 'babel',
loaders: [
'cache-loader',
{
loader: 'babel-loader',
options: babelOptions,
},
],
threadPool: happyThreadPool,
}),
!isProd &&
new ReactRefreshWebpackPlugin({
overlay: false,
}),
].filter(Boolean);
if (isProd) {
plugins.push(new CleanWebpackPlugin());
}
return {
output: {
filename: jsFileName,
chunkFilename: jsFileName,
publicPath,
},
externals: isProd
? [/^react$/, /^react\/lib.*/, /^react-dom$/, /.*react-dom.*/, /^single-spa$/, /^single-spa-react$/, /^moment$/, /^antd$/, /^lodash$/]
: [],
resolve: {
symlinks: false,
extensions: ['.web.jsx', '.web.js', '.ts', '.tsx', '.js', '.jsx', '.json'],
alias: {
// '@pkgs': path.resolve(cwd, 'src/packages'),
'@pkgs': path.resolve(cwd, './node_modules/@didi/d1-packages'),
'@cpts': path.resolve(cwd, 'src/components'),
'@interface': path.resolve(cwd, 'src/interface'),
'@apis': path.resolve(cwd, 'src/api'),
react: path.resolve('./node_modules/react'),
actions: path.resolve(cwd, 'src/actions'),
lib: path.resolve(cwd, 'src/lib'),
constants: path.resolve(cwd, 'src/constants'),
components: path.resolve(cwd, 'src/components'),
container: path.resolve(cwd, 'src/container'),
api: path.resolve(cwd, 'src/api'),
assets: path.resolve(cwd, 'src/assets'),
mobxStore: path.resolve(cwd, 'src/mobxStore'),
},
},
plugins,
module: {
rules: [
{
parser: { system: false },
},
{
test: /\.(js|jsx|ts|tsx)$/,
exclude: /node_modules\/(?!react-intl|@didi\/dcloud-design)/,
use: [
{
loader: 'happypack/loader?id=babel',
},
],
},
{
test: /\.(png|svg|jpeg|jpg|gif|ttf|woff|woff2|eot|pdf)$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: './assets/image/',
esModule: false,
},
},
],
},
{
test: /\.(css|less)$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
},
'css-loader',
{
loader: 'less-loader',
options: {
javascriptEnabled: true,
modifyVars: theme,
},
},
],
},
],
},
optimization: Object.assign(
{
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
chunks: 'all',
name: 'vendor',
priority: 10,
enforce: true,
minChunks: 1,
maxSize: 3500000,
},
},
},
},
isProd
? {
minimizer: [
new TerserJSPlugin({
cache: true,
sourceMap: true,
}),
new OptimizeCSSAssetsPlugin({}),
],
}
: {}
),
devtool: isProd ? 'cheap-module-source-map' : 'source-map',
node: {
fs: 'empty',
net: 'empty',
tls: 'empty',
},
};
};

View File

@@ -0,0 +1,17 @@
const themeConfig = {
primaryColor: '#556ee6',
theme: {
'primary-color': '#556ee6',
'border-radius-base': '2px',
'border-radius-sm': '2px',
'font-size-base': '12px',
'font-family': 'Helvetica Neue, Helvetica, Arial, PingFang SC, Heiti SC, Hiragino Sans GB, Microsoft YaHei, sans-serif',
'font-family-bold':
'HelveticaNeue-Medium, Helvetica Medium, PingFangSC-Medium, STHeitiSC-Medium, Microsoft YaHei Bold, Arial, sans-serif',
},
};
module.exports = {
'prefix-cls': 'layout',
...themeConfig.theme,
};

View File

@@ -0,0 +1,5 @@
const d1Config = require('./d1.json');
d1Config.appConfig.webpackChain = function (config) {
// config.devServer.port(10000);
};
module.exports = d1Config;

View File

@@ -0,0 +1,29 @@
{
"appConfig": {
"appName": "config-manager-fe",
"ident": "config",
"port": "8001",
"webpackCustom": "",
"webpackChain": "",
"entry": [
{
"title": "配置管理",
"name": "/config",
"src": "./src/index.html"
}
],
"layout": "layout-clusters-fe",
"packages": [
"config-manager-fe",
"layout-clusters-fe"
]
},
"entrust": true,
"localBuilderVersion": true,
"extensions": [],
"preset": "@didi/d1-preset-opensource",
"builderType": "@didi/d1-preset-opensource",
"generatorType": "",
"mockDir": "mock",
"webpackCustomPath": "./webpack.config.js"
}

View File

@@ -0,0 +1,14 @@
{
"/api/v1/:id": {
"data": {
"list|5-10": [
{
"name": "@cname()",
"address": "@cname()",
"age|18-50": 100
}
]
},
"code|0-500": 200
}
}

View File

@@ -0,0 +1,102 @@
{
"name": "config-manager-fe",
"port": "8001",
"version": "1.0.0",
"description": "kafka配置管理平台",
"author": "joysunchao <joysunchao@didiglobal.com>",
"systemName": "config",
"ident": "config",
"homepage": "",
"license": "ISC",
"publishConfig": {
"registry": "http://registry.npm.xiaojukeji.com/"
},
"repository": {
"type": "git",
"url": "git@git.xiaojukeji.com:bigdata-cloud/d1.git"
},
"scripts": {
"test": "echo \"Error: run tests from root\" && exit 1",
"start": "cross-env NODE_ENV=development webpack-dev-server",
"build": "rm -rf ../../pub/layout & cross-env NODE_ENV=production webpack --max_old_space_size=8000"
},
"dependencies": {
"babel-preset-react-app": "^10.0.0",
"classnames": "^2.2.6",
"less": "^3.9.0",
"lodash": "^4.17.11",
"mobx": "4.15.7",
"mobx-react": "6.0.0",
"moment": "^2.24.0",
"query-string": "^5.0.1",
"react": "16.12.0",
"react-codemirror2": "^7.2.1",
"react-dom": "16.12.0",
"react-intl": "^3.2.1",
"react-router-cache-route": "^1.11.1",
"single-spa": "^5.8.0",
"single-spa-react": "^2.14.0"
},
"devDependencies": {
"@ant-design/icons": "^4.6.2",
"@babel/core": "^7.5.5",
"@babel/plugin-proposal-class-properties": "^7.4.0",
"@babel/plugin-proposal-decorators": "^7.4.0",
"@babel/plugin-proposal-export-default-from": "^7.2.0",
"@babel/plugin-proposal-export-namespace-from": "^7.5.2",
"@babel/plugin-proposal-object-rest-spread": "^7.4.3",
"@babel/plugin-proposal-optional-chaining": "^7.16.0",
"@babel/plugin-proposal-private-methods": "^7.14.5",
"@babel/plugin-transform-modules-commonjs": "^7.12.1",
"@babel/plugin-transform-object-assign": "^7.12.1",
"@babel/plugin-transform-runtime": "^7.4.3",
"@babel/preset-env": "^7.4.2",
"@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.14.5",
"knowdesign": "^1.3.6",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.1",
"@types/lodash": "^4.14.138",
"@types/react-dom": "^17.0.5",
"@types/react-router-dom": "^5.3.3",
"@types/single-spa-react": "^2.12.0",
"@typescript-eslint/eslint-plugin": "4.13.0",
"@typescript-eslint/parser": "4.13.0",
"axios": "^0.21.1",
"babel-eslint": "10.1.0",
"babel-loader": "^8.2.2",
"babel-plugin-import": "^1.12.0",
"cache-loader": "^4.1.0",
"case-sensitive-paths-webpack-plugin": "^2.2.0",
"clean-webpack-plugin": "^3.0.0",
"cross-env": "^7.0.2",
"css-loader": "^2.1.0",
"eslint": "7.30.0",
"eslint-config-prettier": "8.3.0",
"eslint-plugin-prettier": "3.4.0",
"eslint-plugin-react": "7.22.0",
"eslint-plugin-react-hooks": "4.2.0",
"file-loader": "^6.0.0",
"happypack": "^5.0.1",
"hard-source-webpack-plugin": "^0.13.1",
"html-webpack-plugin": "^4.0.0",
"husky": "4.3.7",
"less-loader": "^4.1.0",
"lint-staged": "10.5.3",
"mini-css-extract-plugin": "^1.3.0",
"mobx": "4.15.7",
"mobx-react": "6.0.0",
"optimize-css-assets-webpack-plugin": "^5.0.1",
"prettier": "2.3.2",
"progress-bar-webpack-plugin": "^1.12.1",
"react-refresh": "^0.10.0",
"react-router-dom": "5.2.1",
"stats-webpack-plugin": "^0.7.0",
"ts-loader": "^8.0.11",
"typescript": "^3.5.3",
"webpack": "^4.40.0",
"webpack-bundle-analyzer": "^4.5.0",
"webpack-cli": "^3.2.3",
"webpack-dev-server": "^3.2.1",
"webpack-merge": "^4.2.1"
}
}

View File

@@ -0,0 +1,7 @@
export const defaultPagination = {
current: 1,
pageSize: 10,
position: 'bottomRight',
showSizeChanger: true,
pageSizeOptions: ['10', '20', '50', '100'],
};

View File

@@ -0,0 +1,41 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
const apiPrefix = '/logi-security/api/v1';
function getApi(path: string) {
return `${apiPrefix}${path}`;
}
const api = {
// 公共
permissionTree: getApi('/permission/tree'),
// 配置
configGroupList: getApi('/config/group/list'),
configList: getApi('/config/page'),
configDetail: getApi('/config/get'),
configSwtichStatus: getApi('/config/switch'), // 切换配置开关状态
addConfig: getApi('/config/add'),
editConfig: getApi('/config/edit'),
delConfig: getApi('/config/del'),
// 用户
userList: getApi('/user/page'),
user: (id: number) => getApi(`/user/${id}`), // 用户详情 / 删除用户
addUser: getApi('/user/add'),
editUser: getApi('/user/edit'),
getUsersByRoleId: (roleId: number) => getApi(`/user/list/role/${roleId}`),
// 角色
editRole: getApi('/role'),
roleList: getApi('/role/page'),
simpleRoleList: getApi('/role/list'),
role: (id: number) => getApi(`/role/${id}`), // 角色详情 / 删除角色
getAssignedUsersByRoleId: (id: number) => getApi(`/role/assign/list/${id}`), // 根据角色 id 获取已分配用户简要信息列表
assignRoles: getApi('/role/assign'),
checkRole: (id: number) => getApi(`/role/delete/check/${id}`), // 判断该角色是否已经分配给用户
// 日志
oplogTypeList: getApi('/oplog/type/list'),
oplogList: getApi('/oplog/page'),
};
export default api;

View File

@@ -0,0 +1,76 @@
import React from 'react';
import { BrowserRouter as Router, Redirect, Switch } from 'react-router-dom';
import _ from 'lodash';
import './constants/axiosConfig';
import dantdZhCN from 'knowdesign/lib/locale/zh_CN';
import dantdEnUS from 'knowdesign/lib/locale/en_US';
import intlZhCN from './locales/zh';
import intlEnUS from './locales/en';
import { AppContainer, RouteGuard, DProLayout } from 'knowdesign';
import { leftMenus, systemKey } from './constants/menu';
import { pageRoutes } from './pages';
import './index.less';
interface ILocaleMap {
[index: string]: any;
}
const localeMap: ILocaleMap = {
'zh-CN': {
dantd: dantdZhCN,
intl: 'zh-CN',
intlMessages: intlZhCN,
},
en: {
dantd: dantdEnUS,
intl: 'en',
intlMessages: intlEnUS,
},
};
export const { Provider, Consumer } = React.createContext('zh');
const defaultLanguage = 'zh';
const AppContent = (props: {
getLicenseInfo?: (cbk: (msg: string) => void) => void | undefined;
licenseEventBus?: Record<string, any> | undefined;
}) => {
const { getLicenseInfo, licenseEventBus } = props;
return (
<div className="config-system">
<DProLayout.Sider prefixCls={'dcd-two-columns'} width={200} theme={'light'} systemKey={systemKey} menuConf={leftMenus} />
<DProLayout.Content>
<RouteGuard
routeList={pageRoutes}
beforeEach={() => {
getLicenseInfo?.((msg) => licenseEventBus?.emit('licenseError', msg));
return Promise.resolve(true);
}}
noMatch={() => <Redirect to="/404" />}
/>
</DProLayout.Content>
</div>
);
};
const App = (props: any) => {
const { getLicenseInfo, licenseEventBus } = props;
const intlMessages = _.get(localeMap[defaultLanguage], 'intlMessages', intlZhCN);
const locale = _.get(localeMap[defaultLanguage], 'intl', 'zh-CN');
const antdLocale = _.get(localeMap[defaultLanguage], 'dantd', dantdZhCN);
return (
<div id="sub-system">
<AppContainer intlProvider={{ locale, messages: intlMessages }} antdProvider={{ locale: antdLocale }}>
<Router basename={systemKey}>
<Switch>
<AppContent getLicenseInfo={getLicenseInfo} licenseEventBus={licenseEventBus} />
</Switch>
</Router>
</AppContainer>
</div>
);
};
export default App;

View File

@@ -0,0 +1,10 @@
.card-bar-container{
padding:16px 24px;
width: 100%;
.card-bar-content{
height: 88px;
width: 100%;
display: flex;
justify-content: space-around;
}
}

View File

@@ -0,0 +1,46 @@
import React from 'react';
import { Select, Form, Input, Switch, Radio, DatePicker, Row, Col, Collapse } from 'knowdesign';
export interface CardBarProps {
cardClumns: any[];
}
const CardBar = (props: CardBarProps) => {
const { cardClumns } = props;
const CardClumnsItem: any = (cardItem: any) => {
const { cardClumnsItemData } = cardItem;
return (
<Row>
<Row>
<col>{cardClumnsItemData.icon}</col>
<col>{cardClumnsItemData.title}</col>
</Row>
<Row>
<col>{cardClumnsItemData.lable}</col>
</Row>
</Row>
);
};
return (
<>
<div className="card-bar-container">
<div className="card-bar-container">
<div>
<div></div>
<div>
<div>title</div>
<div>
<div></div>
<div></div>
</div>
</div>
</div>
{cardClumns &&
cardClumns.length != 0 &&
cardClumns.map((index, item) => {
return <CardClumnsItem key={index} cardClumnsItemData={item}></CardClumnsItem>;
})}
</div>
</div>
</>
);
};
export default CardBar;

View File

@@ -0,0 +1,73 @@
.list-with-hide-container {
flex: 1;
display: flex;
align-items: center;
width: 100%;
.container-item {
flex-shrink: 0;
height: 20px;
padding: 4px 6px;
border-radius: 5px;
margin-right: 4px;
background: rgba(33, 37, 41, 0.08);
font-size: 12px;
color: #495057;
text-align: center;
line-height: 12px;
opacity: 0;
&.show {
opacity: 1;
}
&.hide {
display: none;
}
}
.expand-item {
font-size: 12px;
color: #495057;
line-height: 12px;
opacity: 0;
&.show {
opacity: 1;
}
&.hide {
display: none;
}
}
&.hide {
position: absolute;
z-index: -10000;
opacity: 0;
}
}
.tags-with-hide-popover {
.dcloud-popover-inner {
box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.04), 0 6px 12px 12px rgba(0, 0, 0, 0.04), 0 6px 10px 0 rgba(0, 0, 0, 0.08);
border-radius: 8px;
&-content {
padding: 10px 8px 4px 8px;
}
}
.dcloud-popover-arrow-content {
display: none !important;
}
.container-item-popover {
display: flex;
flex-flow: row wrap;
max-width: 560px;
.container-item {
flex-shrink: 0;
height: 20px;
padding: 4px 6px;
border-radius: 5px;
margin-right: 4px;
background: rgba(33, 37, 41, 0.08);
font-size: 12px;
color: #495057;
text-align: center;
line-height: 12px;
margin-bottom: 6px;
}
}
}

View File

@@ -0,0 +1,120 @@
import { DownOutlined } from '@ant-design/icons';
import { Popover } from 'knowdesign';
import { TooltipPlacement } from 'knowdesign/lib/basic/tooltip';
import React, { useState, useRef, useEffect } from 'react';
import './index.less';
type PropsType = {
list: string[];
expandTagContent: string | ((tagNum: number) => string);
placement?: TooltipPlacement;
};
type TagsState = {
list: string[];
isHideExpandNode: boolean;
endI: number;
calculated: boolean;
};
// 获取 DOM 元素横向 margin 值
const getNodeMargin = (node: Element) => {
const nodeStyle = window.getComputedStyle(node);
return [nodeStyle.marginLeft, nodeStyle.marginRight].reduce((pre, cur) => {
return pre + Number(cur.slice(0, -2));
}, 0);
};
// TODO: 页面宽度变化时重新计算
export default (props: PropsType) => {
const { list = [], expandTagContent, placement = 'bottomRight' } = props;
list.sort();
const ref = useRef(null);
const [curState, setCurState] = useState<TagsState>({
list,
isHideExpandNode: true,
endI: -1,
calculated: false,
});
const [nextTagsList, setNextTagsList] = useState(list);
useEffect(() => {
const f = () => {
// 父盒子信息
const box = ref.current;
const boxWidth = box.offsetWidth;
// 子元素信息
const childrenList = Array.from(ref.current.children) as HTMLElement[] as any;
const len = childrenList.length;
const penultimateNode = childrenList[len - 2];
const penultimateNodeOffsetRight = penultimateNode.offsetLeft + penultimateNode.offsetWidth - box.offsetLeft;
// 如果内容超出展示区域,隐藏一部分
if (penultimateNodeOffsetRight > boxWidth) {
const lastNode = childrenList[len - 1];
const childrenMarin = getNodeMargin(penultimateNode);
let curWidth = lastNode.offsetWidth + getNodeMargin(lastNode);
childrenList.some((children: any, i: number) => {
// 计算下一个元素的宽度
const extraWidth = children.offsetWidth + childrenMarin;
// 如果加入下个元素后宽度未超出,则继续
if (curWidth + extraWidth < boxWidth) {
curWidth += extraWidth;
return false;
} else {
// 否则记录当前索引值 i ,并退出
setCurState({ list: nextTagsList, isHideExpandNode: false, endI: i, calculated: true });
return true;
}
});
} else {
// 隐藏 展示全部 对应的 DOM 元素
setCurState({ list: nextTagsList, isHideExpandNode: true, endI: -1, calculated: true });
}
};
// 在 setTimeout 中执行,保证拿到元素此时已经渲染到页面上,能够拿到正确的数据
setTimeout(() => f());
}, [nextTagsList]);
useEffect(() => {
// 判断数据是否一致
if (list.length !== nextTagsList.length || nextTagsList.some((item, i) => item !== list[i])) {
setNextTagsList(list);
setCurState({ list, isHideExpandNode: true, endI: -1, calculated: false });
}
}, [list]);
return (
<div className="list-with-hide-container" ref={ref}>
{curState.list.map((item, i) => {
return (
<div
key={i}
className={`container-item ${curState.calculated ? (curState.isHideExpandNode ? 'show' : i >= curState.endI ? 'hide' : 'show') : ''
}`}
>
{item}
</div>
);
})}
<Popover
placement={placement}
overlayClassName="tags-with-hide-popover"
content={
<div className="container-item-popover">
{curState.list.map((id) => (
<div key={id} className="container-item">
{id}
</div>
))}
</div>
}
>
<div className={`expand-item ${curState.calculated ? (curState.isHideExpandNode ? 'hide' : 'show') : ''}`}>
{typeof expandTagContent === 'string' ? expandTagContent : expandTagContent(curState.list.length)}
<DownOutlined />
</div>
</Popover>
</div>
);
};

View File

@@ -0,0 +1,27 @@
.typical-list-card {
width: 100%;
height: 100%;
padding: 10px 10px 0 10px;
&-container {
height: 100%;
padding: 16px 24px;
background: #ffffff;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.01), 0 3px 6px 3px rgba(0, 0, 0, 0.01), 0 2px 6px 0 rgba(0, 0, 0, 0.03);
border-radius: 12px 12px 0 0;
.title {
margin-bottom: 16px;
border: none;
font-family: @font-family-bold;
font-size: 18px;
color: #212529;
letter-spacing: 0;
text-align: justify;
line-height: 20px;
}
.operate-bar {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
}
}
}

View File

@@ -0,0 +1,13 @@
import React from 'react';
import './index.less';
export default ({ title, children }) => {
return (
<div className="typical-list-card">
<div className="typical-list-card-container">
<div className="title">{title}</div>
{children}
</div>
</div>
);
};

View File

@@ -0,0 +1,105 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
// @ts-nocheck
import { notification, Utils } from 'knowdesign';
const goLogin = () => {
// notification.error({ message: '当前未登录,将自动跳转到登录页' });
window.history.replaceState({}, '', `/login?redirect=${window.location.href.slice(window.location.origin.length)}`);
};
const serviceInstance = Utils.service;
// 清除 axios 实例默认的响应拦截
serviceInstance.interceptors.response.handlers = [];
// 请求拦截
serviceInstance.interceptors.request.use(
(config: any) => {
const user = Utils.getCookie('X-SSO-USER');
const id = Utils.getCookie('X-SSO-USER-ID');
if ((!user || !id) && !window.location.pathname.toLowerCase().startsWith('/login')) {
goLogin();
} else {
config.headers['X-SSO-USER'] = user; // 请求携带token
config.headers['X-SSO-USER-ID'] = id;
return config;
}
},
(err: any) => {
return err;
}
);
// 响应拦截
serviceInstance.interceptors.response.use(
(config: any) => {
return config.data;
},
(err: any) => {
const config = err.config;
if (!config || !config.retryTimes) return dealResponse(err, config.customNotification);
const { __retryCount = 0, retryDelay = 300, retryTimes } = config;
config.__retryCount = __retryCount;
if (__retryCount >= retryTimes) {
return dealResponse(err);
}
config.__retryCount++;
const delay = new Promise<void>((resolve) => {
setTimeout(() => {
resolve();
}, retryDelay);
});
// 重新发起请求
return delay.then(function () {
return serviceInstance(config);
});
}
);
const dealResponse = (error: any) => {
if (error?.response) {
switch (error.response.status) {
case 401:
goLogin();
break;
case 403:
location.href = '/403';
break;
case 405:
notification.error({
message: '错误',
duration: 3,
description: `${error.response.data.message || '请求方式错误'}`,
});
break;
case 500:
notification.error({
message: '错误',
duration: 3,
description: '服务错误,请重试!',
});
break;
case 502:
notification.error({
message: '错误',
duration: 3,
description: '网络错误,请重试!',
});
break;
default:
notification.error({
message: '连接出错',
duration: 3,
description: `${error.response.status}`,
});
}
} else {
notification.error({
description: '请重试或检查服务',
message: '连接超时! ',
duration: 3,
});
}
return Promise.reject(error);
};

View File

@@ -0,0 +1,34 @@
export const defaultPagination = {
current: 1,
pageSize: 10,
total: 0,
};
export const defaultPaginationConfig = {
showQuickJumper: true,
showSizeChanger: true,
pageSizeOptions: ['10', '20', '50', '100', '200', '500'],
showTotal: (total: number) => `${total}`,
};
export const cellStyle = {
overflow: 'hidden',
whiteSpace: 'nowrap' as any,
textOverflow: 'ellipsis',
cursor: 'pointer',
maxWidth: 150,
};
export const systemCipherKey = 'Szjx2022@666666$';
export const oneDayMillims = 24 * 60 * 60 * 1000;
export const classNamePrefix = 'bdp';
export const timeFormat = 'YYYY-MM-DD HH:mm:ss';
export const dateFormat = 'YYYY-MM-DD';
export const SMALL_DRAWER_WIDTH = 480;
export const MIDDLE_DRAWER_WIDTH = 728;
export const LARGE_DRAWER_WIDTH = 1080;

View File

@@ -0,0 +1,37 @@
const pkgJson = require('../../package');
export const systemKey = pkgJson.ident;
export const leftMenus = {
name: `${systemKey}`,
path: '',
icon: '#icon-kafka',
children: [
{
name: 'setting',
path: 'setting',
icon: 'icon-Cluster',
},
{
name: 'user',
path: 'user',
icon: 'icon-Brokers',
},
{
name: 'operationLog',
path: 'operation-log',
icon: 'icon-Topics',
},
],
};
// key值需要与locale zh 中key值一致
export const permissionPoints = {
[`menu.${systemKey}.home`]: true,
};
export const ROUTER_CACHE_KEYS = {
home: `menu.${systemKey}.home`,
dev: `menu.${systemKey}.dev`,
devDetail: `menu.${systemKey}.dev.detail`,
devTable: `menu.${systemKey}.dev.table`,
};

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<div id="ks-layout-container"></div>
</body>
</html>

View File

@@ -0,0 +1,13 @@
#ks-layout-container {
height: calc(100% - 48px);
#sub-system {
height: 100%;
.config-system {
display: flex;
height: 100%;
.dcloud-layout-content {
padding: 0;
}
}
}
}

View File

@@ -0,0 +1,28 @@
import React from 'react';
import ReactDOM from 'react-dom';
import singleSpaReact from 'single-spa-react';
import App from './app';
function domElementGetter() {
let el = document.getElementById('ks-layout-container');
if (!el) {
el = document.createElement('div');
el.id = 'ks-layout-container';
document.body.appendChild(el);
}
return el;
}
const reactLifecycles = singleSpaReact({
React,
ReactDOM,
rootComponent: App,
domElementGetter,
});
export const bootstrap = [reactLifecycles.bootstrap];
export const mount = [reactLifecycles.mount];
export const unmount = [reactLifecycles.unmount];

View File

@@ -0,0 +1,10 @@
import { systemKey } from '../constants/menu';
export default {
[`menu.${systemKey}.setting`]: 'setting',
[`menu.${systemKey}.user`]: 'userManagement',
[`menu.${systemKey}.operationLog`]: 'operationLog',
'sider.footer.hide': 'hide',
'sider.footer.expand': 'expand',
};

View File

@@ -0,0 +1,12 @@
import { systemKey } from '../constants/menu';
/**
* 用于左侧菜单与顶部路由导航中文展示key值与各页面路径对应比如dashboard页路由/cluster/dashbordkey值menu.cluster.dashborad
*/
export default {
[`menu.${systemKey}.setting`]: '配置管理',
[`menu.${systemKey}.user`]: '用户管理',
[`menu.${systemKey}.operationLog`]: '审计日志',
'sider.footer.hide': '收起',
'sider.footer.expand': '展开',
};

View File

@@ -0,0 +1,67 @@
import React, { useLayoutEffect } from 'react';
import { Utils, AppContainer } from 'knowdesign';
// 权限对应表
export enum ConfigPermissionMap {
SYS_MANAGE = '系统管理',
// 配置管理
CONFIG_ADD = '配置管理-新增配置',
CONFIG_EDIT = '配置管理-编辑配置',
CONFIG_DEL = '配置管理-删除配置',
// 用户管理
USER_DEL = '用户管理-删除人员',
USER_CHANGE_PASS = '用户管理-修改人员密码',
USER_EDIT = '用户管理-编辑人员',
USER_ADD = '用户管理-新增人员',
// 角色管理
ROLE_DEL = '用户管理-删除角色',
ROLE_ASSIGN = '用户管理-分配用户角色',
ROLE_EDIT = '用户管理-编辑角色',
ROLE_ADD = '用户管理-新增角色',
}
export interface PermissionNode {
id: number;
permissionName: ConfigPermissionMap | null;
parentId: number | null;
has: boolean;
leaf: boolean;
childList: PermissionNode[];
}
const CommonConfig = (): JSX.Element => {
const [global, setGlobal] = AppContainer.useGlobalValue();
// 获取权限树
const getPermissionTree = () => {
// 如果未登录,直接退出
const userInfo = localStorage.getItem('userInfo');
if (!userInfo) return false;
const userId = JSON.parse(userInfo).id;
const getUserInfo = Utils.request(`/logi-security/api/v1/user/${userId}`);
const getPermissionTree = Utils.request('/logi-security/api/v1/permission/tree');
Promise.all([getPermissionTree, getUserInfo]).then(([permissionTree, userDetail]: [PermissionNode, any]) => {
const allPermissions = permissionTree.childList;
// 获取用户在系统管理拥有的权限
const userPermissionTree = userDetail.permissionTreeVO.childList;
const configPermissions = userPermissionTree.find((sys) => sys.permissionName === ConfigPermissionMap.SYS_MANAGE);
const userPermissions: ConfigPermissionMap[] = [];
configPermissions && configPermissions.childList.forEach((node) => node.has && userPermissions.push(node.permissionName));
const hasPermission = (permissionName: ConfigPermissionMap) => permissionName && userPermissions.includes(permissionName);
setGlobal((curState: any) => ({ ...curState, permissions: allPermissions, userPermissions, hasPermission, userInfo }));
});
};
useLayoutEffect(() => {
getPermissionTree();
}, []);
return <></>;
};
export default CommonConfig;

View File

@@ -0,0 +1,17 @@
export enum ConfigOperate {
Add,
Edit,
}
export type ConfigProps = {
id?: number;
valueGroup?: string;
valueName?: string;
value?: string;
status?: 0 | 1;
operator?: string;
memo?: string;
};
export type AddConfigProps = Omit<ConfigProps, 'id' | 'operator'>;
export type EditConfigProps = Omit<ConfigProps, 'operator'>;

View File

@@ -0,0 +1,72 @@
.config-manage-page {
.d-table {
.text-overflow-two-row {
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.hover-light:hover {
color: #556ee6;
cursor: pointer;
}
}
}
// 新增/编辑配置抽屉 代码编辑器样式
.config-manage-edit-drawer {
.codemirror-form-item {
> .cm-s-default {
border: 1px solid transparent;
border-radius: 8px;
overflow: hidden;
transition: all 0.3s;
&:hover,
&.CodeMirror-focused {
border-color: #74788d;
}
.CodeMirror-scroll {
background: rgba(33, 37, 41, 0.06);
transition: all 0.3s;
.CodeMirror-gutters {
background: transparent;
border-right: none;
}
}
}
}
.dcloud-form-item-has-error {
.codemirror-form-item {
> .cm-s-default {
border-color: #ff7066;
.CodeMirror-scroll {
background: #fffafa;
}
}
}
}
}
// 列表配置值弹窗
.config-manage-value-modal {
.dcloud-modal-header {
border-bottom: 0;
}
.dcloud-modal-body {
padding: 0 8px 8px 8px;
.react-codemirror2 {
> .cm-s-default {
border-radius: 8px;
overflow: hidden;
.CodeMirror-scroll {
background: #556de60a;
.CodeMirror-gutters {
background: transparent;
border-right: none;
}
}
}
}
}
}

View File

@@ -0,0 +1,490 @@
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
import {
Button,
Form,
Input,
Select,
Switch,
Modal,
message,
ProTable,
Drawer,
Space,
Divider,
Tooltip,
AppContainer,
Utils,
} from 'knowdesign';
import { PlusOutlined } from '@ant-design/icons';
import moment from 'moment';
// 引入代码编辑器
import { Controlled as CodeMirror } from 'react-codemirror2';
import 'codemirror/lib/codemirror.css';
//代码高亮
import 'codemirror/addon/edit/matchbrackets';
import 'codemirror/addon/selection/active-line';
import 'codemirror/addon/edit/closebrackets';
require('codemirror/mode/xml/xml');
require('codemirror/mode/javascript/javascript');
import api from 'api';
import { defaultPagination } from 'constants/common';
import TypicalListCard from '../../components/TypicalListCard';
import { ConfigPermissionMap } from '../CommonConfig';
import { ConfigOperate, ConfigProps } from './config';
import './index.less';
const { request } = Utils;
const { confirm } = Modal;
const { TextArea } = Input;
// 新增/编辑配置抽屉
const EditConfigDrawer = forwardRef((_, ref) => {
const [config, setConfig] = useState<ConfigProps>({});
const [type, setType] = useState<ConfigOperate>(ConfigOperate.Add);
const [form] = Form.useForm();
const [visible, setVisible] = useState<boolean>(false);
const [groupOptions, setGroupOpions] = useState<{ label: string; value: string }[]>([]);
const [confirmLoading, setConfirmLoading] = useState<boolean>(false);
const [codeMirrorInput, setCodeMirrorInput] = useState<string>('');
const callback = useRef(() => {
return;
});
// 提交表单
const onSubmit = () => {
form.validateFields().then((formData) => {
setConfirmLoading(true);
formData.status = formData.status ? 1 : 2;
const isAdd = type === ConfigOperate.Add;
const submitApi = isAdd ? api.addConfig : api.editConfig;
request(submitApi, {
method: isAdd ? 'PUT' : 'POST',
data: Object.assign(formData, isAdd ? {} : { id: config.id }),
}).then(
(res) => {
// 执行回调,刷新列表数据
callback.current();
onClose();
message.success(`成功${isAdd ? '新增' : '更新'}配置`);
},
() => setConfirmLoading(false)
);
});
};
// 展开抽屉
const onOpen = (status: boolean, type: ConfigOperate, cbk: () => void, groupOptions, config: ConfigProps = {}) => {
if (config.value) {
try {
// 如果内容可以格式化为 JSON进行处理
config.value = JSON.stringify(JSON.parse(config.value), null, 2);
} catch (_) {
return;
}
}
form.setFieldsValue({ ...config, status: config.status === 1 });
setConfig(config);
setGroupOpions(groupOptions);
setCodeMirrorInput(config.value);
setType(type);
setVisible(status);
callback.current = cbk;
};
// 关闭抽屉
const onClose = () => {
setVisible(false);
setConfirmLoading(false);
setConfig({});
form.resetFields();
setCodeMirrorInput('');
};
useImperativeHandle(ref, () => ({
onOpen,
}));
return (
<Drawer
className="config-manage-edit-drawer"
title={`${type === ConfigOperate.Add ? '新增' : '编辑'}配置`}
width={480}
visible={visible}
maskClosable={false}
onClose={onClose}
extra={
<Space>
<Button size="small" onClick={onClose}>
</Button>
<Button type="primary" size="small" loading={confirmLoading} onClick={onSubmit}>
</Button>
<Divider type="vertical" />
</Space>
}
>
<Form form={form} layout="vertical">
<Form.Item label="模块" name="valueGroup" rules={[{ required: true, message: '模块不能为空' }]}>
<Select options={groupOptions} placeholder="请选择模块" />
</Form.Item>
<Form.Item label="配置键" name="valueName" rules={[{ required: true, message: '配置键不能为空' }]}>
<Input placeholder="请输入配置键" maxLength={100} />
</Form.Item>
<Form.Item label="配置值" name="value" rules={[{ required: true, message: '配置值不能为空' }]}>
<div>
<CodeMirror
className="codemirror-form-item"
value={codeMirrorInput}
options={{
mode: 'application/json',
lineNumbers: true,
lineWrapper: true,
autoCloseBrackets: true,
smartIndent: true,
tabSize: 2,
}}
onBeforeChange={(editor, data, value) => {
form.setFieldsValue({ value });
form.validateFields(['value']);
setCodeMirrorInput(value);
}}
/>
</div>
</Form.Item>
<Form.Item label="描述" name="memo" rules={[{ required: true, message: '必须输入描述' }]}>
<TextArea placeholder="请输入描述" maxLength={200} />
</Form.Item>
<Form.Item label="启用状态" name="status" valuePropName="checked">
<Switch />
</Form.Item>
</Form>
</Drawer>
);
});
// 配置值详情弹窗
// eslint-disable-next-line react/display-name
const ConfigValueDetail = forwardRef((_, ref) => {
const [visible, setVisible] = useState<boolean>(false);
const [content, setContent] = useState<string>('');
const onClose = () => {
setVisible(false);
setContent('');
};
useImperativeHandle(ref, () => ({
setVisible: (status: boolean, content: string) => {
let transformedContent = '';
try {
// 如果内容可以格式化为 JSON进行处理
transformedContent = JSON.stringify(JSON.parse(content), null, 2);
} catch (_) {
transformedContent = content;
}
setContent(transformedContent);
setVisible(status);
},
}));
return (
<Modal
className="config-manage-value-modal"
title="配置值"
visible={visible}
centered={true}
footer={null}
onCancel={onClose}
maskClosable={false}
destroyOnClose
>
<CodeMirror
value={content}
options={{
mode: 'application/json',
lineNumbers: true,
lineWrapper: true,
autoCloseBrackets: true,
smartIndent: true,
tabSize: 2,
}}
onBeforeChange={() => {
return;
}}
/>
</Modal>
);
});
export default () => {
const [global] = AppContainer.useGlobalValue();
const [loading, setLoading] = useState<boolean>(true);
const [configGroupList, setConfigGroupList] = useState<{ label: string; value: string }[]>([]);
const [data, setData] = useState<ConfigProps[]>([]);
const [pagination, setPagination] = useState<any>(defaultPagination);
const [form] = Form.useForm();
const editDrawerRef = useRef(null);
const configValueModalRef = useRef(null);
const getConfigList = (query = {}) => {
const formData = form.getFieldsValue();
const queryParams = {
page: pagination.current,
size: pagination.pageSize,
...formData,
...query,
};
setLoading(true);
request(api.configList, {
method: 'POST',
data: queryParams,
}).then(
(res: any) => {
const { pageNo, pageSize, pages, total } = res.pagination;
if (pageNo > pages && pages !== 0) {
getConfigList({ page: pages });
return false;
}
setPagination({
...pagination,
current: pageNo,
pageSize,
total,
});
setData(res.bizData);
setLoading(false);
},
() => setLoading(false)
);
};
const columns = useCallback(() => {
const baseColumns = [
{
title: '模块',
dataIndex: 'valueGroup',
lineClampOne: true,
},
{
title: '配置键',
dataIndex: 'valueName',
width: 150,
lineClampOne: true,
render(content) {
return (
<Tooltip title={content}>
<div className="text-overflow-two-row">{content}</div>
</Tooltip>
);
},
},
// TODO: 两行省略
{
title: '配置值',
dataIndex: 'value',
width: 180,
lineClampOne: true,
render(content) {
return (
<div className="text-overflow-two-row hover-light" onClick={() => configValueModalRef.current.setVisible(true, content)}>
{content}
</div>
);
},
},
{
title: '描述',
dataIndex: 'memo',
width: 180,
lineClampOne: true,
},
{
title: '启用状态',
dataIndex: 'status',
render(status: number, record) {
return (
<div style={{ width: 60 }}>
<Switch
checked={status === 1}
size="small"
onChange={() => {
request(api.configSwtichStatus, {
method: 'POST',
data: {
id: record.id,
status: status === 1 ? 2 : 1,
},
}).then((_) => {
getConfigList();
});
}}
/>
</div>
);
},
},
{
title: '最后更新时间',
dataIndex: 'updateTime',
render: (date) => moment(date).format('YYYY-MM-DD HH:mm:ss'),
},
{
title: '最后更新人',
dataIndex: 'operator',
width: 100,
lineClampOne: true,
},
];
if (
global.hasPermission &&
(global.hasPermission(ConfigPermissionMap.CONFIG_EDIT) || global.hasPermission(ConfigPermissionMap.CONFIG_DEL))
) {
baseColumns.push({
title: '操作',
dataIndex: '',
width: 130,
lineClampOne: false,
render(record: ConfigProps) {
return (
<>
{global.hasPermission && global.hasPermission(ConfigPermissionMap.CONFIG_EDIT) ? (
<Button
type="link"
size="small"
onClick={() => editDrawerRef.current.onOpen(true, ConfigOperate.Edit, getConfigList, configGroupList, record)}
style={{ paddingLeft: 0 }}
>
</Button>
) : (
<></>
)}
{global.hasPermission && global.hasPermission(ConfigPermissionMap.CONFIG_DEL) ? (
<Button type="link" size="small" onClick={() => onDelete(record)}>
</Button>
) : (
<></>
)}
</>
);
},
});
}
return baseColumns;
}, [global, getConfigList, configGroupList]);
const onDelete = (record: ConfigProps) => {
confirm({
title: '确定删除配置吗?',
content: `配置⌈${record.valueName}${record.status === 1 ? '为启用状态,无法删除' : ''}`,
centered: true,
okText: '删除',
okType: 'primary',
okButtonProps: {
size: 'small',
disabled: record.status === 1,
danger: true,
},
cancelButtonProps: {
size: 'small',
},
maskClosable: true,
onOk() {
return request(api.editConfig, {
method: 'POST',
data: record.id,
}).then((_) => {
message.success('删除成功');
getConfigList();
});
},
});
};
const onTableChange = (curPagination) => {
getConfigList({ page: curPagination.current, size: curPagination.pageSize });
};
useEffect(() => {
// 获取模块列表
request(api.configGroupList).then((res: string[]) => {
const options = res.map((item) => ({
label: item,
value: item,
}));
setConfigGroupList(options);
});
// 获取配置列表
getConfigList();
}, []);
return (
<>
<TypicalListCard title="配置管理">
<div className="config-manage-page">
<div className="operate-bar">
<Form form={form} layout="inline" onFinish={() => getConfigList({ page: 1 })}>
<Form.Item name="valueGroup">
<Select placeholder="请选择模块" options={configGroupList} allowClear />
</Form.Item>
<Form.Item name="valueName">
<Input placeholder="请输入配置键" />
</Form.Item>
<Form.Item name="memo">
<Input placeholder="请输入描述" />
</Form.Item>
<Form.Item>
<Button type="primary" ghost htmlType="submit">
</Button>
</Form.Item>
</Form>
{global.hasPermission && global.hasPermission(ConfigPermissionMap.CONFIG_ADD) ? (
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => editDrawerRef.current.onOpen(true, ConfigOperate.Add, getConfigList, configGroupList)}
>
</Button>
) : (
<></>
)}
</div>
<ProTable
tableProps={{
showHeader: false,
loading,
rowKey: 'id',
dataSource: data,
paginationProps: pagination,
columns,
lineFillColor: true,
attrs: {
onChange: onTableChange,
scroll: {
scrollToFirstRowOnChange: true,
x: true,
y: 'calc(100vh - 270px)',
},
},
}}
/>
</div>
</TypicalListCard>
{/* 新增/编辑配置抽屉 */}
<EditConfigDrawer ref={editDrawerRef} />
<ConfigValueDetail ref={configValueModalRef} />
</>
);
};

View File

@@ -0,0 +1,7 @@
import React from 'react';
import { Redirect } from 'react-router-dom';
const HomePage = () => {
return <Redirect to="/setting" />;
};
export default HomePage;

View File

@@ -0,0 +1,158 @@
import React, { useEffect, useState } from 'react';
import { Button, Form, Input, Select, ProTable, DatePicker, Utils } from 'knowdesign';
import api from 'api';
import { defaultPagination } from 'constants/common';
import TypicalListCard from '../../components/TypicalListCard';
import './index.less';
import moment from 'moment';
const { request } = Utils;
const { RangePicker } = DatePicker;
export default () => {
const [loading, setLoading] = useState(true);
const [configGroupList, setConfigGroupList] = useState<{ label: string; value: string }[]>([]);
const [data, setData] = useState([]);
const [pagination, setPagination] = useState<any>(defaultPagination);
const [form] = Form.useForm();
const columns = [
{
title: '模块',
dataIndex: 'targetType',
lineClampOne: true,
},
{
title: '操作对象',
dataIndex: 'target',
width: 350,
lineClampOne: true,
},
{
title: '行为',
dataIndex: 'operateType',
width: 80,
lineClampOne: true,
},
{
title: '操作内容',
dataIndex: 'detail',
width: 350,
lineClampOne: true,
},
{
title: '操作时间',
dataIndex: 'updateTime',
width: 200,
render: (date) => moment(date).format('YYYY-MM-DD HH:mm:ss'),
},
{
title: '操作人',
dataIndex: 'operator',
},
];
const getData = (query = {}) => {
const formData = form.getFieldsValue();
if (formData.time) {
formData.startTime = moment(formData.time[0]).valueOf();
formData.endTime = moment(formData.time[1]).valueOf();
}
delete formData.time;
const data = {
page: pagination.current,
size: pagination.pageSize,
...formData,
...query,
};
setLoading(true);
request(api.oplogList, {
method: 'POST',
data,
}).then(
(res: any) => {
const { pageNo, pageSize, pages, total } = res.pagination;
if (pageNo > pages && pages !== 0) {
getData({ page: pages });
return false;
}
setPagination({
...pagination,
current: pageNo,
pageSize,
total,
});
setData(res.bizData);
setLoading(false);
},
() => setLoading(false)
);
};
const onTableChange = (curPagination) => {
getData({ page: curPagination.current, size: curPagination.pageSize });
};
useEffect(() => {
// 获取模块列表
request(api.oplogTypeList).then((res: string[]) => {
const options = res.map((item) => ({
label: item,
value: item,
}));
setConfigGroupList(options);
});
// 获取数据
getData();
}, []);
return (
<>
<TypicalListCard title="操作记录">
<div className="operate-bar">
<Form form={form} layout="inline" onFinish={() => getData({ page: 1 })}>
<Form.Item name="targetType">
<Select placeholder="请选择模块" options={configGroupList} style={{ width: 160 }} allowClear />
</Form.Item>
<Form.Item name="target">
<Input placeholder="请输入操作对象" />
</Form.Item>
<Form.Item name="detail">
<Input placeholder="请输入操作内容" />
</Form.Item>
<Form.Item name="time">
<RangePicker showTime />
</Form.Item>
<Form.Item>
<Button type="primary" ghost htmlType="submit">
</Button>
</Form.Item>
</Form>
</div>
<ProTable
tableProps={{
showHeader: false,
loading,
rowKey: 'id',
dataSource: data,
paginationProps: pagination,
columns,
lineFillColor: true,
attrs: {
onChange: onTableChange,
scroll: {
scrollToFirstRowOnChange: true,
x: true,
y: 'calc(100vh - 270px)',
},
},
}}
/>
</TypicalListCard>
</>
);
};

View File

@@ -0,0 +1,92 @@
import React, { useLayoutEffect, useState } from 'react';
import { Checkbox, Col, FormInstance, Row } from 'knowdesign';
type Option = { label: string; value: string | number };
type CheckValue = string | number;
interface CheckboxGroupType {
options: Option[];
initSelectedOptions?: CheckValue[];
formInstance: FormInstance;
fieldName: string;
groupIdx: number;
disabled?: boolean;
allCheckText?: string;
}
const CheckboxGroupContainer = (props: CheckboxGroupType) => {
const { options, initSelectedOptions = [], formInstance, fieldName, groupIdx, disabled = false, allCheckText = '全选' } = props;
const [checkedList, setCheckedList] = useState<CheckValue[]>([]);
const [indeterminate, setIndeterminate] = useState<boolean>(false);
const [checkAll, setCheckAll] = useState<boolean>(false);
// 更新表单项内容
const updateFieldValue = (curList: CheckValue[]) => {
const curFieldValue = formInstance.getFieldValue(fieldName);
const newFieldValue = [].concat(curFieldValue);
newFieldValue[groupIdx] = curList;
formInstance.setFieldsValue({
[fieldName]: newFieldValue,
});
};
// 选择
const onCheckedChange = (list: CheckValue[]) => {
list.sort();
updateFieldValue(list);
setCheckedList(list);
setIndeterminate(!!list.length && list.length < options.length);
setCheckAll(list.length === options.length);
};
// 全选
const onCheckAllChange = (e) => {
const newOptions = e.target.checked ? options : [];
updateFieldValue(newOptions.map((option) => option.value));
setCheckedList(newOptions.map((option) => option.value));
setIndeterminate(false);
setCheckAll(e.target.checked);
};
useLayoutEffect(() => {
const newInitOptions = [...initSelectedOptions].sort();
if (checkedList.length !== newInitOptions.length) {
setCheckedList(newInitOptions);
updateFieldValue(newInitOptions);
} else {
if (checkedList.some((option, i) => option !== newInitOptions[i])) {
setCheckedList(newInitOptions);
updateFieldValue(newInitOptions);
}
}
}, [initSelectedOptions]);
useLayoutEffect(() => {
setIndeterminate(!!initSelectedOptions.length && initSelectedOptions.length < options.length);
setCheckAll(!!initSelectedOptions.length && options.length === initSelectedOptions.length);
}, [options, initSelectedOptions]);
return (
<div style={{ marginBottom: 30 }}>
<div style={{ marginBottom: 12 }}>
<Checkbox disabled={disabled} indeterminate={indeterminate} onChange={onCheckAllChange} checked={checkAll}>
{allCheckText}
</Checkbox>
</div>
<Checkbox.Group disabled={disabled} style={{ width: '100%' }} value={checkedList} onChange={onCheckedChange}>
<Row gutter={[34, 10]}>
{options.map((option) => {
return (
<Col span={8} key={option.value}>
<Checkbox value={option.value} className="checkbox-content-ellipsis">
{option.label}
</Checkbox>
</Col>
);
})}
</Row>
</Checkbox.Group>
</div>
);
};
export default CheckboxGroupContainer;

View File

@@ -0,0 +1,635 @@
import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
import {
Form,
ProTable,
Button,
Input,
Modal,
Space,
Divider,
Drawer,
Transfer,
Row,
Col,
message,
Tooltip,
Spin,
AppContainer,
Utils,
Popover,
IconFont,
} from 'knowdesign';
import moment from 'moment';
import { CloseOutlined, LoadingOutlined, PlusOutlined } from '@ant-design/icons';
import { defaultPagination } from 'constants/common';
import { RoleProps, PermissionNode, AssignUser, RoleOperate, FormItemPermission } from './config';
import api from 'api';
import CheckboxGroupContainer from './CheckboxGroupContainer';
import { ConfigPermissionMap } from '../CommonConfig';
const { request } = Utils;
const { confirm } = Modal;
const { TextArea } = Input;
// 新增/编辑角色、查看角色详情抽屉
// eslint-disable-next-line react/display-name
const RoleDetailAndUpdate = forwardRef((props, ref): JSX.Element => {
const [global] = AppContainer.useGlobalValue();
const [form] = Form.useForm();
const [visible, setVisible] = useState<boolean>(false);
const [type, setType] = useState<RoleOperate>(RoleOperate.Add);
const [roleDetail, setRoleDetail] = useState<RoleProps>();
const [permissions, setPermissions] = useState<FormItemPermission[]>([]);
const [permissionFormLoading, setPermissionFormLoading] = useState<boolean>(false);
const [initSelectedPermissions, setInitSelectedPermission] = useState<{ [index: string]: [] }>({});
const [confirmLoading, setConfirmLoading] = useState(false);
const callback = useRef(() => {
return;
});
useEffect(() => {
const globalPermissions = global.permissions;
if (globalPermissions && globalPermissions.length) {
const sysPermissions = globalPermissions.map((sys: PermissionNode) => ({
id: sys.id,
name: sys.permissionName,
options: sys.childList.map((node) => ({ label: node.permissionName, value: node.id })),
}));
setPermissions(sysPermissions);
}
}, [global]);
useEffect(() => {
if (roleDetail) {
setPermissionFormLoading(true);
request(api.role(roleDetail.id)).then((res: any) => {
const initSelected = {};
const permissions = res.permissionTreeVO.childList;
permissions.forEach((sys) => {
initSelected[sys.id] = [];
sys.childList.forEach((node) => node.has && initSelected[sys.id].push(node.id));
});
setInitSelectedPermission(initSelected);
setPermissionFormLoading(false);
});
}
}, [roleDetail]);
const onSubmit = () => {
form.validateFields().then((formData) => {
formData.permissionIdList.forEach((arr, i) => {
// 如果分配的系统下的子权限,自动赋予该系统的权限
if (arr !== null && arr.length) {
arr.push(permissions[i].id);
}
});
formData.permissionIdList = formData.permissionIdList.flat();
setConfirmLoading(true);
request(api.editRole, {
method: type === RoleOperate.Add ? 'POST' : 'PUT',
data: Object.assign(formData, type === RoleOperate.Edit ? { id: roleDetail.id } : {}),
}).then(
(res) => {
callback.current();
onClose();
setInitSelectedPermission({});
message.success(`${type === RoleOperate.Add ? '新增' : '编辑'}角色成功`);
},
() => setConfirmLoading(false)
);
});
};
// 打开抽屉
const onOpen = (status: boolean, type: RoleOperate, cbk: () => { return }, record: RoleProps) => {
setInitSelectedPermission({});
form.setFieldsValue(record);
callback.current = cbk;
setRoleDetail(record);
setType(type);
setVisible(status);
};
// 关闭抽屉
const onClose = () => {
setVisible(false);
setRoleDetail(undefined);
form.resetFields();
setConfirmLoading(false);
};
useImperativeHandle(ref, () => ({
onOpen,
}));
return (
<Drawer
title={`${type === RoleOperate.Add ? '新增角色' : type === RoleOperate.Edit ? '编辑角色' : '查看角色详情'}`}
className="role-tab-detail"
width={600}
visible={visible}
maskClosable={false}
onClose={onClose}
extra={
<Space>
{type !== RoleOperate.View && (
<>
<Button size="small" onClick={onClose}>
</Button>
<Button type="primary" size="small" loading={confirmLoading} onClick={onSubmit}>
</Button>
<Divider type="vertical" />
</>
)}
</Space>
}
>
{type === RoleOperate.View ? (
// 查看角色详情
<>
<Row gutter={[12, 12]} className="desc-row">
<Col span={3} className="label-col">
:
</Col>
<Col span={21} className="value-col">
{(roleDetail && roleDetail.roleName) || '-'}
</Col>
<Col span={3} className="label-col">
:
</Col>
<Col span={21} className="value-col">
{(roleDetail && roleDetail.description) || '-'}
</Col>
</Row>
<div className="role-permissions-container">
<div className="title"></div>
<>
{permissions.length ? (
<Spin spinning={permissionFormLoading}>
{permissions.map((permission, i) => (
<CheckboxGroupContainer
key={i}
formInstance={form}
fieldName="permissionIdList"
options={permission.options}
initSelectedOptions={initSelectedPermissions[permission.id] || []}
groupIdx={i}
allCheckText={permission.name}
disabled
/>
))}
</Spin>
) : (
<></>
)}
</>
</div>
</>
) : (
// 新增/编辑角色
<Form form={form} layout="vertical">
<Form.Item
label="角色名称"
name="roleName"
rules={[
{ required: true, message: '角色名称不能为空' },
{
pattern: /^[\u4e00-\u9fa5a-zA-Z0-9_]{3,128}$/,
message: '角色名称只能由中英文大小写、数字、下划线(_)组成长度限制在3128字符',
},
]}
>
<Input placeholder="请输入角色名称" />
</Form.Item>
<Form.Item label="描述" name="description" rules={[{ required: true, message: '描述不能为空' }]}>
<TextArea placeholder="请输入描述" maxLength={200} />
</Form.Item>
<Form.Item
label="分配权限"
name="permissionIdList"
rules={[
() => ({
validator(_, value) {
if (Array.isArray(value) && value.some((item) => !!item.length)) {
return Promise.resolve();
}
return Promise.reject(new Error('请为角色至少分配一项权限'));
},
}),
]}
>
<>
{permissions.length ? (
<Spin spinning={permissionFormLoading}>
{permissions.map((permission, i) => (
<CheckboxGroupContainer
key={i}
formInstance={form}
fieldName="permissionIdList"
options={permission.options}
initSelectedOptions={initSelectedPermissions[permission.id] || []}
groupIdx={i}
allCheckText={permission.name}
/>
))}
</Spin>
) : (
<></>
)}
</>
</Form.Item>
</Form>
)}
</Drawer>
);
});
// 用户角色分配抽屉
// eslint-disable-next-line react/display-name
const AssignRoles = forwardRef((props, ref) => {
// TODO: check 状态
const [visible, setVisible] = useState<boolean>(false);
const [roleInfo, setRoleInfo] = useState<RoleProps>(undefined);
const [users, setUsers] = useState<AssignUser[]>([]);
const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [confirmLoading, setConfirmLoading] = useState<boolean>(false);
const callback = useRef(() => {
return;
});
const onClose = () => {
setVisible(false);
setRoleInfo(undefined);
setUsers([]);
setSelectedUsers([]);
setLoading(false);
setConfirmLoading(false);
};
const filterOption = (inputValue, option) => option.name.includes(inputValue);
const onChange = (newTargetKeys) => {
setSelectedUsers(newTargetKeys);
};
const onSubmit = () => {
if (!roleInfo) return;
setConfirmLoading(true);
request(api.assignRoles, {
method: 'POST',
data: {
flag: false,
id: roleInfo.id,
idList: selectedUsers,
},
}).then(
(res) => {
message.success('成功为角色分配用户');
callback.current();
onClose();
},
() => setConfirmLoading(false)
);
};
useEffect(() => {
if (roleInfo && visible) {
setLoading(true);
request(api.getAssignedUsersByRoleId(roleInfo.id)).then((res: AssignUser[]) => {
const selectedUsers = [];
res.forEach((user) => user.has && selectedUsers.push(user.id));
setUsers(res);
setSelectedUsers(selectedUsers);
setLoading(false);
});
}
}, [visible, roleInfo]);
useImperativeHandle(ref, () => ({
setVisible: (status: boolean, record: RoleProps, cbk: () => { return }) => {
callback.current = cbk;
setRoleInfo(record);
setVisible(status);
},
}));
return (
<Drawer
title="分配角色"
className="role-tab-assign-user"
width={600}
visible={visible}
maskClosable={false}
onClose={onClose}
extra={
<Space>
<Button size="small" onClick={onClose}>
</Button>
<Button type="primary" size="small" disabled={loading} loading={confirmLoading} onClick={onSubmit}>
</Button>
<Divider type="vertical" />
</Space>
}
>
<Row gutter={[12, 12]} className="desc-row">
<Col span={3} className="label-col">
:
</Col>
<Col span={21} className="value-col">
{roleInfo && roleInfo.roleName}
</Col>
<Col span={3} className="label-col">
:
</Col>
<Col span={21} className="value-col">
{roleInfo && roleInfo.description}
</Col>
</Row>
<Spin spinning={loading}>
<Transfer
titles={['未分配用户', '已分配用户']}
dataSource={users}
rowKey={(record) => record.id}
render={(item: AssignUser) => item.name}
showSearch
filterOption={filterOption}
targetKeys={selectedUsers}
onChange={onChange}
pagination
suffix={<IconFont type="icon-fangdajing" />}
/>
</Spin>
</Drawer>
);
});
export default (props: { curTabKey: string }): JSX.Element => {
const [global] = AppContainer.useGlobalValue();
const { curTabKey } = props;
const [roles, setRoles] = useState<RoleProps[]>([]);
const [pagination, setPagination] = useState<any>(defaultPagination);
const [loading, setLoading] = useState<boolean>(true);
const [deleteBtnLoading, setDeleteBtnLoading] = useState<number>(-1);
const [form] = Form.useForm();
const detailRef = useRef(null);
const assignRolesRef = useRef(null);
const columns = [
{
title: '角色ID',
dataIndex: 'roleCode',
},
{
title: '名称',
dataIndex: 'roleName',
},
{
title: '描述',
dataIndex: 'description',
lineClampOne: true,
},
{
title: '分配用户数',
dataIndex: 'authedUserCnt',
width: 100,
render(cnt: Pick<RoleProps, 'authedUserCnt'>, record: RoleProps) {
return (
<Popover
placement="right"
overlayClassName="tags-with-hide-popover"
content={
<div className="container-item-popover">
{record.authedUsers.map((username) => (
<div key={username} className="container-item">
{username}
</div>
))}
</div>
}
>
<Button size="small" type="link">
{cnt}
</Button>
</Popover>
);
},
},
{
title: '最后修改人',
dataIndex: 'lastReviser',
},
{
title: '最后更新时间',
dataIndex: 'updateTime',
width: 180,
render: (date) => moment(date).format('YYYY-MM-DD HH:mm:ss'),
},
{
title: '操作',
width: 260,
render(record) {
return (
<>
<Button
type="link"
size="small"
style={{ paddingLeft: 0 }}
onClick={() => detailRef.current.onOpen(true, RoleOperate.View, getRoleList, record)}
>
</Button>
{global.hasPermission && global.hasPermission(ConfigPermissionMap.ROLE_ASSIGN) ? (
<Button type="link" size="small" onClick={() => assignRolesRef.current.setVisible(true, record, getRoleList)}>
</Button>
) : (
<></>
)}
{global.hasPermission && global.hasPermission(ConfigPermissionMap.ROLE_EDIT) ? (
<Button type="link" size="small" onClick={() => detailRef.current.onOpen(true, RoleOperate.Edit, getRoleList, record)}>
</Button>
) : (
<></>
)}
{global.hasPermission && global.hasPermission(ConfigPermissionMap.ROLE_DEL) ? (
<Button type="link" size="small" onClick={() => onDelete(record)}>
{record.id === deleteBtnLoading ? <LoadingOutlined /> : '删除'}
</Button>
) : (
<></>
)}
</>
);
},
},
];
const getRoleList = (query = {}) => {
const formData = form.getFieldsValue();
const data = {
page: pagination.current,
size: pagination.pageSize,
...formData,
...query,
};
setLoading(true);
request(api.roleList, {
method: 'POST',
data,
}).then(
(res: any) => {
const { pageNo, pageSize, pages, total } = res.pagination;
if (pageNo > pages && pages !== 0) {
getRoleList({ page: pages });
return false;
}
setPagination({
...pagination,
current: pageNo,
pageSize,
total,
});
setRoles(res.bizData);
setLoading(false);
},
() => setLoading(false)
);
};
const onDelete = (record) => {
if (deleteBtnLoading !== -1) return;
setDeleteBtnLoading(record.id);
request(api.checkRole(record.id), {
method: 'DELETE',
}).then(
(res: any) => {
setDeleteBtnLoading(-1);
const userList = res && res.userNameList;
const couldDelete = !(userList && userList.length);
const isShowTooltip = couldDelete ? false : userList.length > 2;
confirm({
// 删除角色弹窗
title: couldDelete ? '确定删除以下角色吗?' : '请先解除角色引用关系',
content: (
<div>
<div>{record.roleName}</div>
{!couldDelete && (
<span>
{isShowTooltip ? (
<Tooltip title={userList.join('')}>
{userList.slice(0, 2).join('')}... {userList.length}
</Tooltip>
) : (
userList.join('')
)}
</span>
)}
</div>
),
okText: couldDelete ? '删除' : '确定',
okType: 'primary',
centered: true,
okButtonProps: {
size: 'small',
danger: couldDelete,
},
cancelButtonProps: {
size: 'small',
},
maskClosable: true,
onOk() {
return (
couldDelete &&
request(api.role(record.id), {
method: 'DELETE',
}).then((_) => {
message.success('删除成功');
getRoleList();
})
);
},
onCancel() {
return;
},
});
},
() => setDeleteBtnLoading(-1)
);
};
const onTableChange = (curPagination) => {
getRoleList({
page: curPagination.current,
size: curPagination.pageSize,
});
};
useEffect(() => {
if (curTabKey === 'role') {
getRoleList();
}
}, [curTabKey]);
return (
<>
<div className="operate-bar">
<Form form={form} layout="inline" onFinish={() => getRoleList({ page: 1 })}>
<Form.Item name="roleName">
<Input placeholder="请输入角色名称" />
</Form.Item>
<Form.Item>
<Button type="primary" ghost htmlType="submit">
</Button>
</Form.Item>
</Form>
{global.hasPermission && global.hasPermission(ConfigPermissionMap.ROLE_ADD) ? (
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => detailRef.current.onOpen(true, RoleOperate.Add, getRoleList, undefined)}
>
</Button>
) : (
<></>
)}
</div>
<ProTable
tableProps={{
showHeader: false,
loading,
rowKey: 'id',
dataSource: roles,
paginationProps: pagination,
columns,
lineFillColor: true,
attrs: {
onChange: onTableChange,
scroll: {
scrollToFirstRowOnChange: true,
x: true,
y: 'calc(100vh - 326px)',
},
},
}}
/>
<RoleDetailAndUpdate ref={detailRef} />
<AssignRoles ref={assignRolesRef} />
</>
);
};

View File

@@ -0,0 +1,396 @@
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
import { Form, ProTable, Select, Button, Input, Modal, message, Drawer, Space, Divider, AppContainer, Utils } from 'knowdesign';
import { PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons';
import moment from 'moment';
import { defaultPagination } from 'constants/common';
import { UserProps, UserOperate } from './config';
import CheckboxGroupContainer from './CheckboxGroupContainer';
import TagsWithHide from '../../components/TagsWithHide/index';
import api from 'api';
import { ConfigPermissionMap } from '../CommonConfig';
const { confirm } = Modal;
const { request } = Utils;
// 正则表达式
const PASSWORD_REGEXP = /^[a-zA-Z0-9_]{6,12}$/;
const USERNAME_REGEXP = /^[a-zA-Z0-9_]{3,128}$/;
const REALNAME_REGEXP = /^[\u4e00-\u9fa5a-zA-Z0-9_]{1,128}$/;
// 编辑用户抽屉
// eslint-disable-next-line react/display-name
const EditUserDrawer = forwardRef((props, ref) => {
const [global] = AppContainer.useGlobalValue();
const [form] = Form.useForm();
const [visible, setVisible] = useState<boolean>(false);
const [confirmLoading, setConfirmLoading] = useState<boolean>(false);
const [type, setType] = useState<UserOperate>(UserOperate.Add);
const [user, setUser] = useState<UserProps>();
const [roleOptions, setRoleOptions] = useState<{ label: string; value: number }[]>([]);
const [initSelectedOptions, setInitSelectedOptions] = useState<number[]>([]);
const callback = useRef(() => {
return;
});
// 提交表单
const onSubmit = () => {
form.validateFields().then((formData) => {
setConfirmLoading(true);
formData.roleIds = formData.roleIds.flat();
if (!formData.pw) {
delete formData.pw;
}
// 密码加密
// formData.pw = Utils.encryptAES(formData.pw, systemCipherKey);
const requestPromise =
type === UserOperate.Add
? request(api.addUser, {
method: 'PUT',
data: formData,
})
: request(api.editUser, {
method: 'POST',
data: { ...formData },
});
requestPromise.then(
(res) => {
callback.current();
onClose();
message.success(`${type === UserOperate.Add ? '新增' : '编辑'}用户成功`);
},
() => setConfirmLoading(false)
);
});
};
// 打开抽屉
const onOpen = (status: boolean, type: UserOperate, cbk: () => { return }, userDetail: UserProps, roles) => {
form.setFieldsValue(userDetail);
setUser(userDetail);
setInitSelectedOptions(userDetail?.roleList ? userDetail.roleList.map((role) => role.id) : []);
setRoleOptions(roles);
setType(type);
setVisible(status);
callback.current = cbk;
};
// 关闭抽屉
const onClose = () => {
setVisible(false);
form.resetFields();
setUser(undefined);
setInitSelectedOptions([]);
setRoleOptions([]);
setConfirmLoading(false);
};
useImperativeHandle(ref, () => ({
onOpen,
}));
return (
<Drawer
title={`${type === UserOperate.Add ? '新增' : '编辑'}用户`}
width={480}
visible={visible}
maskClosable={false}
onClose={onClose}
extra={
<Space>
<Button size="small" onClick={onClose}>
</Button>
<Button type="primary" size="small" loading={confirmLoading} onClick={onSubmit}>
</Button>
<Divider type="vertical" />
</Space>
}
>
<Form form={form} layout="vertical">
<Form.Item
label="用户账号"
name="userName"
rules={[
{ required: true, message: '用户账号不能为空' },
{ pattern: USERNAME_REGEXP, message: '用户账号只能由英文大小写、数字、下划线(_)组成长度限制在3128字符' },
]}
>
<Input disabled={type === UserOperate.Edit} placeholder="请输入用户账号" />
</Form.Item>
{type === UserOperate.Add || global.hasPermission(ConfigPermissionMap.USER_EDIT) ? (
<Form.Item
label="用户实名"
name="realName"
rules={[
{ required: true, message: '用户实名不能为空' },
{ pattern: REALNAME_REGEXP, message: '用户实名只能由中英文大小写、数字、下划线组成长度限制在1128字符' },
]}
>
<Input placeholder="请输入用户实名" />
</Form.Item>
) : (
<></>
)}
{type === UserOperate.Add || global.hasPermission(ConfigPermissionMap.USER_CHANGE_PASS) ? (
<Form.Item
label={`${type === UserOperate.Edit ? '新' : ''}密码`}
name="pw"
tooltip={{ title: '密码支持英文、数字、下划线(_)长度限制在6~12字符', icon: <QuestionCircleOutlined /> }}
rules={[
{ required: type === UserOperate.Add, message: '密码不能为空' },
{ pattern: PASSWORD_REGEXP, message: '密码只能由英文、数字、下划线(_)组成长度限制在6~12字符' },
]}
>
<Input.Password placeholder="请输入密码" />
</Form.Item>
) : (
<></>
)}
{type === UserOperate.Add || global.hasPermission(ConfigPermissionMap.USER_EDIT) ? (
<Form.Item
label="分配角色"
name="roleIds"
rules={[
() => ({
validator(_, value) {
if (Array.isArray(value) && value.some((item) => !!item.length)) {
return Promise.resolve();
}
return Promise.reject(new Error('请为用户至少分配一名角色'));
},
}),
]}
>
<CheckboxGroupContainer
formInstance={form}
fieldName="roleIds"
options={roleOptions}
initSelectedOptions={initSelectedOptions}
groupIdx={0}
/>
</Form.Item>
) : (
<></>
)}
</Form>
</Drawer>
);
});
export default (props: { curTabKey: string }) => {
const { curTabKey } = props;
const [global] = AppContainer.useGlobalValue();
const [loading, setLoading] = useState<boolean>(true);
const [users, setUsers] = useState<UserProps[]>([]);
const [pagination, setPagination] = useState<any>(defaultPagination);
const [simpleRoleList, setSimpleRoleList] = useState<{ value: number; label: string }[]>([]);
const [form] = Form.useForm();
const modalRef = useRef(null);
const editRef = useRef(null);
const getUserList = (query = {}) => {
const formData = form.getFieldsValue();
const data = {
page: pagination.current,
size: pagination.pageSize,
...formData,
...query,
};
setLoading(true);
request(api.userList, {
method: 'POST',
data,
}).then(
(res: any) => {
const { pageNo, pageSize, pages, total } = res.pagination;
if (pageNo > pages && pages !== 0) {
getUserList({ page: pages });
return false;
}
setPagination({
...pagination,
current: pageNo,
pageSize,
total,
});
setUsers(res.bizData);
setLoading(false);
},
() => setLoading(false)
);
};
const delUser = (record: UserProps) => {
confirm({
title: '删除提示',
content: `确定要删除用户⌈${record.userName}⌋吗?`,
centered: true,
maskClosable: true,
okType: 'primary',
okText: '删除',
okButtonProps: {
size: 'small',
danger: true,
},
cancelButtonProps: {
size: 'small',
},
onOk() {
return request(api.user(record.id), {
method: 'DELETE',
}).then((_) => {
message.success('删除成功');
getUserList();
});
},
onCancel() {
return;
},
});
};
const onTableChange = (curPagination) => {
getUserList({
page: curPagination.current,
size: curPagination.pageSize,
});
};
const columns = useCallback(() => {
const baseColumns = [
{
title: '用户账号',
dataIndex: 'userName',
},
{
title: '用户实名',
dataIndex: 'realName',
},
{
title: '分配角色',
dataIndex: 'roleList',
width: 560,
render(roleList) {
const roles = roleList.map((role) => role.roleName);
return <TagsWithHide list={roles} expandTagContent={(num) => `共有${num}个角色`} />;
},
},
{
title: '最后更新时间',
dataIndex: 'updateTime',
render: (date) => moment(date).format('YYYY-MM-DD HH:mm:ss'),
},
];
if (
global.hasPermission &&
(global.hasPermission(ConfigPermissionMap.USER_CHANGE_PASS) ||
global.hasPermission(ConfigPermissionMap.USER_EDIT) ||
global.hasPermission(ConfigPermissionMap.USER_DEL))
) {
baseColumns.push({
title: '操作',
dataIndex: '',
width: 140,
render(record: UserProps) {
return (
<>
{global.hasPermission &&
(global.hasPermission(ConfigPermissionMap.USER_EDIT) || global.hasPermission(ConfigPermissionMap.USER_CHANGE_PASS)) ? (
<Button
type="link"
size="small"
style={{ paddingLeft: 0 }}
onClick={() => editRef.current.onOpen(true, UserOperate.Edit, getUserList, record, simpleRoleList)}
>
</Button>
) : (
<></>
)}
{global.hasPermission && global.hasPermission(ConfigPermissionMap.USER_DEL) ? (
<Button type="link" size="small" onClick={() => delUser(record)}>
</Button>
) : (
<></>
)}
</>
);
},
});
}
return baseColumns;
}, [global, getUserList, simpleRoleList]);
useEffect(() => {
if (curTabKey === 'user') {
getUserList();
request(api.simpleRoleList).then((res: { id: number; roleName: string }[]) => {
const roles = res.map(({ id, roleName }) => ({ label: roleName, value: id }));
setSimpleRoleList(roles);
});
}
}, [curTabKey]);
return (
<>
<div className="operate-bar">
<Form form={form} layout="inline" onFinish={() => getUserList({ page: 1 })}>
<Form.Item name="userName">
<Input placeholder="请输入用户账号" />
</Form.Item>
<Form.Item name="realName">
<Input placeholder="请输入用户实名" />
</Form.Item>
<Form.Item name="roleId">
<Select style={{ width: 190 }} placeholder="选择平台已创建的角色名" allowClear options={simpleRoleList} />
</Form.Item>
<Form.Item>
<Button type="primary" ghost htmlType="submit">
</Button>
</Form.Item>
</Form>
{global.hasPermission && global.hasPermission(ConfigPermissionMap.USER_ADD) ? (
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => editRef.current.onOpen(true, UserOperate.Add, getUserList, {}, simpleRoleList)}
>
</Button>
) : (
<></>
)}
</div>
<ProTable
tableProps={{
showHeader: false,
loading,
rowKey: 'id',
dataSource: users,
paginationProps: pagination,
columns,
lineFillColor: true,
attrs: {
onChange: onTableChange,
scroll: {
scrollToFirstRowOnChange: true,
x: true,
y: 'calc(100vh - 326px)',
},
},
}}
/>
<EditUserDrawer ref={editRef} />
</>
);
};

View File

@@ -0,0 +1,63 @@
export type PermissionNode = {
id: number;
parentId: number;
permissionName: string;
has: boolean;
leaf: boolean;
childList: PermissionNode[];
};
export type UserProps = {
id: number;
userName: string;
realName: string;
email: string;
phone: string;
updateTime: number;
roleList: {
id: number;
roleName: string;
}[];
deptList: {
id: number;
parentId: number;
deptName: string;
}[];
permissionTreeV0: PermissionNode;
};
export type RoleProps = {
id: number;
roleCode: string;
roleName: string;
description: string;
authedUserCnt: number;
authedUsers: string[];
lastReviser: string | null;
createTime: string;
updateTime: string;
permissionTreeV0: PermissionNode;
};
export interface AssignUser {
id: number;
name: string;
has: boolean;
}
export enum UserOperate {
Add,
Edit,
}
export enum RoleOperate {
Add,
Edit,
View,
}
export interface FormItemPermission {
id: number;
name: string;
options: { label: string; value: number }[];
}

View File

@@ -0,0 +1,46 @@
.checkbox-content-ellipsis {
width: 100%;
& > span:last-child {
width: calc(100% - 16px);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.role-tab-detail,
.role-tab-assign-user {
.desc-row {
.label-col,
.value-col {
font-size: 13px;
line-height: 18px;
}
.label-col {
color: #74788d;
text-align: right;
}
.value-col {
color: #495057;
}
}
}
.role-tab-detail .role-permissions-container {
margin-top: 24px;
.title {
height: 40px;
padding: 10px 16px;
border-radius: 8px;
background: #f9f9fa;
font-family: @font-family-bold;
font-size: 13px;
color: #353a40;
text-align: left;
line-height: 20px;
}
}
.role-tab-assign-user .desc-row {
margin-bottom: 24px;
}

View File

@@ -0,0 +1,32 @@
import { Tabs } from 'knowdesign';
import React, { useState } from 'react';
import TypicalListCard from '../../components/TypicalListCard';
import UserTabContent from './UserTabContent';
import RoleTabContent from './RoleTabContent';
import './index.less';
const { TabPane } = Tabs;
const UserManage = () => {
const [curTabKey, setCurTabKey] = useState<string>(window.history.state?.tab || 'user');
const onTabChange = (key) => {
setCurTabKey(key);
window.history.replaceState({ tab: key }, '');
};
return (
<TypicalListCard title="用户管理">
<Tabs defaultActiveKey={curTabKey} onChange={onTabChange}>
<TabPane tab="人员管理" key="user">
<UserTabContent curTabKey={curTabKey} />
</TabPane>
<TabPane tab="角色管理" key="role">
<RoleTabContent curTabKey={curTabKey} />
</TabPane>
</Tabs>
</TypicalListCard>
);
};
export default UserManage;

View File

@@ -0,0 +1,31 @@
import Setting from './ConfigManage';
import User from './UserManage';
import OperationLog from './OperationLog';
import HomePage from './HomePage';
import CommonConfig from './CommonConfig';
export const pageRoutes = [
{
path: '',
exact: false,
component: HomePage,
commonRoute: CommonConfig,
children: [
{
path: 'setting',
exact: true,
component: Setting,
},
{
path: 'user',
exact: true,
component: User,
},
{
path: 'operation-log',
exact: true,
component: OperationLog,
},
],
},
];

View File

@@ -0,0 +1,30 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"*": [
"src/*"
],
"@src/*":["src/*"]
},
"outDir": "./dist/",
"sourceMap": true,
"noImplicitAny": false,
"esModuleInterop": true,
"experimentalDecorators": true,
"allowJs": true,
"moduleResolution": "node",
"module": "es2015",
"target": "es2015",
"downlevelIteration": true,
"allowSyntheticDefaultImports": true,
"jsx": "react",
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"pub/*",
],
}

View File

@@ -0,0 +1,55 @@
/* eslint-disable */
const isProd = process.env.NODE_ENV === 'production';
const pkgJson = require('./package');
const path = require('path');
// const outPath = path.resolve(__dirname, `../../pub/${pkgJson.ident}`);
const outPath = path.resolve(__dirname, `../../../km-rest/src/main/resources/templates/${pkgJson.ident}`);
const HtmlWebpackPlugin = require('html-webpack-plugin');
const merge = require('webpack-merge');
const webpack = require('webpack');
const getWebpackCommonConfig = require('./config/d1-webpack.base');
const config = getWebpackCommonConfig();
module.exports = merge(config, {
mode: isProd ? 'production' : 'development',
entry: {
[pkgJson.ident]: ['./src/index.tsx'],
},
plugins: [
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify(process.env.NODE_ENV),
RUN_ENV: JSON.stringify(process.env.RUN_ENV),
},
}),
new HtmlWebpackPlugin({
meta: {
manifest: 'manifest.json',
},
template: './src/index.html',
inject: 'body',
}),
],
output: {
path: outPath,
publicPath: isProd ? `/${pkgJson.ident}/` : `http://localhost:${pkgJson.port}/${pkgJson.ident}/`,
library: pkgJson.ident,
libraryTarget: 'amd',
},
devtool: isProd ? 'none' : 'cheap-module-eval-source-map',
devServer: {
host: '127.0.0.1',
port: pkgJson.port,
hot: true,
open: false,
publicPath: `http://localhost:${pkgJson.port}/${pkgJson.ident}/`,
inline: true,
disableHostCheck: true,
historyApiFallback: true,
headers: {
'Access-Control-Allow-Origin': '*',
},
proxy: {},
},
});

View File

@@ -0,0 +1,5 @@
test/
scripts/
build/
public/
types/

View File

@@ -0,0 +1,14 @@
node_modules
dist/
pub/
build/
.sass-cache/
.DS_Store
.idea/
.vscode/
coverage
versions/
debug.log
package-lock.json
yarn.lock
.d1-workspace.json

View File

@@ -0,0 +1,32 @@
## Usage
### 启动:
* 招行环境执行 npm start
* 内部环境执行 npm run start:inner
### 构建:
* 招行环境执行 npm build
* 内部环境执行 npm run build:inner
构建后的代码默认会存放到 `../pub` 文件夹里
### 部署
* 内部环境代码提交主干后会自动触发打包部署至http://10.190.14.125:8016
## 目录结构
- config: 开发 & 构建配置
- theme.jsantd 主题配置
- webpack.dev.config.jswebpack 开发环境补充配置,覆盖默认配置
- webpack.build.config.jswebpack 构建补充配置,覆盖默认配置
- webpackConfigResolveAlias.js 文件路径别名配置
- src源代码所在目录
- assets全局资源 img、css
- common: 全局配置、通用方法
- components公共组件
- pages路由匹配的页面组件
- app.jsx 菜单、路由配置组件
- index.html单页
- index.jsx入口文件
- fetk.config.js 开发工具配置页面

View File

@@ -0,0 +1,83 @@
const chalk = require('chalk');
const fs = require('fs');
// const toExcel = require('to-excel').toExcel;
class CountComponentPlugin {
constructor(opts = {}) {
this.opts = {
startCount: true, // 是否开启统计
isExportExcel: false, // 是否生成excel
pathname: '', // 文件路径
...opts,
};
this.total = {
len: 0,
components: {},
};
}
sort(obj) {
this.total.components = Object.fromEntries(Object.entries(obj).sort(([, a], [, b]) => b - a));
}
// 生成excel 文件
// toExcel() {
// const arr = [];
// Object.keys(this.total.components).forEach((key, index) => {
// const value = this.total.components[key];
// const data = {
// id: index + 1,
// component: key,
// count: value,
// };
// arr.push(data);
// });
// const headers = [
// { label: '名次', field: 'id' },
// { label: '组件', field: 'component' },
// { label: '次数', field: 'count' },
// ];
// const content = toExcel.exportXLS(headers, arr, 'filename');
// fs.writeFileSync('filename.xls', content);
// }
toLog() {
this.sort(this.total.components);
Object.keys(this.total.components).forEach((key) => {
const value = this.total.components[key];
const per = Number((value / this.total.len).toPrecision(3)) * 100;
console.log(`\n${chalk.blue(key)} 组件引用次数 ${chalk.green(value)} 引用率 ${chalk.redBright(per)}%`);
});
console.log(`\n组件${chalk.blue('总共')}引用次数 ${chalk.green(this.total.len)}`);
}
apply(compiler) {
const handler = (_compilation, { normalModuleFactory }) => {
normalModuleFactory.hooks.parser.for('javascript/auto').tap('count-component-plugin', (parser) => {
parser.hooks.importSpecifier.tap('count-component-plugin', (_statement, source, _exportName, identifierName) => {
if (source.includes(this.opts.pathname)) {
this.total.len = this.total.len + 1;
const key = identifierName;
this.total.components[key] = this.total.components[key] ? this.total.components[key] + 1 : 1;
}
});
parser.hooks.program.tap('count-component-plugin', (ast) => {
// console.log('+++++++', ast);
});
});
};
const done = () => {
if (!this.opts.startCount) {
return;
}
this.sort(this.total.components);
if (this.opts.isExportExcel) {
this.toLog();
} else {
this.toLog();
}
};
compiler.hooks.compilation.tap('count-component-plugin', handler);
compiler.hooks.done.tap('count-component-plugin-done', done);
}
}
module.exports = CountComponentPlugin;

View File

@@ -0,0 +1,116 @@
/**
* 重置 html 内容
* 注意: HtmlWebpackPlugin hooks 是 beta 版本,正式版本接口可能会变
*/
const HtmlWebpackPlugin = require('html-webpack-plugin');
// const PublicPath = '//img-ys011.didistatic.com/static/bp_fe_daily/bigdata_cloud_KnowStreaming_FE/gn';
const PublicPath = '';
const isProd = process.env.NODE_ENV === 'production';
const commonDepsMap = [
{
name: 'react',
development: '/static/js/react.production.min.js',
production: `${PublicPath}/static/js/react.production.min.js`,
},
{
name: 'react-dom',
development: '/static/js/react-dom.production.min.js',
production: `${PublicPath}/static/js/react-dom.production.min.js`,
},
{
name: 'single-spa',
development: '/static/js/single-spa.min.js',
production: `${PublicPath}/static/js/single-spa.min.js`,
},
{
name: 'single-spa-react',
development: '/static/js/single-spa-react.js',
production: `${PublicPath}/static/js/single-spa-react.js`,
},
{
name: 'moment',
development: '/static/js/moment.min.js',
production: `${PublicPath}/static/js/moment.min.js`,
},
];
function generateSystemJsImportMap() {
const importMap = {
'react-router': 'https://unpkg.com/react-router@5.2.1/umd/react-router.min.js',
'react-router-dom': 'https://unpkg.com/react-router-dom@5.2.1/umd/react-router-dom.min.js',
lodash: 'https://unpkg.com/lodash@4.17.21/lodash.min.js',
history: 'https://unpkg.com/history@5/umd/history.development.js',
echarts: 'https://unpkg.com/echarts@5.3.1/dist/echarts.min.js',
};
//if (process.env.NODE_ENV === 'production') {
commonDepsMap.forEach((o) => {
importMap[o.name] = o[process.env.NODE_ENV];
});
//}
return JSON.stringify({
imports: importMap,
});
}
class CoverHtmlWebpackPlugin {
constructor(options) {
this.isBusiness = options.BUSINESS_VERSION;
}
apply(compiler) {
compiler.hooks.compilation.tap('CoverHtmlWebpackPlugin', (compilation) => {
HtmlWebpackPlugin.getHooks(compilation).beforeEmit.tapAsync('CoverHtmlWebpackPlugin', async (data, cb) => {
const depsMap = `
<script type="systemjs-importmap">
${generateSystemJsImportMap()}
</script>
`;
const portalMap = {
'@portal/layout': '/layout.js',
};
const assetJson = JSON.parse(data.plugin.assetJson);
let links = '';
assetJson.forEach((item) => {
if (/\.js$/.test(item)) {
// TODO: entry 只有一个
portalMap['@portal/layout'] = item;
} else if (/\.css$/.test(item)) {
links += `<link href="${item}" rel="stylesheet">`;
}
});
data.html = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title></title>
${links}
<link href='${isProd ? PublicPath : ''}/favicon.ico' rel='shortcut icon'>
<script src='${isProd ? PublicPath : ''}/static/js/system.min.js'></script>
<script src='${isProd ? PublicPath : ''}/static/js/named-exports.min.js'></script>
<script src='${isProd ? PublicPath : ''}/static/js/use-default.min.js'></script>
<script src='${isProd ? PublicPath : ''}/static/js/amd.js'></script>
${this.isBusiness ? `<script src=${isProd ? PublicPath : ''}/static/js/ksl.min.js></script>` : ''}
</head>
<body>
${depsMap}
<script type="systemjs-importmap">
{
"imports": ${JSON.stringify(portalMap)}
}
</script>
<script>
System.import('@portal/layout');
</script>
<div id="layout"></div>
</body>
</html>
`;
cb(null, data);
});
});
}
}
module.exports = CoverHtmlWebpackPlugin;

View File

@@ -0,0 +1,162 @@
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const ProgressBarPlugin = require('progress-bar-webpack-plugin');
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CoverHtmlWebpackPlugin = require('./CoverHtmlWebpackPlugin.js');
var webpackConfigResolveAlias = require('./webpackConfigResolveAlias');
const TerserJSPlugin = require('terser-webpack-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const theme = require('./theme');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const isProd = process.env.NODE_ENV === 'production';
const babelOptions = {
cacheDirectory: true,
babelrc: false,
presets: [require.resolve('@babel/preset-env'), require.resolve('@babel/preset-typescript'), require.resolve('@babel/preset-react')],
plugins: [
[require.resolve('@babel/plugin-proposal-decorators'), { legacy: true }],
[require.resolve('@babel/plugin-proposal-class-properties'), { loose: true }],
require.resolve('@babel/plugin-proposal-export-default-from'),
require.resolve('@babel/plugin-proposal-export-namespace-from'),
require.resolve('@babel/plugin-proposal-object-rest-spread'),
require.resolve('@babel/plugin-transform-runtime'),
!isProd && require.resolve('react-refresh/babel'),
]
.filter(Boolean)
.concat([
'@babel/plugin-transform-object-assign',
'@babel/plugin-transform-modules-commonjs',
]),
};
module.exports = () => {
const jsFileName = isProd ? '[name]-[chunkhash].js' : '[name].js';
const cssFileName = isProd ? '[name]-[chunkhash].css' : '[name].css';
const plugins = [
new CoverHtmlWebpackPlugin(),
new ProgressBarPlugin(),
new CaseSensitivePathsPlugin(),
new MiniCssExtractPlugin({
filename: cssFileName,
}),
!isProd &&
new ReactRefreshWebpackPlugin({
overlay: false,
}),
].filter(Boolean);
const resolve = {
symlinks: false,
extensions: ['.web.jsx', '.web.js', '.ts', '.tsx', '.js', '.jsx', '.json'],
alias: webpackConfigResolveAlias,
};
if (isProd) {
plugins.push(new CleanWebpackPlugin());
}
if (!isProd) {
resolve.mainFields = ['browser', 'main', 'module'];
}
return {
output: {
filename: jsFileName,
chunkFilename: jsFileName,
library: 'layout',
libraryTarget: 'amd',
},
externals: [
/^react$/,
/^react\/lib.*/,
/^react-dom$/,
/.*react-dom.*/,
/^single-spa$/,
/^single-spa-react$/,
/^moment$/,
/^react-router$/,
/^react-router-dom$/,
],
resolve,
plugins,
module: {
rules: [
{
parser: { system: false },
},
{
test: /\.(js|jsx)$/,
use: [
{
loader: 'babel-loader',
options: babelOptions,
},
],
},
{
test: /\.(ts|tsx)$/,
use: [
{
loader: 'babel-loader',
options: babelOptions,
},
{
loader: 'ts-loader',
options: {
allowTsInNodeModules: true,
},
},
],
},
{
test: /\.(png|svg|jpeg|jpg|gif|ttf|woff|woff2|eot|pdf)$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: './assets/image/',
esModule: false,
},
},
],
},
{
test: /\.(css|less)$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
},
'css-loader',
{
loader: 'less-loader',
options: {
javascriptEnabled: true,
modifyVars: theme,
},
},
],
},
],
},
optimization: isProd
? {
minimizer: [
new TerserJSPlugin({
cache: true,
sourceMap: true,
}),
new OptimizeCSSAssetsPlugin({}),
],
}
: {},
devtool: isProd ? 'cheap-module-source-map' : 'source-map',
node: {
fs: 'empty',
net: 'empty',
tls: 'empty',
},
};
};

View File

@@ -0,0 +1,183 @@
/* eslint-disable */
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const ProgressBarPlugin = require('progress-bar-webpack-plugin');
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CoverHtmlWebpackPlugin = require('./CoverHtmlWebpackPlugin.js');
var webpackConfigResolveAlias = require('./webpackConfigResolveAlias');
const TerserJSPlugin = require('terser-webpack-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const theme = require('./theme');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
const BUSINESS_VERSION = false;
const isProd = process.env.NODE_ENV === 'production';
// const publicPath = isProd ? '//img-ys011.didistatic.com/static/bp_fe_daily/bigdata_cloud_KnowStreaming_FE/gn/' : '/';
const publicPath = '/';
const babelOptions = {
cacheDirectory: true,
babelrc: false,
presets: [require.resolve('@babel/preset-env'), require.resolve('@babel/preset-typescript'), require.resolve('@babel/preset-react')],
plugins: [
[require.resolve('@babel/plugin-proposal-decorators'), { legacy: true }],
[require.resolve('@babel/plugin-proposal-class-properties'), { loose: true }],
[require.resolve('@babel/plugin-proposal-private-property-in-object'), { loose: true }],
[require.resolve('@babel/plugin-proposal-private-methods'), { loose: true }],
require.resolve('@babel/plugin-proposal-export-default-from'),
require.resolve('@babel/plugin-proposal-export-namespace-from'),
require.resolve('@babel/plugin-proposal-object-rest-spread'),
require.resolve('@babel/plugin-transform-runtime'),
!isProd && require.resolve('react-refresh/babel'),
]
.filter(Boolean)
.concat([
[
'babel-plugin-import',
{
libraryName: 'antd',
style: true,
},
],
'@babel/plugin-transform-object-assign',
]),
};
module.exports = () => {
const jsFileName = isProd ? '[name]-[chunkhash].js' : '[name].js';
const cssFileName = isProd ? '[name]-[chunkhash].css' : '[name].css';
const plugins = [
// !isProd && new HardSourceWebpackPlugin(),
new CoverHtmlWebpackPlugin({
BUSINESS_VERSION,
}),
new ProgressBarPlugin(),
new CaseSensitivePathsPlugin(),
new MiniCssExtractPlugin({
filename: cssFileName,
}),
!isProd &&
new ReactRefreshWebpackPlugin({
overlay: false,
}),
].filter(Boolean);
const resolve = {
symlinks: false,
extensions: ['.web.jsx', '.web.js', '.ts', '.tsx', '.js', '.jsx', '.json'],
alias: webpackConfigResolveAlias,
};
if (isProd) {
plugins.push(new CleanWebpackPlugin());
}
if (!isProd) {
resolve.mainFields = ['module', 'browser', 'main'];
}
return {
output: {
filename: jsFileName,
chunkFilename: jsFileName,
library: 'layout',
libraryTarget: 'amd',
publicPath,
},
externals: isProd
? [
/^react$/,
/^react\/lib.*/,
/^react-dom$/,
/.*react-dom.*/,
/^single-spa$/,
/^single-spa-react$/,
/^moment$/,
/^antd$/,
/^lodash$/,
/^echarts$/,
]
: [],
resolve,
plugins,
module: {
rules: [
{
parser: { system: false },
},
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: babelOptions,
},
],
},
{
test: /\.(ts|tsx)$/,
use: [
{
loader: 'babel-loader',
options: babelOptions,
},
{
loader: 'ts-loader',
options: {
allowTsInNodeModules: true,
},
},
],
},
{
test: /\.(png|svg|jpeg|jpg|gif|ttf|woff|woff2|eot|pdf|otf)$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: './assets/image/',
esModule: false,
},
},
],
},
{
test: /\.(css|less)$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
},
'css-loader',
{
loader: 'less-loader',
options: {
javascriptEnabled: true,
modifyVars: theme,
},
},
],
},
],
},
optimization: isProd
? {
minimizer: [
new TerserJSPlugin({
cache: true,
sourceMap: true,
}),
new OptimizeCSSAssetsPlugin({}),
],
}
: {},
devtool: isProd ? 'cheap-module-source-map' : '',
node: {
fs: 'empty',
net: 'empty',
tls: 'empty',
},
};
};
module.exports.BUSINESS_VERSION = BUSINESS_VERSION;

View File

@@ -0,0 +1,154 @@
import * as singleSpa from 'single-spa';
const customProps = {
env: {
NODE_ENV: process.env.NODE_ENV,
},
};
function fetchManifest(url, publicPath) {
return fetch(url)
.then((res) => {
return res.text();
})
.then((data) => {
if (data) {
const manifest = data.match(/<meta name="manifest" content="([\w|\d|-]+.json)">/);
let result = '';
if (publicPath && manifest) {
result = `${publicPath}${manifest[1]}?q=${new Date().getTime()}`;
}
return result;
}
});
}
function prefix(location, ident, matchPath) {
if (matchPath && Object.prototype.toString.call(matchPath) === '[object Function]') {
return matchPath(location);
}
if (location.href === `${location.origin}/${ident}`) {
return true;
}
return location.href.indexOf(`${location.origin}/${ident}`) !== -1;
}
function getStylesheetLink(ident) {
return document.getElementById(`${ident}-stylesheet`);
}
function createStylesheetLink(ident, path) {
const headEle = document.getElementsByTagName('head')[0];
const linkEle = document.createElement('link');
linkEle.id = `${ident}-stylesheet`;
linkEle.rel = 'stylesheet';
// linkEle.href = systemConf[process.env.NODE_ENV].css;
linkEle.href = path;
headEle.appendChild(linkEle);
}
function removeStylesheetLink(ident) {
const linkEle = getStylesheetLink(ident);
if (linkEle) linkEle.remove();
}
async function getPathBySuffix(systemConf, jsonData, suffix) {
let targetPath = '';
_.forEach(Object.values(jsonData.assetsByChunkName), (assetsArr) => {
if (typeof assetsArr === 'string' && assetsArr.indexOf(systemConf.ident) === 0 && _.endsWith(assetsArr, suffix)) {
targetPath = assetsArr;
}
if (Array.isArray(assetsArr)) {
targetPath = assetsArr.find((assetStr) => {
return assetStr.indexOf(systemConf.ident) === 0 && _.endsWith(assetStr, suffix);
});
if (targetPath) {
return false;
}
}
});
return `${systemConf[process.env.NODE_ENV].publicPath}${targetPath}`;
}
async function loadAssertsFileBySuffix(systemConf, jsonData, suffix) {
const chunks = Object.values(jsonData.assetsByChunkName);
const isJS = /js$/.test(suffix);
await Promise.all(
chunks.map(async (assetsArr) => {
let targetPath = '';
if (typeof assetsArr === 'string') {
targetPath = assetsArr;
} else if (Array.isArray(assetsArr)) {
targetPath = assetsArr.find((assetStr) => {
if (isJS) {
return assetStr.indexOf(systemConf.ident) < 0 && _.endsWith(assetStr, suffix);
} else {
return _.endsWith(assetStr, suffix);
}
});
}
if (!targetPath) return Promise.resolve();
if (isJS) {
return System.import(`${systemConf[process.env.NODE_ENV].publicPath}${targetPath}`);
} else {
return createStylesheetLink(systemConf.ident, `${systemConf[process.env.NODE_ENV].publicPath}${targetPath}`);
}
})
);
}
export default function registerApps(systemsConfig, props = {}, mountCbk) {
systemsConfig.forEach(async (systemsConfItem) => {
const { ident, matchPath } = systemsConfItem;
const sysUrl = systemsConfItem[process.env.NODE_ENV].index;
singleSpa.registerApplication(
ident,
async () => {
let manifestUrl = `${sysUrl}?q=${new Date().getTime()}`;
// html 作为入口文件
if (/.+html$/.test(sysUrl)) {
manifestUrl = await fetchManifest(sysUrl, systemsConfItem[process.env.NODE_ENV].publicPath);
}
const lifecyclesFile = await fetch(manifestUrl).then((res) => res.json());
let lifecycles = {};
if (lifecyclesFile) {
await loadAssertsFileBySuffix(systemsConfItem, lifecyclesFile, '.js');
const jsPath = await getPathBySuffix(systemsConfItem, lifecyclesFile, '.js');
lifecycles = await System.import(jsPath);
} else {
lifecycles = lifecyclesFile;
}
const { mount, unmount } = lifecycles;
mount.unshift(async () => {
if (lifecyclesFile) {
await loadAssertsFileBySuffix(systemsConfItem, lifecyclesFile, '.css');
// const cssPath = await getPathBySuffix(systemsConfItem, lifecyclesFile, '.css');
// createStylesheetLink(ident, cssPath);
}
return Promise.resolve();
});
if (mountCbk) {
mount.unshift(async () => {
mountCbk();
return Promise.resolve();
});
}
unmount.unshift(() => {
removeStylesheetLink(ident);
return Promise.resolve();
});
return lifecycles;
},
(location) => prefix(location, ident, matchPath),
{
...customProps,
...props,
}
);
});
singleSpa.start();
}

View File

@@ -0,0 +1,27 @@
const feSystemsConfig = {
systemsConfig: [
{
ident: 'config',
development: {
publicPath: 'http://localhost:8001/config/',
index: 'http://localhost:8001/config/manifest.json',
},
production: { publicPath: '/config/', index: '/config/manifest.json' },
// production: {
// publicPath: '//img-ys011.didistatic.com/static/bp_fe_daily/bigdata_cloud_KnowStreaming_FE/gn/config/',
// index: '//img-ys011.didistatic.com/static/bp_fe_daily/bigdata_cloud_KnowStreaming_FE/gn/config/manifest.json',
// },
},
],
feConfig: {
title: 'Know Streaming',
header: {
mode: 'complicated',
logo: '/static/logo-white.png',
subTitle: '管理平台',
theme: '',
right_links: [],
},
},
};
module.exports = feSystemsConfig;

View File

@@ -0,0 +1,17 @@
const themeConfig = {
primaryColor: '#556ee6',
theme: {
'primary-color': '#556ee6',
'border-radius-base': '2px',
'border-radius-sm': '2px',
'font-size-base': '12px',
'font-family': 'Helvetica Neue, Helvetica, Arial, PingFang SC, Heiti SC, Hiragino Sans GB, Microsoft YaHei, sans-serif',
'font-family-bold':
'HelveticaNeue-Medium, Helvetica Medium, PingFangSC-Medium, STHeitiSC-Medium, Microsoft YaHei Bold, Arial, sans-serif',
},
};
module.exports = {
'prefix-cls': 'layout',
...themeConfig.theme,
};

View File

@@ -0,0 +1,5 @@
var path = require('path');
module.exports = {
react: path.resolve('./node_modules/react'),
};

View File

@@ -0,0 +1,5 @@
const d1Config = require('./d1.json');
d1Config.appConfig.webpackChain = function (config) {
// config.devServer.port(10000);
};
module.exports = d1Config;

View File

@@ -0,0 +1,29 @@
{
"appConfig": {
"appName": "layout-clusters-fe",
"ident": "",
"port": "8000",
"webpackCustom": "",
"webpackChain": "",
"entry": [
{
"title": "多集群管理",
"name": "index",
"src": "./src/index.html"
}
],
"layout": "layout-clusters-fe",
"packages": [
"config-manager-fe",
"layout-clusters-fe"
]
},
"entrust": true,
"localBuilderVersion": true,
"extensions": [],
"preset": "@didi/d1-preset-opensource",
"builderType": "@didi/d1-preset-opensource",
"generatorType": "",
"mockDir": "mock",
"webpackCustomPath": "./webpack.config.js"
}

View File

@@ -0,0 +1,56 @@
{
"development": {
"inner": {
"proxy": {
"/api/v2": {
"target": "https://mock.xiaojukeji.com/mock/8739",
"changeOrigin": true
},
"/sysUser": {
"target": "https://mock.xiaojukeji.com/mock/8739",
"changeOrigin": true
}
},
"loginUrl": "http://mock.xiaojukeji.com/mock/8739"
},
"cmb": {
"proxy": {
"/api/v1/uc": {
"target": "http://cmbkafkagw-dev.paas.cmbchina.cn/",
"pathRewrite": { "^/api/v1/uc": "/uc/api/v1" },
"changeOrigin": true
},
"/api/v2": {
"target": "http://cmbkafkagw-dev.paas.cmbchina.cn/",
"pathRewrite": { "^/api/v2": "/acskafka/api/v2" },
"changeOrigin": true
}
},
"loginUrl": "https://oidc.idc.cmbchina.cn/authorize"
}
},
"production": {
"inner": {
"proxy": {
"/api/v2": "https://mock.xiaojukeji.com/mock/8739",
"changeOrigin": true
},
"loginUrl": "https://mock.xiaojukeji.com/mock/8739"
},
"cmb": {
"proxy": {
"/api/v1/uc": {
"target": "http://cmbkafkagw-dev.paas.cmbchina.cn/",
"pathRewrite": { "^/api/v1/uc": "/uc/api/v1" },
"changeOrigin": true
},
"/api/v1": {
"target": "http://cmbkafkagw-dev.paas.cmbchina.cn/",
"pathRewrite": { "^/api/v1": "/cmbkafka-dev" },
"changeOrigin": true
}
},
"loginUrl": "https://oidc.idc.cmbchina.cn/authorize"
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,120 @@
{
"name": "layout-clusters-fe",
"port": "8000",
"version": "1.0.0",
"ident": "cluster",
"description": "多集群管理&amp;layout",
"author": "joysunchao <joysunchao@didiglobal.com>",
"keywords": [
"layout"
],
"homepage": "",
"license": "ISC",
"repository": {
"type": "git",
"url": ""
},
"scripts": {
"test": "echo \"Error: run tests from root\" && exit 1",
"start": "cross-env NODE_ENV=development webpack-dev-server",
"build": "rm -rf ../../pub/layout & cross-env NODE_ENV=production webpack --max_old_space_size=8000"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"dependencies": {
"@ant-design/compatible": "^1.0.8",
"@ant-design/icons": "^4.6.2",
"@types/react": "^17.0.39",
"@types/react-copy-to-clipboard": "^5.0.2",
"@types/react-dom": "^17.0.11",
"@types/react-highlight-words": "^0.16.0",
"@types/react-router-dom": "^5.3.3",
"@types/react-transition-group": "^4.2.2",
"@types/react-virtualized": "^9.21.13",
"axios": "^0.21.1",
"babel-preset-react-app": "^10.0.0",
"classnames": "^2.2.6",
"crypto-js": "^4.1.1",
"html-webpack-plugin": "^4.0.0",
"lodash": "^4.17.21",
"moment": "^2.24.0",
"react": "16.12.0",
"react-copy-to-clipboard": "^5.0.4",
"react-cron-antd": "^1.1.2",
"react-dom": "16.12.0",
"react-intl": "^3.2.1",
"react-joyride": "^2.5.0",
"single-spa": "5.9.3",
"single-spa-react": "2.14.0",
"webpack-bundle-analyzer": "^4.5.0"
},
"devDependencies": {
"@babel/core": "^7.5.5",
"@babel/plugin-proposal-class-properties": "^7.4.0",
"@babel/plugin-proposal-decorators": "^7.4.0",
"@babel/plugin-proposal-export-default-from": "^7.2.0",
"@babel/plugin-proposal-export-namespace-from": "^7.5.2",
"@babel/plugin-proposal-object-rest-spread": "^7.4.3",
"@babel/plugin-proposal-private-methods": "^7.14.5",
"@babel/plugin-transform-object-assign": "^7.12.1",
"@babel/plugin-transform-runtime": "^7.4.3",
"@babel/polyfill": "^7.12.1",
"@babel/preset-env": "^7.4.2",
"@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.14.5",
"knowdesign": "^1.3.6",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.1",
"@types/crypto-js": "^4.1.0",
"@types/lodash": "^4.14.171",
"@types/node": "^12.12.25",
"@types/pubsub-js": "^1.5.18",
"@typescript-eslint/eslint-plugin": "4.13.0",
"@typescript-eslint/parser": "4.13.0",
"babel-eslint": "10.1.0",
"babel-loader": "^8.2.2",
"babel-plugin-import": "^1.12.0",
"case-sensitive-paths-webpack-plugin": "^2.4.0",
"clean-webpack-plugin": "^3.0.0",
"copy-webpack-plugin": "^5.1.2",
"cross-env": "^7.0.2",
"css-loader": "^2.1.0",
"eslint": "^7.30.0",
"eslint-config-prettier": "8.3.0",
"eslint-plugin-prettier": "3.4.0",
"eslint-plugin-react": "7.22.0",
"eslint-plugin-react-hooks": "4.2.0",
"file-loader": "^6.0.0",
"hard-source-webpack-plugin": "^0.13.1",
"husky": "4.3.7",
"less": "^3.9.0",
"less-loader": "^4.1.0",
"lint-staged": "10.5.3",
"mini-css-extract-plugin": "^1.3.0",
"optimize-css-assets-webpack-plugin": "^5.0.1",
"prettier": "2.3.2",
"progress-bar-webpack-plugin": "^1.12.1",
"query-string": "^7.0.1",
"react-refresh": "^0.10.0",
"react-router-dom": "5.2.1",
"ts-loader": "^8.0.11",
"typescript": "^3.8.2",
"webpack": "^4.40.0",
"webpack-cli": "^3.2.3",
"webpack-dev-server": "^3.2.1",
"webpack-merge": "^4.2.1"
},
"lint-staged": {
"*.{js,tsx}": "eslint"
}
}

View File

@@ -0,0 +1,4 @@
declare module '*.png' {
const value: any;
export default value;
}

View File

@@ -0,0 +1,201 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
const ksPrefix = '/ks-km/api/v3';
const securityPrefix = '/logi-security/api/v1';
function getApi(path: string) {
return `${ksPrefix}${path}`;
}
// 指标类型对应的 type 值
export enum MetricType {
Topic = 100,
Cluster = 101,
Group = 102,
Broker = 103,
Partition = 104,
Replication = 105,
Controls = 901,
}
const api = {
// 登录 & 登出
login: `${securityPrefix}/account/login`,
logout: `${securityPrefix}/account/logout`,
// 全局信息
getUserInfo: (userId: number) => `${securityPrefix}/user/${userId}`,
getPermissionTree: `${securityPrefix}/permission/tree`,
getKafkaVersionItems: () => getApi('/kafka-versions-items'),
getSupportKafkaVersions: (clusterPhyId: string, type: MetricType) =>
getApi(`/clusters/${clusterPhyId}/types/${type}/support-kafka-versions`),
// 生产、消费客户端测试
postClientConsumer: getApi(`/clients/consumer`),
postClientProducer: getApi(`/clients/producer`),
// 集群均衡
getBalanceList: (clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/balance-overview`),
getBrokersMetaList: (clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/brokers-metadata`),
getTopicMetaList: (clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/topics-metadata`),
balanceStrategy: (clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/balance-strategy`),
balancePreview: (clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/balance-preview`),
getBalanceHistory: (clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/balance-history`),
getBalanceForm: (clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/balance-config`),
getBalancePlan: (clusterPhyId: number, jobId: number) => getApi(`/clusters/${clusterPhyId}/balance-plan/${jobId}`),
getPlatformConfig: (clusterPhyId: number, groupName: string) =>
getApi(`/platform-configs/clusters/${clusterPhyId}/groups/${groupName}/configs`),
putPlatformConfig: () => getApi(`/platform-configs`),
getCartInfo: (clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/balance-state`),
// 获取topic元信息
getTopicsMetaData: (topicName: string, clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/topics/${topicName}/metadata`),
getTopicsMetrics: (clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/topic-metrics`),
getConsumerGroup: (topicName: string, clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/topics/${topicName}/groups-basic`),
getTopicMetaData: (clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/topics-metadata`),
getTopicBrokersList: (clusterPhyId: string, topicName: number) => getApi(`/clusters/${clusterPhyId}/topics/${topicName}/brokers`), // 获取 topic brokers 信息
getPartitionMetricInfo: (clusterPhyId: string, topicName: string, brokerId: number, partitionId: number) =>
getApi(`/clusters/${clusterPhyId}/brokers/${brokerId}/topics/${topicName}/partitions/${partitionId}/latest-metrics`), // 获取分区详情数据
// dashbord 接口
phyClustersDashbord: getApi(`/physical-clusters/dashboard`),
supportKafkaVersion: getApi(`/support-kafka-versions`),
phyClusterState: getApi(`/physical-clusters/state`),
getOperatingStateList: (clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/groups-overview`),
// 物理集群接口
phyCluster: getApi(`/physical-clusters`),
getPhyClusterBasic: (clusterPhyId: number) => getApi(`/physical-clusters/${clusterPhyId}/basic`),
getPhyClusterMetrics: (clusterPhyId: number) => getApi(`/physical-clusters/${clusterPhyId}/latest-metrics`),
getClusterBasicExit: (clusterPhyName: string) => getApi(`/physical-clusters/${clusterPhyName}/basic-combine-exist`),
kafkaValidator: getApi(`/utils/kafka-validator`),
// @see https://api-kylin-xg02.intra.xiaojukeji.com/ks-km/swagger-ui.html#/KS-KafkaHealth-%E7%9B%B8%E5%85%B3%E6%8E%A5%E5%8F%A3(REST)/getHealthCheckConfigUsingGET
getClusterHealthyConfigs: (clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/health-configs`),
putPlatformConfigs: getApi(`/platform-configs`),
getClusterChangeLog: (clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/change-records`),
// group详情实时信息
getTopicGroupMetric: (params: { clusterId: number; topicName: string; groupName: string }) =>
getApi(`/clusters/${params.clusterId}/topics/${params.topicName}/groups/${params.groupName}/metric`),
// group详情历史信息
getConsumersMetadata: (clusterPhyId: number, groupName: string, topicName: string) =>
getApi(`/clusters/${clusterPhyId}/groups/${groupName}/topics/${topicName}/metadata-combine-exist`),
getTopicGroupMetricHistory: (clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/group-metrics`),
getTopicGroupPartitionsHistory: (clusterPhyId: number, groupName: string) =>
getApi(`/clusters/${clusterPhyId}/groups/${groupName}/partitions`),
resetGroupOffset: () => getApi('/group-offsets'),
// topics列表
getTopicsList: (clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/topics-overview`),
getReassignmentList: () => getApi(`/reassignment/topics-overview`),
getTaskPlanData: () => getApi(`/reassignment/replicas-change-plan`),
// 创建topic
addTopic: () => getApi(`/topics`),
deleteTopic: () => getApi(`/topics`),
expandPartitions: () => getApi(`/topics/expand-partitions`),
getDefaultTopicConfig: (clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/config-topics/default`),
getTopicState: (clusterPhyId: number, topicName: string) => getApi(`/clusters/${clusterPhyId}/topics/${topicName}/state`),
getTopicMetadata: (clusterPhyId: number, topicName: string) =>
getApi(`/clusters/${clusterPhyId}/topics/${topicName}/metadata-combine-exist`),
// 最新的指标值
getMetricPointsLatest: (clusterPhyId: number) => getApi(`/physical-clusters/${clusterPhyId}/latest-metrics`),
getTopicMetricPointsLatest: (clusterPhyId: number, topicName: string) =>
getApi(`/clusters/${clusterPhyId}/topics/${topicName}/latest-metrics`),
// 健康检查指标
getMetricPoints: (clusterPhyId: number) => getApi(`/physical-clusters/${clusterPhyId}/metric-points`),
// 单个Topic的健康检查指标
getTopicMetricPoints: (clusterPhyId: number, topicName: string) => getApi(`/clusters/${clusterPhyId}/topics/${topicName}/metric-points`),
// Broker列表接口
getBrokersList: (clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/brokers-overview`),
// Broker列表页健康检查指标
getBrokerMetricPoints: (clusterPhyId: number) => getApi(`/physical-clusters/${clusterPhyId}/latest-metrics`),
// Controller列表接口 /api/v3/clusters/{clusterPhyId}/controller-history「controller-change-log」
getChangeLogList: (clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/controller-history`),
getBrokersState: (clusterPhyId: string) => getApi(`/clusters/${clusterPhyId}/brokers-state`), // Broker 基础信息
// Controller列表接口
// getChangeLogList: (clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/controller-change-log`),
// GroupList 列表接口
getGroupACLBindingList: (clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/group-acl-bindings`),
/* Topic 详情 ↓↓↓↓↓↓↓↓↓↓*/
getTopicPartitionsSummary: (clusterPhyId: string, topicName: string) =>
getApi(`/clusters/${clusterPhyId}/topics/${topicName}/brokers-partitions-summary`),
getTopicPartitionsDetail: (clusterPhyId: string, topicName: string) => getApi(`/clusters/${clusterPhyId}/topics/${topicName}/partitions`),
getTopicMessagesList: (topicName: string, clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/topics/${topicName}/records`), // Messages列表
getTopicMessagesMetadata: (topicName: string, clusterPhyId: number) => getApi(`/clusters//${clusterPhyId}/topics/${topicName}/metadata`), // Messages列表
getTopicACLsList: (topicName: string, clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/topics/${topicName}/acl-Bindings`), // ACLs列表
getTopicConfigs: (topicName: string, clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/config-topics/${topicName}/configs`), // Configuration列表
getTopicEditConfig: () => getApi('/config-topics'),
/* Topic 详情 ↑↑↑↑↑↑↑↑↑↑↑*/
/* Broker 详情 ↓↓↓↓↓↓↓↓↓↓*/
getBrokerConfigs: (brokerId: number, clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/config-brokers/${brokerId}/configs`), // Configuration列表
getBrokerDataLogs: (brokerId: number, clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/brokers/${brokerId}/log-dirs`), // ACLs列表
getBrokerMetadata: (brokerId: number | string, clusterPhyId: number | string) =>
getApi(`/clusters/${clusterPhyId}/brokers/${brokerId}/metadata-combine-exist`), // Broker元数据
getBrokerDetailMetricPoints: (brokerId: number | string, clusterPhyId: number | string) =>
getApi(`/clusters/${clusterPhyId}/brokers/${brokerId}/latest-metrics`),
getBrokerEditConfig: () => getApi('/config-brokers'),
/* Broker 详情 ↑↑↑↑↑↑↑↑↑↑↑*/
// 具体资源健康检查详情
getResourceHealthDetail: (clusterPhyId: number, dimensionCode: number, resName: string) =>
getApi(`/clusters/${clusterPhyId}/dimensions/${dimensionCode}/resources/${resName}/health-detail`),
// 列表健康检查详情
getResourceListHealthDetail: (clusterPhyId: number) => getApi(`/clusters/${clusterPhyId}/health-detail`),
// Cluster 单集群详情页
getClusterDefaultMetricData: () => getApi('/physical-clusters/metrics-multi-value'),
getClusterMetricDataList: () => getApi('/physical-clusters/metrics'),
// BrokerDashboard & TopicDashboard 相关
getDashboardMetadata: (clusterPhyId: string, type: MetricType) =>
getApi(`/clusters/${clusterPhyId}/${MetricType[type].toLowerCase()}s-metadata`), // 集群节点信息
getDashboardMetricList: (clusterPhyId: string, type: MetricType) => getApi(`/clusters/${clusterPhyId}/types/${type}/user-metric-config`), // 默认选中的指标项
getDashboardMetricChartData: (clusterPhyId: string, type: MetricType) =>
getApi(`/clusters/${clusterPhyId}/${MetricType[type].toLowerCase()}-metrics`), // 图表数据Z
// ! Jobs 集群任务相关接口
getJobsList: (clusterPhyId: string) => getApi(`/clusters/${clusterPhyId}/jobs-overview`),
getJobsState: (clusterPhyId: string) => getApi(`/clusters/${clusterPhyId}/jobs-state`),
getJobDetail: (clusterPhyId: string, jobId: any) => getApi(`/clusters/${clusterPhyId}/jobs/${jobId}/detail`),
getJobsPlanRebalance: (clusterPhyId: string, jobId: any) => getApi(`/clusters/${clusterPhyId}/balance-plan/${jobId}`),
getJobsScheduleRebalance: (clusterPhyId: string, jobId: any) => getApi(`/clusters/${clusterPhyId}/balance-schedule/${jobId}`),
getJobsDelete: (clusterPhyId: string, jobId: any) => getApi(`/clusters/${clusterPhyId}/jobs/${jobId}`),
getJobTraffic: (clusterPhyId: string, jobId: any, flowLimit: any) => {
return getApi(`/clusters/${clusterPhyId}/jobs/${jobId}/traffic/${flowLimit}`);
},
getJobNodeTraffic: (clusterPhyId: string, jobId: any) => {
return getApi(`/clusters/${clusterPhyId}/jobs/${jobId}/node/traffic`);
},
getJobPartitionDetail: (clusterPhyId: string, jobId: any, topicName: any) => {
return getApi(`/clusters/${clusterPhyId}/jobs/${jobId}/${topicName}/partition-detail`);
},
// Security - ACLs
getACLs: (clusterPhyId: string) => getApi(`/clusters/${clusterPhyId}/acl-bindings`),
addACL: getApi('/kafka-acls/batch'),
delACLs: getApi('/kafka-acls'),
// Security - Users
getKafkaUsers: (clusterPhyId: string) => getApi(`/clusters/${clusterPhyId}/kafka-users`),
kafkaUser: getApi('/kafka-users'),
getKafkaUserToken: (clusterPhyId: string, kafkaUser: string) => getApi(`/clusters/${clusterPhyId}/kafka-users/${kafkaUser}/token`),
updateKafkaUserToken: getApi('/kafka-users/token'),
//迁移任务、扩缩副本任务
createTask: (clusterPhyId: string) => getApi(`/clusters/${clusterPhyId}/jobs`),
//获取topic原数据信息
getOneTopicMetaData: (clusterPhyId: string, topicName: string) => getApi(`/clusters/${clusterPhyId}/topics/${topicName}/metadata`),
//获取迁移任务预览
getMovePlanTaskData: () => getApi(`/reassignment/replicas-move-plan`),
//获取任务详情
getJobsTaskData: (clusterPhyId: string, jobId: string | number) => getApi(`/clusters/${clusterPhyId}/jobs/${jobId}/modify-detail`),
//编辑任务
putJobsTaskData: (clusterPhyId: string) => getApi(`/clusters/${clusterPhyId}/jobs`),
};
export default api;

View File

@@ -0,0 +1,220 @@
/* eslint-disable no-constant-condition */
import '@babel/polyfill';
import React, { useState, useEffect, useLayoutEffect } from 'react';
import { BrowserRouter, Switch, Route, useLocation, useHistory } from 'react-router-dom';
import { get as lodashGet } from 'lodash';
import { DProLayout, AppContainer, IconFont, Menu, Utils, Page403, Page404, Page500, Modal } from 'knowdesign';
import dantdZhCN from 'knowdesign/lib/locale/zh_CN';
import dantdEnUS from 'knowdesign/lib/locale/en_US';
import { DotChartOutlined } from '@ant-design/icons';
import { licenseEventBus } from './constants/axiosConfig';
import intlZhCN from './locales/zh';
import intlEnUS from './locales/en';
import registerApps from '../config/registerApps';
import feSystemsConfig from '../config/systemsConfig';
import './index.less';
import { Login } from './pages/Login';
import { getLicenseInfo } from './constants/common';
import api from './api';
import ClusterContainer from './pages/index';
import NoLicense from './pages/NoLicense';
import ksLogo from './assets/ks-logo.png';
interface ILocaleMap {
[index: string]: any;
}
const localeMap: ILocaleMap = {
'zh-CN': {
dantd: dantdZhCN,
intl: 'zh-CN',
intlMessages: intlZhCN,
},
en: {
dantd: dantdEnUS,
intl: 'en',
intlMessages: intlEnUS,
},
};
const primaryFeConf = feSystemsConfig.feConfig;
const systemsConfig = feSystemsConfig.systemsConfig as any;
const defaultLanguage = 'zh-CN';
export const { Provider, Consumer } = React.createContext('zh');
const judgePage404 = () => {
const { pathname } = window.location;
const paths = pathname.split('/');
const exceptionLocationPaths = ['/404', '/500', '/403'];
const row = systemsConfig.filter((item: any) => item.ident === paths?.[1]);
if (exceptionLocationPaths.indexOf(pathname) < -1 && paths?.[1] && !row.length) {
window.location.href = '/404';
}
};
const logout = () => {
Utils.request(api.logout, {
method: 'POST',
}).then((res) => {
window.location.href = '/login';
});
};
const LicenseLimitModal = () => {
const [visible, setVisible] = useState<boolean>(false);
const [msg, setMsg] = useState<string>('');
useLayoutEffect(() => {
licenseEventBus.on('licenseError', (desc: string) => {
!visible && setVisible(true);
setMsg(desc);
});
return () => {
licenseEventBus.removeAll('licenseError');
};
}, []);
return (
<Modal
visible={visible}
centered={true}
width={400}
zIndex={10001}
title={
<>
<IconFont type="icon-yichang" style={{ marginRight: 10, fontSize: 18 }} />
</>
}
footer={null}
onCancel={() => setVisible(false)}
>
<div style={{ margin: '0 28px', lineHeight: '24px' }}>
<div>
{msg}<a></a>
</div>
</div>
</Modal>
);
};
const AppContent = (props: { setlanguage: (language: string) => void }) => {
const { pathname } = useLocation();
const history = useHistory();
const userInfo = localStorage.getItem('userInfo');
const [curActiveAppName, setCurActiveAppName] = useState('');
useEffect(() => {
if (pathname.startsWith('/config')) {
setCurActiveAppName('config');
} else {
setCurActiveAppName('cluster');
}
}, [pathname]);
return (
<DProLayout.Container
headerProps={{
title: (
<div>
<img className="header-logo" src={ksLogo} />
</div>
),
username: userInfo ? JSON.parse(userInfo)?.userName : '',
icon: <DotChartOutlined />,
quickEntries: [
{
icon: <IconFont type="icon-duojiqunguanli" />,
txt: '多集群管理',
ident: '',
active: curActiveAppName === 'cluster',
},
{
icon: <IconFont type="icon-xitongguanli" />,
txt: '系统管理',
ident: 'config',
active: curActiveAppName === 'config',
},
],
isFixed: false,
userDropMenuItems: [
<Menu.Item key={0} onClick={logout}>
</Menu.Item>,
],
onChangeLanguage: props.setlanguage,
onClickQuickEntry: (qe) => {
history.push({
pathname: '/' + (qe.ident || ''),
});
},
onClickMain: () => {
history.push('/');
},
}}
onMount={(customProps: any) => {
judgePage404();
registerApps(systemsConfig, { ...customProps, getLicenseInfo, licenseEventBus }, () => {
// postMessage();
});
}}
>
<>
<Switch>
<Route path="/403" exact component={Page403} />
<Route path="/404" exact component={Page404} />
<Route path="/500" exact component={Page500} />
<Route
render={() => {
return (
<>
{curActiveAppName === 'cluster' && <ClusterContainer />}
<div id="ks-layout-container" />
</>
);
}}
/>
</Switch>
<LicenseLimitModal />
</>
</DProLayout.Container>
);
};
export default function App(): JSX.Element {
const [language, setlanguage] = useState(navigator.language.substr(0, 2));
const intlMessages = lodashGet(localeMap[language], 'intlMessages', intlZhCN);
const [feConf] = useState(primaryFeConf || {});
const locale = lodashGet(localeMap[defaultLanguage], 'intl', 'zh-CN');
const antdLocale = lodashGet(localeMap[defaultLanguage], 'dantd', dantdZhCN);
const pageTitle = lodashGet(feConf, 'title');
if (pageTitle) {
document.title = pageTitle;
}
useEffect(() => {
window.postMessage(
{
type: 'language',
value: language,
},
window.location.origin
);
}, [language]);
return (
<>
<AppContainer intlProvider={{ locale, messages: intlMessages }} antdProvider={{ locale: antdLocale }} store>
<BrowserRouter basename="">
<Switch>
<Route path="/login" component={Login} />
<Route path="/no-license" exact component={NoLicense} />
<Route render={() => <AppContent setlanguage={setlanguage} />} />
</Switch>
</BrowserRouter>
</AppContainer>
</>
);
}

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="26px" height="6px" viewBox="0 0 26 6" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>形状结合 3</title>
<defs>
<linearGradient x1="-13.5966047%" y1="52.230507%" x2="206.130712%" y2="51.6554654%" id="linearGradient-1">
<stop stop-color="#556EE6" stop-opacity="0.02" offset="0%"></stop>
<stop stop-color="#556EE6" offset="100%"></stop>
</linearGradient>
</defs>
<g id="修改" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="JOB查看进度-均衡计划" transform="translate(-592.000000, -233.000000)" fill="url(#linearGradient-1)">
<g id="编组-18" transform="translate(384.000000, 133.000000)">
<g id="编组-15" transform="translate(161.000000, 9.000000)">
<g id="编组-4" transform="translate(0.000000, 84.000000)">
<g id="形状结合-3" transform="translate(47.000000, 7.562925)">
<path d="M17.0539865,0.079753169 L24.4180254,3.2056022 C24.8792975,3.40140061 25.1173077,3.90284648 24.9962642,4.37384348 L25,4.28609753 C25,4.83838228 24.5522847,5.28609753 24,5.28609753 L1,5.28609753 C0.44771525,5.28609753 6.76353751e-17,4.83838228 0,4.28609753 C-6.76353751e-17,3.73381278 0.44771525,3.28609753 1,3.28609753 L19.49,3.286 L16.2725243,1.92076288 C15.7641435,1.70496803 15.5269557,1.11790769 15.7427505,0.609526894 C15.9585454,0.101146101 16.5456057,-0.136041675 17.0539865,0.079753169 Z" id="形状结合"></path>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="11px" height="3px" viewBox="0 0 11 3" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>编组 6</title>
<g id="修改" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="JOB查看进度-均衡计划" transform="translate(-643.000000, -150.000000)" fill="#74788D">
<g id="编组-18" transform="translate(384.000000, 133.000000)">
<g id="编组-15" transform="translate(161.000000, 9.000000)">
<g id="编组-10" transform="translate(70.000000, 1.000000)">
<g id="编组-6" transform="translate(28.000000, 7.000000)">
<path d="M7.50249727,0.299092488 L9.8984038,2.10453756 C10.1189409,2.27072419 10.1630007,2.58422572 9.99681404,2.80476282 C9.88276439,2.95611182 9.69933345,3.0243448 9.52418346,2.99854706 L9.5,3 L9.5,3 L0.5,3 C0.223857625,3 3.38176876e-17,2.77614237 0,2.5 C-3.38176876e-17,2.22385763 0.223857625,2 0.5,2 L8.097,1.99938169 L6.90068224,1.097728 C6.68014514,0.931541369 6.63608537,0.618039838 6.802272,0.397502732 C6.96845863,0.176965625 7.28196016,0.132905859 7.50249727,0.299092488 Z" id="形状结合"></path>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -0,0 +1,35 @@
function getApi(path: string) {
const prefix = '/api/uic';
return `${prefix}${path}`;
}
function getOrderApi(path: string) {
const prefix = '/api/ticket';
return `${prefix}${path}`;
}
const api = {
login: getApi('/auth/login'),
logout: getApi('/auth/logout'),
selftProfile: getApi('/self/profile'),
selftPassword: getApi('/self/password'),
selftToken: getApi('/self/token'),
user: getApi('/user'),
tenant: getApi('/tenant'),
team: getApi('/team'),
configs: getApi('/configs'),
role: getApi('/role'),
ops: getApi('/ops'),
log: getApi('/log'),
homeStatistics: getApi('/home/statistics'),
project: getApi('/project'),
projects: getApi('/projects'),
queues: getOrderApi('/queues'),
tickets: getOrderApi('/tickets'),
template: getOrderApi('/templates'),
upload: getOrderApi('/file/upload'),
task: '/api/job-ce/task',
};
export default api;

View File

@@ -0,0 +1,4 @@
export const appname = 'ecmc';
export const prefixCls = appname;
export const loginPath = `/login`;
export const defaultPageSizeOptions = ['10', '30', '50', '100', '300', '500', '1000'];

View File

@@ -0,0 +1,18 @@
export const regNonnegativeInteger = /^\d+$/g; // 非负正整数
export const regOddNumber = /^\d*[13579]$/; //奇数
export const regClusterName = /^[\u4E00-\u9FA5A-Za-z0-9\_\-\!\"\#\$\%&'()\*\+,./\:\;\<=\>?\@\[\\\]^\`\{\|\}~]*$/im; // 大、小写字母、数字、-、_ new RegExp('\[a-z0-9_-]$', 'g')
export const regUsername = /^[_a-zA-Z-]*$/; // 大、小写字母、数字、-、_ new RegExp('\[a-z0-9_-]$', 'g')
export const regExp = /^[ ]+$/; // 不能为空
export const regNonnegativeNumber = /^[+]{0,1}(\d+)$|^[+]{0,1}(\d+\.\d+)$/; // 非负数
export const regTwoNumber = /^-?\d+\.?\d{0,2}$/; // 两位小数
export const regTemplateName = /^[a-z0-9\._-]*$/; // 仅支持小写字母、数字、_、-、.的组合
export const regIp = /((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})(\.((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})){3}/g; // ip
export const regKafkaPassword = /^[A-Za-z0-9_\-!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]*$/;

View File

@@ -0,0 +1,72 @@
import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import CardBar from './index';
import { Tag, Utils } from 'knowdesign';
import api from '@src/api';
const ACLsCardBar = () => {
const { clusterId } = useParams<{
clusterId: string;
}>();
const [loading, setLoading] = useState(false);
const [cardData, setCardData] = useState([]);
const cardItems = ['AclEnable', 'Acls', 'AclUsers', 'AclTopics', 'AclGroups'];
const getCartInfo = () => {
return Utils.request(api.getMetricPointsLatest(Number(clusterId)), {
method: 'POST',
data: cardItems,
});
};
useEffect(() => {
setLoading(true);
// 获取右侧状态
getCartInfo().then(
(res: {
clusterPhyId: number;
metrics: {
[metric: string]: number;
};
}) => {
const { AclEnable, Acls, AclUsers, AclTopics, AclGroups } = res.metrics;
const cardMap = [
{
title: 'Enable',
value() {
return (
<span style={{ fontFamily: 'HelveticaNeue', fontSize: 35, color: AclEnable ? '#00C0A2' : '#F58342' }}>
{AclEnable ? 'Yes' : 'No'}
</span>
);
},
customStyle: {
// 自定义cardbar样式
marginLeft: 0,
},
},
{
title: 'ACLs',
value: Acls,
},
{
title: 'Users',
value: AclUsers,
},
{
title: 'Topics',
value: AclTopics,
},
{
title: 'Consumer Groups',
value: AclGroups,
},
];
setCardData(cardMap);
setLoading(false);
}
);
}, [clusterId]);
return <CardBar scene="broker" cardColumns={cardData} loading={loading}></CardBar>;
};
export default ACLsCardBar;

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