初始化3.0.0版本
5
km-console/packages/config-manager-fe/.eslintignore
Normal file
@@ -0,0 +1,5 @@
|
||||
test/
|
||||
scripts/
|
||||
build/
|
||||
public/
|
||||
types/
|
||||
13
km-console/packages/config-manager-fe/.gitignore
vendored
Normal 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
|
||||
11
km-console/packages/config-manager-fe/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# `logi-fe`
|
||||
|
||||
> TODO: description
|
||||
|
||||
## Usage
|
||||
|
||||
### 启动
|
||||
* npm i @didi/d1-cli -g
|
||||
* d1 start
|
||||
|
||||
### 常见问题
|
||||
@@ -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'],
|
||||
},
|
||||
};
|
||||
200
km-console/packages/config-manager-fe/config/d1-webpack.base.js
Normal 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',
|
||||
},
|
||||
};
|
||||
};
|
||||
17
km-console/packages/config-manager-fe/config/theme.js
Executable 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,
|
||||
};
|
||||
5
km-console/packages/config-manager-fe/d1.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const d1Config = require('./d1.json');
|
||||
d1Config.appConfig.webpackChain = function (config) {
|
||||
// config.devServer.port(10000);
|
||||
};
|
||||
module.exports = d1Config;
|
||||
29
km-console/packages/config-manager-fe/d1.json
Normal 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"
|
||||
}
|
||||
14
km-console/packages/config-manager-fe/mock.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"/api/v1/:id": {
|
||||
"data": {
|
||||
"list|5-10": [
|
||||
{
|
||||
"name": "@cname()",
|
||||
"address": "@cname()",
|
||||
"age|18-50": 100
|
||||
}
|
||||
]
|
||||
},
|
||||
"code|0-500": 200
|
||||
}
|
||||
}
|
||||
102
km-console/packages/config-manager-fe/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export const defaultPagination = {
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
position: 'bottomRight',
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['10', '20', '50', '100'],
|
||||
};
|
||||
41
km-console/packages/config-manager-fe/src/api/index.ts
Executable 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;
|
||||
76
km-console/packages/config-manager-fe/src/app.tsx
Normal 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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
34
km-console/packages/config-manager-fe/src/constants/common.ts
Executable 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;
|
||||
37
km-console/packages/config-manager-fe/src/constants/menu.ts
Executable 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`,
|
||||
};
|
||||
10
km-console/packages/config-manager-fe/src/index.html
Executable 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>
|
||||
13
km-console/packages/config-manager-fe/src/index.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
28
km-console/packages/config-manager-fe/src/index.tsx
Normal 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];
|
||||
10
km-console/packages/config-manager-fe/src/locales/en.tsx
Executable 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',
|
||||
};
|
||||
12
km-console/packages/config-manager-fe/src/locales/zh.tsx
Executable file
@@ -0,0 +1,12 @@
|
||||
import { systemKey } from '../constants/menu';
|
||||
/**
|
||||
* 用于左侧菜单与顶部路由导航中文展示,key值与各页面路径对应,比如dashboard页,路由:/cluster/dashbord,key值:menu.cluster.dashborad
|
||||
*/
|
||||
export default {
|
||||
[`menu.${systemKey}.setting`]: '配置管理',
|
||||
[`menu.${systemKey}.user`]: '用户管理',
|
||||
[`menu.${systemKey}.operationLog`]: '审计日志',
|
||||
|
||||
'sider.footer.hide': '收起',
|
||||
'sider.footer.expand': '展开',
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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'>;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
const HomePage = () => {
|
||||
return <Redirect to="/setting" />;
|
||||
};
|
||||
|
||||
export default HomePage;
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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: '角色名称只能由中英文大小写、数字、下划线(_)组成,长度限制在3~128字符',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<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} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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: '用户账号只能由英文大小写、数字、下划线(_)组成,长度限制在3~128字符' },
|
||||
]}
|
||||
>
|
||||
<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: '用户实名只能由中英文大小写、数字、下划线组成,长度限制在1~128字符' },
|
||||
]}
|
||||
>
|
||||
<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} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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 }[];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
31
km-console/packages/config-manager-fe/src/pages/index.ts
Normal 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,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
30
km-console/packages/config-manager-fe/tsconfig.json
Executable 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/*",
|
||||
],
|
||||
}
|
||||
55
km-console/packages/config-manager-fe/webpack.config.js
Normal 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: {},
|
||||
},
|
||||
});
|
||||
5
km-console/packages/layout-clusters-fe/.eslintignore
Normal file
@@ -0,0 +1,5 @@
|
||||
test/
|
||||
scripts/
|
||||
build/
|
||||
public/
|
||||
types/
|
||||
14
km-console/packages/layout-clusters-fe/.gitignore
vendored
Normal 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
|
||||
0
km-console/packages/layout-clusters-fe/CHANGELOG.md
Executable file
32
km-console/packages/layout-clusters-fe/README.md
Normal 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.js:antd 主题配置
|
||||
- webpack.dev.config.js:webpack 开发环境补充配置,覆盖默认配置
|
||||
- webpack.build.config.js:webpack 构建补充配置,覆盖默认配置
|
||||
- webpackConfigResolveAlias.js 文件路径别名配置
|
||||
- src:源代码所在目录
|
||||
- assets:全局资源 img、css
|
||||
- common: 全局配置、通用方法
|
||||
- components:公共组件
|
||||
- pages:路由匹配的页面组件
|
||||
- app.jsx 菜单、路由配置组件
|
||||
- index.html:单页
|
||||
- index.jsx:入口文件
|
||||
- fetk.config.js 开发工具配置页面
|
||||
|
||||
@@ -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;
|
||||
116
km-console/packages/layout-clusters-fe/config/CoverHtmlWebpackPlugin.js
Executable 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;
|
||||
162
km-console/packages/layout-clusters-fe/config/d1-spa-webpack.js
Normal 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',
|
||||
},
|
||||
};
|
||||
};
|
||||
183
km-console/packages/layout-clusters-fe/config/d1-webpack.base.js
Normal 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;
|
||||
154
km-console/packages/layout-clusters-fe/config/registerApps.js
Executable 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();
|
||||
}
|
||||
@@ -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;
|
||||
17
km-console/packages/layout-clusters-fe/config/theme.js
Executable 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,
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
var path = require('path');
|
||||
|
||||
module.exports = {
|
||||
react: path.resolve('./node_modules/react'),
|
||||
};
|
||||
5
km-console/packages/layout-clusters-fe/d1.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const d1Config = require('./d1.json');
|
||||
d1Config.appConfig.webpackChain = function (config) {
|
||||
// config.devServer.port(10000);
|
||||
};
|
||||
module.exports = d1Config;
|
||||
29
km-console/packages/layout-clusters-fe/d1.json
Normal 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"
|
||||
}
|
||||
56
km-console/packages/layout-clusters-fe/env.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
km-console/packages/layout-clusters-fe/favicon.ico
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
120
km-console/packages/layout-clusters-fe/package.json
Normal file
@@ -0,0 +1,120 @@
|
||||
{
|
||||
"name": "layout-clusters-fe",
|
||||
"port": "8000",
|
||||
"version": "1.0.0",
|
||||
"ident": "cluster",
|
||||
"description": "多集群管理&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"
|
||||
}
|
||||
}
|
||||
4
km-console/packages/layout-clusters-fe/src/@types/index.d.ts
vendored
Executable file
@@ -0,0 +1,4 @@
|
||||
declare module '*.png' {
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
201
km-console/packages/layout-clusters-fe/src/api/index.ts
Executable 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;
|
||||
220
km-console/packages/layout-clusters-fe/src/app.tsx
Executable 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 |
@@ -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 |
BIN
km-console/packages/layout-clusters-fe/src/assets/chart.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 337 B |
|
After Width: | Height: | Size: 372 B |
BIN
km-console/packages/layout-clusters-fe/src/assets/dashborad.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
km-console/packages/layout-clusters-fe/src/assets/empty.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
BIN
km-console/packages/layout-clusters-fe/src/assets/ks-logo.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 15 KiB |
BIN
km-console/packages/layout-clusters-fe/src/assets/leftTop.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
km-console/packages/layout-clusters-fe/src/assets/loading.gif
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 142 KiB |
BIN
km-console/packages/layout-clusters-fe/src/assets/state.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
35
km-console/packages/layout-clusters-fe/src/common/api.tsx
Executable 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;
|
||||
4
km-console/packages/layout-clusters-fe/src/common/config.tsx
Executable 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'];
|
||||
18
km-console/packages/layout-clusters-fe/src/common/reg.ts
Normal 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_\-!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]*$/;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,63 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useHistory, useLocation, useParams } from 'react-router-dom';
|
||||
import CardBar from '@src/components/CardBar';
|
||||
import { healthDataProps } from '.';
|
||||
import { Tag, Utils } from 'knowdesign';
|
||||
import Api from '@src/api';
|
||||
import { hashDataParse } from '@src/constants/common';
|
||||
|
||||
export default (props: { record: any }) => {
|
||||
const { record } = props;
|
||||
const urlParams = useParams<{ clusterId: string; brokerId: string }>();
|
||||
const urlLocation = useLocation<any>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [cardData, setCardData] = useState([]);
|
||||
const [healthData, setHealthData] = useState<healthDataProps>({
|
||||
score: 0,
|
||||
passed: 0,
|
||||
total: 0,
|
||||
alive: 0,
|
||||
});
|
||||
const healthItems = ['HealthScore_Topics', 'HealthCheckPassed_Topics', 'HealthCheckTotal_Topics', 'live'];
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
Utils.post(Api.getBrokerDetailMetricPoints(hashDataParse(urlLocation.hash)?.brokerId, urlParams?.clusterId), [
|
||||
'Partitions',
|
||||
'Leaders',
|
||||
'PartitionURP',
|
||||
'HealthScore',
|
||||
'HealthCheckPassed',
|
||||
'HealthCheckTotal',
|
||||
'Alive',
|
||||
]).then((data: any) => {
|
||||
setLoading(false);
|
||||
const rightData = JSON.parse(JSON.stringify(data.metrics));
|
||||
const cordRightMap = [
|
||||
{
|
||||
title: 'Partitions',
|
||||
value: rightData['Partitions'] || '-',
|
||||
},
|
||||
{
|
||||
title: 'Leaders',
|
||||
value: rightData['Leaders'] || '-',
|
||||
},
|
||||
{
|
||||
title: 'Under Replicated Partitions',
|
||||
value: rightData['PartitionURP'] || '-',
|
||||
},
|
||||
];
|
||||
const healthResData: any = {};
|
||||
healthResData.score = data?.metrics?.['HealthScore'] || 0;
|
||||
healthResData.passed = data?.metrics?.['HealthCheckPassed'] || 0;
|
||||
healthResData.total = data?.metrics?.['HealthCheckTotal'] || 0;
|
||||
healthResData.alive = data?.metrics?.['Alive'] || 0;
|
||||
setCardData(cordRightMap);
|
||||
setHealthData(healthResData);
|
||||
// setCardData(data.metrics)
|
||||
});
|
||||
}, []);
|
||||
return (
|
||||
<CardBar record={record} scene="broker" healthData={healthData} cardColumns={cardData} showCardBg={false} loading={loading}></CardBar>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,110 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import CardBar from '@src/components/CardBar';
|
||||
import { healthDataProps } from '.';
|
||||
import { Tag, Utils } from 'knowdesign';
|
||||
import api from '@src/api';
|
||||
|
||||
export default () => {
|
||||
const routeParams = useParams<{
|
||||
clusterId: string;
|
||||
}>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [cardData, setCardData] = useState([]);
|
||||
const [healthData, setHealthData] = useState<healthDataProps>({
|
||||
score: 0,
|
||||
passed: 0,
|
||||
total: 0,
|
||||
alive: 0,
|
||||
});
|
||||
const cardItems = ['Partitions', 'PartitionsSkew', 'Leaders', 'LeadersSkew', 'LogSize'];
|
||||
const healthItems = ['HealthScore_Brokers', 'HealthCheckPassed_Brokers', 'HealthCheckTotal_Brokers', 'Alive'];
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
// 获取左侧健康度
|
||||
const brokerMetric = Utils.post(api.getBrokerMetricPoints(Number(routeParams.clusterId)), healthItems).then((data: any) => {
|
||||
const healthResData: any = {};
|
||||
// healthResData.score = data?.find((item:any) => item.metricName === 'HealthScore_Brokers')?.value || 0;
|
||||
// healthResData.passed = data?.find((item:any) => item.metricName === 'HealthCheckPassed_Brokers')?.value || 0;
|
||||
// healthResData.total = data?.find((item:any) => item.metricName === 'HealthCheckTotal_Brokers')?.value || 0;
|
||||
healthResData.score = data?.metrics?.['HealthScore_Brokers'] || 0;
|
||||
healthResData.passed = data?.metrics?.['HealthCheckPassed_Brokers'] || 0;
|
||||
healthResData.total = data?.metrics?.['HealthCheckTotal_Brokers'] || 0;
|
||||
healthResData.alive = data?.metrics?.['Alive'] || 0;
|
||||
setHealthData(healthResData);
|
||||
});
|
||||
// 获取右侧状态
|
||||
const brokersState = Utils.request(api.getBrokersState(routeParams?.clusterId)).then((data) => {
|
||||
const rightData = JSON.parse(JSON.stringify(data));
|
||||
const cordRightMap = [
|
||||
{
|
||||
title: 'Brokers',
|
||||
value: () => {
|
||||
return (
|
||||
<div style={{ width: '100%', display: 'flex', alignItems: 'end' }}>
|
||||
<span>{rightData?.brokerCount}</span>
|
||||
<span style={{ display: 'flex', fontSize: '13px' }}>
|
||||
{rightData?.brokerVersionList?.map((item: any, key: number) => {
|
||||
return (
|
||||
<Tag
|
||||
style={{
|
||||
padding: '2px 5px',
|
||||
marginLeft: '8px',
|
||||
backgroundColor: '#ECECF6',
|
||||
fontFamily: 'Helvetica Neue, PingFangSC',
|
||||
}}
|
||||
key={key}
|
||||
>
|
||||
{item}
|
||||
</Tag>
|
||||
);
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Controller',
|
||||
value: () => {
|
||||
return rightData?.kafkaController && rightData?.kafkaControllerAlive ? (
|
||||
<div style={{ width: '100%', display: 'flex', alignItems: 'end' }}>
|
||||
<span>{rightData?.kafkaController.brokerId}</span>
|
||||
<span style={{ display: 'flex', fontSize: '13px' }}>
|
||||
<Tag
|
||||
style={{ padding: '2px 5px', marginLeft: '8px', backgroundColor: '#ECECF6', fontFamily: 'Helvetica Neue, PingFang SC' }}
|
||||
>
|
||||
{rightData?.kafkaController.brokerHost}
|
||||
</Tag>
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<span style={{ fontFamily: 'Helvetica Neue' }}>None</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Similar Config',
|
||||
value: () => {
|
||||
return (
|
||||
<>
|
||||
{
|
||||
<span style={{ fontFamily: 'Helvetica Neue', fontSize: 36, color: rightData?.configSimilar ? '' : '#F58342' }}>
|
||||
{rightData?.configSimilar ? 'YES' : 'NO'}
|
||||
</span>
|
||||
}
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
setCardData(cordRightMap);
|
||||
});
|
||||
Promise.all([brokerMetric, brokersState]).then((res) => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, [routeParams.clusterId]);
|
||||
// console.log('cardData', cardData, healthData);
|
||||
return <CardBar scene="broker" healthData={healthData} cardColumns={cardData} loading={loading}></CardBar>;
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import CardBar from '@src/components/CardBar';
|
||||
import { healthDataProps } from '.';
|
||||
import { Utils } from 'knowdesign';
|
||||
import api from '@src/api';
|
||||
|
||||
export default () => {
|
||||
const routeParams = useParams<{
|
||||
clusterId: string;
|
||||
}>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [cardData, setCardData] = useState([]);
|
||||
const [healthData, setHealthData] = useState<healthDataProps>({
|
||||
score: 0,
|
||||
passed: 0,
|
||||
total: 0,
|
||||
alive: 0,
|
||||
});
|
||||
const [healthDetail, setHealthDetail] = useState([]);
|
||||
const cardItems = ['Groups', 'GroupActives', 'GroupEmptys', 'GroupRebalances', 'GroupDeads'];
|
||||
const healthItems = ['HealthScore_Groups', 'HealthCheckPassed_Groups', 'HealthCheckTotal_Groups', 'Alive'];
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
Utils.post(api.getMetricPointsLatest(Number(routeParams.clusterId)), cardItems.concat(healthItems)).then((data: any) => {
|
||||
setLoading(false);
|
||||
// setCardData(data
|
||||
// .filter((item: any) => cardItems.indexOf(item.metricName) >= 0)
|
||||
// .map((item: any) => ({ title: item.metricName, value: item.value }))
|
||||
// )
|
||||
setCardData(
|
||||
cardItems.map((item) => {
|
||||
if (item === 'GroupDeads') {
|
||||
return { title: item, value: <span style={{ color: data.metrics[item] !== 0 ? '#F58342' : '' }}>{data.metrics[item]}</span> };
|
||||
}
|
||||
return { title: item, value: data.metrics[item] };
|
||||
})
|
||||
);
|
||||
const healthResData: any = {};
|
||||
healthResData.score = data.metrics['HealthScore_Groups'] || 0;
|
||||
healthResData.passed = data.metrics['HealthCheckPassed_Groups'] || 0;
|
||||
healthResData.total = data.metrics['HealthCheckTotal_Groups'] || 0;
|
||||
healthResData.alive = data.metrics['Alive'] || 0;
|
||||
setHealthData(healthResData);
|
||||
});
|
||||
}, []);
|
||||
return <CardBar scene="group" healthData={healthData} cardColumns={cardData} loading={loading}></CardBar>;
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import CardBar from '@src/components/CardBar';
|
||||
import { healthDataProps } from '.';
|
||||
import { Tag, Utils } from 'knowdesign';
|
||||
import Api from '@src/api';
|
||||
|
||||
export default () => {
|
||||
const routeParams = useParams<{
|
||||
clusterId: string;
|
||||
}>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [cardData, setCardData] = useState([]);
|
||||
const [healthData, setHealthData] = useState<healthDataProps>({
|
||||
score: 0,
|
||||
passed: 0,
|
||||
total: 0,
|
||||
alive: 0,
|
||||
});
|
||||
const cardItems = ['Partitions', 'PartitionsSkew', 'Leaders', 'LeadersSkew', 'LogSize'];
|
||||
const healthItems = ['HealthScore_Brokers', 'HealthCheckPassed_Brokers', 'HealthCheckTotal_Brokers', 'alive'];
|
||||
const getCordRightMap = (data: any) => {
|
||||
const cordRightMap = [
|
||||
{
|
||||
title: 'Jobs',
|
||||
value: data?.jobNu === 0 || data?.jobNu ? data?.jobNu : '-',
|
||||
customStyle: {
|
||||
// 自定义cardbar样式
|
||||
marginLeft: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Doing',
|
||||
value: data?.runningNu === 0 || data?.runningNu ? data?.runningNu : '-',
|
||||
},
|
||||
{
|
||||
title: 'Prepare',
|
||||
value: data?.waitingNu === 0 || data?.waitingNu ? data?.waitingNu : '-',
|
||||
},
|
||||
{
|
||||
title: 'Success',
|
||||
value: data?.successNu === 0 || data?.successNu ? data?.successNu : '-',
|
||||
},
|
||||
{
|
||||
title: 'Fail',
|
||||
value: data?.failedNu === 0 || data?.failedNu ? data?.failedNu : '-',
|
||||
},
|
||||
];
|
||||
return cordRightMap;
|
||||
};
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
// 获取状态
|
||||
Utils.request(Api.getJobsState(routeParams?.clusterId))
|
||||
.then((data) => {
|
||||
const rightData = JSON.parse(JSON.stringify(data));
|
||||
setCardData(getCordRightMap(rightData));
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setCardData(getCordRightMap({}));
|
||||
setLoading(false);
|
||||
});
|
||||
}, [routeParams.clusterId]);
|
||||
return <CardBar scene="broker" cardColumns={cardData} loading={loading}></CardBar>;
|
||||
};
|
||||
@@ -0,0 +1,415 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import CardBar from './index';
|
||||
import { IconFont, Tag, Utils, Tooltip, Popover } from 'knowdesign';
|
||||
import api from '@src/api';
|
||||
import StateChart from './StateChart';
|
||||
import ClusterNorms from '@src/pages/LoadRebalance/ClusterNorms';
|
||||
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import moment from 'moment';
|
||||
|
||||
const transUnitTimePro = (ms: number, num = 0) => {
|
||||
if (!ms) return '';
|
||||
if (ms < 60000) {
|
||||
return { value: 0, unit: `分钟` };
|
||||
}
|
||||
if (ms >= 60000 && ms < 3600000) {
|
||||
return { value: (ms / 1000 / 60).toFixed(num), unit: `分钟` };
|
||||
}
|
||||
if (ms >= 3600000 && ms < 86400000) {
|
||||
return { value: (ms / 1000 / 60 / 60).toFixed(num), unit: `小时` };
|
||||
}
|
||||
return { value: (ms / 1000 / 60 / 60 / 24).toFixed(num), unit: `天` };
|
||||
};
|
||||
|
||||
const LoadRebalanceCardBar = (props: any) => {
|
||||
const { clusterId } = useParams<{
|
||||
clusterId: string;
|
||||
}>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [cardData, setCardData] = useState([]);
|
||||
const [normsVisible, setNormsVisible] = useState(null);
|
||||
const cardItems = ['AclEnable', 'Acls', 'AclUsers', 'AclTopics', 'AclGroups'];
|
||||
const onClose = () => {
|
||||
setNormsVisible(false);
|
||||
};
|
||||
const getCartInfo = () => {
|
||||
// /api/v3/clusters/${clusterId}/balance-state /ks-km/api/v3/clusters/{clusterPhyId}/balance-state
|
||||
return Utils.request(api.getCartInfo(+clusterId));
|
||||
};
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
// 获取右侧状态
|
||||
getCartInfo()
|
||||
.then((res: any) => {
|
||||
// const { AclEnable, Acls, AclUsers, AclTopics, AclGroups } = res.metrics;
|
||||
const { next, sub, status } = res;
|
||||
const { cpu, disk, bytesIn, bytesOut } = sub;
|
||||
const newNextDate: any = transUnitTimePro(moment(next).valueOf() - moment().valueOf());
|
||||
// const newNextDate = parseInt(`${transUnitTimePro(moment(next).valueOf() - moment().valueOf())}`);
|
||||
const cardMap = [
|
||||
{
|
||||
title() {
|
||||
return (
|
||||
<div style={{ height: '20px' }}>
|
||||
<span style={{ display: 'inline-block', marginRight: '8px' }}>State</span>
|
||||
<IconFont
|
||||
className="cutomIcon-config"
|
||||
style={{ fontSize: '15px' }}
|
||||
onClick={() => setNormsVisible(true)}
|
||||
type="icon-shezhi"
|
||||
></IconFont>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
value() {
|
||||
return (
|
||||
<div style={{ display: 'inline-block', width: '100%' }}>
|
||||
<div style={{ margin: '3px 0 8px' }}>
|
||||
<Tag
|
||||
style={{
|
||||
padding: '2px 4px',
|
||||
backgroundColor: !status ? 'rgba(85,110,230,0.10)' : '#fff3e4',
|
||||
color: !status ? '#556EE6' : '#F58342',
|
||||
}}
|
||||
>
|
||||
{!status ? '已均衡' : '未均衡'}
|
||||
</Tag>
|
||||
{/* <Tag style={{ padding: '2px 4px', backgroundColor: 'rgba(85,110,230,0.10)', color: '#556EE6' }}>已均衡</Tag> */}
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span>
|
||||
周期均衡 <IconFont className="cutomIcon" type={`${!status ? 'icon-zhengchang' : 'icon-warning'}`} />
|
||||
</span>
|
||||
{/* <span>
|
||||
周期均衡 <IconFont className="cutomIcon" type="icon-zhengchang" />
|
||||
</span> */}
|
||||
<span>
|
||||
距下次均衡还剩{newNextDate?.value || 0}
|
||||
{newNextDate?.unit || '分钟'}
|
||||
</span>
|
||||
{/* {<span>距下次均衡还剩{1}小时</span>} */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
className: '',
|
||||
valueClassName: 'custom-card-bar-value', // cardbar value类名
|
||||
customStyle: {
|
||||
// 自定义cardbar样式
|
||||
marginLeft: 0,
|
||||
padding: '12px 12px 8px 12px',
|
||||
},
|
||||
},
|
||||
// {
|
||||
// // title: 'CPU avg',
|
||||
// title() {
|
||||
// return (
|
||||
// <div>
|
||||
// <span style={{ display: 'inline-block', marginRight: '8px' }}>CPU AVG</span>
|
||||
// {!cpu?.interval && cpu?.interval !== 0 && (
|
||||
// <Tooltip overlayClassName="rebalance-tooltip" title="未设置均衡策略">
|
||||
// <QuestionCircleOutlined />
|
||||
// </Tooltip>
|
||||
// )}
|
||||
// {/* <IconFont className="cutomIcon" onClick={() => setNormsVisible(true)} type="icon-shezhi"></IconFont> */}
|
||||
// </div>
|
||||
// );
|
||||
// },
|
||||
// value(visibleType: boolean) {
|
||||
// return (
|
||||
// <div id="CPU" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end' }}>
|
||||
// <div style={{ display: 'inline-block' }}>
|
||||
// <div style={{ margin: '5px 0', fontFamily: 'DIDIFD-Medium' }}>
|
||||
// <span style={{ fontSize: '24px' }}>{cpu?.avg || 0}</span>
|
||||
// <span style={{ fontSize: '14px', display: 'inline-block', marginLeft: '4px' }}>%</span>
|
||||
// </div>
|
||||
// <div style={{ marginTop: '-4px', display: 'flex', justifyContent: 'space-between' }}>
|
||||
// <span>均衡区间: ±{cpu?.interval || 0}%</span>
|
||||
// </div>
|
||||
// </div>
|
||||
// <Popover
|
||||
// // visible={visibleType} // 修改为hover柱状图
|
||||
// overlayClassName="custom-popover"
|
||||
// content={
|
||||
// <div style={{ color: '#495057' }}>
|
||||
// <div>
|
||||
// <IconFont className="cutomIcon" type="icon-chaoguo" />
|
||||
// 超过均衡区间的有: {cpu?.bigNu || 0}
|
||||
// </div>
|
||||
// <div style={{ margin: '6px 0' }}>
|
||||
// <IconFont className="cutomIcon" type="icon-qujian" />
|
||||
// 在均衡区间内的有: {cpu?.betweenNu || 0}
|
||||
// </div>
|
||||
// <div>
|
||||
// <IconFont className="cutomIcon" type="icon-diyu" />
|
||||
// 低于均衡区间的有: {cpu?.smallNu || 0}
|
||||
// </div>
|
||||
// </div>
|
||||
// }
|
||||
// getPopupContainer={(triggerNode: any) => {
|
||||
// return triggerNode;
|
||||
// }}
|
||||
// color="#ffffff"
|
||||
// >
|
||||
// <div style={{ width: '44px', height: '30px' }}>
|
||||
// <StateChart
|
||||
// data={[
|
||||
// { name: 'bigNu', value: cpu?.bigNu || 0 },
|
||||
// { name: 'betweenNu', value: cpu?.betweenNu || 0 },
|
||||
// { name: 'smallNu', value: cpu?.smallNu || 0 },
|
||||
// ]}
|
||||
// />
|
||||
// </div>
|
||||
// </Popover>
|
||||
// </div>
|
||||
// );
|
||||
// },
|
||||
// className: 'custom-card-bar',
|
||||
// valueClassName: 'custom-card-bar-value',
|
||||
// },
|
||||
{
|
||||
title() {
|
||||
return (
|
||||
<div>
|
||||
<span style={{ display: 'inline-block', marginRight: '8px' }}>Disk AVG</span>
|
||||
{!disk?.interval && disk?.interval !== 0 && (
|
||||
<Tooltip overlayClassName="rebalance-tooltip" title="未设置均衡策略">
|
||||
<QuestionCircleOutlined />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
value(visibleType: boolean) {
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end' }}>
|
||||
<div style={{ display: 'inline-block' }}>
|
||||
<div style={{ margin: '5px 0', fontFamily: 'DIDIFD-Medium' }}>
|
||||
<span style={{ fontSize: '24px' }}>{Utils.transBToGB(disk?.avg)}</span>
|
||||
<span style={{ fontSize: '14px', fontFamily: 'HelveticaNeue', display: 'inline-block', marginLeft: '4px' }}>GB</span>
|
||||
</div>
|
||||
<div style={{ marginTop: '-4px', display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span style={{ color: '#74788D' }}>均衡区间: ±{disk?.interval || 0}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<Popover
|
||||
overlayClassName="custom-popover"
|
||||
content={
|
||||
<div style={{ color: '#495057' }}>
|
||||
<div>
|
||||
<IconFont className="cutomIcon" type="icon-chaoguo" />
|
||||
超过均衡区间的有: {disk?.bigNu || 0}
|
||||
</div>
|
||||
<div style={{ margin: '6px 0' }}>
|
||||
<IconFont className="cutomIcon" type="icon-qujian" />
|
||||
在均衡区间内的有: {disk?.betweenNu || 0}
|
||||
</div>
|
||||
<div>
|
||||
<IconFont className="cutomIcon" type="icon-diyu" />
|
||||
低于均衡区间的有: {disk?.smallNu || 0}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
getPopupContainer={(triggerNode: any) => {
|
||||
return triggerNode;
|
||||
}}
|
||||
>
|
||||
<div style={{ width: '44px', height: '30px' }}>
|
||||
<StateChart
|
||||
data={[
|
||||
{ name: 'bigNu', value: disk?.bigNu || 0 },
|
||||
{ name: 'betweenNu', value: disk?.betweenNu || 0 },
|
||||
{ name: 'smallNu', value: disk?.smallNu || 0 },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
className: 'custom-card-bar',
|
||||
valueClassName: 'custom-card-bar-value',
|
||||
},
|
||||
{
|
||||
title() {
|
||||
return (
|
||||
<div>
|
||||
<span style={{ display: 'inline-block', marginRight: '8px' }}>BytesIn AVG</span>
|
||||
{!bytesIn?.interval && bytesIn?.interval !== 0 && (
|
||||
<Tooltip overlayClassName="rebalance-tooltip" title="未设置均衡策略">
|
||||
<QuestionCircleOutlined />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
value(visibleType: boolean) {
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end' }}>
|
||||
<div style={{ display: 'inline-block' }}>
|
||||
<div style={{ margin: '5px 0', fontFamily: 'DIDIFD-Medium' }}>
|
||||
<span style={{ fontSize: '24px' }}>{Utils.transBToMB(bytesIn?.avg || 0)}</span>
|
||||
<span style={{ fontSize: '14px', fontFamily: 'HelveticaNeue', display: 'inline-block', marginLeft: '4px' }}>
|
||||
MB/s
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ marginTop: '-4px', display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span style={{ color: '#74788D' }}>均衡区间: ±{bytesIn?.interval || 0}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<Popover
|
||||
overlayClassName="custom-popover"
|
||||
content={
|
||||
<div style={{ color: '#495057' }}>
|
||||
<div>
|
||||
<IconFont className="cutomIcon" type="icon-chaoguo" />
|
||||
超过均衡区间的有: {bytesIn?.bigNu || 0}
|
||||
</div>
|
||||
<div style={{ margin: '6px 0' }}>
|
||||
<IconFont className="cutomIcon" type="icon-qujian" />
|
||||
在均衡区间内的有: {bytesIn?.betweenNu || 0}
|
||||
</div>
|
||||
<div>
|
||||
<IconFont className="cutomIcon" type="icon-diyu" />
|
||||
低于均衡区间的有: {bytesIn?.smallNu || 0}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
getPopupContainer={(triggerNode: any) => {
|
||||
return triggerNode;
|
||||
}}
|
||||
>
|
||||
<div style={{ width: '44px', height: '30px' }}>
|
||||
<StateChart
|
||||
data={[
|
||||
{ name: 'bigNu', value: bytesIn?.bigNu || 0 },
|
||||
{ name: 'betweenNu', value: bytesIn?.betweenNu || 0 },
|
||||
{ name: 'smallNu', value: bytesIn?.smallNu || 0 },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
className: 'custom-card-bar',
|
||||
valueClassName: 'custom-card-bar-value',
|
||||
},
|
||||
{
|
||||
title() {
|
||||
return (
|
||||
<div>
|
||||
<span style={{ display: 'inline-block', marginRight: '8px' }}>BytesOut AVG</span>
|
||||
{!bytesOut?.interval && bytesOut?.interval !== 0 && (
|
||||
<Tooltip overlayClassName="rebalance-tooltip" title="未设置均衡策略">
|
||||
<QuestionCircleOutlined />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
value(visibleType: boolean) {
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end' }}>
|
||||
<div style={{ display: 'inline-block' }}>
|
||||
<div style={{ margin: '5px 0', fontFamily: 'DIDIFD-Medium' }}>
|
||||
<span style={{ fontSize: '24px' }}>{Utils.transBToMB(bytesOut?.avg || 0)}</span>
|
||||
<span style={{ fontSize: '14px', fontFamily: 'HelveticaNeue', display: 'inline-block', marginLeft: '4px' }}>
|
||||
MB/s
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ marginTop: '-4px', display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span style={{ color: '#74788D' }}>均衡区间: ±{bytesOut?.interval || 0}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<Popover
|
||||
overlayClassName="custom-popover"
|
||||
content={
|
||||
<div style={{ color: '#495057' }}>
|
||||
<div>
|
||||
<IconFont className="cutomIcon" type="icon-chaoguo" />
|
||||
超过均衡区间的有: {bytesOut?.bigNu || 0}
|
||||
</div>
|
||||
<div style={{ margin: '6px 0' }}>
|
||||
<IconFont className="cutomIcon" type="icon-qujian" />
|
||||
在均衡区间内的有: {bytesOut?.betweenNu || 0}
|
||||
</div>
|
||||
<div>
|
||||
<IconFont className="cutomIcon" type="icon-diyu" />
|
||||
低于均衡区间的有: {bytesOut?.smallNu || 0}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
getPopupContainer={(triggerNode: any) => {
|
||||
return triggerNode;
|
||||
}}
|
||||
>
|
||||
<div style={{ width: '44px', height: '30px' }}>
|
||||
<StateChart
|
||||
data={[
|
||||
{ name: 'bigNu', value: bytesOut?.bigNu || 0 },
|
||||
{ name: 'betweenNu', value: bytesOut?.betweenNu || 0 },
|
||||
{ name: 'smallNu', value: bytesOut?.smallNu || 0 },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
className: 'custom-card-bar',
|
||||
valueClassName: 'custom-card-bar-value',
|
||||
},
|
||||
];
|
||||
setCardData(cardMap);
|
||||
})
|
||||
.catch((err) => {
|
||||
const cardMap = [
|
||||
{
|
||||
title() {
|
||||
return (
|
||||
<div>
|
||||
<span style={{ display: 'inline-block', marginRight: '17px' }}>State</span>
|
||||
<IconFont className="cutomIcon-config" onClick={() => setNormsVisible(true)} type="icon-shezhi"></IconFont>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
value: '-',
|
||||
customStyle: {
|
||||
// 自定义cardbar样式
|
||||
marginLeft: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'CPU AVG',
|
||||
value: '-',
|
||||
},
|
||||
{
|
||||
title: 'Disk AVG',
|
||||
value: '-',
|
||||
},
|
||||
{
|
||||
title: 'BytesIn AVG',
|
||||
value: '-',
|
||||
},
|
||||
{
|
||||
title: 'BytesOut AVG',
|
||||
value: '-',
|
||||
},
|
||||
];
|
||||
setCardData(cardMap);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, [clusterId, props?.trigger]);
|
||||
return (
|
||||
<>
|
||||
<CardBar scene="broker" cardColumns={cardData} loading={loading}></CardBar>
|
||||
{<ClusterNorms genData={props?.genData} visible={normsVisible} onClose={onClose} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoadRebalanceCardBar;
|
||||
@@ -0,0 +1,77 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
type EChartsOption = echarts.EChartsOption;
|
||||
|
||||
const EchartsExample = (props: any) => {
|
||||
const lineRef = useRef<any>(null);
|
||||
const myChartRef = useRef<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
initChart();
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
const initChart = () => {
|
||||
myChartRef.current = echarts.init(lineRef.current);
|
||||
// let data = [];
|
||||
// const data = props;
|
||||
|
||||
const option: any = {
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['bigNu', 'betweenNu', 'smallNu'],
|
||||
show: false,
|
||||
nameLocationm: 'start',
|
||||
axisLabel: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
show: false,
|
||||
// boundaryGap: false,
|
||||
// splitNumber: 10,
|
||||
// max: 10,
|
||||
min: 0,
|
||||
axisLabel: {
|
||||
show: false,
|
||||
},
|
||||
// offset: 40,
|
||||
},
|
||||
grid: {
|
||||
top: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: props?.data,
|
||||
type: 'bar',
|
||||
showBackground: true,
|
||||
backgroundStyle: {
|
||||
color: 'rgba(180, 180, 180, 0.2)',
|
||||
},
|
||||
itemStyle: {
|
||||
normal: {
|
||||
color: (params: any) => {
|
||||
// 定义一个颜色数组colorList
|
||||
const colorList = ['#00C0A2', '#CED4DA', '#FF7066'];
|
||||
return colorList[params.dataIndex];
|
||||
},
|
||||
},
|
||||
},
|
||||
barWidth: '12px',
|
||||
},
|
||||
],
|
||||
};
|
||||
myChartRef.current && myChartRef.current.setOption(option, true);
|
||||
};
|
||||
|
||||
return <div ref={lineRef} style={{ width: '100%', height: '100%' }}></div>;
|
||||
};
|
||||
|
||||
export default EchartsExample;
|
||||
@@ -0,0 +1,109 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import CardBar from '@src/components/CardBar';
|
||||
import { healthDataProps } from '.';
|
||||
import { IconFont, Utils } from 'knowdesign';
|
||||
import api from '@src/api';
|
||||
import { healthScoreCondition } from './const';
|
||||
import { hashDataParse } from '@src/constants/common';
|
||||
|
||||
const renderValue = (v: string | number | ((visibleType?: boolean) => JSX.Element), visibleType?: boolean) => {
|
||||
return typeof v === 'function' ? v(visibleType) : v;
|
||||
};
|
||||
export default (props: { record: any }) => {
|
||||
const { record } = props;
|
||||
const routeParams = useParams<{
|
||||
clusterId: string;
|
||||
}>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [cardData, setCardData] = useState([]);
|
||||
const [healthData, setHealthData] = useState<healthDataProps>({
|
||||
score: 0,
|
||||
passed: 0,
|
||||
total: 0,
|
||||
alive: 0,
|
||||
});
|
||||
const [healthDetail, setHealthDetail] = useState([]);
|
||||
const [clusterAlive, setClusterAlive] = useState(0);
|
||||
const healthItems = ['HealthScore', 'HealthCheckPassed', 'HealthCheckTotal', 'alive'];
|
||||
const getNumAndSubTitles = (cardColumnsItemData: any) => {
|
||||
return (
|
||||
<div style={{ width: '100%', display: 'flex', alignItems: 'end' }}>
|
||||
<span>{cardColumnsItemData.value}</span>
|
||||
<div className="sub-title" style={{ transform: 'scale(0.83) translateY(14px)' }}>
|
||||
<span className="txt">{renderValue(cardColumnsItemData.subTitle)}</span>
|
||||
<span className="icon-wrap">
|
||||
{cardColumnsItemData.subTitleStatus ? <IconFont type="icon-zhengchang"></IconFont> : <IconFont type="icon-yichang"></IconFont>}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
const topicName = hashDataParse(location.hash)['topicName'];
|
||||
let detailHealthPromise = Utils.post(api.getTopicMetricPointsLatest(Number(routeParams.clusterId), topicName), healthItems).then(
|
||||
(data: any) => {
|
||||
let healthResData: any = {};
|
||||
healthResData.score = data.metrics['HealthScore'] || 0;
|
||||
healthResData.passed = data.metrics['HealthCheckPassed'] || 0;
|
||||
healthResData.total = data.metrics['HealthCheckTotal'] || 0;
|
||||
// healthResData.alive = data.metrics['alive'] || 0
|
||||
setHealthData(healthResData);
|
||||
}
|
||||
);
|
||||
|
||||
let detailStatePromise = Utils.request(api.getTopicState(Number(routeParams.clusterId), topicName)).then((topicHealthState: any) => {
|
||||
setCardData([
|
||||
{
|
||||
title: 'Partitions',
|
||||
value: () => {
|
||||
return getNumAndSubTitles({
|
||||
value: topicHealthState.partitionCount || '-',
|
||||
subTitle: 'All have a leader',
|
||||
subTitleStatus: topicHealthState.allPartitionHaveLeader,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Replications',
|
||||
value: topicHealthState.replicaFactor || '-',
|
||||
subTitle: `All ISRs = ${topicHealthState.replicaFactor}`,
|
||||
subTitleStatus: topicHealthState.allReplicaInSync,
|
||||
},
|
||||
{
|
||||
title: 'Min ISR',
|
||||
value: topicHealthState.minimumIsr || '-',
|
||||
subTitle: `All ISRs ≥ ${topicHealthState.minimumIsr}`,
|
||||
subTitleStatus: topicHealthState.allPartitionMatchAtMinIsr,
|
||||
},
|
||||
{
|
||||
title: 'Is Compacted',
|
||||
value: () => {
|
||||
return <span style={{ fontFamily: 'HelveticaNeue' }}>{topicHealthState.compacted ? 'YES' : 'NO'}</span>;
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
// 获取集群维度的指标信息
|
||||
let clusterStatePromise = Utils.post(api.getMetricPointsLatest(Number(routeParams.clusterId)), ['Alive']).then(
|
||||
(clusterHealthState: any) => {
|
||||
let clusterAlive = clusterHealthState?.metrics?.Alive || 0;
|
||||
setClusterAlive(clusterAlive);
|
||||
}
|
||||
);
|
||||
Promise.all([detailHealthPromise, detailStatePromise, clusterStatePromise]).then((res) => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
return (
|
||||
<CardBar
|
||||
record={record}
|
||||
scene="topic"
|
||||
healthData={{ ...healthData, alive: clusterAlive }}
|
||||
cardColumns={cardData}
|
||||
showCardBg={false}
|
||||
loading={loading}
|
||||
></CardBar>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import CardBar from '@src/components/CardBar';
|
||||
import { healthDataProps } from '.';
|
||||
import { Utils } from 'knowdesign';
|
||||
import api from '@src/api';
|
||||
|
||||
export default () => {
|
||||
const routeParams = useParams<{
|
||||
clusterId: string;
|
||||
}>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [cardData, setCardData] = useState([]);
|
||||
const [healthData, setHealthData] = useState<healthDataProps>({
|
||||
score: 0,
|
||||
passed: 0,
|
||||
total: 0,
|
||||
alive: 0,
|
||||
});
|
||||
const [healthDetail, setHealthDetail] = useState([]);
|
||||
const cardItems = ['Topics', 'Partitions', 'PartitionNoLeader', 'PartitionMinISR_S', 'PartitionMinISR_E', 'PartitionURP'];
|
||||
const healthItems = ['HealthScore_Topics', 'HealthCheckPassed_Topics', 'HealthCheckTotal_Topics', 'Alive'];
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
Utils.post(api.getMetricPointsLatest(Number(routeParams.clusterId)), cardItems.concat(healthItems)).then((data: any) => {
|
||||
setLoading(false);
|
||||
const metricElmMap: any = {
|
||||
PartitionMinISR_S: () => {
|
||||
return (
|
||||
<>
|
||||
<span style={{ color: '#FF8B56', fontSize: 20, fontWeight: 'bold' }}><</span> Min ISR
|
||||
</>
|
||||
);
|
||||
},
|
||||
PartitionMinISR_E: () => {
|
||||
return (
|
||||
<>
|
||||
<span style={{ color: '#556EE6', fontSize: 20, fontWeight: 'bold' }}><</span> Min ISR
|
||||
</>
|
||||
);
|
||||
},
|
||||
PartitionURP: 'URP',
|
||||
PartitionNoLeader: 'No Leader',
|
||||
};
|
||||
// setCardData(data
|
||||
// .filter(item => cardItems.indexOf(item.name) >= 0)
|
||||
// .map(item => {
|
||||
// return { title: metricElmMap[item.name] || item.name, value: item.value }
|
||||
// })
|
||||
// )
|
||||
setCardData(
|
||||
cardItems.map((item) => {
|
||||
let title = item;
|
||||
if (title === 'PartitionMinISR_E') {
|
||||
title = '= Min ISR';
|
||||
}
|
||||
if (title === 'PartitionMinISR_S') {
|
||||
return {
|
||||
title: '< Min ISR',
|
||||
value: <span style={{ color: data.metrics[item] !== 0 ? '#F58342' : '' }}>{data.metrics[item]}</span>,
|
||||
};
|
||||
}
|
||||
if (title === 'PartitionNoLeader' || title === 'PartitionURP') {
|
||||
return { title, value: <span style={{ color: data.metrics[item] !== 0 ? '#F58342' : '' }}>{data.metrics[item]}</span> };
|
||||
}
|
||||
return { title, value: data.metrics[item] };
|
||||
})
|
||||
);
|
||||
const healthResData: any = {};
|
||||
healthResData.score = data.metrics['HealthScore_Topics'] || 0;
|
||||
healthResData.passed = data.metrics['HealthCheckPassed_Topics'] || 0;
|
||||
healthResData.total = data.metrics['HealthCheckTotal_Topics'] || 0;
|
||||
healthResData.alive = data.metrics['Alive'] || 0;
|
||||
setHealthData(healthResData);
|
||||
});
|
||||
}, []);
|
||||
return <CardBar scene="topic" healthData={healthData} cardColumns={cardData} loading={loading}></CardBar>;
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
export const healthScoreCondition = () => {
|
||||
const n = Date.now()
|
||||
return {
|
||||
startTime: n - 5 * 60 * 1000,
|
||||
endTime: n,
|
||||
aggType: 'avg'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
.card-bar-container {
|
||||
padding: 16px 24px;
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
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;
|
||||
box-sizing: border-box;
|
||||
.card-bar-content {
|
||||
height: 88px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
// justify-content: space-between;
|
||||
align-items: center;
|
||||
.card-bar-health {
|
||||
width: 240px;
|
||||
height: 70px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
// justify-content: space-between;
|
||||
.card-bar-health-process {
|
||||
height: 100%;
|
||||
margin-right: 24px;
|
||||
.dcloud-progress-inner {
|
||||
border-radius: 50%;
|
||||
}
|
||||
.dcloud-progress-status-normal {
|
||||
.dcloud-progress-inner {
|
||||
background: rgba(85, 110, 230, 0.03);
|
||||
}
|
||||
.dcloud-progress-inner:not(.dcloud-progress-circle-gradient) .dcloud-progress-circle-path {
|
||||
stroke: rgb(85, 110, 230);
|
||||
}
|
||||
}
|
||||
.dcloud-progress-status-success {
|
||||
.dcloud-progress-inner {
|
||||
background: rgba(0, 192, 162, 0.03);
|
||||
}
|
||||
.dcloud-progress-inner:not(.dcloud-progress-circle-gradient) .dcloud-progress-circle-path {
|
||||
stroke: rgb(0, 192, 162);
|
||||
}
|
||||
}
|
||||
.dcloud-progress-status-exception {
|
||||
.dcloud-progress-inner {
|
||||
background: rgba(255, 112, 102, 0.03);
|
||||
}
|
||||
.dcloud-progress-inner:not(.dcloud-progress-circle-gradient) .dcloud-progress-circle-path {
|
||||
stroke: rgb(255, 112, 102);
|
||||
}
|
||||
}
|
||||
.dcloud-progress-inner {
|
||||
font-family: DIDIFD-Regular;
|
||||
font-size: 40px !important;
|
||||
}
|
||||
}
|
||||
.state {
|
||||
font-size: 13px;
|
||||
color: #74788d;
|
||||
letter-spacing: 0;
|
||||
text-align: justify;
|
||||
line-height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.health-status-image {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background-size: cover;
|
||||
}
|
||||
.health-status-image-success {
|
||||
background-image: url('../../assets/health-status-success.png');
|
||||
}
|
||||
.health-status-image-exception {
|
||||
background-image: url('../../assets/health-status-exception.png');
|
||||
}
|
||||
.health-status-image-normal {
|
||||
background-image: url('../../assets/health-status-normal.png');
|
||||
}
|
||||
}
|
||||
.value-bar {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
height: 36px;
|
||||
margin-top: 5px;
|
||||
.value {
|
||||
font-family: DIDIFD-Medium;
|
||||
font-size: 40px;
|
||||
color: #212529;
|
||||
line-height: 36px;
|
||||
}
|
||||
.check-detail {
|
||||
width: 52px;
|
||||
height: 15px;
|
||||
background: #ececf6;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
color: #495057;
|
||||
line-height: 15px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.card-bar-colunms {
|
||||
border: 1px solid transparent;
|
||||
min-width: 135px;
|
||||
height: 88px;
|
||||
border-radius: 8px;
|
||||
flex: 1;
|
||||
margin-left: 12px;
|
||||
padding: 12px 20px;
|
||||
.card-bar-colunms-header {
|
||||
font-size: 14px;
|
||||
color: #74788d;
|
||||
letter-spacing: 0;
|
||||
text-align: justify;
|
||||
line-height: 20px;
|
||||
}
|
||||
.card-bar-colunms-body {
|
||||
font-size: 40px;
|
||||
color: #212529;
|
||||
line-height: 36px;
|
||||
display: flex;
|
||||
align-items: end;
|
||||
margin-right: 12px;
|
||||
margin-top: 5px;
|
||||
.num {
|
||||
font-family: DIDIFD-Medium;
|
||||
}
|
||||
.sub-title {
|
||||
font-family: @font-family;
|
||||
font-size: 12px;
|
||||
transform: scale(0.83);
|
||||
white-space: nowrap;
|
||||
.txt {
|
||||
}
|
||||
.icon-wrap {
|
||||
margin-left: 4px;
|
||||
.anticon {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.dcloud-drawer-content-wrapper {
|
||||
.card-bar-container {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
.health-check-res-drawer {
|
||||
.health-res-tags {
|
||||
.dcloud-select-selector {
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-card-bar-value {
|
||||
font-size: 12px !important;
|
||||
line-height: 16px !important;
|
||||
width: 100% !important;
|
||||
.num {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
& > div {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-card-bar {
|
||||
padding: 12px 12px 8px 12px !important;
|
||||
}
|
||||
|
||||
.custom-card-bar:hover {
|
||||
background-color: #ffffff !important;
|
||||
border: 1px solid #556ee6 !important;
|
||||
}
|
||||
|
||||
.custom-popover {
|
||||
width: 150px;
|
||||
.dcloud-popover-inner {
|
||||
border-radius: 4px;
|
||||
}
|
||||
.dcloud-popover-inner-content {
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.cutomIcon-config {
|
||||
display: inline-block;
|
||||
margin-right: 2px;
|
||||
padding: 5px 5px 4px;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.5s;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.cutomIcon-config:hover {
|
||||
background: rgba(33, 37, 41, 0.04);
|
||||
}
|
||||
|
||||
.cutomIcon {
|
||||
display: inline-block;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.rebalance-tooltip {
|
||||
.dcloud-tooltip-inner {
|
||||
min-height: 20px;
|
||||
height: 24px;
|
||||
padding: 4px 6px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Drawer, IconFont, Select, Spin, Table } from 'knowdesign';
|
||||
import { Utils, Progress } from 'knowdesign';
|
||||
import './index.less';
|
||||
import api from '@src/api';
|
||||
import moment from 'moment';
|
||||
import TagsWithHide from '../TagsWithHide/index';
|
||||
import { getHealthProcessColor } from '@src/pages/SingleClusterDetail/config';
|
||||
|
||||
export interface healthDataProps {
|
||||
score: number;
|
||||
passed: number;
|
||||
total: number;
|
||||
alive: number;
|
||||
}
|
||||
export interface CardBarProps {
|
||||
cardColumns?: any[];
|
||||
healthData?: healthDataProps;
|
||||
showCardBg?: boolean;
|
||||
scene: 'topic' | 'broker' | 'group';
|
||||
record?: any;
|
||||
loading?: boolean;
|
||||
needProgress?: boolean;
|
||||
}
|
||||
const renderValue = (v: string | number | ((visibleType?: boolean) => JSX.Element), visibleType?: boolean) => {
|
||||
return typeof v === 'function' ? v(visibleType) : v;
|
||||
};
|
||||
const statusTxtEmojiMap = {
|
||||
success: {
|
||||
emoji: '👍',
|
||||
txt: '优异',
|
||||
},
|
||||
normal: {
|
||||
emoji: '😊',
|
||||
txt: '正常',
|
||||
},
|
||||
exception: {
|
||||
emoji: '👻',
|
||||
txt: '异常',
|
||||
},
|
||||
};
|
||||
const sceneCodeMap = {
|
||||
topic: {
|
||||
code: 2,
|
||||
fieldName: 'topicName',
|
||||
alias: 'Topics',
|
||||
},
|
||||
broker: {
|
||||
code: 1,
|
||||
fieldName: 'brokerId',
|
||||
alias: 'Brokers',
|
||||
},
|
||||
group: {
|
||||
code: 3,
|
||||
fieldName: 'groupName',
|
||||
alias: 'Consumers',
|
||||
},
|
||||
};
|
||||
const CardColumnsItem: any = (cardItem: any) => {
|
||||
const { cardColumnsItemData, showCardBg } = cardItem;
|
||||
const [visibleType, setVisibleType] = useState(false);
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={() => setVisibleType(true)}
|
||||
onMouseLeave={() => setVisibleType(false)}
|
||||
className={`card-bar-colunms ${cardColumnsItemData.className}`}
|
||||
style={{ backgroundColor: showCardBg ? 'rgba(86, 110, 230,0.04)' : 'transparent', ...cardColumnsItemData?.customStyle }}
|
||||
>
|
||||
<div className="card-bar-colunms-header">
|
||||
<span>{cardColumnsItemData.icon}</span>
|
||||
<span>{renderValue(cardColumnsItemData.title)}</span>
|
||||
</div>
|
||||
<div className={`card-bar-colunms-body ${cardColumnsItemData?.valueClassName}`}>
|
||||
<div style={{ marginRight: 12 }} className="num">
|
||||
{cardColumnsItemData.value === '-' ? (
|
||||
<div style={{ fontSize: 20 }}>-</div>
|
||||
) : renderValue(cardColumnsItemData.value) !== undefined ? (
|
||||
renderValue(cardColumnsItemData.value, visibleType)
|
||||
) : (
|
||||
<div style={{ fontSize: 20 }}>-</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const CardBar = (props: CardBarProps) => {
|
||||
const routeParams = useParams<{
|
||||
clusterId: string;
|
||||
}>();
|
||||
const { healthData, cardColumns, showCardBg = true, scene, record, loading, needProgress = true } = props;
|
||||
const [detailDrawerVisible, setDetailDrawerVisible] = useState(false);
|
||||
const [progressStatus, setProgressStatus] = useState<'success' | 'exception' | 'normal'>('success');
|
||||
const [healthCheckDetailList, setHealthCheckDetailList] = useState([]);
|
||||
const [isAlive, setIsAlive] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (healthData) {
|
||||
setProgressStatus(!isAlive ? 'exception' : healthData.score >= 90 ? 'success' : 'normal');
|
||||
setIsAlive(healthData.alive === 1);
|
||||
}
|
||||
}, [healthData, isAlive]);
|
||||
|
||||
useEffect(() => {
|
||||
const sceneObj = sceneCodeMap[scene];
|
||||
const path = record
|
||||
? api.getResourceHealthDetail(Number(routeParams.clusterId), sceneObj.code, record[sceneObj.fieldName])
|
||||
: api.getResourceListHealthDetail(Number(routeParams.clusterId));
|
||||
const promise = record
|
||||
? Utils.request(path)
|
||||
: Utils.request(path, {
|
||||
params: { dimensionCode: sceneObj.code },
|
||||
});
|
||||
promise.then((data: any[]) => {
|
||||
setHealthCheckDetailList(data);
|
||||
});
|
||||
}, []);
|
||||
const columns = [
|
||||
{
|
||||
title: '检查项',
|
||||
dataIndex: 'configDesc',
|
||||
key: 'configDesc',
|
||||
},
|
||||
{
|
||||
title: '权重',
|
||||
dataIndex: 'weightPercent',
|
||||
key: 'weightPercent',
|
||||
},
|
||||
{
|
||||
title: '得分',
|
||||
dataIndex: 'score',
|
||||
key: 'score',
|
||||
},
|
||||
{
|
||||
title: '检查时间',
|
||||
dataIndex: 'updateTime',
|
||||
key: 'updateTime',
|
||||
render: (value: number) => {
|
||||
return moment(value).format('YYYY-MM-DD hh:mm:ss');
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '检查结果',
|
||||
dataIndex: 'passed',
|
||||
key: 'passed',
|
||||
width: 280,
|
||||
render(value: boolean, record: any) {
|
||||
const icon = value ? <IconFont type="icon-zhengchang"></IconFont> : <IconFont type="icon-yichang"></IconFont>;
|
||||
const txt = value ? '已通过' : '未通过';
|
||||
const notPassedResNameList = record.notPassedResNameList || [];
|
||||
return (
|
||||
<div style={{ display: 'flex', width: 240 }}>
|
||||
<div style={{ marginRight: 6 }}>
|
||||
{icon} {txt}
|
||||
</div>
|
||||
{<TagsWithHide list={notPassedResNameList} expandTagContent="更多" />}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
return (
|
||||
<Spin spinning={loading}>
|
||||
<div className="card-bar-container">
|
||||
<div className="card-bar-content">
|
||||
{!loading && healthData && needProgress && (
|
||||
<div className="card-bar-health">
|
||||
<div className="card-bar-health-process">
|
||||
<Progress
|
||||
width={70}
|
||||
type="circle"
|
||||
percent={!isAlive ? 100 : healthData.score}
|
||||
status={progressStatus}
|
||||
format={(percent, successPercent) => {
|
||||
return !isAlive ? (
|
||||
<div
|
||||
style={{
|
||||
fontFamily: 'HelveticaNeue-Medium',
|
||||
fontSize: 22,
|
||||
color: getHealthProcessColor(healthData.score, healthData.alive),
|
||||
}}
|
||||
>
|
||||
Down
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
textIndent: Math.round(percent) >= 100 ? '-4px' : '',
|
||||
color: getHealthProcessColor(healthData.score, healthData.alive),
|
||||
}}
|
||||
>
|
||||
{Math.round(percent)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
strokeWidth={3}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="state">
|
||||
<div className={`health-status-image health-status-image-${progressStatus}`}></div>
|
||||
{sceneCodeMap[scene].alias}状态{statusTxtEmojiMap[progressStatus].txt}
|
||||
</div>
|
||||
<div className="value-bar">
|
||||
<div className="value">{`${healthData?.passed}/${healthData?.total}`}</div>
|
||||
<div className="check-detail" onClick={(_) => setDetailDrawerVisible(true)}>
|
||||
查看详情
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{cardColumns &&
|
||||
cardColumns?.length != 0 &&
|
||||
cardColumns?.map((item: any, index: any) => {
|
||||
return <CardColumnsItem key={index} cardColumnsItemData={item} showCardBg={showCardBg}></CardColumnsItem>;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<Drawer
|
||||
className="health-check-res-drawer"
|
||||
maskClosable={false}
|
||||
title={`${sceneCodeMap[scene].alias}健康状态详情`}
|
||||
placement="right"
|
||||
width={1080}
|
||||
onClose={(_) => setDetailDrawerVisible(false)}
|
||||
visible={detailDrawerVisible}
|
||||
>
|
||||
<Table rowKey={'topicName'} columns={columns} dataSource={healthCheckDetailList} pagination={false} />
|
||||
</Drawer>
|
||||
</Spin>
|
||||
);
|
||||
};
|
||||
export default CardBar;
|
||||