初始化3.0.0版本
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;
|
||||
@@ -0,0 +1,29 @@
|
||||
.codemirror-form-item {
|
||||
> .cm-s-default {
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
&:hover,
|
||||
&.CodeMirror-focused {
|
||||
border-color: #74788d;
|
||||
}
|
||||
.CodeMirror-scroll {
|
||||
background: #f2f2f2;
|
||||
transition: background-color 0.3s ease;
|
||||
.CodeMirror-gutters {
|
||||
background: #f2f2f2;
|
||||
transition: background-color 0.3s ease;
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
&.CodeMirror-empty {
|
||||
color: #adb5bc;
|
||||
}
|
||||
}
|
||||
&-resize {
|
||||
> .cm-s-default {
|
||||
resize: vertical;
|
||||
min-height: 60px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
// 引入代码编辑器
|
||||
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';
|
||||
import 'codemirror/addon/display/placeholder';
|
||||
require('codemirror/mode/xml/xml');
|
||||
require('codemirror/mode/javascript/javascript');
|
||||
import './index.less';
|
||||
|
||||
interface PropsType {
|
||||
defaultInput: string;
|
||||
placeholder?: string;
|
||||
resize?: boolean;
|
||||
onBeforeChange: (value: string) => void;
|
||||
onBlur?: (value: string) => void;
|
||||
}
|
||||
|
||||
const CodeMirrorFormItem = (props: PropsType): JSX.Element => {
|
||||
const { defaultInput, placeholder = '请输入内容', resize = false, onBeforeChange: changeCallback, onBlur } = props;
|
||||
const [input, setInput] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
let formattedInput = '';
|
||||
try {
|
||||
formattedInput = JSON.stringify(JSON.parse(defaultInput), null, 2);
|
||||
} catch (e) {
|
||||
formattedInput = defaultInput;
|
||||
}
|
||||
setInput(formattedInput);
|
||||
}, [defaultInput]);
|
||||
|
||||
const blur = (value: any) => {
|
||||
onBlur && onBlur(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<CodeMirror
|
||||
className={`codemirror-form-item ${resize ? 'codemirror-form-item-resize' : ''}`}
|
||||
value={input}
|
||||
options={{
|
||||
mode: 'application/json',
|
||||
lineNumbers: true,
|
||||
lineWrapper: true,
|
||||
autoCloseBrackets: true,
|
||||
smartIndent: true,
|
||||
tabSize: 2,
|
||||
placeholder,
|
||||
}}
|
||||
onBlur={blur}
|
||||
onBeforeChange={(editor, data, value) => {
|
||||
changeCallback(value);
|
||||
setInput(value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeMirrorFormItem;
|
||||
@@ -0,0 +1,39 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Input, IconFont } from 'knowdesign';
|
||||
import './style/index.less';
|
||||
|
||||
interface IObjectProps {
|
||||
[propName: string]: any;
|
||||
}
|
||||
interface ISearchInputProps {
|
||||
onSearch: (value?: string) => unknown;
|
||||
iconType?: string;
|
||||
attrs?: IObjectProps;
|
||||
}
|
||||
|
||||
const SearchInput: React.FC<ISearchInputProps> = (props: ISearchInputProps) => {
|
||||
const { onSearch, iconType = 'icon-fangdajing', attrs } = props;
|
||||
const [changeVal, setChangeVal] = useState<string>('');
|
||||
|
||||
const onChange = (e: any) => {
|
||||
if (e.target.value === '' || e.target.value === null || e.target.value === undefined) {
|
||||
onSearch('');
|
||||
}
|
||||
attrs?.onChange && attrs?.onChange(e.target.value);
|
||||
setChangeVal(e.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Input.Search
|
||||
{...attrs}
|
||||
suffix={<IconFont type={iconType} onClick={() => onSearch(changeVal)} />}
|
||||
onChange={onChange}
|
||||
className={'dcloud-clustom-input-serach ' + `${attrs?.className}`}
|
||||
onSearch={(val: string) => {
|
||||
onSearch(val);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchInput;
|
||||
@@ -0,0 +1,11 @@
|
||||
.dcloud-clustom-input-serach{
|
||||
|
||||
.dcloud-input-affix-wrapper{
|
||||
border-radius: 2px !important;
|
||||
}
|
||||
|
||||
.dcloud-input-group-addon{
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,646 @@
|
||||
import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||
import { AppContainer, Button, Drawer, IconFont, message, Spin, Table, SingleChart, Utils, Tooltip } from 'knowdesign';
|
||||
import moment from 'moment';
|
||||
import api, { MetricType } from '@src/api';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { debounce } from 'lodash';
|
||||
import { MetricDefaultChartDataType, MetricChartDataType, formatChartData, getDetailChartConfig } from './config';
|
||||
import { UNIT_MAP } from '@src/constants/chartConfig';
|
||||
|
||||
interface ChartDetailProps {
|
||||
metricType: MetricType;
|
||||
metricName: string;
|
||||
queryLines: string[];
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface MetricTableInfo {
|
||||
name: string;
|
||||
avg: number;
|
||||
max: number;
|
||||
min: number;
|
||||
latest: (string | number)[];
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface DataZoomEventProps {
|
||||
type: 'datazoom';
|
||||
// 缩放的开始位置的百分比,0 - 100
|
||||
start: number;
|
||||
// 缩放的结束位置的百分比,0 - 100
|
||||
end: number;
|
||||
}
|
||||
|
||||
// 缩放区默认选中范围比例(0.01~1)
|
||||
const DATA_ZOOM_DEFAULT_SCALE = 0.25;
|
||||
// 选中范围最少展示的时间长度(默认 10 分钟),单位: ms
|
||||
const LEAST_SELECTED_TIME_RANGE = 1 * 60 * 1000;
|
||||
// 单次向服务器请求数据的范围(默认 6 小时,超过后采集频率间隔会变长),单位: ms
|
||||
const DEFAULT_REQUEST_TIME_RANGE = 6 * 60 * 60 * 1000;
|
||||
// 采样间隔,影响前端补点逻辑,单位: ms
|
||||
const DEFAULT_POINT_INTERVAL = 60 * 1000;
|
||||
// 向服务器每轮请求的数量
|
||||
const DEFAULT_REQUEST_COUNT = 6;
|
||||
// 进入详情页默认展示的时间范围
|
||||
const DEFAULT_ENTER_TIME_RANGE = 2 * 60 * 60 * 1000;
|
||||
// 预缓存数据阈值,图表展示数据的开始时间处于前端缓存数据的时间范围的前 40% 时,向服务器请求数据
|
||||
const PRECACHE_THRESHOLD = 0.4;
|
||||
|
||||
// 表格列
|
||||
const colunms = [
|
||||
{
|
||||
title: 'Host',
|
||||
dataIndex: 'name',
|
||||
render(name: string, record: any) {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<div style={{ width: 8, height: 2, marginRight: 4, background: record.color }}></div>
|
||||
<span>{name}</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Avg',
|
||||
dataIndex: 'avg',
|
||||
render(num: number) {
|
||||
return num.toFixed(2);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Max',
|
||||
dataIndex: 'max',
|
||||
render(num: number, record: any) {
|
||||
return (
|
||||
<div>
|
||||
<span>{num.toFixed(2)}</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Min',
|
||||
dataIndex: 'min',
|
||||
render(num: number, record: any) {
|
||||
return (
|
||||
<div>
|
||||
<span>{num.toFixed(2)}</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Latest',
|
||||
dataIndex: 'latest',
|
||||
render(latest: number[]) {
|
||||
return `${latest[1].toFixed(2)}`;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const ChartDetail = (props: ChartDetailProps) => {
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const { clusterId } = useParams<{
|
||||
clusterId: string;
|
||||
}>();
|
||||
const { metricType, metricName, queryLines, onClose } = props;
|
||||
|
||||
// 存储图表相关的不需要触发渲染的数据,用于计算图表展示状态并进行操作
|
||||
const chartInfo = useRef(
|
||||
(() => {
|
||||
// 当前时间减去 1 分钟,避免最近一分钟的数据还没采集到时前端多补一个点
|
||||
const curTime = moment().valueOf() - 60 * 1000;
|
||||
const curTimeRange = [curTime - DEFAULT_ENTER_TIME_RANGE, curTime] as const;
|
||||
|
||||
return {
|
||||
chartInstance: undefined as echarts.ECharts,
|
||||
isLoadedFullData: false,
|
||||
fullTimeRange: curTimeRange,
|
||||
fullMetricData: {} as MetricChartDataType,
|
||||
curTimeRange,
|
||||
oldDataZoomOption: {} as any,
|
||||
sliderPos: [0, 0] as readonly [number, number],
|
||||
sliderRange: '',
|
||||
transformUnit: undefined as [string, number],
|
||||
};
|
||||
})()
|
||||
);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
// 当前展示的图表数据
|
||||
const [curMetricData, setCurMetricData] = useState<MetricChartDataType>();
|
||||
// 图表数据的各项计算指标
|
||||
const [tableInfo, setTableInfo] = useState<MetricTableInfo[]>([]);
|
||||
// 选中展示的图表
|
||||
const [selectedLines, setSelectedLines] = useState<string[]>([]);
|
||||
|
||||
// 请求图表数据
|
||||
const getMetricChartData = ([startTime, endTime]: readonly [number, number]) => {
|
||||
return Utils.post(api.getDashboardMetricChartData(clusterId, metricType), {
|
||||
startTime,
|
||||
endTime,
|
||||
metricsNames: [metricName],
|
||||
topNu: null,
|
||||
[metricType === MetricType.Broker ? 'brokerIds' : 'topics']: queryLines,
|
||||
});
|
||||
};
|
||||
|
||||
const onDataZoomDrag = ({ start, end }: DataZoomEventProps) => {
|
||||
// dispatchAction 更新拖拽位置的情况,直接跳出
|
||||
if (!start && !end) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const {
|
||||
fullTimeRange: [fullStartTimestamp, fullEndTimestamp],
|
||||
curTimeRange: [oldStartTimestamp, oldEndTimestamp],
|
||||
oldDataZoomOption,
|
||||
isLoadedFullData,
|
||||
chartInstance,
|
||||
} = chartInfo.current;
|
||||
const { start: oldStart, end: oldEnd, startValue: oldStartSliderPos, endValue: oldEndSliderPos } = oldDataZoomOption;
|
||||
// 获取拖动后左右滑块的绝对位置
|
||||
const newDataZoomOption = (chartInstance.getOption() as any).dataZoom[0];
|
||||
const { startValue: newStartSliderPos, endValue: newEndSliderPos } = newDataZoomOption;
|
||||
// 计算 扩大/缩小 的比例
|
||||
const oldScale = (oldEnd - oldStart) / 100;
|
||||
const newScale = (end - start) / 100;
|
||||
const scaleRate = newScale / oldScale;
|
||||
|
||||
// 如果滑块整体拖动,则只更新拖动后滑块的位(保留小数点后三位是防止低位值的干扰)
|
||||
if (oldScale.toFixed(3) === newScale.toFixed(3)) {
|
||||
chartInfo.current = {
|
||||
...chartInfo.current,
|
||||
sliderPos: [newStartSliderPos, newEndSliderPos],
|
||||
oldDataZoomOption: newDataZoomOption,
|
||||
};
|
||||
renderTableInfo();
|
||||
|
||||
return false;
|
||||
}
|
||||
// 滑块 左侧/右侧 区域所占时间范围
|
||||
const oldLeftTimeRange = oldStartSliderPos - oldStartTimestamp;
|
||||
const oldRightTimeRange = oldEndTimestamp - oldEndSliderPos;
|
||||
let leftExpandTimeRange = oldLeftTimeRange * scaleRate;
|
||||
let rightExpandTimeRange = oldRightTimeRange * scaleRate;
|
||||
let newStartTimestamp, newEndTimestamp;
|
||||
|
||||
if (scaleRate > 1) {
|
||||
// 2. 滑块拖动后缩放比例变大
|
||||
// 扩张后的右侧边界
|
||||
newEndTimestamp = newEndSliderPos + rightExpandTimeRange;
|
||||
let rightOverRange = 0;
|
||||
// 计算右侧是否能扩张这么多
|
||||
if (newEndTimestamp > fullEndTimestamp) {
|
||||
rightOverRange = newEndTimestamp - fullEndTimestamp;
|
||||
newEndTimestamp = fullEndTimestamp;
|
||||
}
|
||||
|
||||
// 扩张后的左侧边界
|
||||
newStartTimestamp = newStartSliderPos - leftExpandTimeRange - rightOverRange;
|
||||
// 在已经加载到全部数据的情况下,如果左侧扩张后的边界大于左侧最终边界,并且右侧边界还能扩张,则向右扩张
|
||||
if (isLoadedFullData && newStartTimestamp < fullStartTimestamp && newEndTimestamp < fullEndTimestamp) {
|
||||
const leftOverRange = fullStartTimestamp - newStartTimestamp;
|
||||
if (newEndTimestamp + leftOverRange >= fullEndTimestamp) {
|
||||
newEndTimestamp = fullEndTimestamp;
|
||||
} else {
|
||||
newEndTimestamp += leftOverRange;
|
||||
}
|
||||
|
||||
newStartTimestamp = fullStartTimestamp;
|
||||
}
|
||||
} else {
|
||||
// 3. 滑块拖动后缩放比例变小
|
||||
// 判断拖动后选择的时间范围并提示
|
||||
if (newEndSliderPos - newStartSliderPos < LEAST_SELECTED_TIME_RANGE) {
|
||||
// TODO: 补充逻辑
|
||||
updateChartData([oldStartTimestamp, oldEndTimestamp], [oldStartSliderPos, oldEndSliderPos]);
|
||||
message.warning(`当前选择范围小于 ${LEAST_SELECTED_TIME_RANGE / 60 / 1000} 分钟,图表可能无数据`);
|
||||
return true;
|
||||
}
|
||||
|
||||
const isOldLarger = oldScale - DATA_ZOOM_DEFAULT_SCALE > 0.01;
|
||||
const isNewLarger = newScale - DATA_ZOOM_DEFAULT_SCALE > 0.01;
|
||||
if (isOldLarger && isNewLarger) {
|
||||
// 如果拖拽前后比例均高于默认比例,则不对图表展示范围进行操作
|
||||
chartInfo.current = {
|
||||
...chartInfo.current,
|
||||
sliderPos: [newStartSliderPos, newEndSliderPos],
|
||||
oldDataZoomOption: newDataZoomOption,
|
||||
};
|
||||
renderTableInfo();
|
||||
return true;
|
||||
} else {
|
||||
// 如果拖拽前比例高于默认比例,拖拽后比例低于默认比例,则重新计算缩放比例,目的是保证拖拽后显示范围占的比例为默认比例
|
||||
if (isOldLarger && !isNewLarger) {
|
||||
const newScaleRate =
|
||||
(((newEndSliderPos - newStartSliderPos) / DATA_ZOOM_DEFAULT_SCALE) * (1 - DATA_ZOOM_DEFAULT_SCALE)) /
|
||||
(oldLeftTimeRange | oldRightTimeRange);
|
||||
leftExpandTimeRange = oldLeftTimeRange * newScaleRate;
|
||||
rightExpandTimeRange = oldRightTimeRange * newScaleRate;
|
||||
}
|
||||
|
||||
newStartTimestamp = newStartSliderPos - leftExpandTimeRange;
|
||||
newEndTimestamp = newEndSliderPos + rightExpandTimeRange;
|
||||
}
|
||||
}
|
||||
|
||||
// 这时已经获取到了 扩张后需要的图表时间范围 和 扩张后的滑块的绝对位置,更新图表数据
|
||||
updateChartData([newStartTimestamp, newEndTimestamp], [newStartSliderPos, newEndSliderPos]);
|
||||
return true;
|
||||
};
|
||||
|
||||
const updateChartData = (timeRange: [number, number], sliderPos: [number, number]) => {
|
||||
const {
|
||||
fullTimeRange: [fullStartTimestamp, fullEndTimestamp],
|
||||
fullMetricData,
|
||||
isLoadedFullData,
|
||||
} = chartInfo.current;
|
||||
let leftBoundaryTimestamp = Math.floor(timeRange[0]);
|
||||
const isNeedCacheExtraData = leftBoundaryTimestamp < fullStartTimestamp + (fullEndTimestamp - fullStartTimestamp) * PRECACHE_THRESHOLD;
|
||||
|
||||
let isRendered = false;
|
||||
// 如果本地存储的数据足够展示或者已经获取到所有数据,则展示数据
|
||||
if (leftBoundaryTimestamp > fullStartTimestamp || isLoadedFullData) {
|
||||
chartInfo.current = {
|
||||
...chartInfo.current,
|
||||
curTimeRange: [leftBoundaryTimestamp > fullStartTimestamp ? leftBoundaryTimestamp : fullStartTimestamp, timeRange[1]],
|
||||
sliderPos,
|
||||
};
|
||||
renderNewMetricData();
|
||||
isRendered = true;
|
||||
}
|
||||
|
||||
if (!isLoadedFullData && isNeedCacheExtraData) {
|
||||
// 向服务器请求新的数据缓存
|
||||
let reqEndTime = fullStartTimestamp;
|
||||
const requestArr: any[] = [];
|
||||
const requestTimeRanges: [number, number][] = [];
|
||||
for (let i = 0; i < DEFAULT_REQUEST_COUNT; i++) {
|
||||
setTimeout(() => {
|
||||
const nextReqEndTime = reqEndTime - DEFAULT_REQUEST_TIME_RANGE;
|
||||
requestArr.unshift(getMetricChartData([nextReqEndTime, reqEndTime]));
|
||||
requestTimeRanges.unshift([nextReqEndTime, reqEndTime]);
|
||||
reqEndTime = nextReqEndTime;
|
||||
|
||||
// 当最后一次请求发送后,处理返回
|
||||
if (i === DEFAULT_REQUEST_COUNT - 1) {
|
||||
Promise.all(requestArr).then((resList) => {
|
||||
let isSettle = -1;
|
||||
// 填充增量的图表数据
|
||||
resList.forEach((res: MetricDefaultChartDataType[], i) => {
|
||||
// 图表没有返回数据的情况
|
||||
if (!res?.length) {
|
||||
if (isSettle === -1) {
|
||||
chartInfo.current = {
|
||||
...chartInfo.current,
|
||||
// 标记数据已经全部加载完毕
|
||||
isLoadedFullData: true,
|
||||
};
|
||||
isSettle = i;
|
||||
}
|
||||
} else {
|
||||
resolveAdditionChartData(res, requestTimeRanges[i]);
|
||||
}
|
||||
});
|
||||
// 更新左侧边界为当前已获取到数据的最小边界
|
||||
const curLocalStartTimestamp = Number(fullMetricData.metricLines.map((line) => line.data[0][0]).sort()[0]);
|
||||
if (leftBoundaryTimestamp < curLocalStartTimestamp) {
|
||||
leftBoundaryTimestamp = curLocalStartTimestamp;
|
||||
}
|
||||
|
||||
chartInfo.current = {
|
||||
...chartInfo.current,
|
||||
fullTimeRange: [reqEndTime - DEFAULT_REQUEST_TIME_RANGE, fullEndTimestamp],
|
||||
sliderPos,
|
||||
};
|
||||
if (!isRendered) {
|
||||
chartInfo.current = {
|
||||
...chartInfo.current,
|
||||
curTimeRange: [leftBoundaryTimestamp, timeRange[1]],
|
||||
};
|
||||
renderNewMetricData();
|
||||
}
|
||||
});
|
||||
}
|
||||
}, i * 10);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 处理增量图表数据
|
||||
const resolveAdditionChartData = (res: MetricDefaultChartDataType[], timeRange: [number, number]) => {
|
||||
// 格式化图表需要的数据
|
||||
const formattedMetricData = formatChartData(
|
||||
res,
|
||||
global.getMetricDefine || {},
|
||||
metricType,
|
||||
timeRange,
|
||||
DEFAULT_POINT_INTERVAL,
|
||||
false,
|
||||
chartInfo.current.transformUnit
|
||||
) as MetricChartDataType[];
|
||||
// 增量填充图表数据
|
||||
const additionMetricPoints = formattedMetricData[0].metricLines;
|
||||
Object.values(additionMetricPoints).forEach((additionLine) => {
|
||||
const curLines = chartInfo.current.fullMetricData.metricLines;
|
||||
const curLine = curLines.find(({ name: metricName }) => {
|
||||
return additionLine.name === metricName;
|
||||
});
|
||||
if (!curLine) {
|
||||
// 如果没找到,说明是新的节点,直接存储
|
||||
curLines.push(additionLine);
|
||||
} else {
|
||||
curLine.data = additionLine.data.concat(curLine.data);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 根据需要展示的时间范围过滤出对应的数据展示
|
||||
const renderNewMetricData = () => {
|
||||
const { fullMetricData, curTimeRange } = chartInfo.current;
|
||||
const newMetricData = { ...fullMetricData };
|
||||
newMetricData.metricLines = [...newMetricData.metricLines];
|
||||
newMetricData.metricLines.forEach((line, i) => {
|
||||
line = {
|
||||
...line,
|
||||
};
|
||||
line.data = [...line.data];
|
||||
line.data = line.data.filter((point) => {
|
||||
const result = curTimeRange[0] <= point[0] && point[0] <= curTimeRange[1];
|
||||
return result;
|
||||
});
|
||||
newMetricData.metricLines[i] = line;
|
||||
});
|
||||
// 只过滤出当前时间段有数据点的线条,确保 Table 统一展示
|
||||
newMetricData.metricLines = newMetricData.metricLines.filter((line) => line.data.length);
|
||||
setCurMetricData(newMetricData);
|
||||
};
|
||||
|
||||
// 计算当前选中范围
|
||||
const calculateSliderRange = () => {
|
||||
const { sliderPos } = chartInfo.current;
|
||||
let minutes = Number(((sliderPos[1] - sliderPos[0]) / 60 / 1000).toFixed(2));
|
||||
let hours = 0;
|
||||
let days = 0;
|
||||
if (minutes > 60) {
|
||||
hours = Math.floor(minutes / 60);
|
||||
minutes = Number((minutes % 60).toFixed(2));
|
||||
}
|
||||
if (hours > 24) {
|
||||
days = Math.floor(hours / 24);
|
||||
hours = Number((hours % 24).toFixed(2));
|
||||
}
|
||||
|
||||
chartInfo.current = {
|
||||
...chartInfo.current,
|
||||
sliderRange: ` 当前选中范围: ${days > 0 ? `${days} 天 ` : ''}${hours > 0 ? `${hours} 小时 ` : ''}${minutes} 分钟`,
|
||||
};
|
||||
};
|
||||
|
||||
// 遍历图表,获取需要的指标数据,展示到 Table
|
||||
const renderTableInfo = () => {
|
||||
const tableData: MetricTableInfo[] = [];
|
||||
const { sliderPos, chartInstance } = chartInfo.current;
|
||||
const { color }: any = chartInstance.getOption();
|
||||
|
||||
curMetricData.metricLines.forEach(({ name, data }, i) => {
|
||||
const lineInfo: MetricTableInfo = {
|
||||
name,
|
||||
avg: -1,
|
||||
max: -1,
|
||||
min: Number.MAX_SAFE_INTEGER,
|
||||
latest: ['0', -1],
|
||||
color: color[i % color.length],
|
||||
};
|
||||
|
||||
const curShowPoints = data.filter((point) => sliderPos[0] < point[0] && point[0] < sliderPos[1]);
|
||||
// 如果该节点在当前时间范围无数据,直接退出
|
||||
if (!curShowPoints.length) {
|
||||
return false;
|
||||
}
|
||||
const all = curShowPoints.reduce((pre: number, cur) => {
|
||||
const curVal = cur[1] as number;
|
||||
|
||||
if (curVal > lineInfo.max) {
|
||||
lineInfo.max = curVal;
|
||||
}
|
||||
if (curVal < lineInfo.min) {
|
||||
lineInfo.min = curVal;
|
||||
}
|
||||
|
||||
pre += curVal;
|
||||
return pre;
|
||||
}, 0);
|
||||
|
||||
lineInfo.avg = all / curShowPoints.length;
|
||||
lineInfo.latest = curShowPoints[curShowPoints.length - 1];
|
||||
tableData.push(lineInfo);
|
||||
return true;
|
||||
});
|
||||
|
||||
calculateSliderRange();
|
||||
setTableInfo(tableData);
|
||||
setSelectedLines(tableData.map((line) => line.name));
|
||||
};
|
||||
|
||||
const tableLineChange = (keys: string[]) => {
|
||||
const updatedLines: { [name: string]: boolean } = {};
|
||||
selectedLines.forEach((name) => !keys.includes(name) && (updatedLines[name] = false));
|
||||
keys.forEach((name) => !selectedLines.includes(name) && (updatedLines[name] = true));
|
||||
|
||||
// 更新
|
||||
Object.keys(updatedLines).forEach((name) => {
|
||||
chartInfo.current.chartInstance.dispatchAction({
|
||||
type: 'legendToggleSelect',
|
||||
// 图例名称
|
||||
name: name,
|
||||
});
|
||||
});
|
||||
|
||||
setSelectedLines(keys);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (curMetricData) {
|
||||
setTimeout(() => {
|
||||
// 新的图表数据渲染后,更新图表拖拽轴信息
|
||||
chartInfo.current.oldDataZoomOption = (chartInfo.current.chartInstance.getOption() as any).dataZoom[0];
|
||||
});
|
||||
renderTableInfo();
|
||||
}
|
||||
}, [curMetricData]);
|
||||
|
||||
// 进入详情时,首次获取数据
|
||||
useEffect(() => {
|
||||
if (metricType && metricName) {
|
||||
setLoading(true);
|
||||
const { curTimeRange } = chartInfo.current;
|
||||
getMetricChartData(curTimeRange).then((res: any[] | null) => {
|
||||
// 如果图表返回数据
|
||||
if (res?.length) {
|
||||
// 格式化图表需要的数据
|
||||
const formattedMetricData = (
|
||||
formatChartData(
|
||||
res,
|
||||
global.getMetricDefine || {},
|
||||
metricType,
|
||||
curTimeRange,
|
||||
DEFAULT_POINT_INTERVAL,
|
||||
false
|
||||
) as MetricChartDataType[]
|
||||
)[0];
|
||||
// 填充图表数据
|
||||
let initFullTimeRange = curTimeRange;
|
||||
const pointsOfFirstLine = formattedMetricData.metricLines.find((line) => line.data.length).data;
|
||||
if (pointsOfFirstLine) {
|
||||
initFullTimeRange = [pointsOfFirstLine[0][0] as number, pointsOfFirstLine[pointsOfFirstLine.length - 1][0] as number] as const;
|
||||
}
|
||||
|
||||
// 获取单位保存起来
|
||||
let transformUnit = undefined;
|
||||
Object.entries(UNIT_MAP).forEach((unit) => {
|
||||
if (formattedMetricData.metricUnit.includes(unit[0])) {
|
||||
transformUnit = unit;
|
||||
}
|
||||
});
|
||||
|
||||
chartInfo.current = {
|
||||
...chartInfo.current,
|
||||
fullMetricData: formattedMetricData,
|
||||
fullTimeRange: [...initFullTimeRange],
|
||||
curTimeRange: [...initFullTimeRange],
|
||||
sliderPos: [
|
||||
initFullTimeRange[1] - (initFullTimeRange[1] - initFullTimeRange[0]) * DATA_ZOOM_DEFAULT_SCALE,
|
||||
initFullTimeRange[1],
|
||||
],
|
||||
transformUnit,
|
||||
};
|
||||
setCurMetricData(formattedMetricData);
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const debounced = debounce(onDataZoomDrag, 300);
|
||||
|
||||
return (
|
||||
<Spin spinning={loading}>
|
||||
<div className="chart-detail-modal-container">
|
||||
{curMetricData && (
|
||||
<>
|
||||
<div className="detail-title">
|
||||
<div className="left">
|
||||
<div className="title">
|
||||
<Tooltip
|
||||
placement="bottomLeft"
|
||||
title={() => {
|
||||
let content = '';
|
||||
const metricDefine = global.getMetricDefine(metricType, curMetricData.metricName);
|
||||
if (metricDefine) {
|
||||
content = metricDefine.desc;
|
||||
}
|
||||
return content;
|
||||
}}
|
||||
>
|
||||
<span style={{ cursor: 'pointer' }}>
|
||||
<span>{curMetricData.metricName}</span> <span className="unit">({curMetricData.metricUnit}) </span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="info">{chartInfo.current.sliderRange}</div>
|
||||
</div>
|
||||
<div className="right">
|
||||
<Button type="text" size="small" onClick={onClose}>
|
||||
<IconFont type="icon-guanbi" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<SingleChart
|
||||
chartTypeProp="line"
|
||||
wrapStyle={{
|
||||
width: 'auto',
|
||||
height: 462,
|
||||
}}
|
||||
onEvents={{
|
||||
dataZoom: (record: any) => {
|
||||
debounced(record);
|
||||
},
|
||||
}}
|
||||
propChartData={curMetricData.metricLines}
|
||||
optionMergeProps={{ notMerge: true }}
|
||||
getChartInstance={(chartInstance) => {
|
||||
chartInfo.current = {
|
||||
...chartInfo.current,
|
||||
chartInstance,
|
||||
};
|
||||
}}
|
||||
{...getDetailChartConfig(`${curMetricData.metricName}{unit|(${curMetricData.metricUnit})}`, chartInfo.current.sliderPos)}
|
||||
/>
|
||||
<Table
|
||||
className="detail-table"
|
||||
rowKey="name"
|
||||
rowSelection={{
|
||||
// hideSelectAll: true,
|
||||
preserveSelectedRowKeys: true,
|
||||
selectedRowKeys: selectedLines,
|
||||
// getCheckboxProps: (record) => {
|
||||
// return selectedLines.length <= 1 && selectedLines.includes(record.name)
|
||||
// ? {
|
||||
// disabled: true,
|
||||
// }
|
||||
// : {};
|
||||
// },
|
||||
selections: [Table.SELECTION_INVERT, Table.SELECTION_NONE],
|
||||
onChange: (keys: string[]) => tableLineChange(keys),
|
||||
}}
|
||||
scroll={{
|
||||
x: 'max-content',
|
||||
y: 'calc(100vh - 582px)',
|
||||
}}
|
||||
dataSource={tableInfo}
|
||||
columns={colunms as any}
|
||||
pagination={false}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Spin>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
const ChartDrawer = forwardRef((_, ref) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [dashboardType, setDashboardType] = useState<MetricType>();
|
||||
const [metricName, setMetricName] = useState<string>();
|
||||
const [queryLines, setQueryLines] = useState<string[]>([]);
|
||||
|
||||
const onOpen = (dashboardType: MetricType, metricName: string, queryLines: string[]) => {
|
||||
setDashboardType(dashboardType);
|
||||
setMetricName(metricName);
|
||||
setQueryLines(queryLines);
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
setVisible(false);
|
||||
setDashboardType(undefined);
|
||||
setMetricName(undefined);
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
onOpen,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Drawer width={1080} visible={visible} footer={null} closable={false} maskClosable={false} destroyOnClose={true} onClose={onClose}>
|
||||
{dashboardType && metricName && (
|
||||
<ChartDetail metricType={dashboardType} metricName={metricName} queryLines={queryLines} onClose={onClose} />
|
||||
)}
|
||||
</Drawer>
|
||||
);
|
||||
});
|
||||
|
||||
export default ChartDrawer;
|
||||
@@ -0,0 +1,198 @@
|
||||
import { getUnit, getDataNumberUnit, getBasicChartConfig } from '@src/constants/chartConfig';
|
||||
import { MetricType } from '@src/api';
|
||||
import { MetricsDefine } from '@src/pages/CommonConfig';
|
||||
|
||||
export interface MetricInfo {
|
||||
name: string;
|
||||
desc: string;
|
||||
type: number;
|
||||
set: boolean;
|
||||
support: boolean;
|
||||
}
|
||||
|
||||
// 接口返回图表原始数据类型
|
||||
export interface MetricDefaultChartDataType {
|
||||
metricName: string;
|
||||
metricLines: {
|
||||
name: string;
|
||||
createTime: number;
|
||||
updateTime: number;
|
||||
metricPoints: {
|
||||
aggType: string;
|
||||
timeStamp: number;
|
||||
value: number;
|
||||
createTime: number;
|
||||
updateTime: number;
|
||||
}[];
|
||||
}[];
|
||||
}
|
||||
|
||||
// 格式化后图表数据类型
|
||||
export interface MetricChartDataType {
|
||||
metricName: string;
|
||||
metricUnit: string;
|
||||
metricLines: {
|
||||
name: string;
|
||||
data: (string | number)[][];
|
||||
}[];
|
||||
dragKey?: number;
|
||||
}
|
||||
|
||||
// 补点
|
||||
export const supplementaryPoints = (
|
||||
lines: MetricChartDataType['metricLines'],
|
||||
timeRange: readonly [number, number],
|
||||
interval: number,
|
||||
extraCallback?: (point: [number, 0]) => any[]
|
||||
) => {
|
||||
lines.forEach(({ data }) => {
|
||||
let len = data.length;
|
||||
for (let i = 0; i < len; i++) {
|
||||
const timestamp = data[i][0] as number;
|
||||
// 数组第一个点和最后一个点单独处理
|
||||
if (i === 0) {
|
||||
let firstPointTimestamp = data[0][0] as number;
|
||||
while (firstPointTimestamp - interval > timeRange[0]) {
|
||||
const prePointTimestamp = firstPointTimestamp - interval;
|
||||
data.unshift(extraCallback ? extraCallback([prePointTimestamp, 0]) : [prePointTimestamp, 0]);
|
||||
len++;
|
||||
i++;
|
||||
firstPointTimestamp = prePointTimestamp;
|
||||
}
|
||||
}
|
||||
if (i === len - 1) {
|
||||
let lastPointTimestamp = data[len - 1][0] as number;
|
||||
while (lastPointTimestamp + interval < timeRange[1]) {
|
||||
const next = lastPointTimestamp + interval;
|
||||
data.push(extraCallback ? extraCallback([next, 0]) : [next, 0]);
|
||||
lastPointTimestamp = next;
|
||||
}
|
||||
} else if (timestamp + interval < data[i + 1][0]) {
|
||||
data.splice(i + 1, 0, extraCallback ? extraCallback([timestamp + interval, 0]) : [timestamp + interval, 0]);
|
||||
len++;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 格式化图表数据
|
||||
export const formatChartData = (
|
||||
metricData: MetricDefaultChartDataType[],
|
||||
getMetricDefine: (type: MetricType, metric: string) => MetricsDefine[keyof MetricsDefine],
|
||||
metricType: MetricType,
|
||||
timeRange: readonly [number, number],
|
||||
supplementaryInterval: number,
|
||||
needDrag = false,
|
||||
transformUnit: [string, number] = undefined
|
||||
): MetricChartDataType[] => {
|
||||
return metricData.map(({ metricName, metricLines }) => {
|
||||
const curMetricInfo = (getMetricDefine && getMetricDefine(metricType, metricName)) || null;
|
||||
const isByteUnit = curMetricInfo?.unit?.toLowerCase().includes('byte');
|
||||
let maxValue = -1;
|
||||
|
||||
const PointsMapMethod = ({ timeStamp, value }: { timeStamp: number; value: string | number }) => {
|
||||
let parsedValue: string | number = Number(value);
|
||||
|
||||
if (Number.isNaN(parsedValue)) {
|
||||
parsedValue = value;
|
||||
} else {
|
||||
// 为避免出现过小的数字影响图表展示效果,图表值统一只保留到小数点后三位
|
||||
parsedValue = parseFloat(parsedValue.toFixed(3));
|
||||
if (maxValue < parsedValue) maxValue = parsedValue;
|
||||
}
|
||||
|
||||
return [timeStamp, parsedValue];
|
||||
};
|
||||
|
||||
const chartData = Object.assign(
|
||||
{
|
||||
metricName,
|
||||
metricUnit: curMetricInfo?.unit || '',
|
||||
metricLines: metricLines
|
||||
.sort((a, b) => Number(a.name < b.name) - 0.5)
|
||||
.map(({ name, metricPoints }) => ({
|
||||
name,
|
||||
data: metricPoints.map(PointsMapMethod),
|
||||
})),
|
||||
},
|
||||
needDrag ? { dragKey: 999 } : {}
|
||||
);
|
||||
|
||||
chartData.metricLines.forEach(({ data }) => data.sort((a, b) => (a[0] as number) - (b[0] as number)));
|
||||
supplementaryPoints(chartData.metricLines, timeRange, supplementaryInterval);
|
||||
|
||||
// 将所有图表点的值按单位进行转换
|
||||
if (maxValue > 0) {
|
||||
const [unitName, unitSize]: [string, number] = transformUnit || isByteUnit ? getUnit(maxValue) : getDataNumberUnit(maxValue);
|
||||
chartData.metricUnit = isByteUnit
|
||||
? chartData.metricUnit.toLowerCase().replace('byte', unitName)
|
||||
: `${unitName}${chartData.metricUnit}`;
|
||||
chartData.metricLines.forEach(({ data }) => data.forEach((point: any) => (point[1] /= unitSize)));
|
||||
}
|
||||
|
||||
return chartData;
|
||||
});
|
||||
};
|
||||
|
||||
const seriesCallback = (lines: { name: string; data: [number, string | number][] }[]) => {
|
||||
// series 配置
|
||||
return lines.map((line) => {
|
||||
return {
|
||||
...line,
|
||||
lineStyle: {
|
||||
width: 1,
|
||||
},
|
||||
symbol: 'emptyCircle',
|
||||
symbolSize: 4,
|
||||
// emphasis: {
|
||||
// focus: 'self',
|
||||
// },
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// 返回图表配置
|
||||
export const getChartConfig = (title: string, metricLength: number) => {
|
||||
return {
|
||||
option: getBasicChartConfig({
|
||||
title: { show: false },
|
||||
grid: { top: 24 },
|
||||
tooltip: { enterable: metricLength > 9, legendContextMaxHeight: 192 },
|
||||
// xAxis: {
|
||||
// type: 'time',
|
||||
// boundaryGap: ['5%', '5%'],
|
||||
// },
|
||||
}),
|
||||
seriesCallback,
|
||||
};
|
||||
};
|
||||
|
||||
export const getDetailChartConfig = (title: string, sliderPos: readonly [number, number]) => {
|
||||
return {
|
||||
option: getBasicChartConfig({
|
||||
title: {
|
||||
show: false,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'time',
|
||||
boundaryGap: false,
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
},
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'inside',
|
||||
startValue: sliderPos[0],
|
||||
endValue: sliderPos[1],
|
||||
zoomOnMouseWheel: false,
|
||||
},
|
||||
{
|
||||
start: 0,
|
||||
end: 0,
|
||||
},
|
||||
],
|
||||
}),
|
||||
seriesCallback,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,114 @@
|
||||
.topic-dashboard {
|
||||
height: calc(100% - 160px);
|
||||
padding-bottom: 10px;
|
||||
.ks-chart-container-header {
|
||||
margin-top: 12px;
|
||||
}
|
||||
&-container {
|
||||
height: calc(100% - 40px);
|
||||
overflow: auto;
|
||||
.drag-sort-item:last-child {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.dashboard-drag-item-box {
|
||||
position: relative;
|
||||
width: auto;
|
||||
height: 262px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
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);
|
||||
&-title {
|
||||
padding: 18px 0 0 20px;
|
||||
font-family: @font-family-bold;
|
||||
line-height: 16px;
|
||||
.name {
|
||||
font-size: 14px;
|
||||
color: #212529;
|
||||
}
|
||||
.unit {
|
||||
font-size: 12px;
|
||||
color: #495057;
|
||||
}
|
||||
> span {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.expand-icon-box {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
top: 14px;
|
||||
right: 44px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.3s ease;
|
||||
.expand-icon {
|
||||
color: #adb5bc;
|
||||
line-height: 24px;
|
||||
}
|
||||
&:hover {
|
||||
background: rgba(33, 37, 41, 0.06);
|
||||
.expand-icon {
|
||||
color: #74788d;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-detail-modal-container {
|
||||
position: relative;
|
||||
.expand-icon-box {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
top: 14px;
|
||||
right: 44px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.3s ease;
|
||||
.expand-icon {
|
||||
color: #adb5bc;
|
||||
line-height: 24px;
|
||||
}
|
||||
&:hover {
|
||||
background: rgba(33, 37, 41, 0.04);
|
||||
.expand-icon {
|
||||
color: #74788d;
|
||||
}
|
||||
}
|
||||
}
|
||||
.detail-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
.left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.title {
|
||||
font-family: @font-family-bold;
|
||||
font-size: 18px;
|
||||
color: #495057;
|
||||
letter-spacing: 0;
|
||||
.unit {
|
||||
font-family: @font-family-bold;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
}
|
||||
.info {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.detail-table {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { arrayMoveImmutable } from 'array-move';
|
||||
import { Utils, Empty, IconFont, Spin, AppContainer, SingleChart, Tooltip } from 'knowdesign';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import api, { MetricType } from '@src/api';
|
||||
import SingleChartHeader, { KsHeaderOptions } from '../SingleChartHeader';
|
||||
import DragGroup from '../DragGroup';
|
||||
import ChartDetail from './ChartDetail';
|
||||
import { MetricInfo, MetricDefaultChartDataType, MetricChartDataType, formatChartData, getChartConfig } from './config';
|
||||
import './index.less';
|
||||
import { MAX_TIME_RANGE_WITH_SMALL_POINT_INTERVAL } from '@src/constants/common';
|
||||
|
||||
interface IcustomScope {
|
||||
label: string;
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
type ChartFilterOptions = Omit<KsHeaderOptions, 'gridNum'>;
|
||||
|
||||
type PropsType = {
|
||||
type: MetricType;
|
||||
};
|
||||
|
||||
const { EventBus } = Utils;
|
||||
const busInstance = new EventBus();
|
||||
|
||||
const DRAG_GROUP_GUTTER_NUM: [number, number] = [16, 16];
|
||||
|
||||
const DashboardDragChart = (props: PropsType): JSX.Element => {
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const { type: dashboardType } = props;
|
||||
const { clusterId } = useParams<{
|
||||
clusterId: string;
|
||||
}>();
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [scopeList, setScopeList] = useState<IcustomScope[]>([]); // 节点范围列表
|
||||
const [metricsList, setMetricsList] = useState<MetricInfo[]>([]); // 指标列表
|
||||
const [selectedMetricNames, setSelectedMetricNames] = useState<(string | number)[]>([]); // 默认选中的指标的列表
|
||||
const [curHeaderOptions, setCurHeaderOptions] = useState<ChartFilterOptions>();
|
||||
const [metricChartData, setMetricChartData] = useState<MetricChartDataType[]>([]); // 指标图表数据列表
|
||||
const [gridNum, setGridNum] = useState<number>(8); // 图表列布局
|
||||
const chartDetailRef = useRef(null);
|
||||
const chartDragOrder = useRef([]);
|
||||
const curFetchingTimestamp = useRef(0);
|
||||
|
||||
// 获取节点范围列表
|
||||
const getScopeList = async () => {
|
||||
const res: any = await Utils.request(api.getDashboardMetadata(clusterId, dashboardType));
|
||||
const list = res.map((item: any) => {
|
||||
return dashboardType === MetricType.Broker
|
||||
? {
|
||||
label: item.host,
|
||||
value: item.brokerId,
|
||||
}
|
||||
: {
|
||||
label: item.topicName,
|
||||
value: item.topicName,
|
||||
};
|
||||
});
|
||||
setScopeList(list);
|
||||
};
|
||||
|
||||
// 获取指标列表
|
||||
const getMetricList = () => {
|
||||
Utils.request(api.getDashboardMetricList(clusterId, dashboardType)).then((res: MetricInfo[] | null) => {
|
||||
if (!res) return;
|
||||
const showMetrics = res.filter((metric) => metric.support);
|
||||
const selectedMetrics = showMetrics.filter((metric) => metric.set).map((metric) => metric.name);
|
||||
setMetricsList(showMetrics);
|
||||
setSelectedMetricNames(selectedMetrics);
|
||||
});
|
||||
};
|
||||
|
||||
// 更新指标
|
||||
const setMetricList = (metricsSet: { [name: string]: boolean }) => {
|
||||
return Utils.request(api.getDashboardMetricList(clusterId, dashboardType), {
|
||||
method: 'POST',
|
||||
data: {
|
||||
metricsSet,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 根据筛选项获取图表信息
|
||||
const getMetricChartData = () => {
|
||||
!curHeaderOptions.isAutoReload && setLoading(true);
|
||||
const [startTime, endTime] = curHeaderOptions.rangeTime;
|
||||
|
||||
const curTimestamp = Date.now();
|
||||
curFetchingTimestamp.current = curTimestamp;
|
||||
Utils.post(api.getDashboardMetricChartData(clusterId, dashboardType), {
|
||||
startTime,
|
||||
endTime,
|
||||
metricsNames: selectedMetricNames,
|
||||
topNu: curHeaderOptions?.scopeData?.isTop ? curHeaderOptions.scopeData.data : null,
|
||||
[dashboardType === MetricType.Broker ? 'brokerIds' : 'topics']: curHeaderOptions?.scopeData?.isTop
|
||||
? null
|
||||
: curHeaderOptions.scopeData.data,
|
||||
}).then(
|
||||
(res: MetricDefaultChartDataType[] | null) => {
|
||||
// 如果当前请求不是最新请求,则不做任何操作
|
||||
if (curFetchingTimestamp.current !== curTimestamp) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (res === null) {
|
||||
// 结果为 null 时,不展示图表
|
||||
setMetricChartData([]);
|
||||
} else {
|
||||
// 格式化图表需要的数据
|
||||
const supplementaryInterval = (endTime - startTime > MAX_TIME_RANGE_WITH_SMALL_POINT_INTERVAL ? 10 : 1) * 60 * 1000;
|
||||
const formattedMetricData = formatChartData(
|
||||
res,
|
||||
global.getMetricDefine || {},
|
||||
dashboardType,
|
||||
curHeaderOptions.rangeTime,
|
||||
supplementaryInterval,
|
||||
true
|
||||
) as MetricChartDataType[];
|
||||
// 处理图表的拖拽顺序
|
||||
if (chartDragOrder.current && chartDragOrder.current.length) {
|
||||
// 根据当前拖拽顺序排列图表数据
|
||||
formattedMetricData.forEach((metric) => {
|
||||
const i = chartDragOrder.current.indexOf(metric.metricName);
|
||||
metric.dragKey = i === -1 ? 999 : i;
|
||||
});
|
||||
formattedMetricData.sort((a, b) => a.dragKey - b.dragKey);
|
||||
}
|
||||
// 更新当前拖拽顺序(处理新增或减少图表的情况)
|
||||
chartDragOrder.current = formattedMetricData.map((data) => data.metricName);
|
||||
|
||||
setMetricChartData(formattedMetricData);
|
||||
}
|
||||
setLoading(false);
|
||||
},
|
||||
() => {
|
||||
if (curFetchingTimestamp.current === curTimestamp) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// 筛选项变化或者点击刷新按钮
|
||||
const ksHeaderChange = (ksOptions: KsHeaderOptions) => {
|
||||
// 重新渲染图表
|
||||
if (gridNum !== ksOptions.gridNum) {
|
||||
setGridNum(ksOptions.gridNum || 8);
|
||||
busInstance.emit('chartResize');
|
||||
} else {
|
||||
// 如果为相对时间,则当前时间减去 1 分钟,避免最近一分钟的数据还没采集到时前端多补一个点
|
||||
if (ksOptions.isRelativeRangeTime) {
|
||||
ksOptions.rangeTime = ksOptions.rangeTime.map((timestamp) => timestamp - 60 * 1000) as [number, number];
|
||||
}
|
||||
setCurHeaderOptions({
|
||||
isRelativeRangeTime: ksOptions.isRelativeRangeTime,
|
||||
isAutoReload: ksOptions.isAutoReload,
|
||||
rangeTime: ksOptions.rangeTime,
|
||||
scopeData: ksOptions.scopeData,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 指标选中项更新回调
|
||||
const indicatorChangeCallback = (newMetricNames: (string | number)[]) => {
|
||||
const updateMetrics: { [name: string]: boolean } = {};
|
||||
// 需要选中的指标
|
||||
newMetricNames.forEach((name) => !selectedMetricNames.includes(name) && (updateMetrics[name] = true));
|
||||
// 取消选中的指标
|
||||
selectedMetricNames.forEach((name) => !newMetricNames.includes(name) && (updateMetrics[name] = false));
|
||||
|
||||
const requestPromise = Object.keys(updateMetrics).length ? setMetricList(updateMetrics) : Promise.resolve();
|
||||
requestPromise.then(
|
||||
() => getMetricList(),
|
||||
() => getMetricList()
|
||||
);
|
||||
|
||||
return requestPromise;
|
||||
};
|
||||
|
||||
// 拖拽开始回调,触发图表的 onDrag 事件( 设置为 true ),禁止同步展示图表的 tooltip
|
||||
const dragStart = () => {
|
||||
busInstance.emit('onDrag', true);
|
||||
};
|
||||
|
||||
// 拖拽结束回调,更新图表顺序,并触发图表的 onDrag 事件( 设置为 false ),允许同步展示图表的 tooltip
|
||||
const dragEnd = ({ oldIndex, newIndex }: { oldIndex: number; newIndex: number }) => {
|
||||
busInstance.emit('onDrag', false);
|
||||
chartDragOrder.current = arrayMoveImmutable(chartDragOrder.current, oldIndex, newIndex);
|
||||
setMetricChartData(arrayMoveImmutable(metricChartData, oldIndex, newIndex));
|
||||
};
|
||||
|
||||
// 监听盒子宽度变化,重置图表宽度
|
||||
const observeDashboardWidthChange = () => {
|
||||
const targetNode = document.getElementsByClassName('dcd-two-columns-layout-sider-footer')[0];
|
||||
targetNode && targetNode.addEventListener('click', () => busInstance.emit('chartResize'));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedMetricNames.length && curHeaderOptions) {
|
||||
getMetricChartData();
|
||||
}
|
||||
}, [curHeaderOptions, selectedMetricNames]);
|
||||
|
||||
useEffect(() => {
|
||||
// 初始化页面,获取 scope 和 metric 信息
|
||||
getScopeList();
|
||||
getMetricList();
|
||||
|
||||
setTimeout(() => observeDashboardWidthChange());
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div id="dashboard-drag-chart" className="topic-dashboard">
|
||||
<SingleChartHeader
|
||||
onChange={ksHeaderChange}
|
||||
nodeScopeModule={{
|
||||
customScopeList: scopeList,
|
||||
scopeName: `自定义 ${dashboardType === MetricType.Broker ? 'Broker' : 'Topic'} 范围`,
|
||||
showSearch: dashboardType === MetricType.Topic,
|
||||
}}
|
||||
indicatorSelectModule={{
|
||||
hide: false,
|
||||
metricType: dashboardType,
|
||||
tableData: metricsList,
|
||||
selectedRows: selectedMetricNames,
|
||||
submitCallback: indicatorChangeCallback,
|
||||
}}
|
||||
/>
|
||||
<div className="topic-dashboard-container">
|
||||
<Spin spinning={loading} style={{ height: 400 }}>
|
||||
{metricChartData && metricChartData.length ? (
|
||||
<div className="no-group-con">
|
||||
<DragGroup
|
||||
sortableContainerProps={{
|
||||
onSortStart: dragStart,
|
||||
onSortEnd: dragEnd,
|
||||
axis: 'xy',
|
||||
useDragHandle: true,
|
||||
}}
|
||||
gridProps={{
|
||||
span: gridNum,
|
||||
gutter: DRAG_GROUP_GUTTER_NUM,
|
||||
}}
|
||||
>
|
||||
{metricChartData.map((data) => {
|
||||
const { metricName, metricUnit, metricLines } = data;
|
||||
|
||||
return (
|
||||
<div key={metricName} className="dashboard-drag-item-box">
|
||||
<div className="dashboard-drag-item-box-title">
|
||||
<Tooltip
|
||||
placement="topLeft"
|
||||
title={() => {
|
||||
let content = '';
|
||||
const metricDefine = global.getMetricDefine(dashboardType, metricName);
|
||||
if (metricDefine) {
|
||||
content = metricDefine.desc;
|
||||
}
|
||||
return content;
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
<span className="name">{metricName}</span>
|
||||
<span className="unit">({metricUnit})</span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div
|
||||
className="expand-icon-box"
|
||||
onClick={() => {
|
||||
const linesName = scopeList.map((item) => item.value);
|
||||
chartDetailRef.current.onOpen(dashboardType, metricName, linesName);
|
||||
}}
|
||||
>
|
||||
<IconFont type="icon-chuangkoufangda" className="expand-icon" />
|
||||
</div>
|
||||
<SingleChart
|
||||
chartKey={metricName}
|
||||
chartTypeProp="line"
|
||||
showHeader={false}
|
||||
wrapStyle={{
|
||||
width: 'auto',
|
||||
height: 222,
|
||||
}}
|
||||
connectEventName={`${dashboardType}BoardDragChart`}
|
||||
eventBus={busInstance}
|
||||
propChartData={metricLines}
|
||||
optionMergeProps={{ replaceMerge: curHeaderOptions.isAutoReload ? ['xAxis'] : ['series'] }}
|
||||
{...getChartConfig(`${metricName}{unit|(${metricUnit})}`, metricLines.length)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</DragGroup>
|
||||
</div>
|
||||
) : loading ? (
|
||||
<></>
|
||||
) : (
|
||||
<Empty description="数据为空,请选择指标或刷新" image={Empty.PRESENTED_IMAGE_CUSTOM} style={{ padding: '100px 0' }} />
|
||||
)}
|
||||
</Spin>
|
||||
</div>
|
||||
{/* 图表详情 */}
|
||||
<ChartDetail ref={chartDetailRef} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardDragChart;
|
||||
@@ -0,0 +1,15 @@
|
||||
.drag-container {
|
||||
}
|
||||
.drag-sort-item {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
.drag-handle-icon {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
right: 18px;
|
||||
color: #adb5bc;
|
||||
font-size: 16px;
|
||||
z-index: 1000;
|
||||
cursor: move;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Col, IconFont, Row } from 'knowdesign';
|
||||
import React from 'react';
|
||||
import { SortableContainer, SortableContainerProps, SortableHandle, SortableElement, SortableElementProps } from 'react-sortable-hoc';
|
||||
import './index.less';
|
||||
|
||||
interface PropsType extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children: React.ReactNode;
|
||||
sortableContainerProps?: SortableContainerProps;
|
||||
gridProps?: {
|
||||
span: number;
|
||||
gutter: [number, number];
|
||||
};
|
||||
dragItemProps?: Omit<SortableElementProps, 'index'>;
|
||||
}
|
||||
|
||||
interface SortableItemProps {
|
||||
useDragHandle?: boolean;
|
||||
span: number;
|
||||
}
|
||||
|
||||
// 拖拽容器
|
||||
const DragContainer = SortableContainer(({ children, gutter }: any) => (
|
||||
<Row className="drag-container" gutter={gutter}>
|
||||
{children}
|
||||
</Row>
|
||||
));
|
||||
|
||||
// 拖拽按钮
|
||||
const DragHandle = SortableHandle(() => <IconFont className="drag-handle-icon" type="icon-tuozhuai1" />);
|
||||
|
||||
// 拖拽项
|
||||
const SortableItem = SortableElement(
|
||||
({ children, sortableItemProps }: { children: React.ReactNode; sortableItemProps: SortableItemProps }) => (
|
||||
<Col className="drag-sort-item" span={sortableItemProps.span}>
|
||||
{sortableItemProps?.useDragHandle && <DragHandle />}
|
||||
{children}
|
||||
</Col>
|
||||
)
|
||||
);
|
||||
|
||||
const DragGroup: React.FC<PropsType> = ({ children, gridProps = { span: 8, gutter: [10, 10] }, sortableContainerProps }) => {
|
||||
return (
|
||||
<DragContainer pressDelay={0} {...sortableContainerProps} gutter={gridProps.gutter}>
|
||||
{React.Children.map(children, (child: any, index: number) => {
|
||||
// 如果传入 child 有 key 值就复用,没有的话使用 index 作为 key
|
||||
const key = typeof child === 'object' && child !== null ? child.key || index : index;
|
||||
|
||||
return (
|
||||
<SortableItem
|
||||
key={key}
|
||||
index={index}
|
||||
sortableItemProps={{
|
||||
useDragHandle: sortableContainerProps.useDragHandle || false,
|
||||
span: gridProps.span,
|
||||
}}
|
||||
>
|
||||
{child}
|
||||
</SortableItem>
|
||||
);
|
||||
})}
|
||||
</DragContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default DragGroup;
|
||||
@@ -0,0 +1,321 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Drawer, Button, Space, Divider, AppContainer, ProTable, IconFont } from 'knowdesign';
|
||||
import { IindicatorSelectModule } from './index';
|
||||
import './style/indicator-drawer.less';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
interface PropsType extends React.HTMLAttributes<HTMLDivElement> {
|
||||
onClose: () => void;
|
||||
visible: boolean;
|
||||
isGroup?: boolean; // 是否分组
|
||||
indicatorSelectModule: IindicatorSelectModule;
|
||||
}
|
||||
|
||||
interface MetricInfo {
|
||||
name: string;
|
||||
unit: string;
|
||||
desc: string;
|
||||
}
|
||||
|
||||
interface SelectedMetrics {
|
||||
[category: string]: string[];
|
||||
}
|
||||
|
||||
type CategoryData = {
|
||||
category: string;
|
||||
metrics: MetricInfo[];
|
||||
};
|
||||
|
||||
const ExpandedRow = ({ metrics, category, selectedMetrics, selectedMetricChange }: any) => {
|
||||
const innerColumns = [
|
||||
{
|
||||
title: '指标名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: '单位',
|
||||
dataIndex: 'unit',
|
||||
key: 'unit',
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'desc',
|
||||
key: 'desc',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
padding: '12px 16px',
|
||||
margin: '0 7px',
|
||||
border: '1px solid #EFF2F7',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: '#ffffff',
|
||||
}}
|
||||
>
|
||||
<ProTable
|
||||
tableProps={{
|
||||
showHeader: false,
|
||||
noPagination: true,
|
||||
rowKey: 'name',
|
||||
columns: innerColumns,
|
||||
dataSource: metrics,
|
||||
attrs: {
|
||||
rowSelection: {
|
||||
hideSelectAll: true,
|
||||
selectedRowKeys: selectedMetrics,
|
||||
onChange: (keys: string[]) => {
|
||||
selectedMetricChange(category, keys);
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const IndicatorDrawer = ({ onClose, visible, indicatorSelectModule }: PropsType) => {
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const { pathname } = useLocation();
|
||||
const [confirmLoading, setConfirmLoading] = useState<boolean>(false);
|
||||
const [categoryData, setCategoryData] = useState<CategoryData[]>([]);
|
||||
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
|
||||
const [childrenSelectedRowKeys, setChildrenSelectedRowKeys] = useState<SelectedMetrics>({});
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: `${pathname.endsWith('/broker') ? 'Broker' : pathname.endsWith('/topic') ? 'Topic' : 'Cluster'} Metrics`,
|
||||
dataIndex: 'category',
|
||||
key: 'category',
|
||||
},
|
||||
];
|
||||
|
||||
const formateTableData = () => {
|
||||
const tableData = indicatorSelectModule.tableData;
|
||||
const categoryData: {
|
||||
[category: string]: MetricInfo[];
|
||||
} = {};
|
||||
|
||||
tableData.forEach(({ name, desc }) => {
|
||||
const metricDefine = global.getMetricDefine(indicatorSelectModule?.metricType, name);
|
||||
const returnData = {
|
||||
name,
|
||||
desc,
|
||||
unit: metricDefine?.unit,
|
||||
};
|
||||
if (metricDefine.category) {
|
||||
if (!categoryData[metricDefine.category]) {
|
||||
categoryData[metricDefine.category] = [returnData];
|
||||
} else {
|
||||
categoryData[metricDefine.category].push(returnData);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const result = Object.entries(categoryData).map(([category, data]) => ({
|
||||
category,
|
||||
metrics: data.sort((a, b) => Number(a.name > b.name) - 0.5),
|
||||
}));
|
||||
setCategoryData(result);
|
||||
};
|
||||
|
||||
const formateSelectedKeys = () => {
|
||||
const newKeys = indicatorSelectModule.selectedRows;
|
||||
const result: SelectedMetrics = {};
|
||||
const selectedCategories: string[] = [];
|
||||
|
||||
newKeys.forEach((name: string) => {
|
||||
const metricDefine = global.getMetricDefine(indicatorSelectModule?.metricType, name);
|
||||
if (metricDefine) {
|
||||
if (!result[metricDefine.category]) {
|
||||
result[metricDefine.category] = [name];
|
||||
} else {
|
||||
result[metricDefine.category].push(name);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Object.entries(result).forEach(([curCategory, metrics]) => {
|
||||
if (metrics.length) {
|
||||
selectedCategories.push(curCategory);
|
||||
}
|
||||
});
|
||||
|
||||
setSelectedCategories(selectedCategories);
|
||||
|
||||
setChildrenSelectedRowKeys((cur) => ({
|
||||
...cur,
|
||||
...result,
|
||||
}));
|
||||
};
|
||||
|
||||
const rowChange = (newCategorys: string[]) => {
|
||||
newCategorys.sort();
|
||||
const prevCategories = selectedCategories;
|
||||
const addCategories: string[] = [];
|
||||
const delCategories: string[] = [];
|
||||
// 需要选中的指标
|
||||
newCategorys.forEach((name) => !prevCategories.includes(name) && addCategories.push(name));
|
||||
// 取消选中的指标
|
||||
prevCategories.forEach((name) => !newCategorys.includes(name) && delCategories.push(name));
|
||||
|
||||
const changedCategories: SelectedMetrics = {};
|
||||
// 1. 选中,即选中所有子项
|
||||
addCategories.forEach((curCategory) => {
|
||||
let childrenData: string[] = [];
|
||||
categoryData.some(({ category, metrics }) => {
|
||||
if (curCategory === category) {
|
||||
childrenData = metrics.map(({ name }) => name);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
changedCategories[curCategory] = childrenData;
|
||||
});
|
||||
// 2. 取消选中,取消选中所有子项
|
||||
delCategories.forEach((curCategory) => {
|
||||
changedCategories[curCategory] = [];
|
||||
});
|
||||
|
||||
setSelectedCategories(newCategorys);
|
||||
setChildrenSelectedRowKeys((cur) => ({
|
||||
...cur,
|
||||
...changedCategories,
|
||||
}));
|
||||
};
|
||||
|
||||
const childrenRowChange = (curCategory: string, keys: string[]) => {
|
||||
// 更新选中的指标项
|
||||
setChildrenSelectedRowKeys((cur) => ({
|
||||
...cur,
|
||||
[curCategory]: keys,
|
||||
}));
|
||||
// 更新父级选中状态
|
||||
if (keys.length === 0) {
|
||||
const i = selectedCategories.indexOf(curCategory);
|
||||
if (i !== -1) {
|
||||
const newCategories = [...selectedCategories];
|
||||
newCategories.splice(i, 1);
|
||||
setSelectedCategories(newCategories);
|
||||
}
|
||||
} else {
|
||||
const curCategoryData = categoryData.find((value) => value.category === curCategory);
|
||||
if (curCategoryData.metrics.length === keys.length) {
|
||||
setSelectedCategories([...selectedCategories, curCategory]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const submitRowKeys = () => {
|
||||
setConfirmLoading(true);
|
||||
|
||||
const allRowKeys: string[] = [];
|
||||
Object.entries(childrenSelectedRowKeys).forEach(([, arr]) => allRowKeys.push(...arr));
|
||||
|
||||
indicatorSelectModule.submitCallback(allRowKeys).then(
|
||||
() => {
|
||||
setConfirmLoading(false);
|
||||
onClose();
|
||||
},
|
||||
() => {
|
||||
setConfirmLoading(false);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const rowSelection = {
|
||||
selectedRowKeys: selectedCategories,
|
||||
onChange: rowChange,
|
||||
// getCheckboxProps: (record: any) => indicatorSelectModule.checkboxProps && indicatorSelectModule.checkboxProps(record),
|
||||
getCheckboxProps: (record: CategoryData) => {
|
||||
const isAllSelected = record.metrics.length === childrenSelectedRowKeys[record.category]?.length;
|
||||
const isNotCheck = !childrenSelectedRowKeys[record.category] || childrenSelectedRowKeys[record.category]?.length === 0;
|
||||
return {
|
||||
indeterminate: !isNotCheck && !isAllSelected,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
useEffect(formateTableData, [indicatorSelectModule.tableData]);
|
||||
|
||||
useEffect(() => {
|
||||
visible && formateSelectedKeys();
|
||||
}, [visible, indicatorSelectModule.selectedRows]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Drawer
|
||||
title={indicatorSelectModule.drawerTitle || '指标筛选'}
|
||||
width="868px"
|
||||
forceRender={true}
|
||||
onClose={onClose}
|
||||
visible={visible}
|
||||
maskClosable={false}
|
||||
extra={
|
||||
<Space>
|
||||
<Button size="small" onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
disabled={Object.entries(childrenSelectedRowKeys).every(([, arr]) => !arr.length)}
|
||||
loading={confirmLoading}
|
||||
onClick={submitRowKeys}
|
||||
>
|
||||
确定
|
||||
</Button>
|
||||
<Divider type="vertical" />
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<ProTable
|
||||
tableProps={{
|
||||
rowKey: 'category',
|
||||
columns: columns,
|
||||
dataSource: categoryData,
|
||||
attrs: {
|
||||
rowSelection: rowSelection,
|
||||
expandable: {
|
||||
expandRowByClick: true,
|
||||
expandedRowRender: (record: CategoryData) => (
|
||||
<ExpandedRow
|
||||
metrics={record.metrics}
|
||||
category={record.category}
|
||||
selectedMetrics={childrenSelectedRowKeys[record.category] || []}
|
||||
selectedMetricChange={childrenRowChange}
|
||||
/>
|
||||
),
|
||||
expandIcon: ({ expanded, onExpand, record }: any) => {
|
||||
return expanded ? (
|
||||
<IconFont
|
||||
style={{ fontSize: '16px' }}
|
||||
type="icon-xia"
|
||||
onClick={(e: any) => {
|
||||
onExpand(record, e);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<IconFont
|
||||
style={{ fontSize: '16px' }}
|
||||
type="icon-jiantou_1"
|
||||
onClick={(e: any) => {
|
||||
onExpand(record, e);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default IndicatorDrawer;
|
||||
@@ -0,0 +1,205 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Radio, Input, Popover, Space, Checkbox, Row, Col, Button, IconFont } from 'knowdesign';
|
||||
import { InodeScopeModule } from './index';
|
||||
import './style/node-scope.less';
|
||||
|
||||
interface propsType {
|
||||
change: Function;
|
||||
nodeScopeModule: InodeScopeModule;
|
||||
}
|
||||
|
||||
const OptionsDefault = [
|
||||
{
|
||||
label: 'Top 5',
|
||||
value: 5,
|
||||
},
|
||||
{
|
||||
label: 'Top 10',
|
||||
value: 10,
|
||||
},
|
||||
{
|
||||
label: 'Top 15',
|
||||
value: 15,
|
||||
},
|
||||
];
|
||||
|
||||
const NodeScope = ({ nodeScopeModule, change }: propsType) => {
|
||||
const {
|
||||
customScopeList: customList,
|
||||
scopeName = '自定义节点范围',
|
||||
showSearch = false,
|
||||
searchPlaceholder = '输入内容进行搜索',
|
||||
} = nodeScopeModule;
|
||||
const [topNum, setTopNum] = useState<number>(5);
|
||||
const [isTop, setIsTop] = useState(true);
|
||||
const [audioOptions, setAudioOptions] = useState(OptionsDefault);
|
||||
const [scopeSearchValue, setScopeSearchValue] = useState('');
|
||||
const [inputValue, setInputValue] = useState<string>(null);
|
||||
const [indeterminate, setIndeterminate] = useState(false);
|
||||
const [popVisible, setPopVisible] = useState(false);
|
||||
const [checkAll, setCheckAll] = useState(false);
|
||||
const [checkedListTemp, setCheckedListTemp] = useState([]);
|
||||
const [checkedList, setCheckedList] = useState([]);
|
||||
const [allCheckedList, setAllCheckedList] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const all = customList?.map((item) => item.value) || [];
|
||||
setAllCheckedList(all);
|
||||
}, [customList]);
|
||||
|
||||
useEffect(() => {
|
||||
if (topNum) {
|
||||
const timeOption = audioOptions.find((item) => item.value === topNum);
|
||||
|
||||
setInputValue(timeOption?.label);
|
||||
setCheckedListTemp([]);
|
||||
setCheckedList([]);
|
||||
|
||||
setPopVisible(false);
|
||||
}
|
||||
}, [topNum]);
|
||||
|
||||
useEffect(() => {
|
||||
setIndeterminate(!!checkedListTemp.length && checkedListTemp.length < allCheckedList.length);
|
||||
setCheckAll(checkedListTemp?.length === allCheckedList.length);
|
||||
}, [checkedListTemp]);
|
||||
|
||||
const customSure = () => {
|
||||
if (checkedListTemp?.length > 0) {
|
||||
setCheckedList(checkedListTemp);
|
||||
change(checkedListTemp, false);
|
||||
setIsTop(false);
|
||||
setTopNum(null);
|
||||
setInputValue(`已选${checkedListTemp?.length}项`);
|
||||
setPopVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
const customCancel = () => {
|
||||
setCheckedListTemp(checkedList);
|
||||
setPopVisible(false);
|
||||
};
|
||||
|
||||
const visibleChange = (visible: any) => {
|
||||
setCheckedListTemp(checkedList);
|
||||
setPopVisible(visible);
|
||||
};
|
||||
|
||||
const periodtimeChange = (e: any) => {
|
||||
const topNum = e.target.value;
|
||||
setTopNum(topNum);
|
||||
change(topNum, true);
|
||||
setIsTop(true);
|
||||
};
|
||||
|
||||
const onCheckAllChange = (e: any) => {
|
||||
setCheckedListTemp(e.target.checked ? allCheckedList : []);
|
||||
setIndeterminate(false);
|
||||
setCheckAll(e.target.checked);
|
||||
};
|
||||
|
||||
const checkChange = (val: any) => {
|
||||
setCheckedListTemp(val);
|
||||
// setIndeterminate(!!val.length && val.length < allCheckedList.length);
|
||||
// setCheckAll(val?.length === allCheckedList.length);
|
||||
};
|
||||
|
||||
const clickContent = (
|
||||
<div className="dd-node-scope-module">
|
||||
{/* <span>时间:</span> */}
|
||||
<div className="flx_con">
|
||||
<div className="flx_l">
|
||||
<h6 className="time_title">选择top范围</h6>
|
||||
<Radio.Group
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
className="topNum-radio-group"
|
||||
// options={audioOptions}
|
||||
onChange={periodtimeChange}
|
||||
value={topNum}
|
||||
>
|
||||
<Space direction="vertical" size={16}>
|
||||
{audioOptions.map((item, index) => (
|
||||
<Radio value={item.value} key={index}>
|
||||
{item.label}
|
||||
</Radio>
|
||||
))}
|
||||
</Space>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
<div className="flx_r">
|
||||
<h6 className="time_title">{scopeName}</h6>
|
||||
<div className="custom-scope">
|
||||
<div className="check-row">
|
||||
<Checkbox className="check-all" indeterminate={indeterminate} onChange={onCheckAllChange} checked={checkAll}>
|
||||
全选
|
||||
</Checkbox>
|
||||
<Input
|
||||
className="search-input"
|
||||
suffix={
|
||||
<IconFont type="icon-fangdajing" style={{ fontSize: '16px' }} />
|
||||
}
|
||||
size="small"
|
||||
placeholder={searchPlaceholder}
|
||||
onChange={(e) => setScopeSearchValue(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="fixed-height">
|
||||
<Checkbox.Group style={{ width: '100%' }} onChange={checkChange} value={checkedListTemp}>
|
||||
<Row gutter={[10, 12]}>
|
||||
{customList
|
||||
.filter((item) => !showSearch || item.label.includes(scopeSearchValue))
|
||||
.map((item) => (
|
||||
<Col span={12} key={item.value}>
|
||||
<Checkbox value={item.value}>{item.label}</Checkbox>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</Checkbox.Group>
|
||||
</div>
|
||||
|
||||
<div className="btn-con">
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
className="btn-sure"
|
||||
onClick={customSure}
|
||||
disabled={checkedListTemp?.length > 0 ? false : true}
|
||||
>
|
||||
确定
|
||||
</Button>
|
||||
<Button size="small" onClick={customCancel}>
|
||||
取消
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<div id="d-node-scope">
|
||||
<Popover
|
||||
trigger={['click']}
|
||||
visible={popVisible}
|
||||
content={clickContent}
|
||||
placement="bottomRight"
|
||||
overlayClassName="d-node-scope-popover"
|
||||
onVisibleChange={visibleChange}
|
||||
>
|
||||
<span className="input-span">
|
||||
<Input
|
||||
className={isTop ? 'relativeTime d-node-scope-input' : 'absoluteTime d-node-scope-input'}
|
||||
value={inputValue}
|
||||
readOnly={true}
|
||||
suffix={<IconFont type="icon-jiantou1" rotate={90} style={{ color: '#74788D' }}></IconFont>}
|
||||
/>
|
||||
</span>
|
||||
</Popover>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NodeScope;
|
||||
|
After Width: | Height: | Size: 8.4 KiB |
@@ -0,0 +1,193 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Tooltip, Select, IconFont, Utils, Divider } from 'knowdesign';
|
||||
import moment from 'moment';
|
||||
import { DRangeTime } from 'knowdesign';
|
||||
import IndicatorDrawer from './IndicatorDrawer';
|
||||
import NodeScope from './NodeScope';
|
||||
|
||||
import './style/index.less';
|
||||
import { MetricType } from 'src/api';
|
||||
|
||||
export interface Inode {
|
||||
name: string;
|
||||
desc: string;
|
||||
}
|
||||
|
||||
export interface KsHeaderOptions {
|
||||
rangeTime: [number, number];
|
||||
isRelativeRangeTime: boolean;
|
||||
isAutoReload: boolean;
|
||||
gridNum?: number;
|
||||
scopeData?: {
|
||||
isTop: boolean;
|
||||
data: number | number[];
|
||||
};
|
||||
}
|
||||
export interface IindicatorSelectModule {
|
||||
metricType?: MetricType;
|
||||
hide?: boolean;
|
||||
drawerTitle?: string;
|
||||
selectedRows: (string | number)[];
|
||||
checkboxProps?: (record: any) => { [props: string]: any };
|
||||
tableData?: Inode[];
|
||||
submitCallback?: (value: (string | number)[]) => Promise<any>;
|
||||
}
|
||||
|
||||
export interface IfilterData {
|
||||
hostName?: string;
|
||||
logCollectTaskId?: string | number;
|
||||
pathId?: string | number;
|
||||
agent?: string;
|
||||
}
|
||||
|
||||
export interface IcustomScope {
|
||||
label: string;
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
export interface InodeScopeModule {
|
||||
customScopeList: IcustomScope[];
|
||||
scopeName?: string;
|
||||
showSearch?: boolean;
|
||||
searchPlaceholder?: string;
|
||||
change?: () => void;
|
||||
}
|
||||
interface PropsType {
|
||||
indicatorSelectModule?: IindicatorSelectModule;
|
||||
hideNodeScope?: boolean;
|
||||
hideGridSelect?: boolean;
|
||||
nodeScopeModule?: InodeScopeModule;
|
||||
onChange: (options: KsHeaderOptions) => void;
|
||||
}
|
||||
|
||||
// 列布局选项
|
||||
const GRID_SIZE_OPTIONS = [
|
||||
{
|
||||
label: '3列',
|
||||
value: 8,
|
||||
},
|
||||
{
|
||||
label: '2列',
|
||||
value: 12,
|
||||
},
|
||||
{
|
||||
label: '1列',
|
||||
value: 24,
|
||||
},
|
||||
];
|
||||
|
||||
const SingleChartHeader = ({
|
||||
indicatorSelectModule,
|
||||
nodeScopeModule = {
|
||||
customScopeList: [],
|
||||
},
|
||||
hideNodeScope = false,
|
||||
hideGridSelect = false,
|
||||
onChange: onChangeCallback,
|
||||
}: PropsType): JSX.Element => {
|
||||
const [gridNum, setGridNum] = useState<number>(GRID_SIZE_OPTIONS[0].value);
|
||||
const [rangeTime, setRangeTime] = useState<[number, number]>(() => {
|
||||
const curTimeStamp = moment().valueOf();
|
||||
return [curTimeStamp - 15 * 60 * 1000, curTimeStamp];
|
||||
});
|
||||
const [isRelativeRangeTime, setIsRelativeRangeTime] = useState(true);
|
||||
const [isAutoReload, setIsAutoReload] = useState(false);
|
||||
const [indicatorDrawerVisible, setIndicatorDrawerVisible] = useState(false);
|
||||
|
||||
const [scopeData, setScopeData] = useState<{
|
||||
isTop: boolean;
|
||||
data: any;
|
||||
}>({
|
||||
isTop: true,
|
||||
data: 5,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
onChangeCallback({
|
||||
rangeTime,
|
||||
scopeData,
|
||||
gridNum,
|
||||
isRelativeRangeTime,
|
||||
isAutoReload,
|
||||
});
|
||||
setIsAutoReload(false);
|
||||
}, [rangeTime, scopeData, gridNum, isRelativeRangeTime]);
|
||||
|
||||
// 当时间范围为相对时间时,每隔 1 分钟刷新一次时间
|
||||
useEffect(() => {
|
||||
let relativeTimer: number;
|
||||
if (isRelativeRangeTime) {
|
||||
relativeTimer = window.setInterval(() => {
|
||||
setIsAutoReload(true);
|
||||
reloadRangeTime();
|
||||
}, 1 * 60 * 1000);
|
||||
}
|
||||
|
||||
return () => {
|
||||
relativeTimer && window.clearInterval(relativeTimer);
|
||||
};
|
||||
}, [isRelativeRangeTime, rangeTime]);
|
||||
|
||||
const sizeChange = (value: number) => {
|
||||
setGridNum(value);
|
||||
};
|
||||
|
||||
const timeChange = (curRangeTime: [number, number], isRelative: boolean) => {
|
||||
setRangeTime([...curRangeTime]);
|
||||
setIsRelativeRangeTime(isRelative);
|
||||
};
|
||||
|
||||
const reloadRangeTime = () => {
|
||||
const timeLen = rangeTime[1] - rangeTime[0] || 0;
|
||||
const curTimeStamp = moment().valueOf();
|
||||
setRangeTime([curTimeStamp - timeLen, curTimeStamp]);
|
||||
};
|
||||
|
||||
const openIndicatorDrawer = () => {
|
||||
setIndicatorDrawerVisible(true);
|
||||
};
|
||||
|
||||
const closeIndicatorDrawer = () => {
|
||||
setIndicatorDrawerVisible(false);
|
||||
};
|
||||
|
||||
const nodeScopeChange = (data: any, isTop?: any) => {
|
||||
setScopeData({
|
||||
isTop,
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="ks-chart-container">
|
||||
<div className="ks-chart-container-header">
|
||||
<div className="header-left">
|
||||
<div className="icon-box" onClick={reloadRangeTime}>
|
||||
<IconFont className="icon" type="icon-shuaxin1" />
|
||||
</div>
|
||||
<Divider type="vertical" style={{ height: 20, top: 0 }} />
|
||||
<DRangeTime timeChange={timeChange} rangeTimeArr={rangeTime} />
|
||||
</div>
|
||||
<div className="header-right">
|
||||
{!hideNodeScope && <NodeScope nodeScopeModule={nodeScopeModule} change={nodeScopeChange} />}
|
||||
{!hideGridSelect && (
|
||||
<Select className="grid-select" style={{ width: 70 }} value={gridNum} options={GRID_SIZE_OPTIONS} onChange={sizeChange} />
|
||||
)}
|
||||
<Divider type="vertical" style={{ height: 20, top: 0 }} />
|
||||
<Tooltip title="点击指标筛选,可选择指标" placement="bottomRight">
|
||||
<div className="icon-box" onClick={openIndicatorDrawer}>
|
||||
<IconFont className="icon" type="icon-shezhi1" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!indicatorSelectModule?.hide && (
|
||||
<IndicatorDrawer visible={indicatorDrawerVisible} onClose={closeIndicatorDrawer} indicatorSelectModule={indicatorSelectModule} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SingleChartHeader;
|
||||
@@ -0,0 +1,87 @@
|
||||
@root-entry-name: 'default';
|
||||
@import '~knowdesign/es/basic/style/themes/index';
|
||||
@import '~knowdesign/es/basic/style/mixins/index';
|
||||
@collapse-prefix-cls: ~'@{ant-prefix}-collapse';
|
||||
|
||||
.clearfix() {
|
||||
&::before {
|
||||
display: table;
|
||||
content: '';
|
||||
}
|
||||
|
||||
&::after {
|
||||
display: table;
|
||||
clear: both;
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
.ks-chart-container {
|
||||
.query-module-container {
|
||||
padding: 20px 24px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #f6f6f6;
|
||||
}
|
||||
.d1-row {
|
||||
width: auto;
|
||||
}
|
||||
&-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-bottom: 12px;
|
||||
.header-left,
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.icon-box {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
.icon {
|
||||
font-size: 20px;
|
||||
color: #74788d;
|
||||
}
|
||||
&:hover {
|
||||
background: #21252904;
|
||||
.refresh-icon {
|
||||
color: #495057;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.header-right {
|
||||
.grid-select {
|
||||
margin-left: 12px;
|
||||
.dcloud-select-selector {
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.no-group-con {
|
||||
padding: 0 24px;
|
||||
}
|
||||
.drag-sort-item {
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
}
|
||||
.@{ant-prefix}-collapse {
|
||||
&-ghost {
|
||||
> .@{collapse-prefix-cls}-item {
|
||||
> .@{collapse-prefix-cls}-header {
|
||||
padding: 10px 24px;
|
||||
}
|
||||
> .@{collapse-prefix-cls}-content {
|
||||
> .@{collapse-prefix-cls}-content-box {
|
||||
padding: 20px 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
@root-entry-name: 'default';
|
||||
@import '~knowdesign/es/basic/style/themes/index';
|
||||
@import '~knowdesign/es/basic/style/mixins/index';
|
||||
|
||||
// .dd-indicator-drawer {
|
||||
// @drawerItemH: 27px;
|
||||
// @primary-color: #556ee6;
|
||||
// &.contain-tab {
|
||||
// .@{ant-prefix}-drawer-body {
|
||||
// padding: 0 20px 20px 20px;
|
||||
// }
|
||||
// .@{ant-prefix}-layout-sider {
|
||||
// height: ~'calc(100vh - 268px)';
|
||||
// }
|
||||
// .@{ant-prefix}-spin-container {
|
||||
// height: ~'calc(100vh - 268px - 12px)';
|
||||
// .@{ant-prefix}-table {
|
||||
// height: ~'calc(100% - 36px)';
|
||||
// overflow: auto;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .hide {
|
||||
// display: none;
|
||||
// }
|
||||
// .@{ant-prefix}-drawer-body {
|
||||
// padding: 12px 20px;
|
||||
// .label-name {
|
||||
// height: 34px;
|
||||
// line-height: 34px;
|
||||
// font-size: 13px;
|
||||
// color: #495057;
|
||||
// }
|
||||
// }
|
||||
// .@{ant-prefix}-spin-container {
|
||||
// height: ~'calc(100vh - 218px - 12px)';
|
||||
// .@{ant-prefix}-table {
|
||||
// height: ~'calc(100% - 36px)';
|
||||
// overflow: auto;
|
||||
// }
|
||||
// }
|
||||
// .@{ant-prefix}-layout-sider {
|
||||
// height: ~'calc(100vh - 218px)';
|
||||
// overflow: auto;
|
||||
// }
|
||||
// .@{ant-prefix}-menu-overflow {
|
||||
// // display: inline-flex;
|
||||
// &::before,
|
||||
// &::after {
|
||||
// width: 0;
|
||||
// }
|
||||
// }
|
||||
|
||||
// .@{ant-prefix}-tree {
|
||||
// padding: 20px;
|
||||
// &.@{ant-prefix}-tree-directory {
|
||||
// .@{ant-prefix}-tree-treenode {
|
||||
// padding-right: 24px;
|
||||
// &:not(.@{ant-prefix}-tree-treenode-switcher-open) {
|
||||
// padding-right: 0;
|
||||
// }
|
||||
// &:not(.@{ant-prefix}-tree-treenode-switcher-close) {
|
||||
// padding-right: 0;
|
||||
// }
|
||||
// &.@{ant-prefix}-tree-treenode-selected {
|
||||
// &::before {
|
||||
// background: transparent;
|
||||
// }
|
||||
// }
|
||||
// height: 27px;
|
||||
// &::before {
|
||||
// bottom: 0;
|
||||
// }
|
||||
// .@{ant-prefix}-tree-switcher {
|
||||
// position: absolute;
|
||||
// right: 0;
|
||||
// z-index: 8;
|
||||
// line-height: @drawerItemH;
|
||||
// color: #495057;
|
||||
// .anticon {
|
||||
// vertical-align: middle;
|
||||
// font-size: 16px;
|
||||
// transform: rotate(90deg);
|
||||
// }
|
||||
// &_close {
|
||||
// .@{ant-prefix}-tree-switcher-icon svg {
|
||||
// transform: rotate(-180deg);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .@{ant-prefix}-tree-node-content-wrapper {
|
||||
// padding-left: 4px;
|
||||
// width: 100%;
|
||||
// height: @drawerItemH;
|
||||
// line-height: @drawerItemH;
|
||||
// min-height: @drawerItemH;
|
||||
// font-weight: bold;
|
||||
// color: #495057;
|
||||
// &.@{ant-prefix}-tree-node-content-wrapper-normal {
|
||||
// font-weight: normal;
|
||||
// color: #74788d;
|
||||
// .@{ant-prefix}-tree-title {
|
||||
// width: ~'calc(100%)';
|
||||
// }
|
||||
// }
|
||||
// &.@{ant-prefix}-tree-node-selected {
|
||||
// color: @primary-color;
|
||||
// }
|
||||
// .@{ant-prefix}-tree-icon__customize {
|
||||
// line-height: 22px;
|
||||
// .anticon {
|
||||
// vertical-align: middle;
|
||||
// font-size: 16px;
|
||||
// }
|
||||
// }
|
||||
// .@{ant-prefix}-tree-iconEle {
|
||||
// width: auto;
|
||||
// margin-right: 8px;
|
||||
// }
|
||||
// .@{ant-prefix}-tree-title {
|
||||
// display: inline-block;
|
||||
// width: ~'calc(100% - 24px)';
|
||||
// overflow: hidden;
|
||||
// overflow: hidden;
|
||||
// text-overflow: ellipsis;
|
||||
// white-space: nowrap;
|
||||
// & > span {
|
||||
// padding-left: 3px;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,146 @@
|
||||
@root-entry-name: 'default';
|
||||
@import '~knowdesign/es/basic/style/themes/index';
|
||||
@import '~knowdesign/es/basic/style/mixins/index';
|
||||
|
||||
#d-node-scope {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
.input-span {
|
||||
cursor: pointer;
|
||||
}
|
||||
.d-node-scope-input {
|
||||
height: 36px;
|
||||
background: rgba(33, 37, 41, 0.06);
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
&:hover {
|
||||
border: 1px solid #74788d;
|
||||
background: rgba(33, 37, 41, 0.06);
|
||||
box-shadow: none;
|
||||
}
|
||||
&:focus {
|
||||
border: 1px solid #74788d;
|
||||
background: rgba(33, 37, 41, 0.06);
|
||||
box-shadow: none;
|
||||
}
|
||||
&.dcloud-input-affix-wrapper-focused {
|
||||
border: 1px solid #74788d;
|
||||
background: rgba(33, 37, 41, 0.06);
|
||||
box-shadow: none;
|
||||
}
|
||||
&.relativeTime {
|
||||
width: 160px;
|
||||
}
|
||||
&.absoluteTime {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
input {
|
||||
cursor: pointer;
|
||||
}
|
||||
.@{ant-prefix}-input {
|
||||
background: transparent;
|
||||
}
|
||||
.@{ant-prefix}-input-suffix {
|
||||
font-size: 18px;
|
||||
color: #74788d;
|
||||
}
|
||||
}
|
||||
}
|
||||
.d-node-scope-popover {
|
||||
border-radius: 12px;
|
||||
.@{ant-prefix}-popover-arrow {
|
||||
display: none;
|
||||
}
|
||||
.@{ant-prefix}-popover-inner {
|
||||
border-radius: 12px;
|
||||
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);
|
||||
}
|
||||
.@{ant-prefix}-popover-inner-content {
|
||||
padding: 16px 24px;
|
||||
width: 479px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
&.@{ant-prefix}-popover-placement-bottomRight {
|
||||
// padding-top: 0;
|
||||
}
|
||||
}
|
||||
.dd-node-scope-module {
|
||||
.flx_con {
|
||||
display: flex;
|
||||
.time_title {
|
||||
font-size: 14px;
|
||||
color: #212529;
|
||||
margin-bottom: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.flx_l {
|
||||
width: 140px;
|
||||
.@{ant-prefix}-radio {
|
||||
display: none;
|
||||
& + * {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
.@{ant-prefix}-radio-wrapper-checked {
|
||||
color: #556ee6;
|
||||
}
|
||||
}
|
||||
.flx_r {
|
||||
flex: 1;
|
||||
.@{ant-prefix}-checkbox + span {
|
||||
padding-left: 4px;
|
||||
color: #495057;
|
||||
}
|
||||
.check-all {
|
||||
margin: 0 16px;
|
||||
}
|
||||
.custom-scope {
|
||||
min-width: 266px;
|
||||
height: 193px;
|
||||
box-sizing: border-box;
|
||||
padding: 9px 0 12px 0;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
.check-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
.search-input {
|
||||
width: 160px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
}
|
||||
.fixed-height {
|
||||
padding: 12px 16px;
|
||||
height: 110px;
|
||||
overflow: auto;
|
||||
}
|
||||
.btn-con {
|
||||
padding: 12px 16px;
|
||||
.@{ant-prefix}-btn {
|
||||
width: 56px;
|
||||
}
|
||||
.btn-sure {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.time-radio-group {
|
||||
.dcloud-radio-wrapper {
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
.@{ant-prefix}-picker {
|
||||
border: 0;
|
||||
background: rgba(33, 37, 41, 0.04);
|
||||
}
|
||||
.@{ant-prefix}-picker-range {
|
||||
width: 300px;
|
||||
.@{ant-prefix}-picker-clear {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
@root-entry-name: 'default';
|
||||
@import '~knowdesign/es/basic/style/themes/index';
|
||||
@import '~knowdesign/es/basic/style/mixins/index';
|
||||
|
||||
.query-select {
|
||||
.horizontal {
|
||||
.@{ant-prefix}-col {
|
||||
display: flex;
|
||||
.@{ant-prefix}-select {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
.label-name {
|
||||
height: 34px;
|
||||
line-height: 34px;
|
||||
font-size: 13px;
|
||||
color: #495057;
|
||||
display: inline-block;
|
||||
}
|
||||
.@{ant-prefix}-row {
|
||||
.@{ant-prefix}-select {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import React from 'react';
|
||||
import { SingleChart, Spin } from 'knowdesign';
|
||||
|
||||
const CHART_CONFIG = {
|
||||
grid: {
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
xAxis: {
|
||||
show: false,
|
||||
type: 'category',
|
||||
axisLabel: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
show: false,
|
||||
axisLabel: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
},
|
||||
toolBox: {
|
||||
show: false,
|
||||
},
|
||||
tooltip: {
|
||||
show: false,
|
||||
},
|
||||
};
|
||||
|
||||
const LINE_CONFIG = {
|
||||
lineStyle: {
|
||||
color: '#556EE6',
|
||||
width: 1,
|
||||
},
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0,
|
||||
color: 'rgba(178,191,255,0.24)',
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: 'rgba(142,167,244,0.04)', // 100% 处的颜色
|
||||
},
|
||||
],
|
||||
global: false, // 缺省为 false
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const SmallChart = (props: {
|
||||
width: string | number;
|
||||
height: string | number;
|
||||
loading?: boolean;
|
||||
chartData: {
|
||||
name: string;
|
||||
data: [number | string, number | string];
|
||||
};
|
||||
}) => {
|
||||
const { chartData, loading = false, width, height } = props;
|
||||
|
||||
if (loading) {
|
||||
return <Spin spinning={loading} style={{ width, height }} />;
|
||||
}
|
||||
|
||||
if (!chartData || !chartData?.data?.length) {
|
||||
return <div style={{ width, height }}></div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<SingleChart
|
||||
wrapStyle={{
|
||||
width,
|
||||
height,
|
||||
zIndex: 0,
|
||||
minWidth: '82px',
|
||||
}}
|
||||
option={CHART_CONFIG}
|
||||
chartTypeProp="line"
|
||||
propChartData={[chartData]}
|
||||
seriesCallback={(lines: any) => {
|
||||
return lines.map((line: any) => {
|
||||
line.data.sort((a: any, b: any) => a[0] - b[0]);
|
||||
return {
|
||||
...line,
|
||||
...LINE_CONFIG,
|
||||
};
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SmallChart;
|
||||
@@ -0,0 +1,34 @@
|
||||
@switch-bar-padding: 2px;
|
||||
|
||||
.d-switch-tab {
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding: 2px;
|
||||
border-radius: 6px;
|
||||
background: rgba(33, 37, 41, 0.06);
|
||||
&-content {
|
||||
z-index: 1;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
line-height: 23px;
|
||||
text-align: center;
|
||||
color: #74788d;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s;
|
||||
&-active {
|
||||
color: #556ee6;
|
||||
}
|
||||
}
|
||||
&-bar {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
top: @switch-bar-padding;
|
||||
top: @switch-bar-padding;
|
||||
width: 23px;
|
||||
height: calc(100% - @switch-bar-padding * 2);
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
pointer-events: none;
|
||||
transition: width 0.3s, left 0.3s, right 0.3s;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import React, { useLayoutEffect, useRef, useState } from 'react';
|
||||
import './index.less';
|
||||
|
||||
interface SwitchTabProps {
|
||||
defaultKey: string;
|
||||
onChange: (key: string) => void;
|
||||
children: any;
|
||||
}
|
||||
|
||||
interface TabItemProps {
|
||||
key: string;
|
||||
children: JSX.Element;
|
||||
}
|
||||
|
||||
const TabItem = (props: TabItemProps) => {
|
||||
const { key, children } = props;
|
||||
return <div key={key}>{children}</div>;
|
||||
};
|
||||
|
||||
const SwitchTab = (props: SwitchTabProps) => {
|
||||
const { defaultKey, onChange, children } = props;
|
||||
const tabRef = useRef();
|
||||
const [activeKey, setActiveKey] = useState<string>(defaultKey);
|
||||
const [pos, setPos] = useState({
|
||||
left: 0,
|
||||
width: 0,
|
||||
});
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (tabRef.current) {
|
||||
[...(tabRef?.current as HTMLDivElement)?.children].some((node: HTMLElement) => {
|
||||
if (node.className.includes('active')) {
|
||||
setPos({
|
||||
left: node?.offsetLeft || 0,
|
||||
width: node?.offsetWidth || 0,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}, [activeKey]);
|
||||
|
||||
return (
|
||||
<div ref={tabRef} className="d-switch-tab">
|
||||
{children.map((content: any) => {
|
||||
const key = content.key;
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className={`d-switch-tab-content d-switch-tab-content-${activeKey === key ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
setActiveKey(key);
|
||||
onChange(key);
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="d-switch-tab-bar" style={{ ...pos }}></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SwitchTab.TabItem = TabItem;
|
||||
|
||||
export default SwitchTab;
|
||||
@@ -0,0 +1,75 @@
|
||||
.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;
|
||||
flex-shrink: 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: 0;
|
||||
}
|
||||
}
|
||||
.dcloud-popover-arrow-content {
|
||||
display: none !important;
|
||||
}
|
||||
.container-item-popover {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
max-width: 560px;
|
||||
padding: 10px 8px 4px 8px;
|
||||
.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,127 @@
|
||||
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 | number)[];
|
||||
expandTagContent: string | ((tagNum: number) => string);
|
||||
placement?: TooltipPlacement;
|
||||
};
|
||||
|
||||
type TagsState = {
|
||||
list: (string | number)[];
|
||||
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;
|
||||
if (!box) {
|
||||
return;
|
||||
}
|
||||
const boxWidth = box.offsetWidth;
|
||||
// 子元素信息
|
||||
const childrenList = Array.from(ref.current.children) as HTMLElement[] as any;
|
||||
const len = childrenList.length;
|
||||
const penultimateNode = len > 1 ? childrenList[len - 2] : null;
|
||||
const penultimateNodeOffsetRight = penultimateNode ? penultimateNode.offsetLeft + penultimateNode.offsetWidth - box.offsetLeft : 0;
|
||||
// 如果内容超出展示区域,隐藏一部分
|
||||
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={
|
||||
curState.list.length ? (
|
||||
<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,210 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { ArrowLeftOutlined } from '@ant-design/icons';
|
||||
import { Button, Divider, Drawer, IconFont, Select, Space, Table, Utils } from 'knowdesign';
|
||||
import Api, { MetricType } from '@src/api/index';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
export default (props: any) => {
|
||||
const { taskPlanData, onClickPreview, onClickSavePreview, brokerList = [] } = props;
|
||||
const routeParams = useParams<{ clusterId: string }>();
|
||||
const [visible, setVisible] = useState(false);
|
||||
// const [brokerList, setBrokerList] = useState([]);
|
||||
// 存储每个topic的各个partition的编辑状态以及编辑后的目标BrokerID列表
|
||||
const [reassignBrokerIdListEditStatusMap, setReassignBrokerIdListEditStatusMap] = useState<{
|
||||
[key: string]: {
|
||||
[key: number]: {
|
||||
currentBrokerIdList: Array<number>;
|
||||
reassignBrokerIdList: Array<number>;
|
||||
isEdit: boolean;
|
||||
};
|
||||
};
|
||||
}>({});
|
||||
const onClose = () => {
|
||||
setVisible(false);
|
||||
};
|
||||
const setCurTopicPartitionEditStatus = (record: any, v: any, status: boolean) => {
|
||||
let reassignBrokerIdListEditStatusMapCopy = JSON.parse(JSON.stringify(reassignBrokerIdListEditStatusMap));
|
||||
reassignBrokerIdListEditStatusMapCopy[record.topicName][v].isEdit = status;
|
||||
setReassignBrokerIdListEditStatusMap(reassignBrokerIdListEditStatusMapCopy);
|
||||
};
|
||||
const columns = [
|
||||
{
|
||||
title: 'Topic',
|
||||
dataIndex: 'topicName',
|
||||
},
|
||||
{
|
||||
title: '源BrokerID',
|
||||
dataIndex: 'currentBrokerIdList',
|
||||
render: (a: any) => {
|
||||
return a.join(',');
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '目标BrokerID',
|
||||
dataIndex: 'reassignBrokerIdList',
|
||||
render: (a: any) => {
|
||||
return a.join(',');
|
||||
},
|
||||
},
|
||||
];
|
||||
useEffect(() => {
|
||||
let mapObj = taskPlanData.reduce((acc: any, cur: any) => {
|
||||
acc[cur.topicName] = cur.partitionPlanList.reduce((pacc: any, pcur: any) => {
|
||||
pacc[pcur.partitionId] = {
|
||||
currentBrokerIdList: pcur.currentBrokerIdList,
|
||||
reassignBrokerIdList: pcur.reassignBrokerIdList,
|
||||
isEdit: false,
|
||||
};
|
||||
return pacc;
|
||||
}, {});
|
||||
return acc;
|
||||
}, {});
|
||||
setReassignBrokerIdListEditStatusMap(mapObj);
|
||||
}, [taskPlanData]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
style={{ marginTop: 20 }}
|
||||
type="link"
|
||||
onClick={() => {
|
||||
setVisible(true);
|
||||
onClickPreview && onClickPreview(taskPlanData);
|
||||
}}
|
||||
>
|
||||
预览任务计划
|
||||
</Button>
|
||||
<Drawer
|
||||
title={
|
||||
<Space size={0}>
|
||||
<Button
|
||||
className="drawer-title-left-button"
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<IconFont type="icon-fanhui1" />}
|
||||
onClick={onClose}
|
||||
/>
|
||||
<Divider type="vertical" />
|
||||
<span style={{ paddingLeft: '5px' }}>任务计划</span>
|
||||
</Space>
|
||||
}
|
||||
width={600}
|
||||
placement="right"
|
||||
onClose={onClose}
|
||||
visible={visible}
|
||||
className="preview-task-plan-drawer"
|
||||
maskClosable={false}
|
||||
destroyOnClose
|
||||
// closeIcon={<ArrowLeftOutlined />}
|
||||
>
|
||||
<Table
|
||||
rowKey={'topicName'}
|
||||
dataSource={taskPlanData}
|
||||
columns={columns}
|
||||
pagination={false}
|
||||
expandable={{
|
||||
expandedRowRender: (topicRecord) => {
|
||||
let partitionPlanList = topicRecord.partitionPlanList;
|
||||
const columns = [
|
||||
{
|
||||
title: 'Partition',
|
||||
dataIndex: 'partitionId',
|
||||
},
|
||||
{
|
||||
title: '源BrokerID',
|
||||
dataIndex: 'currentBrokerIdList',
|
||||
render: (a: any) => {
|
||||
return a.join(',');
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '目标BrokerID',
|
||||
dataIndex: 'reassignBrokerIdList',
|
||||
render: (a: any, partitionRecord: any) => {
|
||||
let isEdit = reassignBrokerIdListEditStatusMap[topicRecord.topicName][partitionRecord.partitionId].isEdit;
|
||||
let reassignBrokerIdList =
|
||||
reassignBrokerIdListEditStatusMap[topicRecord.topicName][partitionRecord.partitionId].reassignBrokerIdList;
|
||||
return isEdit ? (
|
||||
<Select
|
||||
value={reassignBrokerIdList}
|
||||
mode="multiple"
|
||||
onChange={(selBrokerIds) => {
|
||||
let reassignBrokerIdListEditStatusMapCopy = JSON.parse(JSON.stringify(reassignBrokerIdListEditStatusMap));
|
||||
reassignBrokerIdListEditStatusMapCopy[topicRecord.topicName][partitionRecord.partitionId].reassignBrokerIdList =
|
||||
selBrokerIds;
|
||||
setReassignBrokerIdListEditStatusMap(reassignBrokerIdListEditStatusMapCopy);
|
||||
}}
|
||||
style={{ width: '122px' }}
|
||||
>
|
||||
{brokerList.length > 0 &&
|
||||
brokerList?.map((broker: any) => (
|
||||
<Option key={Math.random()} value={broker.brokerId}>
|
||||
{broker.brokerId}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
) : (
|
||||
a.join(',')
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'partitionId',
|
||||
render: (v: any) => {
|
||||
let isEdit = reassignBrokerIdListEditStatusMap[topicRecord.topicName][v].isEdit;
|
||||
return isEdit ? (
|
||||
<>
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => {
|
||||
onClickSavePreview && onClickSavePreview(reassignBrokerIdListEditStatusMap);
|
||||
}}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => {
|
||||
setCurTopicPartitionEditStatus(topicRecord, v, false);
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => {
|
||||
setCurTopicPartitionEditStatus(topicRecord, v, true);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
return (
|
||||
<div className="partition-task-plan-list">
|
||||
<Table
|
||||
rowKey={'partitionId'}
|
||||
dataSource={partitionPlanList}
|
||||
columns={columns}
|
||||
pagination={{
|
||||
simple: true,
|
||||
}}
|
||||
></Table>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}}
|
||||
></Table>
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,544 @@
|
||||
// 批量扩缩副本
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
DatePicker,
|
||||
Drawer,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Row,
|
||||
Select,
|
||||
Table,
|
||||
Tag,
|
||||
Utils,
|
||||
AppContainer,
|
||||
message,
|
||||
Divider,
|
||||
Space,
|
||||
} from 'knowdesign';
|
||||
import './index.less';
|
||||
import Api, { MetricType } from '@src/api/index';
|
||||
import moment from 'moment';
|
||||
import PreviewTaskPlan from './PreviewTaskPlan';
|
||||
import type { RangePickerProps } from 'knowdesign/es/basic/date-picker';
|
||||
import { timeFormat } from '@src/constants/common';
|
||||
|
||||
const { TextArea } = Input;
|
||||
const { Option } = Select;
|
||||
|
||||
const jobNameMap: any = {
|
||||
expandAndReduce: '批量扩缩容',
|
||||
transfer: '批量迁移',
|
||||
};
|
||||
|
||||
interface DefaultConfig {
|
||||
jobId?: number | string;
|
||||
type?: string;
|
||||
topics: Array<any>;
|
||||
drawerVisible: boolean;
|
||||
onClose: () => void;
|
||||
genData?: () => any;
|
||||
jobStatus?: number;
|
||||
}
|
||||
|
||||
export default (props: DefaultConfig) => {
|
||||
const { type = 'expandAndReduce', topics, drawerVisible, onClose, jobId, genData, jobStatus } = props;
|
||||
const routeParams = useParams<{ clusterId: string }>();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [topicData, setTopicData] = useState([]);
|
||||
const [brokerList, setBrokerList] = useState([]);
|
||||
const [taskPlanData, setTaskPlanData] = useState([]);
|
||||
const [selectBrokerList, setSelectBrokerList] = useState([]);
|
||||
const [topicNewReplicas, setTopicNewReplicas] = useState([]);
|
||||
const [form] = Form.useForm();
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const [loadingTopic, setLoadingTopic] = useState<boolean>(true);
|
||||
const [targetNodeVisible, setTargetNodeVisible] = useState(false);
|
||||
const [topicMetaData, setTopicMetaData] = useState([]);
|
||||
const [topicSelectValue, setTopicSelectValue] = useState(topics);
|
||||
|
||||
const topicDataColumns = [
|
||||
{
|
||||
title: 'Topic名称',
|
||||
dataIndex: 'topicName',
|
||||
},
|
||||
{
|
||||
title: '近三天平均流量',
|
||||
dataIndex: 'latestDaysAvgBytesInList',
|
||||
render: (value: any) => {
|
||||
return (
|
||||
<div className="custom-tag-wrap">
|
||||
{value.map((item: any, index: any) => (
|
||||
<div key={index} className="custom-tag">
|
||||
{item && item.value ? `${Utils.formatSize(+item.value)}/S` : '-'}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '近三天峰值流量&时间',
|
||||
dataIndex: 'latestDaysMaxBytesInList',
|
||||
render: (value: any) => {
|
||||
return (
|
||||
<div className="custom-tag-wrap">
|
||||
{value.map((item: any, index: any) => (
|
||||
<div key={index} className="custom-tag">
|
||||
<div>{item && item.value ? `${Utils.formatSize(+item.value)}/S` : '-'}</div>
|
||||
<div className="time">{item && item.timeStamp ? moment(item.timeStamp * 1000).format('HH:mm:ss') : '-'}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Partition数',
|
||||
dataIndex: 'partitionNum',
|
||||
},
|
||||
{
|
||||
title: '当前副本数',
|
||||
dataIndex: 'replicaNum',
|
||||
},
|
||||
{
|
||||
title: '最终副本数',
|
||||
dataIndex: 'replicaNum',
|
||||
// eslint-disable-next-line react/display-name
|
||||
render: (v: any, _r: any, index: number) => {
|
||||
return (
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={brokerList.length || 1}
|
||||
value={topicNewReplicas[index]}
|
||||
defaultValue={topicNewReplicas[index]}
|
||||
onChange={(v) => {
|
||||
if (v > topicData[index]?.replicaNum) {
|
||||
setTargetNodeVisible(true);
|
||||
} else if (
|
||||
v <= topicData[index]?.replicaNum &&
|
||||
topicData.filter((item, key) => topicNewReplicas[key] && item.replicaNum < topicNewReplicas[key]).length < 1
|
||||
) {
|
||||
setTargetNodeVisible(false);
|
||||
}
|
||||
const topicNewReplicasCopy = JSON.parse(JSON.stringify(topicNewReplicas));
|
||||
topicNewReplicasCopy[index] = v;
|
||||
setTopicNewReplicas(topicNewReplicasCopy);
|
||||
}}
|
||||
></InputNumber>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
const onDrawerClose = () => {
|
||||
form.resetFields();
|
||||
setTopicData([]);
|
||||
setSelectBrokerList([]);
|
||||
// setLoadingTopic(true);
|
||||
setVisible(false);
|
||||
setTargetNodeVisible(false);
|
||||
setTopicNewReplicas([]);
|
||||
onClose();
|
||||
};
|
||||
const getReassignmentList = (topiclist?: any) => {
|
||||
return Utils.post(Api.getReassignmentList(), {
|
||||
clusterId: Number(routeParams.clusterId),
|
||||
topicNameList: topiclist,
|
||||
});
|
||||
};
|
||||
const getJobsTaskData = () => {
|
||||
const params = {
|
||||
clusterId: routeParams.clusterId,
|
||||
jobId: jobId,
|
||||
};
|
||||
return Utils.request(Api.getJobsTaskData(params.clusterId, params.jobId), params);
|
||||
};
|
||||
const getTaskPlanData = (params: any) => {
|
||||
return Utils.post(Api.getTaskPlanData(), params);
|
||||
};
|
||||
const onClickPreview = (data?: any) => {
|
||||
if (!targetNodeVisible) {
|
||||
const planParams = topicData.map((item, index) => {
|
||||
return {
|
||||
brokerIdList: [],
|
||||
clusterId: routeParams.clusterId,
|
||||
newReplicaNum: topicNewReplicas[index],
|
||||
topicName: item.topicName,
|
||||
};
|
||||
});
|
||||
getTaskPlanData(planParams).then((res: any) => {
|
||||
setTaskPlanData(res.topicPlanList);
|
||||
});
|
||||
}
|
||||
if (selectBrokerList.length === 0) return;
|
||||
if (topicNewReplicas.find((item) => item > selectBrokerList.length)) return;
|
||||
!data &&
|
||||
form.validateFields(['brokerList']).then((e) => {
|
||||
const planParams = topicSelectValue.map((item, index) => {
|
||||
return {
|
||||
brokerIdList: selectBrokerList,
|
||||
clusterId: routeParams.clusterId,
|
||||
newReplicaNum: topicNewReplicas[index] || item.replicaNum,
|
||||
topicName: item,
|
||||
};
|
||||
});
|
||||
getTaskPlanData(planParams).then((res: any) => {
|
||||
setTaskPlanData(res.topicPlanList);
|
||||
});
|
||||
});
|
||||
};
|
||||
const onClickSavePreview = (data: any) => {
|
||||
const taskPlanDataCopy = JSON.parse(JSON.stringify(taskPlanData));
|
||||
const hasError: any[] = [];
|
||||
taskPlanDataCopy.forEach((topic: any, index: number) => {
|
||||
const partitionIds = Object.keys(data[topic.topicName]);
|
||||
const newReassignBrokerIdList = partitionIds.reduce((acc: Array<number>, cur: string) => {
|
||||
const ressignBrokerIdList = data[topic.topicName][cur].reassignBrokerIdList;
|
||||
if (ressignBrokerIdList.length !== topicNewReplicas[index]) {
|
||||
hasError.push(topic.topicName + ' Partition ' + cur);
|
||||
}
|
||||
acc.push(...ressignBrokerIdList);
|
||||
return acc;
|
||||
}, []);
|
||||
topic.reassignBrokerIdList = Array.from(new Set(newReassignBrokerIdList));
|
||||
topic.partitionPlanList.forEach((partition: any) => {
|
||||
partition.reassignBrokerIdList = data[topic.topicName][partition.partitionId].reassignBrokerIdList;
|
||||
});
|
||||
});
|
||||
if (hasError.length) {
|
||||
message.error(hasError.join(',') + '副本数与目标节点数不一致');
|
||||
} else {
|
||||
setTaskPlanData(taskPlanDataCopy);
|
||||
}
|
||||
};
|
||||
const checkRep = (_: any, value: any[]) => {
|
||||
if (value && value.length && topicNewReplicas.find((rep) => rep && rep > value.length)) {
|
||||
return Promise.reject('节点数低于Topic最大副本数');
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
const disabledDate: RangePickerProps['disabledDate'] = (current) => {
|
||||
// 不能选择小于当前时间
|
||||
return current && current <= moment().add(-1, 'days').endOf('day');
|
||||
};
|
||||
const range = (start: number, end: number) => {
|
||||
const result = [];
|
||||
for (let i = start; i < end; i++) {
|
||||
result.push(i);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const disabledDateTime = (current: any) => {
|
||||
return {
|
||||
disabledHours: () => (current > moment() ? [] : range(0, moment().hour())),
|
||||
disabledMinutes: () => (current > moment() ? [] : range(0, moment().add(1, 'minute').minute())),
|
||||
// disabledSeconds: () => [55, 56],
|
||||
};
|
||||
};
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
const planParams = topicData.map((item, index) => {
|
||||
return {
|
||||
brokerIdList: [],
|
||||
clusterId: routeParams.clusterId,
|
||||
newReplicaNum: item.replicaNum,
|
||||
topicName: item.topicName,
|
||||
};
|
||||
});
|
||||
getTaskPlanData(planParams).then((res: any) => {
|
||||
setTaskPlanData(res.topicPlanList);
|
||||
});
|
||||
}
|
||||
}, [topics]);
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue;
|
||||
}, [topics]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!drawerVisible) return;
|
||||
onClickPreview();
|
||||
}, [selectBrokerList, topicNewReplicas]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!drawerVisible) return;
|
||||
if (jobId) {
|
||||
setLoadingTopic(true);
|
||||
getJobsTaskData()
|
||||
.then((res: any) => {
|
||||
const jobData = (res && JSON.parse(res.jobData)) || {};
|
||||
const planTime = res?.planTime && moment(res.planTime, timeFormat);
|
||||
const { topicPlanList = [], throttleUnitB = 0, jobDesc = '' } = jobData;
|
||||
let selectedBrokerList: any[] = [];
|
||||
const topicData = topicPlanList.map((topic: any) => {
|
||||
selectedBrokerList = topic.reassignBrokerIdList;
|
||||
return {
|
||||
...topic,
|
||||
topicName: topic.topicName,
|
||||
latestDaysAvgBytesInList: topic.latestDaysAvgBytesInList || [],
|
||||
latestDaysMaxBytesInList: topic.latestDaysMaxBytesInList || [],
|
||||
partitionIdList: topic.partitionIdList,
|
||||
replicaNum: topic.presentReplicaNum,
|
||||
retentionMs: topic.originalRetentionTimeUnitMs,
|
||||
};
|
||||
});
|
||||
setTopicData(topicData);
|
||||
const newReplica = topicPlanList.map((t: any) => t.newReplicaNum || []);
|
||||
setTopicNewReplicas(newReplica);
|
||||
// const needMovePartitions = topicPlanList.map((t: any) => t.partitionIdList || []);
|
||||
// setNeedMovePartitions(needMovePartitions);
|
||||
// const MoveDataTimeRanges = topicPlanList.map((t: any) => {
|
||||
// const timeHour = t.reassignRetentionTimeUnitMs / 1000 / 60 / 60;
|
||||
// return timeHour > 1 ? Math.floor(timeHour) : timeHour.toFixed(2);
|
||||
// });
|
||||
// setMoveDataTimeRanges(MoveDataTimeRanges);
|
||||
setSelectBrokerList(selectedBrokerList);
|
||||
|
||||
form.setFieldsValue({
|
||||
brokerList: selectedBrokerList,
|
||||
throttle: throttleUnitB / 1024 / 1024,
|
||||
planTime,
|
||||
description: res?.jobDesc,
|
||||
topicList: topicSelectValue,
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
setLoadingTopic(false);
|
||||
});
|
||||
}
|
||||
}, [drawerVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!drawerVisible) return;
|
||||
setVisible(true);
|
||||
Utils.request(Api.getDashboardMetadata(routeParams.clusterId, MetricType.Broker)).then((res: any) => {
|
||||
setBrokerList(res || []);
|
||||
});
|
||||
}, [drawerVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!drawerVisible) return;
|
||||
!jobId &&
|
||||
Utils.request(Api.getTopicMetaData(+routeParams.clusterId))
|
||||
.then((res: any) => {
|
||||
const filterRes = res.filter((item: any) => item.type !== 1);
|
||||
const topics = (filterRes || []).map((item: any) => {
|
||||
return {
|
||||
label: item.topicName,
|
||||
value: item.topicName,
|
||||
partitionIdList: item.partitionIdList,
|
||||
};
|
||||
});
|
||||
setTopicMetaData(topics);
|
||||
})
|
||||
.catch((err) => {
|
||||
message.error(err);
|
||||
});
|
||||
}, [drawerVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!jobId) {
|
||||
setLoadingTopic(true);
|
||||
drawerVisible &&
|
||||
getReassignmentList(topicSelectValue)
|
||||
.then((res: any[]) => {
|
||||
setTopicData(res);
|
||||
const newReplica = res.map((t) => t.replicaNum || []);
|
||||
setTopicNewReplicas(newReplica);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoadingTopic(false);
|
||||
});
|
||||
}
|
||||
}, [topicSelectValue, drawerVisible]);
|
||||
|
||||
const addReassign = () => {
|
||||
// if (selectBrokerList.length && topicNewReplicas.find((item) => item > selectBrokerList.length)) return;
|
||||
form.validateFields().then((e) => {
|
||||
const formData = form.getFieldsValue();
|
||||
const handledData = {
|
||||
creator: global.userInfo.userName,
|
||||
jobType: 1, // type 0 topic迁移 1 扩缩容 2集群均衡
|
||||
planTime: formData.planTime,
|
||||
jobStatus: jobId ? jobStatus : 2, //status 2 创建
|
||||
target: topicSelectValue.join(','),
|
||||
id: jobId || '',
|
||||
jobDesc: formData.description,
|
||||
jobData: JSON.stringify({
|
||||
clusterId: routeParams.clusterId,
|
||||
jobDesc: formData.description,
|
||||
throttleUnitB: formData.throttle * 1024 * 1024,
|
||||
topicPlanList: topicSelectValue.map((topic, index) => {
|
||||
return {
|
||||
clusterId: routeParams.clusterId,
|
||||
topicName: topic,
|
||||
partitionIdList: topic.partitionIdList,
|
||||
partitionNum: topicData[index].partitionNum,
|
||||
presentReplicaNum: topicData[index].replicaNum,
|
||||
newReplicaNum: topicNewReplicas[index],
|
||||
originalBrokerIdList: taskPlanData[index].currentBrokerIdList,
|
||||
reassignBrokerIdList: taskPlanData[index].reassignBrokerIdList,
|
||||
originalRetentionTimeUnitMs: topicData[index].retentionMs,
|
||||
reassignRetentionTimeUnitMs: topicData[index].retentionMs,
|
||||
latestDaysAvgBytesInList: topicData[index].latestDaysAvgBytesInList,
|
||||
latestDaysMaxBytesInList: topicData[index].latestDaysMaxBytesInList,
|
||||
partitionPlanList: taskPlanData[index].partitionPlanList,
|
||||
};
|
||||
}),
|
||||
}),
|
||||
};
|
||||
if (jobId) {
|
||||
Utils.put(Api.putJobsTaskData(routeParams.clusterId), handledData)
|
||||
.then(() => {
|
||||
message.success('扩缩副本任务编辑成功');
|
||||
onDrawerClose();
|
||||
genData();
|
||||
})
|
||||
.catch((err: any) => {
|
||||
console.log(err, 'err');
|
||||
});
|
||||
} else {
|
||||
Utils.post(Api.createTask(routeParams.clusterId), handledData)
|
||||
.then(() => {
|
||||
message.success('扩缩副本任务创建成功');
|
||||
onDrawerClose();
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Drawer
|
||||
push={false}
|
||||
title={jobNameMap[type]}
|
||||
width={1080}
|
||||
placement="right"
|
||||
onClose={onDrawerClose}
|
||||
visible={visible}
|
||||
className="topic-job-drawer"
|
||||
maskClosable={false}
|
||||
destroyOnClose
|
||||
extra={
|
||||
<Space>
|
||||
<Button
|
||||
size="small"
|
||||
style={{ marginRight: 8 }}
|
||||
onClick={(_) => {
|
||||
// setVisible(false);
|
||||
onDrawerClose();
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button size="small" type="primary" onClick={addReassign}>
|
||||
确定
|
||||
</Button>
|
||||
<Divider type="vertical" />
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<div className="wrap">
|
||||
<h4 className="title">{jobNameMap[type]}Topic</h4>
|
||||
|
||||
{!jobId && (
|
||||
<Form form={form}>
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item>
|
||||
<Select
|
||||
placeholder="请选择Topic,可多选"
|
||||
mode="multiple"
|
||||
onChange={(v: any) => {
|
||||
setTopicSelectValue(v);
|
||||
}}
|
||||
options={topicMetaData}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
)}
|
||||
<Table dataSource={topicData} columns={topicDataColumns} pagination={false} loading={loadingTopic} />
|
||||
<Form form={form} layout="vertical" className="task-form">
|
||||
<Row>
|
||||
{targetNodeVisible && (
|
||||
<Col span={12}>
|
||||
<Form.Item name="brokerList" label="目标节点" rules={[{ required: true }, { validator: checkRep }]}>
|
||||
<Select
|
||||
placeholder="请选择Broker,可多选"
|
||||
mode="multiple"
|
||||
onChange={(v: any) => {
|
||||
setSelectBrokerList(v);
|
||||
}}
|
||||
>
|
||||
{brokerList.map((item, index) => (
|
||||
<Option key={index} value={item.brokerId}>
|
||||
{item.brokerId}
|
||||
{`(${item.host})`}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
)}
|
||||
<Col span={1}>
|
||||
{/* taskPlanData是传给组件的初始值
|
||||
点击预览任务计划,触发onClickPreview回调,发起请求获取taskPlanData
|
||||
组件内部改完点击每一行保存时,再通过onClickSavePreview回调向外派发数据 */}
|
||||
<PreviewTaskPlan
|
||||
taskPlanData={taskPlanData}
|
||||
onClickPreview={onClickPreview}
|
||||
onClickSavePreview={onClickSavePreview}
|
||||
brokerList={brokerList}
|
||||
></PreviewTaskPlan>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<h4 className="title">迁移任务配置</h4>
|
||||
<Row gutter={32} className="topic-execution-time">
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="throttle"
|
||||
label="限流"
|
||||
rules={[
|
||||
{ required: true },
|
||||
{
|
||||
validator: (r: any, v: number) => {
|
||||
if ((v || v === 0) && v <= 0) {
|
||||
return Promise.reject('限流值不能小于等于0');
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber style={{ width: '100%' }} max={99999} addonAfter="MB/S"></InputNumber>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="planTime" label="任务执行时间" rules={[{ required: true }]}>
|
||||
<DatePicker
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
showTime
|
||||
style={{ width: '100%' }}
|
||||
disabledDate={disabledDate}
|
||||
disabledTime={disabledDateTime}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<h4 className="title">描述</h4>
|
||||
<Form.Item name="description" label="任务描述" rules={[{ required: true }]}>
|
||||
<TextArea placeholder="暂支持 String 格式" style={{ height: 110 }} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,555 @@
|
||||
// 批量迁移
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
DatePicker,
|
||||
Drawer,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Row,
|
||||
Select,
|
||||
Table,
|
||||
Utils,
|
||||
AppContainer,
|
||||
message,
|
||||
Space,
|
||||
Divider,
|
||||
Transfer,
|
||||
IconFont,
|
||||
} from 'knowdesign';
|
||||
import './index.less';
|
||||
import Api, { MetricType } from '@src/api/index';
|
||||
import moment from 'moment';
|
||||
import PreviewTaskPlan from './PreviewTaskPlan';
|
||||
import { timeFormat } from '@src/lib/utils';
|
||||
import type { RangePickerProps } from 'knowdesign/es/basic/date-picker';
|
||||
|
||||
const { TextArea } = Input;
|
||||
const { Option } = Select;
|
||||
|
||||
const jobNameMap: any = {
|
||||
expandAndReduce: '批量扩缩容',
|
||||
transfer: '批量迁移',
|
||||
};
|
||||
|
||||
interface DefaultConfig {
|
||||
jobId?: number | string;
|
||||
type?: string;
|
||||
topics: Array<any>;
|
||||
drawerVisible: boolean;
|
||||
onClose: () => void;
|
||||
genData?: () => any;
|
||||
jobStatus?: number;
|
||||
}
|
||||
|
||||
export default (props: DefaultConfig) => {
|
||||
const { type = 'transfer', topics, drawerVisible, onClose, jobId, genData, jobStatus } = props;
|
||||
const routeParams = useParams<{ clusterId: string }>();
|
||||
const [visible, setVisible] = useState(drawerVisible);
|
||||
const [topicData, setTopicData] = useState([]);
|
||||
const [brokerList, setBrokerList] = useState([]);
|
||||
const [taskPlanData, setTaskPlanData] = useState([]);
|
||||
const [selectBrokerList, setSelectBrokerList] = useState([]);
|
||||
const [topicNewReplicas, setTopicNewReplicas] = useState([]);
|
||||
const [needMovePartitions, setNeedMovePartitions] = useState([]);
|
||||
const [moveDataTimeRanges, setMoveDataTimeRanges] = useState([]);
|
||||
const [form] = Form.useForm();
|
||||
const [global] = AppContainer.useGlobalValue();
|
||||
const [loadingTopic, setLoadingTopic] = useState<boolean>(true);
|
||||
const [topicMetaData, setTopicMetaData] = useState([]);
|
||||
const [topicSelectValue, setTopicSelectValue] = useState(topics);
|
||||
|
||||
const topicDataColumns = [
|
||||
{
|
||||
title: 'Topic名称',
|
||||
dataIndex: 'topicName',
|
||||
},
|
||||
{
|
||||
title: '近三天平均流量',
|
||||
dataIndex: 'latestDaysAvgBytesInList',
|
||||
render: (value: any) => {
|
||||
return (
|
||||
<div className="custom-tag-nowrap">
|
||||
{value.map((item: any, index: any) => (
|
||||
<div key={index} className="custom-tag">
|
||||
{item && item.value ? `${Utils.formatSize(+item.value)}/S` : '-'}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '近三天峰值流量&时间',
|
||||
dataIndex: 'latestDaysMaxBytesInList',
|
||||
render: (value: any) => {
|
||||
return (
|
||||
<div className="custom-tag-wrap">
|
||||
{value.map((item: any, index: any) => (
|
||||
<div className="custom-tag" key={index}>
|
||||
<div>{item && item.value ? `${Utils.formatSize(+item.value)}/S` : '-'}</div>
|
||||
<div className="time">{item && item.timeStamp ? moment(item.timeStamp * 1000).format('HH:mm:ss') : '-'}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '需迁移Partition',
|
||||
dataIndex: 'partitionIdList',
|
||||
render: (v: any, r: any, i: number) => {
|
||||
return (
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
placeholder="下拉多选分区ID"
|
||||
defaultValue={v}
|
||||
value={needMovePartitions[i]}
|
||||
mode="multiple"
|
||||
onChange={(a: any) => {
|
||||
const needMovePartitionsCopy = JSON.parse(JSON.stringify(needMovePartitions));
|
||||
needMovePartitionsCopy[i] = a;
|
||||
setNeedMovePartitions(needMovePartitionsCopy);
|
||||
}}
|
||||
>
|
||||
{v.map((p: any, index: any) => (
|
||||
<Option key={index} value={p}>
|
||||
{p}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '副本数',
|
||||
dataIndex: 'replicaNum',
|
||||
},
|
||||
{
|
||||
title: '数据保存时间',
|
||||
dataIndex: 'retentionMs',
|
||||
render: (v: any) => {
|
||||
return timeFormat(v);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '迁移数据时间范围',
|
||||
dataIndex: 'newRetentionMs',
|
||||
render: (v: any, r: any, i: number) => {
|
||||
return (
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={99999}
|
||||
defaultValue={moveDataTimeRanges[i]}
|
||||
value={moveDataTimeRanges[i]}
|
||||
onChange={(n: number) => {
|
||||
const moveDataTimeRangesCopy = JSON.parse(JSON.stringify(moveDataTimeRanges));
|
||||
moveDataTimeRangesCopy[i] = n;
|
||||
setMoveDataTimeRanges(moveDataTimeRangesCopy);
|
||||
}}
|
||||
formatter={(value) => (value ? `${value} h` : '')}
|
||||
// @ts-ignore
|
||||
parser={(value) => value.replace('h', '')}
|
||||
></InputNumber>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
const onDrawerClose = () => {
|
||||
form.resetFields();
|
||||
setTopicData([]);
|
||||
setSelectBrokerList([]);
|
||||
setTopicNewReplicas([]);
|
||||
setNeedMovePartitions([]);
|
||||
setMoveDataTimeRanges([]);
|
||||
// setLoadingTopic(true);
|
||||
setVisible(false);
|
||||
onClose();
|
||||
};
|
||||
const getReassignmentList = (topiclist?: any) => {
|
||||
return Utils.post(Api.getReassignmentList(), {
|
||||
clusterId: Number(routeParams.clusterId),
|
||||
topicNameList: topiclist,
|
||||
});
|
||||
};
|
||||
const getJobsTaskData = () => {
|
||||
const params = {
|
||||
clusterId: routeParams.clusterId,
|
||||
jobId: jobId,
|
||||
};
|
||||
return Utils.request(Api.getJobsTaskData(params.clusterId, params.jobId), params);
|
||||
};
|
||||
const getTaskPlanData = (params: any) => {
|
||||
return Utils.post(Api.getMovePlanTaskData(), params);
|
||||
};
|
||||
const onClickPreview = (data?: any) => {
|
||||
if (selectBrokerList.length === 0) return;
|
||||
if (topicNewReplicas.find((item) => item > selectBrokerList.length)) return;
|
||||
!data &&
|
||||
form.validateFields(['brokerList']).then((e) => {
|
||||
const planParams = topicSelectValue.map((item, index) => {
|
||||
return {
|
||||
brokerIdList: selectBrokerList,
|
||||
clusterId: routeParams.clusterId,
|
||||
enableRackAwareness: false,
|
||||
newReplicaNum: topicNewReplicas[index],
|
||||
partitionIdList: needMovePartitions[index],
|
||||
topicName: item,
|
||||
};
|
||||
});
|
||||
getTaskPlanData(planParams).then((res: any) => {
|
||||
setTaskPlanData(res.topicPlanList);
|
||||
});
|
||||
});
|
||||
};
|
||||
const onClickSavePreview = (data: any) => {
|
||||
const taskPlanDataCopy = JSON.parse(JSON.stringify(taskPlanData));
|
||||
const hasError: any[] = [];
|
||||
taskPlanDataCopy.forEach((topic: any, index: number) => {
|
||||
const partitionIds = Object.keys(data[topic.topicName]);
|
||||
const newReassignBrokerIdList = partitionIds.reduce((acc: Array<number>, cur: string) => {
|
||||
const ressignBrokerIdList = data[topic.topicName][cur].reassignBrokerIdList;
|
||||
if (ressignBrokerIdList.length !== topicNewReplicas[index]) {
|
||||
hasError.push(topic.topicName + ' Partition ' + cur);
|
||||
}
|
||||
acc.push(...ressignBrokerIdList);
|
||||
return acc;
|
||||
}, []);
|
||||
topic.reassignBrokerIdList = Array.from(new Set(newReassignBrokerIdList));
|
||||
topic.partitionPlanList.forEach((partition: any) => {
|
||||
partition.reassignBrokerIdList = data[topic.topicName][partition.partitionId].reassignBrokerIdList;
|
||||
});
|
||||
});
|
||||
if (hasError.length) {
|
||||
message.error(hasError.join(',') + '副本数与目标节点数不一致');
|
||||
} else {
|
||||
setTaskPlanData(taskPlanDataCopy);
|
||||
}
|
||||
};
|
||||
const checkRep = (_: any, value: any[]) => {
|
||||
if (value && value.length && topicNewReplicas.find((rep) => rep && rep > value.length)) {
|
||||
return Promise.reject('节点数低于Topic最大副本数');
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
const disabledDate: RangePickerProps['disabledDate'] = (current) => {
|
||||
// 不能选择小于当前时间
|
||||
return current && current <= moment().add(-1, 'days').endOf('day');
|
||||
};
|
||||
const range = (start: number, end: number) => {
|
||||
const result = [];
|
||||
for (let i = start; i < end; i++) {
|
||||
result.push(i);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const disabledDateTime = (current: any) => {
|
||||
return {
|
||||
disabledHours: () => (current > moment() ? [] : range(0, moment().hour())),
|
||||
disabledMinutes: () => (current > moment() ? [] : range(0, moment().add(1, 'minute').minute())),
|
||||
// disabledSeconds: () => [55, 56],
|
||||
};
|
||||
};
|
||||
|
||||
const nodeChange = (val: any) => {
|
||||
setSelectBrokerList(val);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!drawerVisible) return;
|
||||
onClickPreview();
|
||||
}, [selectBrokerList, needMovePartitions, moveDataTimeRanges]);
|
||||
|
||||
useEffect(() => {
|
||||
if (topics.length === 0 || !drawerVisible) return;
|
||||
if (jobId) {
|
||||
setLoadingTopic(true);
|
||||
getJobsTaskData()
|
||||
.then((res: any) => {
|
||||
const jobData = (res && JSON.parse(res.jobData)) || {};
|
||||
const planTime = res?.planTime && moment(res.planTime, 'YYYY-MM-DD HH:mm:ss');
|
||||
const { topicPlanList = [], throttleUnitB = 0, jobDesc = '' } = jobData;
|
||||
let selectedBrokerList: any[] = [];
|
||||
const topicData = topicPlanList.map((topic: any) => {
|
||||
selectedBrokerList = topic.reassignBrokerIdList;
|
||||
return {
|
||||
...topic,
|
||||
topicName: topic.topicName,
|
||||
latestDaysAvgBytesInList: topic.latestDaysAvgBytesInList || [],
|
||||
latestDaysMaxBytesInList: topic.latestDaysMaxBytesInList || [],
|
||||
partitionIdList: topic.partitionIdList,
|
||||
replicaNum: topic.presentReplicaNum,
|
||||
retentionMs: topic.originalRetentionTimeUnitMs,
|
||||
// newRetentionMs: topic.reassignRetentionTimeUnitMs,
|
||||
};
|
||||
});
|
||||
setTopicData(topicData);
|
||||
const newReplica = topicPlanList.map((t: any) => t.newReplicaNum || []);
|
||||
setTopicNewReplicas(newReplica);
|
||||
const needMovePartitions = topicPlanList.map((t: any) => t.partitionIdList || []);
|
||||
setNeedMovePartitions(needMovePartitions);
|
||||
const MoveDataTimeRanges = topicPlanList.map((t: any) => {
|
||||
const timeHour = t.reassignRetentionTimeUnitMs / 1000 / 60 / 60;
|
||||
return timeHour > 1 ? Math.floor(timeHour) : timeHour.toFixed(2);
|
||||
});
|
||||
setMoveDataTimeRanges(MoveDataTimeRanges);
|
||||
setSelectBrokerList(selectedBrokerList);
|
||||
form.setFieldsValue({
|
||||
brokerList: selectedBrokerList,
|
||||
throttle: throttleUnitB / 1024 / 1024,
|
||||
planTime,
|
||||
description: res?.jobDesc,
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
setLoadingTopic(false);
|
||||
});
|
||||
}
|
||||
}, [drawerVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!drawerVisible) return;
|
||||
!jobId &&
|
||||
drawerVisible &&
|
||||
Utils.request(Api.getTopicMetaData(+routeParams.clusterId))
|
||||
.then((res: any) => {
|
||||
const filterRes = res.filter((item: any) => item.type !== 1);
|
||||
const topics = (filterRes || []).map((item: any) => {
|
||||
return {
|
||||
label: item.topicName,
|
||||
value: item.topicName,
|
||||
partitionIdList: item.partitionIdList,
|
||||
};
|
||||
});
|
||||
setTopicMetaData(topics);
|
||||
})
|
||||
.catch((err) => {
|
||||
message.error(err);
|
||||
});
|
||||
}, [drawerVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!drawerVisible) return;
|
||||
if (!jobId) {
|
||||
setLoadingTopic(true);
|
||||
drawerVisible &&
|
||||
getReassignmentList(topicSelectValue)
|
||||
.then((res: any[]) => {
|
||||
setTopicData(res);
|
||||
const newReplica = res.map((t) => t.replicaNum || []);
|
||||
setTopicNewReplicas(newReplica);
|
||||
const needMovePartitions = res.map((t) => t.partitionIdList || []);
|
||||
setNeedMovePartitions(needMovePartitions);
|
||||
const MoveDataTimeRanges = res.map((t) => {
|
||||
const timeHour = t.retentionMs / 1000 / 60 / 60;
|
||||
return timeHour > 1 ? Math.floor(timeHour) : timeHour.toFixed(2);
|
||||
});
|
||||
setMoveDataTimeRanges(MoveDataTimeRanges);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoadingTopic(false);
|
||||
});
|
||||
}
|
||||
}, [topicSelectValue, drawerVisible]);
|
||||
|
||||
// ---------- 新增穿梭框替换目标节点select
|
||||
useEffect(() => {
|
||||
if (!drawerVisible) return;
|
||||
setVisible(true);
|
||||
Utils.request(Api.getDashboardMetadata(routeParams.clusterId, MetricType.Broker)).then((res: any) => {
|
||||
const dataDe = res || [];
|
||||
const dataHandle = dataDe.map((item: any) => {
|
||||
return {
|
||||
...item,
|
||||
key: item.brokerId,
|
||||
title: `${item.brokerId} (${item.host})`,
|
||||
};
|
||||
});
|
||||
setBrokerList(dataHandle);
|
||||
});
|
||||
}, [drawerVisible]);
|
||||
|
||||
const addReassign = () => {
|
||||
if (selectBrokerList.length && topicNewReplicas.find((item) => item > selectBrokerList.length)) return;
|
||||
form.validateFields().then((e) => {
|
||||
const formData = form.getFieldsValue();
|
||||
const handledData = {
|
||||
creator: global.userInfo.userName,
|
||||
jobType: 0, // type 0 topic迁移 1 扩缩容 2集群均衡
|
||||
planTime: formData.planTime,
|
||||
jobStatus: jobId ? jobStatus : 2, //status 2 创建
|
||||
target: topicSelectValue.join(','),
|
||||
id: jobId || '',
|
||||
jobDesc: formData.description,
|
||||
jobData: JSON.stringify({
|
||||
clusterId: routeParams.clusterId,
|
||||
jobDesc: formData.description,
|
||||
throttleUnitB: formData.throttle * 1024 * 1024,
|
||||
topicPlanList: topicSelectValue.map((topic, index) => {
|
||||
return {
|
||||
clusterId: routeParams.clusterId,
|
||||
topicName: topic,
|
||||
partitionIdList: needMovePartitions[index],
|
||||
partitionNum: needMovePartitions[index].length,
|
||||
presentReplicaNum: topicNewReplicas[index],
|
||||
newReplicaNum: topicNewReplicas[index] || topic.replicaNum,
|
||||
originalBrokerIdList: taskPlanData[index].currentBrokerIdList,
|
||||
reassignBrokerIdList: taskPlanData[index].reassignBrokerIdList,
|
||||
originalRetentionTimeUnitMs: topicData[index].retentionMs,
|
||||
reassignRetentionTimeUnitMs: moveDataTimeRanges[index] * 60 * 60 * 1000,
|
||||
latestDaysAvgBytesInList: topicData[index].latestDaysAvgBytesInList,
|
||||
latestDaysMaxBytesInList: topicData[index].latestDaysMaxBytesInList,
|
||||
partitionPlanList: taskPlanData[index].partitionPlanList,
|
||||
};
|
||||
}),
|
||||
}),
|
||||
};
|
||||
if (jobId) {
|
||||
Utils.put(Api.putJobsTaskData(routeParams.clusterId), handledData)
|
||||
.then(() => {
|
||||
message.success('迁移任务编辑成功');
|
||||
onDrawerClose();
|
||||
genData();
|
||||
})
|
||||
.catch((err: any) => {
|
||||
console.log(err, 'err');
|
||||
});
|
||||
} else {
|
||||
Utils.post(Api.createTask(routeParams.clusterId), handledData).then(() => {
|
||||
message.success('迁移任务创建成功');
|
||||
onDrawerClose();
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
push={false}
|
||||
title={jobNameMap[type]}
|
||||
width={1080}
|
||||
placement="right"
|
||||
onClose={onDrawerClose}
|
||||
visible={visible}
|
||||
className="topic-job-drawer"
|
||||
maskClosable={false}
|
||||
destroyOnClose
|
||||
extra={
|
||||
<Space>
|
||||
<Button
|
||||
size="small"
|
||||
style={{ marginRight: 8 }}
|
||||
onClick={(_) => {
|
||||
// setVisible(false);
|
||||
onDrawerClose();
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button size="small" type="primary" onClick={addReassign}>
|
||||
确定
|
||||
</Button>
|
||||
<Divider type="vertical" />
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<div className="wrap">
|
||||
<h4 className="title">{jobNameMap[type]}Topic</h4>
|
||||
|
||||
{!jobId && (
|
||||
<Form form={form}>
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item>
|
||||
<Select
|
||||
placeholder="请选择Topic,可多选"
|
||||
mode="multiple"
|
||||
onChange={(v: any) => {
|
||||
setTopicSelectValue(v);
|
||||
}}
|
||||
options={topicMetaData}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
)}
|
||||
|
||||
<Table dataSource={topicData} columns={topicDataColumns} pagination={false} loading={loadingTopic} />
|
||||
<Form form={form} layout="vertical" className="task-form">
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item name="brokerList" label="目标节点" rules={[{ required: true }, { validator: checkRep }]}>
|
||||
<Transfer
|
||||
dataSource={brokerList}
|
||||
showSearch
|
||||
filterOption={(inputValue, option) => option.host.indexOf(inputValue) > -1}
|
||||
targetKeys={selectBrokerList}
|
||||
onChange={nodeChange}
|
||||
render={(item) => item.title}
|
||||
titles={['待选节点', '已选节点']}
|
||||
customHeader
|
||||
showSelectedCount
|
||||
locale={{ itemUnit: '', itemsUnit: '' }}
|
||||
suffix={<IconFont type="icon-fangdajing" />}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
{/* taskPlanData是传给组件的初始值
|
||||
点击预览任务计划,触发onClickPreview回调,发起请求获取taskPlanData
|
||||
组件内部改完点击每一行保存时,再通过onClickSavePreview回调向外派发数据 */}
|
||||
<PreviewTaskPlan
|
||||
taskPlanData={taskPlanData}
|
||||
onClickPreview={onClickPreview}
|
||||
onClickSavePreview={onClickSavePreview}
|
||||
brokerList={brokerList}
|
||||
></PreviewTaskPlan>
|
||||
</Col>
|
||||
</Row>
|
||||
<h4 className="title">迁移任务配置</h4>
|
||||
<Row gutter={32} className="topic-execution-time">
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="throttle"
|
||||
label="限流"
|
||||
rules={[
|
||||
{ required: true },
|
||||
{
|
||||
validator: (r: any, v: number) => {
|
||||
if ((v || v === 0) && v <= 0) {
|
||||
return Promise.reject('限流值不能小于或等于0');
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
// formatter={(value) => `${value} MB/s`}
|
||||
// parser={(value) => value.replace('MB/s', '')}
|
||||
addonAfter="MB/S"
|
||||
max={99999}
|
||||
></InputNumber>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="planTime" label="任务执行时间" rules={[{ required: true }]}>
|
||||
<DatePicker showTime style={{ width: '100%' }} disabledDate={disabledDate} disabledTime={disabledDateTime} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<h4 className="title">描述</h4>
|
||||
<Form.Item name="description" label="任务描述" rules={[{ required: true }]}>
|
||||
<TextArea placeholder="暂支持 String 格式" style={{ height: 110 }} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
.topic-job-drawer {
|
||||
.divider {
|
||||
margin-left: 8px;
|
||||
margin-right: 8px;
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
background-color: #CED4DA;
|
||||
}
|
||||
|
||||
.dcloud-drawer-body {
|
||||
// padding: 0 20px;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
.title {
|
||||
font-size: 16px;
|
||||
color: #212529;
|
||||
letter-spacing: 0;
|
||||
line-height: 25px;
|
||||
margin: 16px 0;
|
||||
font-family: @font-family-bold;
|
||||
}
|
||||
|
||||
.topic-execution-time{
|
||||
.dcloud-form-item-has-error{
|
||||
.dcloud-picker{
|
||||
border-color: #ff7066 !important;
|
||||
background: #fffafa !important;
|
||||
}
|
||||
}
|
||||
.dcloud-picker{
|
||||
background-color: rgba(33, 37, 41, 0.06);
|
||||
border-color: transparent;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.dcloud-picker-focused,.dcloud-picker:hover{
|
||||
border-color: #74788d;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-tag-wrap {
|
||||
display: flex;
|
||||
|
||||
.custom-tag {
|
||||
padding: 4px;
|
||||
background: #ECECF6;
|
||||
border-radius: 5px;
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
color: #495057;
|
||||
margin-right: 4px;
|
||||
|
||||
.time {
|
||||
color: #74788D;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.task-form {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.dcloud-select-selector {
|
||||
max-height: 100px;
|
||||
overflow: scroll;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-task-plan-drawer {
|
||||
.partition-task-plan-list {
|
||||
padding: 12px 16px;
|
||||
|
||||
.dcloud-table .dcloud-table-content .dcloud-table-thead .dcloud-table-cell,
|
||||
.dcloud-table .dcloud-table-content .dcloud-table-tbody .dcloud-table-cell {
|
||||
background: #F8F9FA;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
#react-joyride-portal {
|
||||
.react-joyride__overlay {
|
||||
min-width: 1440px;
|
||||
}
|
||||
}
|
||||
.joyride-tooltip {
|
||||
min-width: 284px;
|
||||
max-width: 384px;
|
||||
padding: 16px 24px;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
&-header {
|
||||
margin-bottom: 8px;
|
||||
font-family: @font-family-bold;
|
||||
font-size: 16px;
|
||||
color: #212529;
|
||||
letter-spacing: 0;
|
||||
line-height: 22px;
|
||||
}
|
||||
&-body {
|
||||
font-size: 14px;
|
||||
color: #74788d;
|
||||
letter-spacing: 0;
|
||||
line-height: 22px;
|
||||
font-weight: 400;
|
||||
}
|
||||
&-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 28px;
|
||||
&-left {
|
||||
color: #74788d;
|
||||
}
|
||||
&-right {
|
||||
.dcloud-btn {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { Button, Space } from 'knowdesign';
|
||||
import React, { useLayoutEffect, useState } from 'react';
|
||||
import Joyride, { Step, TooltipRenderProps } from 'react-joyride';
|
||||
import './index.less';
|
||||
|
||||
interface TourGuideProps {
|
||||
run: boolean;
|
||||
guide: {
|
||||
key: string;
|
||||
steps: Step[];
|
||||
};
|
||||
}
|
||||
|
||||
// 全部配置项参考: https://github.com/gilbarbara/react-joyride/blob/3e08384415a831b20ce21c8423b6c271ad419fbf/src/styles.js
|
||||
const joyrideCommonStyle = {
|
||||
options: {
|
||||
zIndex: 2000,
|
||||
},
|
||||
spotlight: {
|
||||
borderRadius: 12,
|
||||
},
|
||||
};
|
||||
|
||||
const JoyrideTooltip = (props: TooltipRenderProps) => {
|
||||
const { continuous, index, size, step, isLastStep, backProps, skipProps, primaryProps, tooltipProps } = props;
|
||||
|
||||
return (
|
||||
<div className="joyride-tooltip" {...tooltipProps}>
|
||||
{step.title && <div className="joyride-tooltip-header">{step.title}</div>}
|
||||
<div className="joyride-tooltip-body">{step.content}</div>
|
||||
<div className="joyride-tooltip-footer">
|
||||
<div className="joyride-tooltip-footer-left">
|
||||
{index + 1} / {size}
|
||||
</div>
|
||||
<div className="joyride-tooltip-footer-right">
|
||||
{/* {index > 0 && (
|
||||
<Button {...backProps} size="small">
|
||||
上一个
|
||||
</Button>
|
||||
)} */}
|
||||
<Space>
|
||||
<Button {...skipProps} size="small" type="text">
|
||||
跳过
|
||||
</Button>
|
||||
{continuous && (
|
||||
<Button {...primaryProps} size="small" type="primary">
|
||||
{isLastStep ? '我知道了' : '下一个'}
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const TourGuide = ({ guide, run: ready }: TourGuideProps) => {
|
||||
const [run, setRun] = useState<boolean>(false);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (ready) {
|
||||
const curGuideKey = guide.key;
|
||||
const guidedStorage = localStorage.getItem('guided');
|
||||
let guidedInfo: string[];
|
||||
|
||||
try {
|
||||
guidedInfo = JSON.parse(guidedStorage) || [];
|
||||
if (!guidedInfo.includes(curGuideKey)) {
|
||||
guidedInfo.push(curGuideKey);
|
||||
localStorage.setItem('guided', JSON.stringify(guidedInfo));
|
||||
setRun(true);
|
||||
}
|
||||
} catch (err) {
|
||||
err;
|
||||
}
|
||||
}
|
||||
}, [ready]);
|
||||
|
||||
return (
|
||||
<Joyride
|
||||
steps={guide.steps}
|
||||
run={run}
|
||||
continuous
|
||||
hideCloseButton
|
||||
showProgress
|
||||
disableCloseOnEsc
|
||||
disableOverlayClose
|
||||
disableScrolling
|
||||
disableScrollParentFix
|
||||
tooltipComponent={JoyrideTooltip}
|
||||
styles={joyrideCommonStyle}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export * from './steps';
|
||||
export default TourGuide;
|
||||
@@ -0,0 +1,84 @@
|
||||
const MultiPageSteps = {
|
||||
key: 'MultiPage',
|
||||
steps: [
|
||||
{
|
||||
target: '.cluster-header-card',
|
||||
title: 'Cluster 总数',
|
||||
content: '这里展示了集群的数量和运行状态',
|
||||
disableBeacon: true,
|
||||
placement: 'bottom-start' as const,
|
||||
},
|
||||
{
|
||||
target: '.header-filter-bottom',
|
||||
title: '版本选择、健康分',
|
||||
content: '这里展示了版本、健康分的统计信息,并可以进行筛选',
|
||||
placement: 'bottom-start' as const,
|
||||
},
|
||||
{
|
||||
target: '.multi-cluster-list-item:first-child',
|
||||
title: '集群卡片',
|
||||
content: '这里展示了每个集群状态、指标等综合信息,点击可以 进入单集群管理页面',
|
||||
placement: 'bottom-start' as const,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const ClusterDetailSteps = {
|
||||
key: 'ClusterDetail',
|
||||
steps: [
|
||||
{
|
||||
target: '.single-cluster-detail .left-sider',
|
||||
title: '集群概览',
|
||||
content: '这里展示了集群的整体健康状态和统计信息',
|
||||
disableBeacon: true,
|
||||
placement: 'right-start' as const,
|
||||
},
|
||||
{
|
||||
target: '.single-cluster-detail .cluster-detail .left-sider .healthy-state-status .icon',
|
||||
title: '设置健康度',
|
||||
content: '点击这里可以设置集群的健康检查项、权重及规则',
|
||||
placement: 'right-start' as const,
|
||||
styles: {
|
||||
spotlight: {
|
||||
borderRadius: 6,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
target: '.single-cluster-detail .cluster-detail .left-sider .healthy-state-btn',
|
||||
title: '查看健康状态详情',
|
||||
content: '点击这里可以查看集群的健康状态的检查结果',
|
||||
placement: 'right-start' as const,
|
||||
styles: {
|
||||
spotlight: {
|
||||
borderRadius: 6,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
target: '.single-cluster-detail .cluster-detail .left-sider .title .edit-icon-box',
|
||||
title: '编辑集群信息',
|
||||
content: '点击这里可以查看集群配置信息,并且可以对信息进行编辑',
|
||||
placement: 'right-start' as const,
|
||||
styles: {
|
||||
spotlight: {
|
||||
borderRadius: 6,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
target: '.single-cluster-detail .ks-chart-container-header .header-right .icon-box',
|
||||
title: '指标筛选',
|
||||
content: '点击这里可以对展示的图表进行筛选',
|
||||
placement: 'left-start' as const,
|
||||
},
|
||||
{
|
||||
target: '.single-cluster-detail .cluster-detail .change-log-panel',
|
||||
title: '历史变更记录',
|
||||
content: '这里展示了配置变更的历史记录',
|
||||
placement: 'left-start' as const,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export { MultiPageSteps, ClusterDetailSteps };
|
||||
@@ -0,0 +1,123 @@
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
// @ts-nocheck
|
||||
|
||||
import { notification, Utils } from 'knowdesign';
|
||||
const { EventBus } = Utils;
|
||||
export const licenseEventBus = new EventBus();
|
||||
|
||||
export const goLogin = () => {
|
||||
if (!window.location.pathname.includes('login')) {
|
||||
// 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) => {
|
||||
const res: { code: number; message: string; data: any } = config.data;
|
||||
if (res.code !== 0 && res.code !== 200) {
|
||||
const desc = res.message;
|
||||
// TODO: ---
|
||||
if (res.code === 1000000000 || res.code === 1000000001 || res.code === 1000000002) {
|
||||
licenseEventBus.emit('licenseError', desc);
|
||||
} else {
|
||||
notification.error({
|
||||
message: desc,
|
||||
duration: 3,
|
||||
});
|
||||
}
|
||||
throw res;
|
||||
}
|
||||
return res;
|
||||
},
|
||||
(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);
|
||||
};
|
||||
@@ -0,0 +1,213 @@
|
||||
import moment from 'moment';
|
||||
|
||||
export const UNIT_MAP = {
|
||||
TB: Math.pow(1024, 4),
|
||||
GB: Math.pow(1024, 3),
|
||||
MB: Math.pow(1024, 2),
|
||||
KB: 1024,
|
||||
};
|
||||
|
||||
export const DATA_NUMBER_MAP = {
|
||||
十亿: Math.pow(1000, 3),
|
||||
百万: Math.pow(1000, 2),
|
||||
千: 1000,
|
||||
};
|
||||
|
||||
export const getUnit = (value: number) => Object.entries(UNIT_MAP).find(([, size]) => value / size >= 1) || ['Byte', 1];
|
||||
|
||||
export const getDataNumberUnit = (value: number) => Object.entries(DATA_NUMBER_MAP).find(([, size]) => value / size >= 1) || ['', 1];
|
||||
|
||||
// 图表 tooltip 基础展示样式
|
||||
const tooltipFormatter = (date: any, arr: any, tooltip: any) => {
|
||||
// 从大到小排序
|
||||
// arr = arr.sort((a: any, b: any) => b.value - a.value);
|
||||
const str = arr
|
||||
.map(
|
||||
(item: any) => `<div style="margin: 3px 0;">
|
||||
<div style="display:flex;align-items:center;">
|
||||
<div style="margin-right:4px;width:8px;height:2px;background-color:${item.color};"></div>
|
||||
<div style="flex:1;display:flex;justify-content:space-between;align-items:center;overflow: hidden;">
|
||||
<span style="font-size:12px;color:#74788D;pointer-events:auto;margin-left:2px;line-height: 18px;font-family: HelveticaNeue;overflow: hidden; text-overflow: ellipsis; white-space: no-wrap;">
|
||||
${item.seriesName}
|
||||
</span>
|
||||
<span style="font-size:12px;color:#212529;line-height:18px;font-family:HelveticaNeue-Medium; padding-left: 6px;">
|
||||
${parseFloat(Number(item.value[1]).toFixed(3))}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
)
|
||||
.join('');
|
||||
|
||||
return `<div style="margin: 0px 0 0; position: relative; z-index: 99;width: ${
|
||||
tooltip.customWidth ? tooltip.customWidth + 'px' : 'fit-content'
|
||||
};">
|
||||
<div style="padding: 8px 0;height: 100%;">
|
||||
<div style="font-size:12px;padding: 0 12px;color:#212529;line-height:20px;font-family: HelveticaNeue;">
|
||||
${date}
|
||||
</div>
|
||||
<div style="${
|
||||
tooltip.legendContextMaxHeight ? 'max-height: ' + tooltip.legendContextMaxHeight + 'px' : ''
|
||||
}; margin: 4px 0 0 0;overflow-y:auto;padding: 0 12px;">
|
||||
${str}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
};
|
||||
|
||||
// 折线图基础主题配置,返回 echarts 配置项。详见 https://echarts.apache.org/zh/option.html
|
||||
export const getBasicChartConfig = (props: any = {}) => {
|
||||
const { title = {}, grid = {}, legend = {}, xAxis = {}, yAxis = {}, tooltip = {}, ...restConfig } = props;
|
||||
return {
|
||||
title: {
|
||||
show: true,
|
||||
text: '示例标题',
|
||||
textStyle: {
|
||||
fontSize: 14,
|
||||
fontFamily: 'HelveticaNeue-Medium',
|
||||
color: '#212529',
|
||||
letterSpacing: 0.5,
|
||||
lineHeight: 16,
|
||||
rich: {
|
||||
unit: {
|
||||
fontSize: 12,
|
||||
fontFamily: 'HelveticaNeue-Medium',
|
||||
color: '#495057',
|
||||
lineHeight: 16,
|
||||
},
|
||||
},
|
||||
},
|
||||
top: 12,
|
||||
left: 16,
|
||||
zlevel: 1,
|
||||
...title,
|
||||
},
|
||||
// 图表整体布局
|
||||
grid: {
|
||||
zlevel: 0,
|
||||
top: 60,
|
||||
left: 22,
|
||||
right: 16,
|
||||
bottom: 40,
|
||||
containLabel: true,
|
||||
...grid,
|
||||
},
|
||||
// 图例配置
|
||||
legend: {
|
||||
zlevel: 1,
|
||||
type: 'scroll',
|
||||
orient: 'horizontal',
|
||||
left: 20,
|
||||
top: 'auto',
|
||||
bottom: 12,
|
||||
icon: 'rect',
|
||||
itemHeight: 2,
|
||||
itemWidth: 8,
|
||||
itemGap: 8,
|
||||
textStyle: {
|
||||
width: 85,
|
||||
overflow: 'truncate',
|
||||
ellipsis: '...',
|
||||
fontSize: 11,
|
||||
lineHeight: 12,
|
||||
color: '#74788D',
|
||||
},
|
||||
pageIcons: {
|
||||
horizontal: [
|
||||
'path://M474.496 512l151.616 151.616a9.6 9.6 0 0 1 0 13.568l-31.68 31.68a9.6 9.6 0 0 1-13.568 0l-190.08-190.08a9.6 9.6 0 0 1 0-13.568l190.08-190.08a9.6 9.6 0 0 1 13.568 0l31.68 31.68a9.6 9.6 0 0 1 0 13.568L474.496 512z',
|
||||
'path://M549.504 512L397.888 360.384a9.6 9.6 0 0 1 0-13.568l31.68-31.68a9.6 9.6 0 0 1 13.568 0l190.08 190.08a9.6 9.6 0 0 1 0 13.568l-190.08 190.08a9.6 9.6 0 0 1-13.568 0l-31.68-31.68a9.6 9.6 0 0 1 0-13.568L549.504 512z',
|
||||
],
|
||||
},
|
||||
pageIconColor: '#495057',
|
||||
pageIconInactiveColor: '#ADB5BC',
|
||||
pageIconSize: 6,
|
||||
...legend,
|
||||
},
|
||||
// 横坐标配置
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: true,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#c5c5c5',
|
||||
width: 1,
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
formatter: (value: number) => {
|
||||
value = Number(value);
|
||||
return [`{date|${moment(value).format('MM-DD')}}`, `{time|${moment(value).format('HH:mm')}}`].join('\n');
|
||||
},
|
||||
padding: 0,
|
||||
rich: {
|
||||
date: {
|
||||
color: '#495057',
|
||||
fontSize: 11,
|
||||
lineHeight: 18,
|
||||
fontFamily: 'HelveticaNeue',
|
||||
},
|
||||
time: {
|
||||
color: '#ADB5BC',
|
||||
fontSize: 11,
|
||||
lineHeight: 11,
|
||||
fontFamily: 'HelveticaNeue',
|
||||
},
|
||||
},
|
||||
},
|
||||
...xAxis,
|
||||
},
|
||||
// 纵坐标配置
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
interval: 0,
|
||||
hideOverlap: false,
|
||||
color: '#495057',
|
||||
fontSize: 12,
|
||||
lineHeight: 20,
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
width: 1,
|
||||
type: 'dashed',
|
||||
color: ['#E4E7ED'],
|
||||
},
|
||||
},
|
||||
...yAxis,
|
||||
},
|
||||
// 提示框浮层配置
|
||||
tooltip: {
|
||||
position: function (pos: any, params: any, el: any, elRect: any, size: any) {
|
||||
const tooltipWidth = el.offsetWidth || 120;
|
||||
const result =
|
||||
tooltipWidth + pos[0] < size.viewSize[0]
|
||||
? {
|
||||
top: 40,
|
||||
left: pos[0] + 30,
|
||||
}
|
||||
: {
|
||||
top: 40,
|
||||
left: pos[0] - tooltipWidth - 30,
|
||||
};
|
||||
return result;
|
||||
},
|
||||
formatter: function (params: any) {
|
||||
let res = '';
|
||||
if (params != null && params.length > 0) {
|
||||
// 传入tooltip是为了便于拿到外部传入的控制这个自定义浮层的样式
|
||||
// 例如tooltip里写customWidth: 200,则tooltipFormatter里可以取出这个宽度使用
|
||||
res += tooltipFormatter(moment(Number(params[0].axisValue)).format('YYYY-MM-DD HH:mm'), params, tooltip);
|
||||
}
|
||||
return res;
|
||||
},
|
||||
extraCssText:
|
||||
'padding: 0;box-shadow: 0 -2px 4px 0 rgba(0,0,0,0.02), 0 2px 6px 6px rgba(0,0,0,0.02), 0 2px 6px 0 rgba(0,0,0,0.06);border-radius: 8px;',
|
||||
axisPointer: {
|
||||
type: 'line',
|
||||
},
|
||||
...tooltip,
|
||||
},
|
||||
...restConfig,
|
||||
};
|
||||
};
|
||||
139
km-console/packages/layout-clusters-fe/src/constants/common.ts
Executable file
@@ -0,0 +1,139 @@
|
||||
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;
|
||||
|
||||
// 小间隔(1 分钟)图表点的最大请求时间范围,单位: ms
|
||||
export const MAX_TIME_RANGE_WITH_SMALL_POINT_INTERVAL = 6 * 60 * 60 * 1000;
|
||||
|
||||
export const primaryColor = '#556EE6';
|
||||
|
||||
export const numberToFixed = (value: number, num = 2) => {
|
||||
if (value === null || isNaN(value)) return '-';
|
||||
value = Number(value);
|
||||
return Number.isInteger(value) ? value : value.toFixed(num);
|
||||
};
|
||||
|
||||
const K = 1024;
|
||||
const M = 1024 * K;
|
||||
const G = 1024 * M;
|
||||
const T = 1024 * G;
|
||||
|
||||
export const getSizeAndUnit: any = (value: any, unitSuffix?: any, num = 2) => {
|
||||
if (value === null || value === undefined || isNaN(+value)) {
|
||||
return { value: null, unit: '', valueWithUnit: '-' };
|
||||
}
|
||||
|
||||
if (value <= K) {
|
||||
return { value: numberToFixed(value, num), unit: '' + unitSuffix, valueWithUnit: numberToFixed(value) + '' + unitSuffix };
|
||||
}
|
||||
if (value > K && value < M) {
|
||||
return { value: numberToFixed(value / K, num), unit: 'K' + unitSuffix, valueWithUnit: numberToFixed(value / K) + 'K' + unitSuffix };
|
||||
}
|
||||
if (value >= M && value < G) {
|
||||
return { value: numberToFixed(value / M, num), unit: 'M' + unitSuffix, valueWithUnit: numberToFixed(value / M) + 'M' + unitSuffix };
|
||||
}
|
||||
if (value >= G && value < T) {
|
||||
return { value: numberToFixed(value / G, num), unit: 'G' + unitSuffix, valueWithUnit: numberToFixed(value / G) + 'G' + unitSuffix };
|
||||
}
|
||||
if (value >= T) {
|
||||
return { value: numberToFixed(value / T, num), unit: 'T' + unitSuffix, valueWithUnit: numberToFixed(value / T) + 'T' + unitSuffix };
|
||||
}
|
||||
return '-';
|
||||
};
|
||||
|
||||
export const orderTypeMap: any = {
|
||||
ascend: 'asc',
|
||||
descend: 'desc',
|
||||
};
|
||||
|
||||
export const dealTableRequestParams = ({ searchKeywords, pageNo, pageSize, sorter, filters, isPhyId = undefined }: any) => {
|
||||
const _params = {
|
||||
searchKeywords,
|
||||
pageNo,
|
||||
pageSize,
|
||||
isPhyId,
|
||||
};
|
||||
if (sorter && sorter.field && sorter.order) {
|
||||
Object.assign(_params, {
|
||||
sortField: sorter.field,
|
||||
sortType: orderTypeMap[sorter.order],
|
||||
});
|
||||
}
|
||||
if (filters) {
|
||||
const filterDTOList = [];
|
||||
for (const key of Object.keys(filters)) {
|
||||
if (filters[key]) {
|
||||
filterDTOList.push({
|
||||
fieldName: key,
|
||||
fieldValueList: filters[key],
|
||||
});
|
||||
}
|
||||
}
|
||||
Object.assign(_params, {
|
||||
filterDTOList,
|
||||
});
|
||||
}
|
||||
|
||||
return _params;
|
||||
};
|
||||
|
||||
// url hash Parse
|
||||
export const hashDataParse = (hash: string) => {
|
||||
const newHashData: any = {};
|
||||
hash
|
||||
.slice(1)
|
||||
.split('&')
|
||||
.forEach((str: string) => {
|
||||
const hashStr = str.split('=');
|
||||
newHashData[hashStr[0]] = hashStr[1];
|
||||
});
|
||||
|
||||
return newHashData;
|
||||
};
|
||||
|
||||
const BUSINESS_VERSION = process.env.BUSINESS_VERSION;
|
||||
|
||||
export const getLicenseInfo = (cbk: (msg: string) => void) => {
|
||||
if (BUSINESS_VERSION) {
|
||||
const info = (window as any).code;
|
||||
if (!info) {
|
||||
setTimeout(() => getLicenseInfo(cbk), 1000);
|
||||
} else {
|
||||
const res = info() || {};
|
||||
if (res.code !== 0) {
|
||||
cbk(res.msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
145
km-console/packages/layout-clusters-fe/src/constants/menu.ts
Executable file
@@ -0,0 +1,145 @@
|
||||
import { ClustersPermissionMap } from '@src/pages/CommonConfig';
|
||||
|
||||
const pkgJson = require('../../package');
|
||||
export const systemKey = pkgJson.ident;
|
||||
|
||||
export const leftMenus = (clusterId?: string) => ({
|
||||
name: `${systemKey}`,
|
||||
icon: 'icon-jiqun',
|
||||
path: `cluster/${clusterId}`,
|
||||
children: [
|
||||
{
|
||||
name: 'cluster',
|
||||
path: 'cluster',
|
||||
icon: 'icon-Cluster',
|
||||
children: [
|
||||
{
|
||||
name: 'overview',
|
||||
path: '',
|
||||
icon: '#icon-luoji',
|
||||
},
|
||||
process.env.BUSINESS_VERSION
|
||||
? {
|
||||
name: 'balance',
|
||||
path: 'balance',
|
||||
icon: '#icon-luoji',
|
||||
}
|
||||
: undefined,
|
||||
].filter((m) => m),
|
||||
},
|
||||
{
|
||||
name: 'broker',
|
||||
path: 'broker',
|
||||
icon: 'icon-Brokers',
|
||||
children: [
|
||||
{
|
||||
name: 'dashbord',
|
||||
path: '',
|
||||
icon: '#icon-luoji',
|
||||
},
|
||||
{
|
||||
name: 'list',
|
||||
path: 'list',
|
||||
icon: '#icon-jiqun1',
|
||||
},
|
||||
{
|
||||
name: 'controller-changelog',
|
||||
path: 'controller-changelog',
|
||||
icon: '#icon-jiqun1',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'topic',
|
||||
path: 'topic',
|
||||
icon: 'icon-Topics',
|
||||
children: [
|
||||
{
|
||||
name: 'dashbord',
|
||||
path: '',
|
||||
icon: 'icon-luoji',
|
||||
},
|
||||
{
|
||||
name: 'list',
|
||||
path: 'list',
|
||||
icon: 'icon-luoji',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'consumer-group',
|
||||
path: 'consumers',
|
||||
icon: 'icon-ConsumerGroups',
|
||||
// children: [
|
||||
// {
|
||||
// name: 'operating-state',
|
||||
// path: 'operating-state/list',
|
||||
// icon: '#icon-luoji',
|
||||
// },
|
||||
// {
|
||||
// name: 'group-list',
|
||||
// path: 'group-list',
|
||||
// icon: '#icon-luoji',
|
||||
// },
|
||||
// ],
|
||||
},
|
||||
process.env.BUSINESS_VERSION
|
||||
? {
|
||||
name: 'produce-consume',
|
||||
path: 'testing',
|
||||
icon: 'icon-a-ProduceConsume',
|
||||
permissionPoint: [ClustersPermissionMap.TEST_CONSUMER, ClustersPermissionMap.TEST_PRODUCER],
|
||||
children: [
|
||||
{
|
||||
name: 'producer',
|
||||
path: 'producer',
|
||||
icon: 'icon-luoji',
|
||||
permissionPoint: ClustersPermissionMap.TEST_PRODUCER,
|
||||
},
|
||||
{
|
||||
name: 'consumer',
|
||||
path: 'consumer',
|
||||
icon: 'icon-luoji',
|
||||
permissionPoints: ClustersPermissionMap.TEST_CONSUMER,
|
||||
},
|
||||
],
|
||||
}
|
||||
: undefined,
|
||||
{
|
||||
name: 'security',
|
||||
path: 'security',
|
||||
icon: 'icon-ACLs',
|
||||
children: [
|
||||
{
|
||||
name: 'acls',
|
||||
path: 'acls',
|
||||
icon: 'icon-luoji',
|
||||
},
|
||||
{
|
||||
name: 'users',
|
||||
path: 'users',
|
||||
icon: 'icon-luoji',
|
||||
},
|
||||
],
|
||||
},
|
||||
// {
|
||||
// name: 'acls',
|
||||
// path: 'acls',
|
||||
// icon: 'icon-wodegongzuotai',
|
||||
// },
|
||||
{
|
||||
name: 'jobs',
|
||||
path: 'jobs',
|
||||
icon: 'icon-Jobs',
|
||||
},
|
||||
].filter((m) => m),
|
||||
});
|
||||
|
||||
// key值需要与locale zh 中key值一致
|
||||
export const permissionPoints = {
|
||||
[`menu.${systemKey}.home`]: true,
|
||||
};
|
||||
|
||||
export const ROUTER_CACHE_KEYS = {
|
||||
home: `menu.${systemKey}.home`,
|
||||
};
|
||||
0
km-console/packages/layout-clusters-fe/src/index.html
Executable file
260
km-console/packages/layout-clusters-fe/src/index.less
Normal file
@@ -0,0 +1,260 @@
|
||||
@font-face {
|
||||
font-family: 'DIDIFD-Medium';
|
||||
src: url(assets/DIDIFD-Medium.woff2) format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'DIDIFD-Black';
|
||||
src: url(assets/DIDIFD-Black.woff2) format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'DIDIFD-Regular';
|
||||
src: url(assets/DIDIFD-Regular.woff2) format('woff2');
|
||||
}
|
||||
|
||||
* {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
@body-background: #ebebf3;
|
||||
|
||||
html {
|
||||
}
|
||||
|
||||
body {
|
||||
}
|
||||
|
||||
#layout {
|
||||
height: 100%;
|
||||
.dcd-layout-two-columns {
|
||||
> div:nth-child(3) {
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dcd-layout-two-columns .dcd-layout-two-columns-header .left .header-logo {
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.ant-layout {
|
||||
background-color: #f9f9fa;
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
li {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.mt-20 {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.mb-20 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.mt-24 {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.mb-24 {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: #46d677;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #ef645c;
|
||||
}
|
||||
|
||||
.ml-5 {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.ml-10 {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.ml-15 {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.mr-5 {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.mr-10 {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.mr-15 {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.pt-20 {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.fr {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.ant-select-selector {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.table-item-tags {
|
||||
.item-nums {
|
||||
background: rgba(42, 143, 255, 0.06);
|
||||
border: 0 solid #2a8fff;
|
||||
border-radius: 1px;
|
||||
|
||||
transform: rotate(-360deg);
|
||||
font-family: @font-family-bold;
|
||||
font-size: 10px;
|
||||
color: #2a8fff;
|
||||
text-align: right;
|
||||
line-height: 10px;
|
||||
}
|
||||
|
||||
.ant-tag {
|
||||
background: #f8fafd;
|
||||
border: 1px solid #eaeef5;
|
||||
border-radius: 1px;
|
||||
font-size: 12px;
|
||||
color: #303a51;
|
||||
text-align: center;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-popover {
|
||||
max-width: 560px;
|
||||
max-height: 200px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
|
||||
.ant-tag {
|
||||
margin: 5px;
|
||||
background: #f8fafd;
|
||||
border: 1px solid #eaeef5;
|
||||
border-radius: 1px;
|
||||
font-size: 12px;
|
||||
color: #303a51;
|
||||
text-align: center;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.line-chart-sp {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
padding-top: 20px;
|
||||
|
||||
&.no-padding {
|
||||
padding-top: 0px;
|
||||
}
|
||||
|
||||
.chart-op {
|
||||
position: absolute;
|
||||
top: -29px;
|
||||
right: 0px;
|
||||
|
||||
span {
|
||||
margin-left: 19px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cmb-page-header {
|
||||
&.ant-page-header {
|
||||
background: #ffffff;
|
||||
border-radius: 2px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
padding: 7px 24px 4px 24px;
|
||||
font-family: Helvetica-Bold;
|
||||
font-size: 18px;
|
||||
color: #374053;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-metric-box {
|
||||
.ant-form-item {
|
||||
margin-bottom: 0px !important;
|
||||
}
|
||||
|
||||
.metric-box-chart {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
padding: 0px 20px 20px 20px;
|
||||
}
|
||||
|
||||
.select-time-box {
|
||||
margin-bottom: 0px !important;
|
||||
}
|
||||
|
||||
.select-type-time-form {
|
||||
justify-content: flex-end;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-table-wrapper.dantd-table .ant-table-pagination.ant-pagination {
|
||||
margin: 10px 0px 0px 0px !important;
|
||||
}
|
||||
|
||||
.table-alert {
|
||||
margin-top: 10px !important;
|
||||
margin-left: 24px !important;
|
||||
margin-right: 24px !important;
|
||||
text-align: left;
|
||||
|
||||
&.no-margin {
|
||||
margin: 0px !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.small-margin {
|
||||
margin: 10px 0px !important;
|
||||
}
|
||||
|
||||
.ant-alert-message {
|
||||
color: #f4a838;
|
||||
}
|
||||
}
|
||||
|
||||
.dcloud-layout-content {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.d-table-box-header {
|
||||
// 去除DTable的margin
|
||||
margin-bottom: 0px !important;
|
||||
}
|
||||
|
||||
// 抽屉内 Alert 特殊样式
|
||||
.dcloud-drawer {
|
||||
&-body {
|
||||
.drawer-alert-full-screen {
|
||||
border: none;
|
||||
margin-left: -20px;
|
||||
margin-right: -20px;
|
||||
.dcloud-alert-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
35
km-console/packages/layout-clusters-fe/src/index.tsx
Executable file
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import App from './app';
|
||||
import './style-addition.less';
|
||||
|
||||
|
||||
// function invalidModal(downloadBrowserUrl?: string) {
|
||||
// Modal.warning({
|
||||
// title: '浏览器版本过低',
|
||||
// content: (
|
||||
// <div>
|
||||
// 正在使用的浏览器版本过低,将不能正常浏览本平台。为了保证更好的使用体验,请升级至Chrome 70以上版本。
|
||||
// {
|
||||
// downloadBrowserUrl
|
||||
// ? <div>
|
||||
// <a href={downloadBrowserUrl}>下载最新版本</a>
|
||||
// </div> : null
|
||||
// }
|
||||
// </div>
|
||||
// ),
|
||||
// });
|
||||
// }
|
||||
|
||||
// if (!isValidBrowser) {
|
||||
// fetch(api.downloadBrowser).then((res) => {
|
||||
// return res.json();
|
||||
// }).then((res) => {
|
||||
// invalidModal(res.dat);
|
||||
// }).catch((e) => {
|
||||
// console.log(e);
|
||||
// invalidModal();
|
||||
// });
|
||||
// }
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById('layout'));
|
||||
84
km-console/packages/layout-clusters-fe/src/interface/index.tsx
Executable file
@@ -0,0 +1,84 @@
|
||||
import React from 'react';
|
||||
// TODO: 菜单配置接口有点乱,看是否可以归类下,以及是否可以去掉一些非必要的属性
|
||||
export interface MenuConfItem {
|
||||
key?: string;
|
||||
name: string | React.ReactNode;
|
||||
path: string;
|
||||
icon?: string;
|
||||
children?: MenuConfItem[];
|
||||
visible?: boolean;
|
||||
rootVisible?: boolean;
|
||||
to?: string;
|
||||
divider?: boolean;
|
||||
target?: string;
|
||||
getQuery?: (query: any) => any;
|
||||
}
|
||||
|
||||
export interface TreeNode {
|
||||
id: number;
|
||||
pid: number;
|
||||
name: string;
|
||||
path: string;
|
||||
type: number;
|
||||
leaf: number;
|
||||
children?: TreeNode[];
|
||||
icon_color?: string;
|
||||
icon_char?: string;
|
||||
cate?: string;
|
||||
note?: string;
|
||||
selectable?: boolean;
|
||||
}
|
||||
|
||||
export interface ResponseDat {
|
||||
list: any[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
err: string;
|
||||
dat: any | ResponseDat;
|
||||
}
|
||||
|
||||
export interface UserProfile {
|
||||
id: number;
|
||||
username: string;
|
||||
dispname: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
im: string;
|
||||
isroot: boolean;
|
||||
}
|
||||
|
||||
export interface Tenant {
|
||||
id: number;
|
||||
ident: string;
|
||||
name: string;
|
||||
note: string;
|
||||
}
|
||||
|
||||
export interface Team {
|
||||
id: number;
|
||||
ident: string;
|
||||
name: string;
|
||||
note: string;
|
||||
mgmt: number;
|
||||
}
|
||||
|
||||
export interface Role {
|
||||
id: number;
|
||||
name: string;
|
||||
note: string;
|
||||
cate: 'global' | 'local';
|
||||
operations: string[];
|
||||
}
|
||||
|
||||
export interface Order {
|
||||
id: number;
|
||||
title: string;
|
||||
levels: number;
|
||||
cc: string;
|
||||
content: string;
|
||||
scheduleStartTime: string;
|
||||
status: string;
|
||||
creator: string;
|
||||
}
|
||||
32
km-console/packages/layout-clusters-fe/src/lib/url-parser.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
interface IMap {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export default () => {
|
||||
const Url = {
|
||||
hash: {} as IMap,
|
||||
search: {} as IMap,
|
||||
} as {
|
||||
hash: IMap;
|
||||
search: IMap;
|
||||
[key: string]: IMap;
|
||||
};
|
||||
|
||||
window.location.hash
|
||||
.slice(1)
|
||||
.split('&')
|
||||
.forEach((str) => {
|
||||
const kv = str.split('=');
|
||||
Url.hash[kv[0]] = kv[1];
|
||||
});
|
||||
|
||||
window.location.search
|
||||
.slice(1)
|
||||
.split('&')
|
||||
.forEach((str) => {
|
||||
const kv = str.split('=');
|
||||
Url.search[kv[0]] = kv[1];
|
||||
});
|
||||
|
||||
return Url;
|
||||
};
|
||||
88
km-console/packages/layout-clusters-fe/src/lib/utils.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
export const getRandomStr = (length?: number) => {
|
||||
const NUM_list = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||
const LOW_LETTERS_LIST = [
|
||||
'a',
|
||||
'b',
|
||||
'c',
|
||||
'd',
|
||||
'e',
|
||||
'f',
|
||||
'g',
|
||||
'h',
|
||||
'i',
|
||||
'j',
|
||||
'k',
|
||||
'l',
|
||||
'm',
|
||||
'n',
|
||||
'o',
|
||||
'p',
|
||||
'q',
|
||||
'r',
|
||||
's',
|
||||
't',
|
||||
'u',
|
||||
'v',
|
||||
'w',
|
||||
'x',
|
||||
'y',
|
||||
'z',
|
||||
];
|
||||
const CAP_LETTERS_LIST = LOW_LETTERS_LIST.map((v) => v.toUpperCase());
|
||||
const SPECIAL_LIST = [
|
||||
'!',
|
||||
'"',
|
||||
'#',
|
||||
'$',
|
||||
'%',
|
||||
'&',
|
||||
"'",
|
||||
'(',
|
||||
')',
|
||||
'*',
|
||||
'+',
|
||||
'-',
|
||||
'.',
|
||||
'/',
|
||||
':',
|
||||
';',
|
||||
'<',
|
||||
'=',
|
||||
'>',
|
||||
'?',
|
||||
'@',
|
||||
'[',
|
||||
'\\',
|
||||
']',
|
||||
'^',
|
||||
'_',
|
||||
'`',
|
||||
'{',
|
||||
'|',
|
||||
'}',
|
||||
'~',
|
||||
];
|
||||
const ALL_LIST = [...NUM_list, ...LOW_LETTERS_LIST, ...CAP_LETTERS_LIST];
|
||||
const randomNum = (Math.random() * 128) | 0;
|
||||
const randomKeys = new Array(length ?? randomNum).fill('');
|
||||
|
||||
for (let i = 0; i < randomKeys.length; i++) {
|
||||
// ALL_LIST 随机字符
|
||||
const index = (Math.random() * ALL_LIST.length) | 0;
|
||||
randomKeys[i] = ALL_LIST[index - 1];
|
||||
}
|
||||
return randomKeys.join('');
|
||||
};
|
||||
export const timeFormat = function formatDuring(mss: number) {
|
||||
var days = Math.floor(mss / (1000 * 60 * 60 * 24));
|
||||
var hours = Math.floor((mss % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||
var minutes = Math.floor((mss % (1000 * 60 * 60)) / (1000 * 60));
|
||||
var seconds = (mss % (1000 * 60)) / 1000;
|
||||
var parts = [
|
||||
{ v: days, unit: "天" },
|
||||
{ v: hours, unit: "小时" },
|
||||
{ v: minutes, unit: "分钟" },
|
||||
{ v: seconds, unit: "秒" },
|
||||
]
|
||||
return parts.filter(o => o.v > 0).map((o: any) => `${o.v}${o.unit}`).join();
|
||||
}
|
||||
9
km-console/packages/layout-clusters-fe/src/locales/en.tsx
Executable file
@@ -0,0 +1,9 @@
|
||||
export default {
|
||||
yes: 'yes',
|
||||
no: 'no',
|
||||
login: 'Login',
|
||||
logout: 'Logout',
|
||||
register: 'Register',
|
||||
'login.title': 'Login',
|
||||
'login.ldap': 'Use LDAP',
|
||||
};
|
||||
62
km-console/packages/layout-clusters-fe/src/locales/zh.tsx
Executable file
@@ -0,0 +1,62 @@
|
||||
import { systemKey } from '../constants/menu';
|
||||
|
||||
export default {
|
||||
yes: '是',
|
||||
no: '否',
|
||||
login: '登录',
|
||||
logout: '退出登录',
|
||||
register: '注册',
|
||||
'login.title': '账户登录',
|
||||
'login.ldap': '使用LDAP账号登录',
|
||||
|
||||
'add-task': '添加任务',
|
||||
'sider.footer.hide': '收起',
|
||||
'sider.footer.expand': '展开',
|
||||
|
||||
'test.result': '测试结果',
|
||||
'add.task': '添加任务',
|
||||
'test.client.stop': 'Stop',
|
||||
'test.client.clear': 'Clear',
|
||||
'test.client.run': 'Run',
|
||||
|
||||
[`menu.${systemKey}.cluster`]: 'Cluster',
|
||||
[`menu.${systemKey}.cluster.overview`]: 'Overview',
|
||||
[`menu.${systemKey}.cluster.balance`]: 'Load Rebalance',
|
||||
|
||||
[`menu.${systemKey}.broker`]: 'Broker',
|
||||
[`menu.${systemKey}.broker.dashbord`]: 'Overview',
|
||||
[`menu.${systemKey}.broker.list`]: 'Brokers',
|
||||
[`menu.${systemKey}.broker.controller-changelog`]: 'Controller',
|
||||
|
||||
[`menu.${systemKey}.topic`]: 'Topic',
|
||||
[`menu.${systemKey}.topic.dashbord`]: 'Overview',
|
||||
[`menu.${systemKey}.topic.list`]: 'Topics',
|
||||
|
||||
[`menu.${systemKey}.produce-consume`]: 'Testing',
|
||||
[`menu.${systemKey}.produce-consume.producer`]: 'Produce',
|
||||
[`menu.${systemKey}.produce-consume.consumer`]: 'Consume',
|
||||
|
||||
[`menu.${systemKey}.security`]: 'Security',
|
||||
[`menu.${systemKey}.security.acls`]: 'ACLs',
|
||||
[`menu.${systemKey}.security.users`]: 'Users',
|
||||
|
||||
[`menu.${systemKey}.consumer-group`]: 'Consumer',
|
||||
[`menu.${systemKey}.consumer-group.operating-state`]: 'Operating State',
|
||||
[`menu.${systemKey}.consumer-group.group-list`]: 'GroupList',
|
||||
|
||||
[`menu.${systemKey}.acls`]: 'ACLs',
|
||||
|
||||
[`menu.${systemKey}.jobs`]: 'Job',
|
||||
|
||||
'access.cluster': '接入集群',
|
||||
'access.cluster.low.version.tip': '监测到当前Version较低,建议维护Zookeeper信息以便得到更好的产品体验',
|
||||
'edit.cluster': '编辑集群',
|
||||
'check.detail': '查看详情',
|
||||
'healthy.setting': '健康度设置',
|
||||
'delete.cluster.confirm.title': '确定要删除此集群吗?',
|
||||
'delete.cluster.confirm.tip': '删除集群不会删除集群内的资源,仅解除平台的纳管关系!请再次输入集群名称进行确认',
|
||||
'delete.cluster.confirm.cluster': '请再次输入集群名称进行确认',
|
||||
'btn.delete': '删除',
|
||||
'btn.cancel': '取消',
|
||||
'btn.ok': '确定',
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import { timeFormat } from '../../constants/common';
|
||||
import moment from 'moment';
|
||||
import { getSizeAndUnit } from '../../constants/common';
|
||||
|
||||
export const getControllerChangeLogListColumns = (arg?: any) => {
|
||||
const columns = [
|
||||
{
|
||||
title: 'Change Time',
|
||||
dataIndex: 'timestamp',
|
||||
key: 'timestamp',
|
||||
render: (t: number) => (t ? moment(t).format(timeFormat) : '-'),
|
||||
},
|
||||
{
|
||||
title: 'Broker ID',
|
||||
dataIndex: 'brokerId',
|
||||
key: 'brokerId',
|
||||
// eslint-disable-next-line react/display-name
|
||||
render: (t: number, r: any) => {
|
||||
return t === -1 ? (
|
||||
'-'
|
||||
) : (
|
||||
<a
|
||||
onClick={() => {
|
||||
window.location.hash = `brokerId=${t || ''}&host=${r.brokerHost || ''}`;
|
||||
}}
|
||||
>
|
||||
{t}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Broker Host',
|
||||
dataIndex: 'brokerHost',
|
||||
key: 'brokerHost',
|
||||
},
|
||||
];
|
||||
return columns;
|
||||
};
|
||||
|
||||
export const defaultPagination = {
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
position: 'bottomRight',
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['10', '20', '50', '100', '200', '500'],
|
||||
};
|
||||