This is an automated email from the ASF dual-hosted git repository.
zakwu pushed a commit to branch TUBEMQ-336
in repository https://gitbox.apache.org/repos/asf/incubator-tubemq.git
The following commit(s) were added to refs/heads/TUBEMQ-336 by this push:
new 9dfa8ad feat(webclient): rebuild web client
9dfa8ad is described below
commit 9dfa8ad2fc05be302813180c8d0b7837e48e46f1
Author: zakwu <[email protected]>
AuthorDate: Fri Nov 6 14:49:05 2020 +0800
feat(webclient): rebuild web client
---
web/.env | 2 +
web/.eslintignore | 5 +
web/.eslintrc | 28 ++
web/.gitignore | 26 ++
web/.prettierrc | 4 +
web/.stylelintrc | 3 +
web/README.md | 33 ++
web/config-overrides.js | 52 +++
web/mock/_constant.js | 3 +
web/mock/app.js | 6 +
web/package.json | 110 ++++++
web/public/favicon.ico | Bin 0 -> 1226 bytes
web/public/index.html | 43 +++
web/public/logo192.png | Bin 0 -> 33077 bytes
web/public/logo512.png | Bin 0 -> 9664 bytes
web/public/manifest.json | 24 ++
web/public/robots.txt | 3 +
web/src/components/Breadcrumb/index.less | 8 +
web/src/components/Breadcrumb/index.tsx | 47 +++
web/src/components/Layout/index.less | 31 ++
web/src/components/Layout/index.tsx | 76 ++++
web/src/components/Modalx/index.less | 17 +
web/src/components/Modalx/index.tsx | 49 +++
web/src/components/Tablex/index.less | 16 +
web/src/components/Tablex/index.tsx | 114 ++++++
web/src/components/Tablex/tableFilterHelper.ts | 32 ++
web/src/components/TitleWrap/index.less | 11 +
web/src/components/TitleWrap/index.tsx | 22 ++
web/src/components/index.tsx | 3 +
web/src/configs/index.ts | 3 +
web/src/configs/menus/index.tsx | 50 +++
web/src/constants/broker.ts | 22 ++
web/src/constants/person.ts | 4 +
web/src/constants/topic.ts | 10 +
web/src/context/globalContext.ts | 12 +
web/src/defaultSettings.js | 6 +
web/src/hooks/index.ts | 52 +++
web/src/index.tsx | 11 +
web/src/pages/Broker/commonModal.tsx | 274 +++++++++++++
web/src/pages/Broker/detail.tsx | 378 ++++++++++++++++++
web/src/pages/Broker/index.less | 9 +
web/src/pages/Broker/index.tsx | 280 ++++++++++++++
web/src/pages/Broker/query.tsx | 128 +++++++
web/src/pages/Cluster/index.less | 0
web/src/pages/Cluster/index.tsx | 143 +++++++
web/src/pages/Issue/consumeGroupDetail.tsx | 95 +++++
web/src/pages/Issue/index.less | 0
web/src/pages/Issue/index.tsx | 98 +++++
web/src/pages/NotFound/index.tsx | 5 +
web/src/pages/Topic/commonModal.tsx | 349 +++++++++++++++++
web/src/pages/Topic/detail.tsx | 510 +++++++++++++++++++++++++
web/src/pages/Topic/index.less | 9 +
web/src/pages/Topic/index.tsx | 279 ++++++++++++++
web/src/pages/Topic/query.tsx | 180 +++++++++
web/src/react-app-env.d.ts | 1 +
web/src/router.tsx | 55 +++
web/src/routes/index.tsx | 37 ++
web/src/serviceWorker.ts | 146 +++++++
web/src/setupProxy.js | 12 +
web/src/store/global.ts | 30 ++
web/src/typings/index.ts | 1 +
web/src/typings/router.ts | 14 +
web/src/utils/index.ts | 45 +++
web/tsconfig.json | 33 ++
web/tsconfig.paths.json | 8 +
65 files changed, 4057 insertions(+)
diff --git a/web/.env b/web/.env
new file mode 100644
index 0000000..936a9dc
--- /dev/null
+++ b/web/.env
@@ -0,0 +1,2 @@
+PORT=3000
+SKIP_PREFLIGHT_CHECK=true
diff --git a/web/.eslintignore b/web/.eslintignore
new file mode 100644
index 0000000..6bba8e4
--- /dev/null
+++ b/web/.eslintignore
@@ -0,0 +1,5 @@
+node_modules/**
+dist/**
+public/**
+src/serviceWorker.ts
+mock
diff --git a/web/.eslintrc b/web/.eslintrc
new file mode 100644
index 0000000..472dd8d
--- /dev/null
+++ b/web/.eslintrc
@@ -0,0 +1,28 @@
+{
+ "extends": [
+ "plugin:@typescript-eslint/recommended",
+ "react-app",
+ "plugin:prettier/recommended"
+ ],
+ "plugins": ["@typescript-eslint", "react", "prettier"],
+ "env": {
+ "browser": true,
+ "node": true,
+ "mocha": true
+ },
+ "globals": {
+ "Babel": true,
+ "React": true
+ },
+ "settings": {
+ "react": {
+ "version": "detect"
+ }
+ },
+ "rules": {
+ "@typescript-eslint/no-explicit-any": 0,
+ "@typescript-eslint/explicit-function-return-type": 0,
+ "jsx-a11y/anchor-is-valid": 0,
+ "react-hooks/exhaustive-deps": 0
+ }
+}
diff --git a/web/.gitignore b/web/.gitignore
new file mode 100644
index 0000000..448f7e5
--- /dev/null
+++ b/web/.gitignore
@@ -0,0 +1,26 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring
files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# production
+/build
+
+# misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+package-lock.json
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+yarn.lock
+.history
diff --git a/web/.prettierrc b/web/.prettierrc
new file mode 100644
index 0000000..c1a6f66
--- /dev/null
+++ b/web/.prettierrc
@@ -0,0 +1,4 @@
+{
+ "singleQuote": true,
+ "trailingComma": "es5"
+}
diff --git a/web/.stylelintrc b/web/.stylelintrc
new file mode 100644
index 0000000..2e8ff58
--- /dev/null
+++ b/web/.stylelintrc
@@ -0,0 +1,3 @@
+{
+ "extends": ["stylelint-config-standard", "stylelint-config-prettier"]
+}
diff --git a/web/README.md b/web/README.md
new file mode 100644
index 0000000..d96c9e5
--- /dev/null
+++ b/web/README.md
@@ -0,0 +1,33 @@
+# web
+This project was bootstrapped with [React
Seed](https://github.com/reactseed/reactseed).
+
+## Available Scripts
+Inside the newly created project, you can run some built-in commands:
+
+### `npm start` or `yarn start`
+
+Runs the app in development mode.
+Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
+
+The page will reload if you make edits.
+You will also see any lint errors in the console.
+
+### `npm test` or `yarn test`
+
+Launches the test runner in the interactive watch mode.
+See the section about [running
tests](https://create-react-app.dev/docs/running-tests/) for more information.
+
+### `npm run build` or `yarn build`
+
+Builds the app for production to the build folder.
+It correctly bundles React in production mode and optimizes the build for the
best performance.
+
+The build is minified and the filenames include the hashes.
+Your app is ready to be deployed!
+
+See the section about
[deployment](https://create-react-app.dev/docs/deployment/) for more
information.
+
+### `npm run analyze` or `yarn analyze`
+
+Analyzes JavaScript bundles using the source maps.
+> You need to run `npm run build` or `yarn build` before analysis.
diff --git a/web/config-overrides.js b/web/config-overrides.js
new file mode 100644
index 0000000..bb8515b
--- /dev/null
+++ b/web/config-overrides.js
@@ -0,0 +1,52 @@
+/* eslint-disable @typescript-eslint/no-var-requires */
+const path = require('path');
+const webpack = require('webpack');
+const devServer = require('@reactseed/devserver');
+const AntdDayjsWebpackPlugin = require('antd-dayjs-webpack-plugin');
+const {
+ override,
+ addWebpackAlias,
+ addLessLoader,
+ overrideDevServer,
+ addWebpackPlugin,
+ fixBabelImports,
+ addBabelPlugin,
+} = require('customize-cra');
+const nodeModulesPath = path.resolve(__dirname, 'node_modules');
+const nodeModules = pkg => path.resolve(nodeModulesPath, pkg);
+
+module.exports = {
+ webpack: override(
+ addBabelPlugin('react-hot-loader/babel'),
+ addLessLoader({
+ javascriptEnabled: true,
+ }),
+ addWebpackAlias({
+ '@': path.resolve(__dirname, 'src'),
+ }),
+ fixBabelImports('antd', {
+ libraryDirectory: 'lib',
+ style: 'css',
+ }),
+ addWebpackPlugin(
+ new AntdDayjsWebpackPlugin(),
+ new webpack.HotModuleReplacementPlugin()
+ ),
+ config => {
+ if (config.mode === 'development') {
+ config.resolve.alias['react-dom'] = path.resolve(
+ __dirname,
+ 'node_modules/@hot-loader/react-dom'
+ );
+ config.entry.unshift(nodeModules('react-hot-loader/patch'));
+ }
+ return config;
+ }
+ ),
+ devServer: overrideDevServer(devServer, config => {
+ config.inline = true;
+ // eslint-disable-next-line no-undef
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10000);
+ return config;
+ }),
+};
diff --git a/web/mock/_constant.js b/web/mock/_constant.js
new file mode 100644
index 0000000..286afb4
--- /dev/null
+++ b/web/mock/_constant.js
@@ -0,0 +1,3 @@
+module.exports = {
+ apiPrefix: '/api',
+};
diff --git a/web/mock/app.js b/web/mock/app.js
new file mode 100644
index 0000000..6a109a8
--- /dev/null
+++ b/web/mock/app.js
@@ -0,0 +1,6 @@
+const { apiPrefix } = require('./_constant');
+const packageJSON = require('../package.json');
+
+module.exports = {
+ [`GET ${apiPrefix}/app`]: packageJSON,
+};
diff --git a/web/package.json b/web/package.json
new file mode 100644
index 0000000..19ecc93
--- /dev/null
+++ b/web/package.json
@@ -0,0 +1,110 @@
+{
+ "name": "web",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@ant-design/icons": "^4.2.1",
+ "@ant-design/pro-layout": "^5.0.12",
+ "@reactseed/use-redux": "^0.0.3",
+ "@reactseed/use-request": "^0.0.2",
+ "@types/lodash": "^4.14.155",
+ "@umijs/use-request": "^1.4.3",
+ "antd": "^4.2.2",
+ "immer": "^6.0.9",
+ "lodash": "^4.17.15",
+ "react": "16.13.0",
+ "react-dom": "16.13.0",
+ "react-redux": "^7.2.0",
+ "react-router-dom": "^5.2.0",
+ "redux": "^4.0.5"
+ },
+ "scripts": {
+ "analyze": "source-map-explorer 'build/static/js/*.js'",
+ "start": "EXTEND_ESLINT=true react-app-rewired start",
+ "build": "react-app-rewired build",
+ "test": "react-app-rewired test",
+ "commit": "git cz",
+ "prettier": "prettier --write 'src/**/*.{ts,tsx}'",
+ "eslint": "eslint --fix --no-error-on-unmatched-pattern
'src/**/*.{ts,tsx}'",
+ "stylelint": "stylelint 'src/**/*.less' --syntax less --allow-empty-input"
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ },
+ "lint-staged": {
+ "*.{ts,tsx}": [
+ "npm run eslint",
+ "npm run prettier"
+ ],
+ "*.{less}": [
+ "npm run stylelint"
+ ]
+ },
+ "pre-commit": "lint-staged",
+ "husky": {
+ "hooks": {
+ "pre-commit": "lint-staged",
+ "commit-msg": "commitlint -E HUSKY_GIT_PARAMS --verbose --no-verify"
+ }
+ },
+ "commitlint": {
+ "extends": [
+ "@commitlint/config-conventional"
+ ]
+ },
+ "config": {
+ "commitizen": {
+ "path": "./node_modules/cz-conventional-changelog"
+ }
+ },
+ "devDependencies": {
+ "@commitlint/cli": "^8.3.5",
+ "@commitlint/config-conventional": "^8.3.4",
+ "@hot-loader/react-dom": "=16.13.0",
+ "@reactseed/devserver": "^0.0.2",
+ "@testing-library/jest-dom": "^4.2.4",
+ "@testing-library/react": "^9.3.2",
+ "@testing-library/user-event": "^7.1.2",
+ "@types/jest": "^25.2.1",
+ "@types/node": "^13.13.5",
+ "@types/react": "^16.9.35",
+ "@types/react-dom": "^16.9.8",
+ "@types/react-redux": "^7.1.9",
+ "@types/react-router-dom": "^5.1.5",
+ "antd-dayjs-webpack-plugin": "^1.0.0",
+ "babel-plugin-import": "^1.13.0",
+ "customize-cra": "^0.9.1",
+ "cz-conventional-changelog": "^3.2.0",
+ "eslint": "^6.8.0",
+ "eslint-config-prettier": "^6.10.0",
+ "eslint-config-react-app": "^5.2.0",
+ "eslint-plugin-prettier": "^3.1.2",
+ "eslint-plugin-react": "^7.19.0",
+ "eslint-plugin-react-hooks": "^4.0.2",
+ "http-proxy-middleware": "^1.0.1",
+ "husky": "^4.2.5",
+ "less": "^3.11.1",
+ "less-loader": "^5.0.0",
+ "lint-staged": "^10.2.2",
+ "prettier": "^1.19.1",
+ "react-app-rewire-hot-loader": "^2.0.1",
+ "react-app-rewired": "^2.1.5",
+ "react-hot-loader": "^4.12.21",
+ "react-scripts": "3.4.0",
+ "source-map-explorer": "^2.3.1",
+ "stylelint": "^13.3.3",
+ "stylelint-config-prettier": "^8.0.1",
+ "stylelint-config-standard": "^20.0.0",
+ "typescript": "~3.8.3",
+ "use-immer": "^0.4.0"
+ }
+}
diff --git a/web/public/favicon.ico b/web/public/favicon.ico
new file mode 100644
index 0000000..094e7d6
Binary files /dev/null and b/web/public/favicon.ico differ
diff --git a/web/public/index.html b/web/public/index.html
new file mode 100644
index 0000000..b833b69
--- /dev/null
+++ b/web/public/index.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8" />
+ <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <meta name="theme-color" content="#000000" />
+ <meta
+ name="description"
+ content="Web site created using create-react-app"
+ />
+ <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
+ <!--
+ manifest.json provides metadata used when your web app is installed on a
+ user's mobile device or desktop. See
https://developers.google.com/web/fundamentals/web-app-manifest/
+ -->
+ <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
+ <!--
+ Notice the use of %PUBLIC_URL% in the tags above.
+ It will be replaced with the URL of the `public` folder during the build.
+ Only files inside the `public` folder can be referenced from the HTML.
+
+ Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
+ work correctly both with client-side routing and a non-root public URL.
+ Learn how to configure a non-root public URL by running `npm run build`.
+ -->
+ <title>web</title>
+ </head>
+ <body>
+ <noscript>You need to enable JavaScript to run this app.</noscript>
+ <div id="root"></div>
+ <!--
+ This HTML file is a template.
+ If you open it directly in the browser, you will see an empty page.
+
+ You can add webfonts, meta tags, or analytics to this file.
+ The build step will place the bundled scripts into the <body> tag.
+
+ To begin the development, run `npm start` or `yarn start`.
+ To create a production bundle, use `npm run build` or `yarn build`.
+ -->
+ </body>
+</html>
diff --git a/web/public/logo192.png b/web/public/logo192.png
new file mode 100644
index 0000000..904a235
Binary files /dev/null and b/web/public/logo192.png differ
diff --git a/web/public/logo512.png b/web/public/logo512.png
new file mode 100644
index 0000000..a4e47a6
Binary files /dev/null and b/web/public/logo512.png differ
diff --git a/web/public/manifest.json b/web/public/manifest.json
new file mode 100644
index 0000000..4e45728
--- /dev/null
+++ b/web/public/manifest.json
@@ -0,0 +1,24 @@
+{
+ "short_name": "web",
+ "name": "web",
+ "icons": [{
+ "src": "favicon.ico",
+ "sizes": "64x64 32x32 24x24 16x16",
+ "type": "image/x-icon"
+ },
+ {
+ "src": "logo192.png",
+ "type": "image/png",
+ "sizes": "192x192"
+ },
+ {
+ "src": "logo512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ }
+ ],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/web/public/robots.txt b/web/public/robots.txt
new file mode 100644
index 0000000..e9e57dc
--- /dev/null
+++ b/web/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/web/src/components/Breadcrumb/index.less
b/web/src/components/Breadcrumb/index.less
new file mode 100644
index 0000000..f02c2b7
--- /dev/null
+++ b/web/src/components/Breadcrumb/index.less
@@ -0,0 +1,8 @@
+.breadcrumb-wrapper {
+ background: #fff;
+ padding: 10px 20px;
+}
+
+.ant-pro-basicLayout-content {
+ margin: 1px 0 0 0;
+}
\ No newline at end of file
diff --git a/web/src/components/Breadcrumb/index.tsx
b/web/src/components/Breadcrumb/index.tsx
new file mode 100644
index 0000000..c4113bf
--- /dev/null
+++ b/web/src/components/Breadcrumb/index.tsx
@@ -0,0 +1,47 @@
+import React from 'react';
+import { MenuDataItem } from '@ant-design/pro-layout';
+import { Link } from 'react-router-dom';
+import { useLocation } from '@/hooks';
+import './index.less';
+import { Breadcrumb } from 'antd';
+
+export interface BreadcrumbProps {
+ breadcrumbMap?: Map<string, import('@umijs/route-utils').MenuDataItem>;
+ appendParams?: string;
+}
+
+const BasicLayout: React.FC<BreadcrumbProps> = props => {
+ const location = useLocation();
+ const { breadcrumbMap, appendParams } = props;
+
+ const pathSnippets = location.pathname.split('/').filter(i => i);
+ const breadcrumbItems = pathSnippets.map((_, index) => {
+ const breadcrumbNameMap = {} as any;
+ breadcrumbMap &&
+ breadcrumbMap.forEach((t: MenuDataItem) => {
+ breadcrumbNameMap[t.key as string] = t.name;
+ });
+ const url = `/${pathSnippets.slice(0, index + 1).join('/')}`;
+ if (appendParams && index === pathSnippets.length - 1) {
+ return (
+ <Breadcrumb.Item key={url}>
+ <Link to={url}>{appendParams}</Link>
+ </Breadcrumb.Item>
+ );
+ }
+
+ return (
+ <Breadcrumb.Item key={url}>
+ <Link to={url}>{breadcrumbNameMap[url]}</Link>
+ </Breadcrumb.Item>
+ );
+ });
+
+ return (
+ <>
+ <Breadcrumb className="breadcrumb-wrapper">{breadcrumbItems}</Breadcrumb>
+ </>
+ );
+};
+
+export default BasicLayout;
diff --git a/web/src/components/Layout/index.less
b/web/src/components/Layout/index.less
new file mode 100644
index 0000000..6e0ed86
--- /dev/null
+++ b/web/src/components/Layout/index.less
@@ -0,0 +1,31 @@
+#root {
+ height: 100%;
+}
+
+.header-wrapper {
+ width: 400px;
+}
+
+.header-span {
+ float: left;
+}
+
+// global css
+.main-container {
+ background: #fff;
+ padding: 10px 20px 20px 20px;
+}
+
+.search-wrapper {
+ margin-bottom: 20px;
+}
+
+.options-wrapper {
+ a {
+ margin-right: 8px;
+ }
+}
+
+.ant-layout-sider-collapsed .ant-pro-sider-menu-logo {
+ margin-left: -15px;
+}
\ No newline at end of file
diff --git a/web/src/components/Layout/index.tsx
b/web/src/components/Layout/index.tsx
new file mode 100644
index 0000000..a4a4ddd
--- /dev/null
+++ b/web/src/components/Layout/index.tsx
@@ -0,0 +1,76 @@
+import React, { useState, useEffect, useMemo, useContext } from 'react';
+import ProBasicLayout, {
+ SettingDrawer,
+ getMenuData,
+ MenuDataItem,
+ SettingDrawerProps,
+} from '@ant-design/pro-layout';
+import { Link } from 'react-router-dom';
+import { useLocation } from '@/hooks';
+import { isDevelopEnv } from '@/utils';
+import initSetting from '@/defaultSettings';
+import { menus } from '@/configs';
+import './index.less';
+import GlobalContext from '@/context/globalContext';
+
+const BasicLayout: React.FC = props => {
+ const { cluster, setBreadMap } = useContext(GlobalContext);
+ const location = useLocation();
+ const [settings, setSetting] = useState<SettingDrawerProps['settings']>(
+ initSetting as SettingDrawerProps['settings']
+ );
+ const [openKeys, setOpenKeys] = useState<string[]>([]);
+ const [selectedKeys, setSelectedKeys] = useState<string[]>(['/']);
+ const isDev = isDevelopEnv();
+ const { pathname } = location;
+ const { breadcrumbMap, menuData } = useMemo(() => getMenuData(menus), []);
+ // set breadmap 4 children page 2 use
+ setBreadMap && setBreadMap(breadcrumbMap);
+ useEffect(() => {
+ const select = breadcrumbMap.get(pathname);
+ if (select) {
+ setOpenKeys((select as MenuDataItem)['pro_layout_parentKeys']);
+ setSelectedKeys([(select as MenuDataItem)['key'] as string]);
+ }
+ }, [breadcrumbMap, pathname]);
+
+ return (
+ <>
+ <ProBasicLayout
+ title="TubeMQ"
+ logo="/logo192.png"
+ menuDataRender={() => menuData}
+ menuItemRender={(menuItemProps, defaultDom) => {
+ if (menuItemProps.isUrl || !menuItemProps.path) {
+ return defaultDom;
+ }
+ return <Link to={menuItemProps.path}>{defaultDom}</Link>;
+ }}
+ headerRender={(menuItemProps, defaultDom) => (
+ <div className="header-wrapper">
+ <span className="header-span">{defaultDom}</span>
+ <span>{cluster}</span>
+ </div>
+ )}
+ menuProps={{
+ selectedKeys,
+ openKeys,
+ onOpenChange: setOpenKeys,
+ }}
+ {...settings}
+ >
+ {props.children}
+ </ProBasicLayout>
+
+ {isDev && (
+ <SettingDrawer
+ getContainer={() => document.getElementById('root')}
+ settings={settings}
+ onSettingChange={setSetting}
+ />
+ )}
+ </>
+ );
+};
+
+export default BasicLayout;
diff --git a/web/src/components/Modalx/index.less
b/web/src/components/Modalx/index.less
new file mode 100644
index 0000000..6b03518
--- /dev/null
+++ b/web/src/components/Modalx/index.less
@@ -0,0 +1,17 @@
+.psw-set {
+ border-top: 1px solid #ccc;
+ padding-top: 20px;
+
+ .pws-label {
+ float: left;
+ line-height: 32px;
+ }
+
+ .psw-input {
+ width: 300px;
+ }
+}
+
+.enhance {
+ color: red;
+}
\ No newline at end of file
diff --git a/web/src/components/Modalx/index.tsx
b/web/src/components/Modalx/index.tsx
new file mode 100644
index 0000000..6d42e82
--- /dev/null
+++ b/web/src/components/Modalx/index.tsx
@@ -0,0 +1,49 @@
+/**
+ * TABLE COMPONENT WITH SEARCH
+ */
+import { Modal, Input } from 'antd';
+import * as React from 'react';
+import { ModalProps } from 'antd/lib/modal';
+import { ReactElement } from 'react';
+import './index.less';
+
+const { useState } = React;
+
+export interface OKProps {
+ e: React.MouseEvent<HTMLElement>;
+ psw: string;
+ params?: any;
+}
+
+type ComProps = {
+ context?: number;
+ children?: ReactElement;
+ onOk?: (p: OKProps) => {};
+ params?: any;
+};
+
+const Comp = (props: ComProps & Omit<ModalProps, 'onOk'>) => {
+ const { params } = props;
+ const [psw, setPsw] = useState('');
+ const onOk = (e: React.MouseEvent<HTMLElement>) => {
+ props.onOk && props.onOk({ e, psw, params });
+ };
+
+ return (
+ <>
+ <Modal {...props} className="textWrap" width="60%" onOk={onOk}>
+ {props.children}
+ <div className="psw-set">
+ <label className="pws-label">机器授权:</label>
+ <Input
+ className="psw-input"
+ placeholder="请输入机器授权字段,验证操作权限"
+ onChange={e => setPsw(e.target.value)}
+ />
+ </div>
+ </Modal>
+ </>
+ );
+};
+
+export default Comp;
diff --git a/web/src/components/Tablex/index.less
b/web/src/components/Tablex/index.less
new file mode 100644
index 0000000..f49a245
--- /dev/null
+++ b/web/src/components/Tablex/index.less
@@ -0,0 +1,16 @@
+.textWrap {
+ white-space: pre-line;
+ word-break: break-word;
+}
+.pb10{
+ padding-bottom: 10px;
+}
+.pd10{
+ padding: 10px;
+}
+.mt10{
+ margin-top: 10px;
+}
+.mb10{
+ margin-bottom: 10px;
+}
diff --git a/web/src/components/Tablex/index.tsx
b/web/src/components/Tablex/index.tsx
new file mode 100644
index 0000000..ec41277
--- /dev/null
+++ b/web/src/components/Tablex/index.tsx
@@ -0,0 +1,114 @@
+/**
+ * TABLE COMPONENT WITH SEARCH
+ */
+import { Table, Input, Row, Button, Col, Tooltip } from 'antd';
+import * as React from 'react';
+import { TableProps } from 'antd/lib/table';
+import { CaretDownFilled, CaretUpFilled } from '@ant-design/icons';
+import { isEmptyParam } from '@/utils';
+import { useEffect } from 'react';
+import './index.less';
+
+const { useState } = React;
+
+const { Search } = Input;
+
+interface ComProps extends TableProps<any> {
+ filterFnX?: (value: any) => void;
+ columns?: any;
+ dataSourceX?: any;
+ searchPlaceholder?: string;
+ defaultSearchKey?: string;
+ isTruePagination?: boolean;
+ showSearch?: boolean;
+ searchWidth?: number;
+ searchStyle?: any;
+}
+
+const Comp = (props: ComProps) => {
+ const {
+ columns,
+ filterFnX,
+ searchPlaceholder,
+ expandable,
+ defaultSearchKey,
+ isTruePagination,
+ showSearch = true,
+ searchWidth = 8,
+ searchStyle = {},
+ } = props;
+ const [filterKey, setFilterKey] = useState(defaultSearchKey);
+ // 自动增加排序
+ if (columns) {
+ columns.forEach((t: any) => {
+ t.sorter = (a: any, b: any) =>
+ a[(t as any).dataIndex] - b[(t as any).dataIndex] >= 0 ? 1 : -1;
+ });
+ }
+ if (expandable && !expandable.expandIcon) {
+ expandable.expandIcon = ({ expanded, onExpand, record }) =>
+ expanded ? (
+ <CaretUpFilled onClick={e => onExpand(record, e)} />
+ ) : (
+ <CaretDownFilled onClick={e => onExpand(record, e)} />
+ );
+ }
+ const opts = { ...props };
+ const dataSource =
+ (!filterKey && isEmptyParam(opts.dataSourceX)) || isTruePagination
+ ? opts.dataSource
+ : opts.dataSourceX;
+ // 如有默认,先处理一次
+ useEffect(() => {
+ if (defaultSearchKey === undefined) return;
+
+ setFilterKey(defaultSearchKey || '');
+ filterFnX && filterFnX(defaultSearchKey || '');
+ }, [defaultSearchKey, filterFnX]);
+ // 分页如果只有一页,自动隐藏
+ opts.pagination = Object.assign(
+ {
+ hideOnSinglePage: true,
+ },
+ {
+ ...opts.pagination,
+ }
+ );
+
+ const onChange = (e: any) => {
+ if (!isTruePagination) {
+ filterFnX && filterFnX(e.target.value);
+ }
+
+ setFilterKey(e.target.value);
+ };
+
+ return (
+ <>
+ {showSearch && filterFnX && (
+ <Row gutter={20} className="mb10" style={{ position: 'relative' }}>
+ <Col span={searchWidth} style={{ padding: 0, ...searchStyle }}>
+ <Tooltip title={filterKey}>
+ <Search
+ value={filterKey}
+ onChange={onChange}
+ onSearch={v => filterFnX(v)}
+ allowClear
+ placeholder={searchPlaceholder || '字符串大小写敏感'}
+ enterButton={
+ <Button type="primary" onClick={filterFnX}>
+ 搜索
+ </Button>
+ }
+ />
+ </Tooltip>
+ </Col>
+ </Row>
+ )}
+
+ <Table {...opts} dataSource={dataSource} className="textWrap" />
+ </>
+ );
+};
+
+export default Comp;
diff --git a/web/src/components/Tablex/tableFilterHelper.ts
b/web/src/components/Tablex/tableFilterHelper.ts
new file mode 100644
index 0000000..0032e8f
--- /dev/null
+++ b/web/src/components/Tablex/tableFilterHelper.ts
@@ -0,0 +1,32 @@
+interface TableFilterHelperProp {
+ key: string;
+ targetArray: Array<any>;
+ srcArray: Array<any>;
+ filterList: Array<any>;
+ updateFunction?: (p: Array<any>) => void;
+}
+const tableFilterHelper = (p: TableFilterHelperProp): any[] => {
+ const { key, srcArray = [], filterList, updateFunction } = p;
+ const res: any[] = [];
+
+ if (key) {
+ srcArray.forEach(it => {
+ const tar = filterList.map(t => {
+ return it[t];
+ });
+ let isFilterRight = false;
+ tar.forEach(t => {
+ if ((t + '').indexOf(key) > -1) isFilterRight = true;
+ });
+ if (isFilterRight) {
+ res.push(it);
+ }
+ });
+ }
+
+ if (updateFunction) updateFunction(res);
+
+ return res;
+};
+
+export default tableFilterHelper;
diff --git a/web/src/components/TitleWrap/index.less
b/web/src/components/TitleWrap/index.less
new file mode 100644
index 0000000..b64cf1b
--- /dev/null
+++ b/web/src/components/TitleWrap/index.less
@@ -0,0 +1,11 @@
+.title-wrap-title {
+ font-size: 16px;
+ font-weight: bold;
+ margin-top: 15px;
+ margin-bottom: 15px;
+ position: relative;
+}
+
+.split-border {
+ border-top: 1px solid #eee;
+}
diff --git a/web/src/components/TitleWrap/index.tsx
b/web/src/components/TitleWrap/index.tsx
new file mode 100644
index 0000000..47624f4
--- /dev/null
+++ b/web/src/components/TitleWrap/index.tsx
@@ -0,0 +1,22 @@
+import * as React from 'react';
+import './index.less';
+
+interface ComProps {
+ title: any;
+ children?: any;
+ wrapperStyle?: any;
+ hasSplit?: boolean;
+}
+
+const Comp = (props: ComProps) => {
+ const { hasSplit = true } = props;
+
+ return (
+ <div style={props.wrapperStyle} className={hasSplit ? 'split-border' : ''}>
+ <div className="title-wrap-title">{props.title}</div>
+ {props.children}
+ </div>
+ );
+};
+
+export default Comp;
diff --git a/web/src/components/index.tsx b/web/src/components/index.tsx
new file mode 100644
index 0000000..d7314f6
--- /dev/null
+++ b/web/src/components/index.tsx
@@ -0,0 +1,3 @@
+import Layout from './Layout';
+
+export { Layout };
diff --git a/web/src/configs/index.ts b/web/src/configs/index.ts
new file mode 100644
index 0000000..1cb36a6
--- /dev/null
+++ b/web/src/configs/index.ts
@@ -0,0 +1,3 @@
+import menus from './menus';
+
+export { menus };
diff --git a/web/src/configs/menus/index.tsx b/web/src/configs/menus/index.tsx
new file mode 100644
index 0000000..e689571
--- /dev/null
+++ b/web/src/configs/menus/index.tsx
@@ -0,0 +1,50 @@
+import React from 'react';
+import { Route } from '@/typings/router';
+import {
+ NodeExpandOutlined,
+ ClusterOutlined,
+ SettingOutlined,
+} from '@ant-design/icons';
+/*
+ * Note:
+ * Menu items with children need to set a key starting with "/"
+ * @see
https://github.com/umijs/route-utils/blob/master/src/transformRoute/transformRoute.ts#L219
+ */
+
+const menus: Route[] = [
+ {
+ path: '/issue',
+ name: '分发查询',
+ icon: <NodeExpandOutlined />,
+ hideChildrenInMenu: true,
+ children: [
+ {
+ path: '/:id',
+ name: '消费组详情',
+ },
+ ],
+ },
+ {
+ name: '配置管理',
+ key: '/other',
+ icon: <SettingOutlined />,
+ path: '/other',
+ children: [
+ {
+ path: '/broker',
+ name: 'Broker列表',
+ },
+ {
+ path: '/topic',
+ name: 'topic列表',
+ },
+ ],
+ },
+ {
+ path: '/cluster',
+ name: '集群管理',
+ icon: <ClusterOutlined />,
+ },
+];
+
+export default menus;
diff --git a/web/src/constants/broker.ts b/web/src/constants/broker.ts
new file mode 100644
index 0000000..bc6ac45
--- /dev/null
+++ b/web/src/constants/broker.ts
@@ -0,0 +1,22 @@
+export const BROKER_INFO_ZH_MAP = {
+ acceptPublish: '可发布',
+ acceptSubscribe: '可订阅',
+ brokerId: 'BrokerId',
+ brokerIp: 'BrokerIP',
+ brokerPort: 'BrokerPort',
+ brokerTLSPort: 'TLS端口',
+ brokerVersion: '版本',
+ enableTLS: '启用TLS',
+ isAutoForbidden: '自动屏蔽',
+ isBrokerOnline: 'broker注册',
+ isConfChanged: '配置变更',
+ isConfLoaded: '变更加载',
+ isRepAbnormal: '上报异常',
+ manageStatus: '管理状态',
+ runStatus: '运行状态',
+ subStatus: '运行子状态',
+ 'runInfo.acceptPublish': 'broker可发布状态',
+ 'runInfo.acceptSubscribe': 'broker可订阅状态',
+ 'runInfo.numPartitions': 'broker分区数',
+ 'runInfo.brokerManageStatus': 'broker运行状态',
+};
diff --git a/web/src/constants/person.ts b/web/src/constants/person.ts
new file mode 100644
index 0000000..eadf34f
--- /dev/null
+++ b/web/src/constants/person.ts
@@ -0,0 +1,4 @@
+export const PERSON_INFO_ZH_MAP = {
+ createDate: '创建时间',
+ createUser: '创建人',
+};
diff --git a/web/src/constants/topic.ts b/web/src/constants/topic.ts
new file mode 100644
index 0000000..d4c8abf
--- /dev/null
+++ b/web/src/constants/topic.ts
@@ -0,0 +1,10 @@
+export const TOPIC_INFO_ZH_MAP = {
+ topicName: 'TopicName',
+ infoCount: '配置Broker数',
+ totalCfgNumPart: '配置分区数',
+ totalRunNumPartCount: '运行分区数',
+ isSrvAcceptPublish: '可发布',
+ isSrvAcceptSubscribe: '可订阅',
+ enableAuthControl: '权限受控',
+ groupCount: '授权消费组',
+};
diff --git a/web/src/context/globalContext.ts b/web/src/context/globalContext.ts
new file mode 100644
index 0000000..0540e02
--- /dev/null
+++ b/web/src/context/globalContext.ts
@@ -0,0 +1,12 @@
+// global context
+import React from 'react';
+import { BreadcrumbProps } from '@/components/Breadcrumb';
+export interface GlobalContextProps {
+ cluster?: string;
+ setCluster?: Function;
+ breadMap?: BreadcrumbProps['breadcrumbMap'];
+ setBreadMap?: Function;
+ userInfo?: any;
+}
+
+export default React.createContext<GlobalContextProps>({});
diff --git a/web/src/defaultSettings.js b/web/src/defaultSettings.js
new file mode 100644
index 0000000..684a9a3
--- /dev/null
+++ b/web/src/defaultSettings.js
@@ -0,0 +1,6 @@
+export default {
+ layout: 'sidemenu',
+ contentWidth: 'Fluid',
+ navTheme: 'dark',
+ primaryColor: '#1890ff',
+};
diff --git a/web/src/hooks/index.ts b/web/src/hooks/index.ts
new file mode 100644
index 0000000..8340c9e
--- /dev/null
+++ b/web/src/hooks/index.ts
@@ -0,0 +1,52 @@
+import { useHistory, useLocation } from 'react-router-dom';
+import useRequest, { axios } from '@reactseed/use-request';
+import useRedux from '@reactseed/use-redux';
+import { message } from 'antd';
+
+interface DataProps {
+ data: any;
+ errorCode: number;
+ errMsg: number;
+ result: boolean;
+}
+// handler for old type interface
+axios.interceptors.request.use(
+ config => {
+ const urlArr = (config.url as any).split('/');
+ config.url = '/webapi.htm';
+ config.params = config.params || {};
+ config.params['type'] = urlArr[2];
+ config.params['method'] = urlArr[3];
+
+ return config;
+ },
+ function(error) {
+ return Promise.reject(error);
+ }
+);
+
+axios.interceptors.response.use(
+ ({ data }) => {
+ if (data.errCode !== 0) {
+ message.error(data.errMsg);
+ return Promise.reject(data);
+ }
+
+ // admin_query_master_group_info interface design no good need handle
+ if (
+ Object.keys(data).includes('groupName') &&
+ Object.keys(data).includes('groupStatus')
+ ) {
+ data.data = {
+ data: data.data,
+ groupName: data.groupName,
+ groupStatus: data.groupStatus,
+ };
+ }
+ return data || [];
+ },
+ function(error) {
+ return Promise.reject(error);
+ }
+);
+export { useHistory, useLocation, useRequest, useRedux };
diff --git a/web/src/index.tsx b/web/src/index.tsx
new file mode 100644
index 0000000..416097f
--- /dev/null
+++ b/web/src/index.tsx
@@ -0,0 +1,11 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import App from '@/router';
+import * as serviceWorker from './serviceWorker';
+
+ReactDOM.render(<App />, document.getElementById('root'));
+
+// If you want your app to work offline and load faster, you can change
+// unregister() to register() below. Note this comes with some pitfalls.
+// Learn more about service workers: https://bit.ly/CRA-PWA
+serviceWorker.unregister();
diff --git a/web/src/pages/Broker/commonModal.tsx
b/web/src/pages/Broker/commonModal.tsx
new file mode 100644
index 0000000..8dc8654
--- /dev/null
+++ b/web/src/pages/Broker/commonModal.tsx
@@ -0,0 +1,274 @@
+import { boolean2Chinese } from '@/utils';
+import Table from '@/components/Tablex';
+import { Col, Form, Input, Row } from 'antd';
+import Modal, { OKProps } from '@/components/Modalx';
+import React from 'react';
+import Query from '@/pages/Broker/query';
+import { FormProps } from 'antd/lib/form';
+
+export const OPTIONS = [
+ {
+ value: 'online',
+ name: '上线',
+ },
+ {
+ value: 'offline',
+ name: '下线',
+ },
+ {
+ value: 'reload',
+ name: '重载',
+ },
+ {
+ value: 'delete',
+ name: '删除',
+ },
+];
+export const OPTIONS_VALUES = OPTIONS.map(t => t.value);
+
+// interface
+export declare type BrokerData = any[];
+export interface BrokerResultData {
+ acceptPublish: string;
+ acceptSubscribe: string;
+ brokerId: number;
+ brokerIp: string;
+ brokerPort: number;
+ brokerTLSPort: number;
+ brokerVersion: string;
+ enableTLS: boolean;
+ isAutoForbidden: boolean;
+ isBrokerOnline: string;
+ isConfChanged: string;
+ isConfLoaded: string;
+ isRepAbnormal: boolean;
+ manageStatus: string;
+ runStatus: string;
+ subStatus: string;
+ [key: string]: any;
+}
+export interface BrokerModalProps {
+ type: string;
+ title: string;
+ updateFunction: (draft: any) => any;
+ params?: any;
+}
+// exports broker modal
+// render funcs
+const renderBrokerOptions = (modalParams: any, dataSource: any[]) => {
+ const columns = [
+ {
+ title: 'Broker',
+ render: (t: string, r: BrokerResultData) => {
+ return `${r.brokerId}#${r.brokerIp}:${r.brokerPort}`;
+ },
+ },
+ {
+ title: 'BrokerIP',
+ dataIndex: 'brokerIp',
+ },
+ {
+ title: '管理状态',
+ dataIndex: 'manageStatus',
+ },
+ {
+ title: '运行状态',
+ dataIndex: 'runStatus',
+ },
+ {
+ title: '运行子状态',
+ dataIndex: 'subStatus',
+ },
+ {
+ title: '可发布',
+ render: (t: string) => boolean2Chinese(t),
+ },
+ {
+ title: '可订阅',
+ render: (t: string) => boolean2Chinese(t),
+ },
+ ];
+ return <Table columns={columns} dataSource={dataSource} rowKey="brokerId" />;
+};
+const renderNewBroker = (form: any) => {
+ const brokerFormArr = [
+ {
+ name: 'brokerId',
+ defaultValue: '0',
+ },
+ {
+ name: 'numPartitions',
+ defaultValue: '3',
+ },
+ {
+ name: 'brokerIp',
+ defaultValue: '',
+ },
+ {
+ name: 'brokerPort',
+ defaultValue: '8123',
+ },
+ {
+ name: 'deleteWhen',
+ defaultValue: '0 0 6,18 * * ?',
+ },
+ {
+ name: 'deletePolicy',
+ defaultValue: 'delete,168h',
+ },
+ {
+ name: 'unflushThreshold',
+ defaultValue: '1000',
+ },
+ {
+ name: 'unflushInterval',
+ defaultValue: '10000',
+ },
+ {
+ name: 'acceptPublish',
+ defaultValue: 'true',
+ },
+ {
+ name: 'acceptSubscribe',
+ defaultValue: 'true',
+ },
+ ];
+
+ return (
+ <Form form={form}>
+ <Row gutter={24}>
+ {brokerFormArr.map((t, index) => (
+ <Col span={12} key={'brokerFormArr' + index}>
+ <Form.Item
+ labelCol={{ span: 12 }}
+ label={t.name}
+ name={t.name}
+ initialValue={t.defaultValue}
+ >
+ <Input />
+ </Form.Item>
+ </Col>
+ ))}
+ </Row>
+ </Form>
+ );
+};
+const renderEditBroker = (modalParams: any, form: FormProps['form']) => {
+ const { params: p } = modalParams;
+ const pickArr = [
+ 'numPartitions',
+ 'unflushThreshold',
+ 'unflushInterval',
+ 'deleteWhen',
+ 'deletePolicy',
+ 'acceptPublish',
+ 'acceptSubscribe',
+ ];
+ const brokerFormArr: Array<{
+ name: string;
+ defaultValue: string;
+ }> = [];
+ pickArr.forEach(t => {
+ brokerFormArr.push({
+ name: t,
+ defaultValue: p[t],
+ });
+ });
+
+ return (
+ <Form form={form}>
+ <Row gutter={24}>
+ {brokerFormArr.map((t, index) => (
+ <Col span={12} key={'brokerFormArr' + index}>
+ <Form.Item
+ labelCol={{ span: 12 }}
+ label={t.name}
+ name={t.name}
+ initialValue={t.defaultValue}
+ >
+ <Input />
+ </Form.Item>
+ </Col>
+ ))}
+ </Row>
+ </Form>
+ );
+};
+const renderBrokerStateChange = (modalParams: any) => {
+ const { params } = modalParams;
+
+ return (
+ <div>
+ 请确认<span className="enhance">{params.option}</span> ID:{' '}
+ <span className="enhance">{params.id}</span> 的 Broker?
+ </div>
+ );
+};
+export const onOpenModal = (p: BrokerModalProps) => {
+ const { type, title, updateFunction, params } = p;
+ if (typeof params === 'function') {
+ p.params = params();
+ }
+ updateFunction((m: any) => {
+ m.type = type;
+ m.params = params;
+ Object.assign(m, {
+ params,
+ visible: type,
+ title,
+ onOk: (p: OKProps) => {
+ updateFunction((m: any) => {
+ if (type === 'newBroker' || type === 'editBroker') {
+ p.params = f && f.getFieldsValue();
+ }
+ m.okParams = p;
+ m.isOk = Date.now();
+ });
+ },
+ onCancel: () => {
+ updateFunction((m: any) => {
+ m.visible = false;
+ m.isOk = null;
+ });
+ },
+ });
+ });
+};
+
+interface ComProps {
+ modalParams: any;
+ data: any[];
+}
+let f: FormProps['form'];
+const Comp = (props: ComProps) => {
+ const { modalParams, data } = props;
+ const [form] = Form.useForm();
+ f = form;
+
+ return (
+ <Modal {...modalParams}>
+ <div>
+ {modalParams.type &&
+ OPTIONS_VALUES.includes(modalParams.type) &&
+ renderBrokerOptions(
+ modalParams,
+ data.filter((t: BrokerResultData) =>
+ modalParams.params.includes(t.brokerId)
+ )
+ )}
+ {modalParams.type === 'newBroker' && renderNewBroker(form)}
+ {modalParams.type === 'editBroker' &&
+ renderEditBroker(modalParams, form)}
+ {modalParams.type === 'brokerStateChange' &&
+ renderBrokerStateChange(modalParams)}
+ </div>
+ <Query
+ fire={modalParams.isOk}
+ params={modalParams.okParams}
+ type={modalParams.query || modalParams.type}
+ />
+ </Modal>
+ );
+};
+
+export default Comp;
diff --git a/web/src/pages/Broker/detail.tsx b/web/src/pages/Broker/detail.tsx
new file mode 100644
index 0000000..28a7d06
--- /dev/null
+++ b/web/src/pages/Broker/detail.tsx
@@ -0,0 +1,378 @@
+import React, { ReactNode, useContext, useState } from 'react';
+import GlobalContext from '@/context/globalContext';
+import Breadcrumb from '@/components/Breadcrumb';
+import Table from '@/components/Tablex';
+import TitleWrap from '@/components/TitleWrap';
+import { Form, Button, Spin, Col, Row, Switch, Tabs } from 'antd';
+import { useImmer } from 'use-immer';
+import './index.less';
+import { useRequest } from '@/hooks';
+import { useParams } from 'react-router-dom';
+import { boolean2Chinese, transParamsWithConstantsMap } from '@/utils';
+import { BROKER_INFO_ZH_MAP } from '@/constants/broker';
+import tableFilterHelper from '@/components/Tablex/tableFilterHelper';
+import CommonModal, { OPTIONS, onOpenModal, BrokerData } from './commonModal';
+
+declare type BrokerQueryData = {
+ withDetail: boolean;
+ brokerId: string;
+};
+
+declare type TopicQueryData = {
+ withTopic: boolean;
+ brokerId: string;
+};
+
+const { TabPane } = Tabs;
+
+const Detail: React.FC = () => {
+ const { id } = useParams();
+ const { breadMap } = useContext(GlobalContext);
+ const [form] = Form.useForm();
+ const [modalParams, updateModelParams] = useImmer<any>({});
+ const [acceptPublish, setAcceptPublish] = useState<any>(false);
+ const [acceptSubscribe, setAcceptSubscribe] = useState<any>(false);
+ const [filterData, updateFilterData] = useImmer<any>({});
+ const queryBrokerConf = useRequest<any>(
+ (
+ data: BrokerQueryData = {
+ withDetail: true,
+ brokerId: id,
+ }
+ ) => ({
+ url: '/api/op_query/admin_query_broker_run_status',
+ data: {
+ ...data,
+ },
+ }),
+ {
+ onSuccess: data => {
+ setAcceptPublish(data[0]['acceptPublish'] === 'true');
+ setAcceptSubscribe(data[0]['acceptSubscribe'] === 'true');
+ },
+ }
+ );
+ const queryTopicInfo = useRequest<any>(
+ (
+ data: TopicQueryData = {
+ withTopic: true,
+ brokerId: id,
+ }
+ ) => ({
+ url: '/api/op_query/admin_query_broker_configure',
+ data: {
+ ...data,
+ },
+ })
+ );
+
+ // render
+ const renderConf = () => {
+ const columns = [
+ {
+ title: '类别',
+ dataIndex: `type`,
+ },
+ {
+ title: transParamsWithConstantsMap(BROKER_INFO_ZH_MAP,
'acceptPublish'),
+ dataIndex: 'acceptPublish',
+ render: (t: string) => boolean2Chinese(t),
+ },
+ {
+ title: transParamsWithConstantsMap(
+ BROKER_INFO_ZH_MAP,
+ 'acceptSubscribe'
+ ),
+ dataIndex: 'acceptSubscribe',
+ render: (t: string) => boolean2Chinese(t),
+ },
+ {
+ title: transParamsWithConstantsMap(
+ BROKER_INFO_ZH_MAP,
+ 'unflushThreshold'
+ ),
+ dataIndex: 'unflushThreshold',
+ },
+ {
+ title: transParamsWithConstantsMap(
+ BROKER_INFO_ZH_MAP,
+ 'unflushInterval'
+ ),
+ dataIndex: 'unflushInterval',
+ },
+ {
+ title: transParamsWithConstantsMap(BROKER_INFO_ZH_MAP, 'deleteWhen'),
+ dataIndex: 'deleteWhen',
+ },
+ {
+ title: transParamsWithConstantsMap(BROKER_INFO_ZH_MAP, 'deletePolicy'),
+ dataIndex: 'deletePolicy',
+ },
+ {
+ title: transParamsWithConstantsMap(BROKER_INFO_ZH_MAP,
'numPartitions'),
+ dataIndex: 'numPartitions',
+ },
+ {
+ title: '操作',
+ render: (t: string, r: BrokerData) => {
+ return <a onClick={() => onEditConf(r)}>编辑</a>;
+ },
+ },
+ ];
+ const { data } = queryBrokerConf;
+ if (!data || !data[0]) return null;
+ const { BrokerSyncStatusInfo } = data[0];
+ const dataSource = [];
+ dataSource.push({
+ type: '缺省配置',
+ ...BrokerSyncStatusInfo.curBrokerDefaultConfInfo,
+ });
+ dataSource.push({
+ type: '最近上报',
+ ...BrokerSyncStatusInfo.reportedBrokerDefaultConfInfo,
+ });
+ dataSource.push({
+ type: '最近下发',
+ ...BrokerSyncStatusInfo.lastPushBrokerDefaultConfInfo,
+ });
+
+ return <Table columns={columns} dataSource={dataSource} rowKey="type" />;
+ };
+ const renderTopics = (type: string): ReactNode => {
+ const columns = [
+ {
+ title: 'topicName',
+ dataIndex: `topicName`,
+ },
+ {
+ title: transParamsWithConstantsMap(BROKER_INFO_ZH_MAP,
'numPartitions'),
+ dataIndex: 'numPartitions',
+ },
+ {
+ title: transParamsWithConstantsMap(BROKER_INFO_ZH_MAP,
'acceptPublish'),
+ dataIndex: 'acceptPublish',
+ render: (t: string) => boolean2Chinese(t),
+ },
+ {
+ title: transParamsWithConstantsMap(
+ BROKER_INFO_ZH_MAP,
+ 'acceptSubscribe'
+ ),
+ dataIndex: 'acceptSubscribe',
+ render: (t: string) => boolean2Chinese(t),
+ },
+ {
+ title: transParamsWithConstantsMap(
+ BROKER_INFO_ZH_MAP,
+ 'unflushThreshold'
+ ),
+ dataIndex: 'unflushThreshold',
+ },
+ {
+ title: transParamsWithConstantsMap(
+ BROKER_INFO_ZH_MAP,
+ 'unflushInterval'
+ ),
+ dataIndex: 'unflushInterval',
+ },
+ {
+ title: transParamsWithConstantsMap(BROKER_INFO_ZH_MAP, 'deleteWhen'),
+ dataIndex: 'deleteWhen',
+ },
+ {
+ title: transParamsWithConstantsMap(BROKER_INFO_ZH_MAP, 'deletePolicy'),
+ dataIndex: 'deletePolicy',
+ },
+ ];
+ const { data } = queryBrokerConf;
+ if (!data || !data[0]) return null;
+ const { BrokerSyncStatusInfo } = data[0];
+ let dataSource: any[] = [];
+ if (type === 'cur') {
+ dataSource = BrokerSyncStatusInfo.curBrokerTopicSetConfInfo;
+ } else if (type === 'lastPush') {
+ dataSource = BrokerSyncStatusInfo.lastPushBrokerTopicSetConfInfo;
+ } else if (type === 'lastReported') {
+ dataSource = BrokerSyncStatusInfo.reportedBrokerTopicSetConfInfo;
+ }
+
+ return (
+ <Table
+ columns={columns}
+ dataSource={dataSource}
+ rowKey={r => type + r.topicName}
+ dataSourceX={filterData.list}
+ searchPlaceholder="请输入TopicName搜索"
+ searchStyle={{
+ position: 'absolute',
+ top: '-55px',
+ right: '10px',
+ zIndex: 1,
+ }}
+ filterFnX={value =>
+ tableFilterHelper({
+ key: value,
+ srcArray: dataSource,
+ targetArray: filterData.list,
+ updateFunction: res =>
+ updateFilterData(filterData => {
+ filterData.list = res;
+ }),
+ filterList: ['topicName'],
+ })
+ }
+ />
+ );
+ };
+
+ // event
+ // acceptPublish && acceptSubscribe event
+ const onSwitchChange = (e: boolean, type: string) => {
+ let option = '';
+ if (type === 'acceptPublish') {
+ option = e ? '发布' : '禁止可发布';
+ } else if (type === 'acceptSubscribe') {
+ option = e ? '订阅' : '禁止可订阅';
+ }
+
+ onOpenModal({
+ type: 'brokerStateChange',
+ title: `请确认操作`,
+ updateFunction: updateModelParams,
+ params: {
+ option,
+ id: queryBrokerConf.data[0].brokerId,
+ callback: () => {
+ if (type === 'acceptPublish') {
+ setAcceptPublish(e);
+ } else if (type === 'acceptSubscribe') {
+ setAcceptSubscribe(e);
+ }
+ },
+ },
+ });
+ };
+
+ const onOptions = (type: string) => {
+ onOpenModal({
+ type,
+ title: `确认进行【${OPTIONS.find(t => t.value === type)?.name}】操作?`,
+ updateFunction: updateModelParams,
+ params: [queryBrokerConf.data[0].brokerId],
+ });
+ };
+
+ // new broker
+ const onEditConf = (r: BrokerData) => {
+ onOpenModal({
+ type: 'editBroker',
+ title: '编辑Broker',
+ updateFunction: updateModelParams,
+ params: r,
+ });
+ };
+
+ return (
+ <Spin spinning={queryBrokerConf.loading && queryTopicInfo.loading}>
+ <Breadcrumb
+ breadcrumbMap={breadMap}
+ appendParams={`Broker(${id})详情`}
+ />
+ <div className="main-container">
+ <TitleWrap title="运行状态" wrapperStyle={{ position: 'relative' }}>
+ <div className="broker-detail-options-wrapper">
+ <Switch
+ className="mr10"
+ checked={acceptPublish}
+ checkedChildren="订阅"
+ unCheckedChildren="订阅"
+ onChange={e => onSwitchChange(e, 'acceptPublish')}
+ />
+ <Switch
+ className="mr10"
+ checked={acceptSubscribe}
+ checkedChildren="发布"
+ unCheckedChildren="发布"
+ onChange={e => onSwitchChange(e, 'acceptSubscribe')}
+ />
+ <Button
+ className="mr10"
+ type="primary"
+ size="small"
+ onClick={() => onOptions('online')}
+ >
+ 上线
+ </Button>
+ <Button
+ className="mr10"
+ type="primary"
+ size="small"
+ onClick={() => onOptions('offline')}
+ >
+ 下线
+ </Button>
+ <Button
+ className="mr10"
+ type="primary"
+ size="small"
+ onClick={() => onOptions('reload')}
+ >
+ 重载
+ </Button>
+ </div>
+ <Form form={form}>
+ <Row gutter={24}>
+ {queryBrokerConf.data &&
+ Object.keys(queryBrokerConf.data[0]).map(
+ (t: string, index: number) => {
+ const label = transParamsWithConstantsMap(
+ BROKER_INFO_ZH_MAP,
+ t
+ );
+ const ignoreList = [
+ 'acceptPublish',
+ 'brokerVersion',
+ 'acceptSubscribe',
+ ];
+ if (
+ queryBrokerConf.data[0][t] instanceof Object ||
+ !label ||
+ ignoreList.includes(t)
+ )
+ return null;
+ return (
+ <Col span={12} key={'queryBrokerConf' + index}>
+ <Form.Item labelCol={{ span: 12 }} label={label}>
+ {queryBrokerConf.data[0][t] + ''}
+ </Form.Item>
+ </Col>
+ );
+ }
+ )}
+ </Row>
+ </Form>
+ </TitleWrap>
+ <TitleWrap title="缺省配置">{renderConf()}</TitleWrap>
+ <TitleWrap title="Topic集合配置">
+ <Tabs>
+ <TabPane tab="当前配置" key="cur">
+ {renderTopics('cur')}
+ </TabPane>
+ <TabPane tab="最后下发" key="lastPush">
+ {renderTopics('lastPush')}
+ </TabPane>
+ <TabPane tab="最后上报" key="lastReported">
+ {renderTopics('lastReported')}
+ </TabPane>
+ </Tabs>
+ </TitleWrap>
+ </div>
+ <CommonModal
+ modalParams={modalParams}
+ data={[queryBrokerConf.data && queryBrokerConf.data[0]]}
+ />
+ </Spin>
+ );
+};
+
+export default Detail;
diff --git a/web/src/pages/Broker/index.less b/web/src/pages/Broker/index.less
new file mode 100644
index 0000000..c95e98d
--- /dev/null
+++ b/web/src/pages/Broker/index.less
@@ -0,0 +1,9 @@
+.broker-detail-options-wrapper {
+ position: absolute;
+ top: 15px;
+ right: 0;
+
+ .mr10 {
+ margin-right: 10px;
+ }
+}
\ No newline at end of file
diff --git a/web/src/pages/Broker/index.tsx b/web/src/pages/Broker/index.tsx
new file mode 100644
index 0000000..7cc91dc
--- /dev/null
+++ b/web/src/pages/Broker/index.tsx
@@ -0,0 +1,280 @@
+import React, { useContext, useState } from 'react';
+import GlobalContext from '@/context/globalContext';
+import Breadcrumb from '@/components/Breadcrumb';
+import Table from '@/components/Tablex';
+import { Form, Select, Button, Spin, Switch, message } from 'antd';
+import { useImmer } from 'use-immer';
+import { useRequest } from '@/hooks';
+import tableFilterHelper from '@/components/Tablex/tableFilterHelper';
+import { boolean2Chinese, transParamsWithConstantsMap } from '@/utils';
+import { BROKER_INFO_ZH_MAP } from '@/constants/broker';
+import './index.less';
+import { Link } from 'react-router-dom';
+import CommonModal, {
+ OPTIONS,
+ onOpenModal,
+ BrokerResultData,
+ BrokerData,
+} from './commonModal';
+
+const { Option } = Select;
+const Broker: React.FC = () => {
+ // column config
+ const columns = [
+ {
+ title: transParamsWithConstantsMap(BROKER_INFO_ZH_MAP, 'brokerId'),
+ dataIndex: 'brokerId',
+ fixed: 'left',
+ render: (t: Array<any>) => <Link to={'/broker/' + t}>{t}</Link>,
+ },
+ {
+ title: transParamsWithConstantsMap(BROKER_INFO_ZH_MAP, 'brokerIp'),
+ dataIndex: 'brokerIp',
+ },
+ {
+ title: transParamsWithConstantsMap(BROKER_INFO_ZH_MAP, 'brokerPort'),
+ dataIndex: 'brokerPort',
+ },
+ {
+ title: transParamsWithConstantsMap(BROKER_INFO_ZH_MAP, 'manageStatus'),
+ dataIndex: 'manageStatus',
+ },
+ {
+ title: transParamsWithConstantsMap(BROKER_INFO_ZH_MAP, 'runStatus'),
+ dataIndex: 'runStatus',
+ },
+ {
+ title: transParamsWithConstantsMap(BROKER_INFO_ZH_MAP, 'subStatus'),
+ dataIndex: 'subStatus',
+ },
+ {
+ title: transParamsWithConstantsMap(BROKER_INFO_ZH_MAP, 'acceptPublish'),
+ dataIndex: 'acceptPublish',
+ render: (t: string, r: BrokerResultData) => {
+ return (
+ <Switch
+ checked={t === 'true'}
+ onChange={e => onSwitchChange(e, r, 'acceptPublish')}
+ />
+ );
+ },
+ },
+ {
+ title: transParamsWithConstantsMap(BROKER_INFO_ZH_MAP,
'acceptSubscribe'),
+ dataIndex: 'acceptSubscribe',
+ render: (t: string, r: BrokerResultData) => {
+ return (
+ <Switch
+ checked={t === 'true'}
+ onChange={e => onSwitchChange(e, r, 'acceptSubscribe')}
+ />
+ );
+ },
+ },
+ {
+ title: transParamsWithConstantsMap(BROKER_INFO_ZH_MAP, 'isConfChanged'),
+ dataIndex: 'isConfChanged',
+ render: (t: string) => boolean2Chinese(t),
+ },
+ {
+ title: transParamsWithConstantsMap(BROKER_INFO_ZH_MAP, 'isConfLoaded'),
+ dataIndex: 'isConfLoaded',
+ render: (t: string) => boolean2Chinese(t),
+ },
+ {
+ title: transParamsWithConstantsMap(BROKER_INFO_ZH_MAP, 'isBrokerOnline'),
+ dataIndex: 'isBrokerOnline',
+ render: (t: string) => boolean2Chinese(t),
+ },
+ {
+ title: transParamsWithConstantsMap(BROKER_INFO_ZH_MAP, 'acceptPublish'),
+ dataIndex: 'isBrokerOnline',
+ render: (t: string) => boolean2Chinese(t),
+ },
+ {
+ title: transParamsWithConstantsMap(BROKER_INFO_ZH_MAP, 'brokerTLSPort'),
+ dataIndex: 'brokerTLSPort',
+ render: (t: string) => boolean2Chinese(t),
+ },
+ {
+ title: transParamsWithConstantsMap(BROKER_INFO_ZH_MAP, 'enableTLS'),
+ dataIndex: 'enableTLS',
+ render: (t: boolean) => boolean2Chinese(t),
+ },
+ {
+ title: transParamsWithConstantsMap(BROKER_INFO_ZH_MAP, 'isRepAbnormal'),
+ dataIndex: 'isRepAbnormal',
+ render: (t: boolean) => boolean2Chinese(t),
+ },
+ {
+ title: transParamsWithConstantsMap(BROKER_INFO_ZH_MAP,
'isAutoForbidden'),
+ dataIndex: 'isAutoForbidden',
+ render: (t: boolean) => boolean2Chinese(t),
+ },
+ {
+ title: '操作',
+ dataIndex: 'brokerIp',
+ fixed: 'right',
+ width: 180,
+ render: (t: string, r: any) => {
+ return (
+ <span className="options-wrapper">
+ {OPTIONS.map(t => (
+ <a key={t.value} onClick={() => onOptionsChange(t.value, r)}>
+ {t.name}
+ </a>
+ ))}
+ </span>
+ );
+ },
+ },
+ ];
+ const { breadMap } = useContext(GlobalContext);
+ const [modalParams, updateModelParams] = useImmer<any>({});
+ const [filterData, updateFilterData] = useImmer<any>({});
+ const [selectBroker, setSelectBroker] = useState<any>([]);
+ const [brokerList, updateBrokerList] = useImmer<BrokerData>([]);
+ const [form] = Form.useForm();
+ // init query
+ const { data, loading, run } = useRequest<any, BrokerData>(
+ (data: BrokerResultData) => ({
+ url: '/api/op_query/admin_query_broker_run_status',
+ data: data,
+ }),
+ {
+ onSuccess: data => {
+ updateBrokerList(d => {
+ Object.assign(d, data);
+ });
+ },
+ }
+ );
+
+ // table event
+ // acceptSubscribe && acceptPublish options
+ const onSwitchChange = (e: boolean, r: BrokerResultData, type: string) => {
+ let option = '';
+ if (type === 'acceptPublish') {
+ option = e ? '发布' : '禁止可发布';
+ } else if (type === 'acceptSubscribe') {
+ option = e ? '订阅' : '禁止可订阅';
+ }
+
+ onOpenModal({
+ type: 'brokerStateChange',
+ title: `请确认操作`,
+ updateFunction: updateModelParams,
+ params: {
+ option,
+ id: r.brokerId,
+ type,
+ callback: () => {
+ const index = data.findIndex(
+ (t: BrokerResultData) => t.brokerId === r.brokerId
+ );
+ updateBrokerList(d => {
+ d[index][type] = e + '';
+ });
+ },
+ },
+ });
+ };
+ // new broker
+ const onNewBroker = () => {
+ onOpenModal({
+ type: 'newBroker',
+ title: '新建Broker',
+ updateFunction: updateModelParams,
+ });
+ };
+ // online, offline, etc.
+ const onOptionsChange = (type: string, r?: BrokerResultData) => {
+ if (!r && !selectBroker.length) {
+ form.resetFields();
+ return message.error('批量操作至少选择一列!');
+ }
+
+ onOpenModal({
+ type,
+ title: `确认进行【${OPTIONS.find(t => t.value === type)?.name}】操作?`,
+ updateFunction: updateModelParams,
+ params: r ? [r.brokerId] : selectBroker,
+ });
+ };
+ // table select
+ const onBrokerTableSelectChange = (p: any[]) => {
+ setSelectBroker(p);
+ };
+
+ return (
+ <Spin spinning={loading}>
+ <Breadcrumb breadcrumbMap={breadMap} />
+ <div className="main-container">
+ <div
+ className="search-wrapper"
+ style={{ float: 'right', marginRight: '-16px' }}
+ >
+ <Form form={form} layout={'inline'}>
+ <Form.Item label="批量操作" name="optionType">
+ <Select
+ style={{ width: 120 }}
+ onChange={(v: string) => onOptionsChange(v)}
+ placeholder="请选择操作"
+ >
+ {OPTIONS.map(t => (
+ <Option value={t.value} key={t.value}>
+ {t.name}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ <Form.Item>
+ <Button
+ type="primary"
+ onClick={() => onNewBroker()}
+ style={{ margin: '0 10px 0 10px' }}
+ >
+ 新增
+ </Button>
+ <Button type="primary" onClick={() => run()}>
+ 刷新
+ </Button>
+ </Form.Item>
+ </Form>
+ </div>
+ <Table
+ rowSelection={{ onChange: onBrokerTableSelectChange }}
+ columns={columns}
+ dataSource={brokerList}
+ rowKey="brokerId"
+ searchPlaceholder="请输入关键字搜索"
+ searchWidth={12}
+ dataSourceX={filterData.list}
+ scroll={{ x: 2500 }}
+ filterFnX={value =>
+ tableFilterHelper({
+ key: value,
+ srcArray: data,
+ targetArray: filterData.list,
+ updateFunction: res =>
+ updateFilterData(filterData => {
+ filterData.list = res;
+ }),
+ filterList: [
+ 'brokerId',
+ 'brokerIp',
+ 'brokerPort',
+ 'runStatus',
+ 'subStatus',
+ 'manageStatus',
+ ],
+ })
+ }
+ />
+ </div>
+ <CommonModal modalParams={modalParams} data={data} />
+ </Spin>
+ );
+};
+
+export default Broker;
diff --git a/web/src/pages/Broker/query.tsx b/web/src/pages/Broker/query.tsx
new file mode 100644
index 0000000..9e89789
--- /dev/null
+++ b/web/src/pages/Broker/query.tsx
@@ -0,0 +1,128 @@
+import * as React from 'react';
+import './index.less';
+import { OKProps } from '@/components/Modalx';
+import { useRequest } from '@/hooks';
+import { useContext, useEffect } from 'react';
+import GlobalContext from '@/context/globalContext';
+
+interface ComProps {
+ fire: string;
+ params: any;
+ type: string;
+}
+
+const Comp = (props: ComProps) => {
+ const { fire } = props;
+ const { userInfo } = useContext(GlobalContext);
+ // eslint-disable-next-line
+ useEffect(() => {
+ const { params, type } = props;
+ dispatchAction(type, params);
+ }, [fire, props]);
+
+ const dispatchAction = (type: string, p: OKProps) => {
+ if (!fire) return null;
+ let promise;
+ switch (type) {
+ case 'newBroker':
+ promise = newBroker(p);
+ break;
+ case 'editBroker':
+ promise = editBroker(p);
+ break;
+ case 'brokerStateChange':
+ promise = brokerAcceptPublish(type, p);
+ break;
+ case 'online':
+ case 'offline':
+ case 'reload':
+ case 'delete':
+ promise = brokerOptions(type, p);
+ break;
+ }
+
+ promise &&
+ promise.then(t => {
+ const { callback } = p.params;
+ if (t.statusCode !== 0 && callback) callback(t);
+ });
+ };
+
+ const newBrokerQuery = useRequest<any, any>(
+ data => ({ url: '/api/op_modify/admin_add_broker_configure', ...data }),
+ { manual: true }
+ );
+ const newBroker = (p: OKProps) => {
+ const { params } = p;
+ return newBrokerQuery.run({
+ data: {
+ ...params,
+ confModAuthToken: p.psw,
+ createUser: userInfo.userName,
+ },
+ });
+ };
+
+ const updateBrokerQuery = useRequest<any, any>(
+ data => ({ url: '/api/op_modify/admin_update_broker_configure', ...data }),
+ { manual: true }
+ );
+ const editBroker = (p: OKProps) => {
+ const { params } = p;
+ return updateBrokerQuery.run({
+ data: {
+ ...params,
+ confModAuthToken: p.psw,
+ createUser: userInfo.userName,
+ },
+ });
+ };
+
+ const brokerOptionsQuery = useRequest<any, any>(
+ (url, data) => ({ url, ...data }),
+ { manual: true }
+ );
+ const brokerOptions = (type: string, p: OKProps) => {
+ const { params } = p;
+ return brokerOptionsQuery.run(
+ `/api/op_modify/admin_${type}_broker_configure`,
+ {
+ data: {
+ brokerId: params ? params?.join(',') :
params?.selectBroker.join(','),
+ confModAuthToken: p.psw,
+ createUser: userInfo.userName,
+ },
+ }
+ );
+ };
+
+ const brokerAcceptPublishQuery = useRequest<any, any>(
+ (url, data) => ({ url, ...data }),
+ { manual: true }
+ );
+ const brokerAcceptPublish = (type: string, p: OKProps) => {
+ const { params } = p;
+ const data: any = {
+ brokerId: params.id,
+ confModAuthToken: p.psw,
+ createUser: userInfo.userName,
+ };
+ if (params.type === 'acceptPublish') {
+ data.isAcceptPublish = params.option;
+ }
+ if (params.type === 'acceptSubscribe') {
+ data.isAcceptSubscribe = params.option;
+ }
+
+ return brokerAcceptPublishQuery.run(
+ `/api/op_modify/admin_set_broker_read_or_write`,
+ {
+ data,
+ }
+ );
+ };
+
+ return <></>;
+};
+
+export default Comp;
diff --git a/web/src/pages/Cluster/index.less b/web/src/pages/Cluster/index.less
new file mode 100644
index 0000000..e69de29
diff --git a/web/src/pages/Cluster/index.tsx b/web/src/pages/Cluster/index.tsx
new file mode 100644
index 0000000..6b82403
--- /dev/null
+++ b/web/src/pages/Cluster/index.tsx
@@ -0,0 +1,143 @@
+import React, { useContext } from 'react';
+import GlobalContext from '@/context/globalContext';
+import Breadcrumb from '@/components/Breadcrumb';
+import Table from '@/components/Tablex';
+import { Spin } from 'antd';
+import './index.less';
+import { useRequest } from '@/hooks';
+import Modal, { OKProps } from '@/components/Modalx';
+import { useImmer } from 'use-immer';
+
+interface ClusterResultData {
+ groupName: string;
+ groupStatus: string;
+ hostName: string;
+ index: number;
+ port: string;
+ nodeStatus: string;
+ length: number;
+}
+
+const queryClusterList = (data: ClusterResultData) => ({
+ url: '/api/op_query/admin_query_master_group_info',
+ data: data,
+});
+
+const Cluster: React.FC = () => {
+ const { breadMap } = useContext(GlobalContext);
+ const [modalParams, updateModelParams] = useImmer<any>({
+ title: '请确认操作',
+ });
+ const { data, loading } = useRequest<any, any>(queryClusterList, {
+ formatResult: d => {
+ return {
+ list: d.data.map((t: any) => ({
+ groupName: d.groupName,
+ groupStatus: d.groupStatus,
+ hostName: t.hostName,
+ index: t.index,
+ port: t.port,
+ nodeStatus: t.statusInfo.nodeStatus,
+ length: d.data.length,
+ })),
+ };
+ },
+ });
+ const columns = [
+ {
+ title: '集群名',
+ dataIndex: 'groupName',
+ render: (t: string, r: ClusterResultData, index: number) => {
+ return {
+ children: t,
+ props: {
+ rowSpan: index === 0 ? r.length : 0,
+ },
+ };
+ },
+ },
+ {
+ title: '集群状态',
+ dataIndex: 'groupStatus',
+ render: (t: string, r: ClusterResultData, index: number) => {
+ return {
+ children: t,
+ props: {
+ rowSpan: index === 0 ? r.length : 0,
+ },
+ };
+ },
+ },
+ {
+ title: '节点名',
+ render: (t: string, r: ClusterResultData) => {
+ return `${r.groupName}-${r.hostName}`;
+ },
+ },
+ {
+ title: 'IP地址',
+ render: (t: string, r: ClusterResultData) => {
+ return `${r.hostName}-${r.port}`;
+ },
+ },
+ {
+ title: '节点名',
+ dataIndex: 'nodeStatus',
+ },
+ {
+ title: '操作',
+ render: (t: string, r: ClusterResultData, index: number) => {
+ return {
+ children: (
+ <span className="options-wrapper">
+ <a onClick={() => onSwitchCluster(t, r)}>切换</a>
+ </span>
+ ),
+ props: {
+ rowSpan: index === 0 ? r.length : 0,
+ },
+ };
+ },
+ },
+ ];
+
+ const switchClusterQuery = useRequest<any, any>(
+ (data?: ClusterResultData) => ({
+ url: '/api/op_modify/admin_transfer_current_master',
+ data,
+ }),
+ { manual: true }
+ );
+ const onSwitchCluster = (t: string, r: ClusterResultData) => {
+ updateModelParams(d => {
+ d = Object.assign(d, {
+ visible: true,
+ onOk: (p: OKProps) => {
+ switchClusterQuery.run({
+ confModAuthToken: p.psw,
+ });
+ },
+ onCancel: () => {
+ updateModelParams((m: any) => {
+ m.visible = false;
+ });
+ },
+ });
+ });
+ };
+ return (
+ <Spin spinning={loading}>
+ <Breadcrumb breadcrumbMap={breadMap}></Breadcrumb>
+ <div className="main-container">
+ <Table columns={columns} dataSource={data?.list}
rowKey="index"></Table>
+ </div>
+ <Modal {...modalParams}>
+ <div>
+ 确认<span className="enhance">切换</span>集群?
+ </div>
+ </Modal>
+ </Spin>
+ );
+};
+
+export default Cluster;
diff --git a/web/src/pages/Issue/consumeGroupDetail.tsx
b/web/src/pages/Issue/consumeGroupDetail.tsx
new file mode 100644
index 0000000..3c5090f
--- /dev/null
+++ b/web/src/pages/Issue/consumeGroupDetail.tsx
@@ -0,0 +1,95 @@
+import React, { useContext } from 'react';
+import GlobalContext from '@/context/globalContext';
+import Breadcrumb from '@/components/Breadcrumb';
+import Table from '@/components/Tablex';
+import tableFilterHelper from '@/components/Tablex/tableFilterHelper';
+import { Spin } from 'antd';
+import { useImmer } from 'use-immer';
+import './index.less';
+import { useRequest } from '@/hooks';
+import { useParams } from 'react-router-dom';
+
+declare type ConsumeGroupData = any[];
+interface ConsumeGroupQueryData {
+ consumeGroup: string;
+}
+
+// column config
+const columns = [
+ {
+ title: '消费者ID',
+ dataIndex: 'consumerId',
+ },
+ {
+ title: '消费Topic',
+ dataIndex: 'topicName',
+ },
+ {
+ title: 'broker地址',
+ dataIndex: 'brokerAddr',
+ },
+ {
+ title: '分区ID',
+ dataIndex: 'partId',
+ },
+];
+
+const queryUser = (data: ConsumeGroupQueryData) => ({
+ url: '/api/op_query/admin_query_consume_group_detail',
+ data: data,
+});
+
+const ConsumeGroupDetail: React.FC = () => {
+ const { id } = useParams();
+ const { breadMap } = useContext(GlobalContext);
+ const [filterData, updateFilterData] = useImmer<any>({});
+ const { data, loading } = useRequest<any, ConsumeGroupData>(
+ () =>
+ queryUser({
+ consumeGroup: id,
+ }),
+ {
+ formatResult: data => {
+ const d = data[0];
+ return {
+ list: d.parInfo.map((t: any) => ({
+ consumerId: d.consumerId,
+ ...t,
+ })),
+ };
+ },
+ }
+ );
+
+ return (
+ <Spin spinning={loading}>
+ <Breadcrumb
+ breadcrumbMap={breadMap}
+ appendParams={`消费组详情(${id})`}
+ ></Breadcrumb>
+ <div className="main-container">
+ <Table
+ columns={columns}
+ dataSource={data?.list}
+ rowKey="brokerAddr"
+ searchPlaceholder="请输入 broker地址/分区ID 搜索"
+ dataSourceX={filterData.list}
+ filterFnX={value =>
+ tableFilterHelper({
+ key: value,
+ srcArray: data?.list,
+ targetArray: filterData.list,
+ updateFunction: res =>
+ updateFilterData(filterData => {
+ filterData.list = res;
+ }),
+ filterList: ['brokerAddr', 'partId'],
+ })
+ }
+ ></Table>
+ </div>
+ </Spin>
+ );
+};
+
+export default ConsumeGroupDetail;
diff --git a/web/src/pages/Issue/index.less b/web/src/pages/Issue/index.less
new file mode 100644
index 0000000..e69de29
diff --git a/web/src/pages/Issue/index.tsx b/web/src/pages/Issue/index.tsx
new file mode 100644
index 0000000..54f5ad3
--- /dev/null
+++ b/web/src/pages/Issue/index.tsx
@@ -0,0 +1,98 @@
+import React, { useContext } from 'react';
+import GlobalContext from '@/context/globalContext';
+import Breadcrumb from '@/components/Breadcrumb';
+import Table from '@/components/Tablex';
+import { Form, Input, Button, Spin } from 'antd';
+import { useImmer } from 'use-immer';
+import './index.less';
+import { useRequest } from '@/hooks';
+import { Link } from 'react-router-dom';
+
+declare type IssueData = any[];
+interface IssueQueryData {
+ topicName?: string;
+ consumeGroup?: string;
+}
+
+// column config
+const columns = [
+ {
+ title: '消费组',
+ dataIndex: 'consumeGroup',
+ render: (t: Array<any>) => <Link to={'/issue/' + t}>{t}</Link>,
+ },
+ {
+ title: '消费Topic',
+ dataIndex: 'topicSet',
+ render: (t: Array<any>) => {
+ return t.join(',');
+ },
+ },
+ {
+ title: '消费分区',
+ dataIndex: 'consumerNum',
+ },
+];
+
+const queryIssueList = (data: IssueQueryData) => ({
+ url: '/api/op_query/admin_query_sub_info',
+ data: data,
+});
+
+const Issue: React.FC = () => {
+ const { breadMap } = useContext(GlobalContext);
+ const [form] = Form.useForm();
+ const [formValues, updateFormValues] = useImmer<any>({});
+ const { data, loading, run } = useRequest<any, IssueData>(queryIssueList,
{});
+
+ const onValuesChange = (p: any) => {
+ updateFormValues(d => {
+ Object.assign(d, p);
+ });
+ };
+ const onSearch = () => {
+ run(formValues);
+ };
+
+ const onReset = () => {
+ form.resetFields();
+ run({});
+ };
+
+ return (
+ <Spin spinning={loading}>
+ <Breadcrumb breadcrumbMap={breadMap}></Breadcrumb>
+ <div className="main-container">
+ <div className="search-wrapper">
+ <Form form={form} layout={'inline'} onValuesChange={onValuesChange}>
+ <Form.Item label="Topic 名称" name="topicName">
+ <Input placeholder="" />
+ </Form.Item>
+ <Form.Item label="消费组" name="consumeGroup">
+ <Input placeholder="" />
+ </Form.Item>
+ <Form.Item>
+ <Button
+ type="primary"
+ onClick={onSearch}
+ style={{ margin: '0 20px' }}
+ >
+ 查询
+ </Button>
+ <Button type="default" onClick={onReset}>
+ 重置
+ </Button>
+ </Form.Item>
+ </Form>
+ </div>
+ <Table
+ columns={columns}
+ dataSource={data}
+ rowKey="consumeGroup"
+ ></Table>
+ </div>
+ </Spin>
+ );
+};
+
+export default Issue;
diff --git a/web/src/pages/NotFound/index.tsx b/web/src/pages/NotFound/index.tsx
new file mode 100644
index 0000000..8a0e971
--- /dev/null
+++ b/web/src/pages/NotFound/index.tsx
@@ -0,0 +1,5 @@
+import React from 'react';
+
+const NotFound: React.FC = () => <div>404</div>;
+
+export default NotFound;
diff --git a/web/src/pages/Topic/commonModal.tsx
b/web/src/pages/Topic/commonModal.tsx
new file mode 100644
index 0000000..9d7783c
--- /dev/null
+++ b/web/src/pages/Topic/commonModal.tsx
@@ -0,0 +1,349 @@
+import { boolean2Chinese } from '@/utils';
+import Table from '@/components/Tablex';
+import { Col, Form, Input, message, Row } from 'antd';
+import Modal, { OKProps } from '@/components/Modalx';
+import React from 'react';
+import Query from '@/pages/Topic/query';
+import { FormProps } from 'antd/lib/form';
+
+export const OPTIONS = [
+ {
+ value: 'delete',
+ name: '删除',
+ },
+];
+export const OPTIONS_VALUES = OPTIONS.map(t => t.value);
+
+// interface
+export declare type TopicData = any[];
+export interface TopicResultData {
+ topicName: string;
+ infoCount: string;
+ totalCfgNumPart: string;
+ totalRunNumPartCount: string;
+ isSrvAcceptPublish: string | number;
+ isSrvAcceptSubscribe: string | number;
+ enableAuthControl: string | number;
+ [key: string]: any;
+}
+export interface TopicModalProps {
+ type: string;
+ title?: string;
+ updateFunction: (draft: any) => any;
+ params?: any;
+}
+interface ComProps {
+ modalParams: any;
+ data: any[];
+}
+// exports broker modal
+// render funcs
+const renderTopicOptions = (modalParams: any, dataSource: any[]) => {
+ const columns = [
+ {
+ title: 'Topic',
+ render: (t: string, r: TopicResultData) => {
+ return `${r.brokerId}#${r.brokerIp}:${r.brokerPort}`;
+ },
+ },
+ {
+ title: 'TopicIP',
+ dataIndex: 'brokerIp',
+ },
+ {
+ title: '管理状态',
+ dataIndex: 'manageStatus',
+ },
+ {
+ title: '运行状态',
+ dataIndex: 'runStatus',
+ },
+ {
+ title: '运行子状态',
+ dataIndex: 'subStatus',
+ },
+ {
+ title: '可发布',
+ render: (t: string) => boolean2Chinese(t),
+ },
+ {
+ title: '可订阅',
+ render: (t: string) => boolean2Chinese(t),
+ },
+ ];
+ return <Table columns={columns} dataSource={dataSource} rowKey="brokerId" />;
+};
+const renderNewTopic = (form: any) => {
+ const brokerFormArr = [
+ {
+ name: 'topicName',
+ defaultValue: '',
+ },
+ {
+ name: 'numPartitions',
+ defaultValue: '3',
+ },
+ {
+ name: 'deleteWhen',
+ defaultValue: '0 0 6,18 * * ?',
+ },
+ {
+ name: 'deletePolicy',
+ defaultValue: 'delete,168h',
+ },
+ {
+ name: 'unflushThreshold',
+ defaultValue: '1000',
+ },
+ {
+ name: 'unflushInterval',
+ defaultValue: '10000',
+ },
+ {
+ name: 'acceptPublish',
+ defaultValue: 'true',
+ },
+ {
+ name: 'acceptSubscribe',
+ defaultValue: 'true',
+ },
+ ];
+
+ return (
+ <Form form={form}>
+ <Row gutter={24}>
+ {brokerFormArr.map((t, index) => (
+ <Col span={12} key={'brokerFormArr' + index}>
+ <Form.Item
+ labelCol={{ span: 12 }}
+ label={t.name}
+ name={t.name}
+ initialValue={t.defaultValue}
+ >
+ <Input />
+ </Form.Item>
+ </Col>
+ ))}
+ </Row>
+ </Form>
+ );
+};
+const renderChooseBroker = (modalParams: any) => {
+ const { params } = modalParams;
+ const columns = [
+ {
+ title: 'Broker',
+ render: (t: string, r: TopicResultData) => {
+ return `${r.brokerId}#${r.brokerIp}:${r.brokerPort}`;
+ },
+ },
+ {
+ title: '实例数',
+ dataIndex: ['runInfo', 'numTopicStores'],
+ },
+ {
+ title: '当前运行状态',
+ dataIndex: ['runInfo', 'brokerManageStatus'],
+ },
+ {
+ title: '可发布',
+ dataIndex: ['runInfo', 'acceptPublish'],
+ render: (t: string) => boolean2Chinese(t),
+ },
+ {
+ title: '可订阅',
+ dataIndex: ['runInfo', 'acceptSubscribe'],
+ render: (t: string) => boolean2Chinese(t),
+ },
+ ];
+ const onChangeSelect = (p: any) => {
+ selectBroker = p;
+ };
+ return (
+ <Table
+ rowSelection={{ onChange: onChangeSelect }}
+ columns={columns}
+ dataSource={params.data}
+ rowKey="brokerId"
+ />
+ );
+};
+const renderEditTopic = (modalParams: any, form: FormProps['form']) => {
+ const { params: p } = modalParams;
+ const pickArr = [
+ 'topicName',
+ 'numPartitions',
+ 'unflushThreshold',
+ 'unflushInterval',
+ 'deleteWhen',
+ 'deletePolicy',
+ 'acceptPublish',
+ 'acceptSubscribe',
+ ];
+ const brokerFormArr: Array<{
+ name: string;
+ defaultValue: string;
+ }> = [];
+ pickArr.forEach(t => {
+ brokerFormArr.push({
+ name: t,
+ defaultValue: p[t],
+ });
+ });
+
+ return (
+ <Form form={form}>
+ <Row gutter={24}>
+ {brokerFormArr.map((t, index) => (
+ <Col span={12} key={'brokerFormArr' + index}>
+ <Form.Item
+ labelCol={{ span: 12 }}
+ label={t.name}
+ name={t.name}
+ initialValue={t.defaultValue}
+ >
+ <Input />
+ </Form.Item>
+ </Col>
+ ))}
+ </Row>
+ </Form>
+ );
+};
+const renderTopicStateChange = (modalParams: any) => {
+ const { params } = modalParams;
+
+ return (
+ <div>
+ 请确认<span className="enhance">{params.option}</span> 以下broker列表的
+ topic :<span className="enhance">({params.topicName})</span> 的 Topic?
+ {renderChooseBroker(modalParams)}
+ </div>
+ );
+};
+const renderDeleteTopic = (modalParams: any) => {
+ const { params } = modalParams;
+
+ return (
+ <div>
+ 请确认<span className="enhance">删除</span> 以下broker列表的 topic :
+ <span className="enhance">({params.topicName})</span> 吗?
+ {renderChooseBroker(modalParams)}
+ </div>
+ );
+};
+const renderDeleteConsumeGroup = (modalParams: any) => {
+ const { params } = modalParams;
+
+ return (
+ <div>
+ 确认<span className="enhance">删除</span> 以下 :
+ <span className="enhance">({params.groupName})</span> 吗?
+ </div>
+ );
+};
+const renderAuthorizeControlChange = (modalParams: any) => {
+ const { params } = modalParams;
+
+ return (
+ <div>
+ 请确认
+ <span className="enhance">
+ {params.value ? '启动' : '关闭'}topic
+ <span className="enhance">({params.topicName})</span>的消费组授权控制
+ </span>
+ 吗?
+ </div>
+ );
+};
+export const onOpenModal = (p: TopicModalProps) => {
+ const { type, title, updateFunction, params } = p;
+ updateFunction((m: any) => {
+ m.type = type;
+ m.params = params;
+ Object.assign(m, {
+ params,
+ visible: type,
+ title,
+ onOk: (p: OKProps) => {
+ updateFunction((m: any) => {
+ if (type === 'newTopic' || type === 'editTopic') {
+ p.params = Object.assign(f && f.getFieldsValue(), {
+ callback: p.params.callback,
+ });
+ }
+
+ if (
+ type === 'chooseBroker' ||
+ type === 'topicStateChange' ||
+ type === 'deleteTopic'
+ ) {
+ if (!selectBroker.length) {
+ message.error('至少选择一列!');
+ return;
+ }
+
+ // end
+ if (type === 'chooseBroker') {
+ m.query =
+ p.params.subType === 'edit'
+ ? 'endEditChooseBroker'
+ : 'endChooseBroker';
+ }
+ p.params = Object.assign({}, p.params, {
+ selectBroker,
+ });
+ }
+
+ m.okParams = p;
+ m.isOk = Date.now();
+ });
+ },
+ onCancel: () =>
+ updateFunction((m: any) => {
+ m.visible = false;
+ m.isOk = null;
+ }),
+ });
+ });
+};
+
+let selectBroker: any[] = [];
+let f: FormProps['form'];
+const Comp = (props: ComProps) => {
+ const { modalParams, data } = props;
+ const [form] = Form.useForm();
+ f = form;
+
+ return (
+ <Modal {...modalParams}>
+ <div>
+ {modalParams.type &&
+ OPTIONS_VALUES.includes(modalParams.type) &&
+ renderTopicOptions(
+ modalParams,
+ data.filter((t: TopicResultData) =>
+ modalParams.params.includes(t.brokerId)
+ )
+ )}
+ {modalParams.type === 'newTopic' && renderNewTopic(form)}
+ {modalParams.type === 'chooseBroker' &&
renderChooseBroker(modalParams)}
+ {modalParams.type === 'editTopic' && renderEditTopic(modalParams,
form)}
+ {modalParams.type === 'topicStateChange' &&
+ renderTopicStateChange(modalParams)}
+ {modalParams.type === 'deleteTopic' && renderDeleteTopic(modalParams)}
+ {modalParams.type === 'deleteConsumeGroup' &&
+ renderDeleteConsumeGroup(modalParams)}
+ {modalParams.type === 'authorizeControl' &&
+ renderAuthorizeControlChange(modalParams)}
+ </div>
+ <Query
+ fire={modalParams.isOk}
+ params={modalParams.okParams}
+ type={modalParams.visible && (modalParams.query || modalParams.type)}
+ />
+ </Modal>
+ );
+};
+
+export default Comp;
diff --git a/web/src/pages/Topic/detail.tsx b/web/src/pages/Topic/detail.tsx
new file mode 100644
index 0000000..0c9965c
--- /dev/null
+++ b/web/src/pages/Topic/detail.tsx
@@ -0,0 +1,510 @@
+import React, { ReactNode, useContext, useState } from 'react';
+import GlobalContext from '@/context/globalContext';
+import Breadcrumb from '@/components/Breadcrumb';
+import Table from '@/components/Tablex';
+import TitleWrap from '@/components/TitleWrap';
+import { Form, Button, Spin, Col, Row, Switch } from 'antd';
+import { useImmer } from 'use-immer';
+import './index.less';
+import { useRequest } from '@/hooks';
+import { useParams } from 'react-router-dom';
+import { boolean2Chinese, transParamsWithConstantsMap } from '@/utils';
+import tableFilterHelper from '@/components/Tablex/tableFilterHelper';
+import CommonModal, { onOpenModal, TopicResultData } from './commonModal';
+import BrokerModal, {
+ onOpenModal as onOpenBrokerModal,
+} from '@/pages/Broker/commonModal';
+import { BROKER_INFO_ZH_MAP } from '@/constants/broker';
+import { PERSON_INFO_ZH_MAP } from '@/constants/person';
+import { TOPIC_INFO_ZH_MAP } from '@/constants/topic';
+
+declare type TopicQueryData = {
+ topicName: string;
+};
+
+const Detail: React.FC = () => {
+ const { name } = useParams();
+ const { breadMap } = useContext(GlobalContext);
+ const [form] = Form.useForm();
+ const [modalParams, updateModelParams] = useImmer<any>({});
+ const [brokerModalParams, updateBrokerModalParams] = useImmer<any>({});
+ const [isSrvAcceptPublish, setIsSrvAcceptPublish] = useState<any>(false);
+ const [isSrvAcceptSubscribe, setIsSrvAcceptSubscribe] = useState<any>(false);
+ const [enableAuthControl, setEnableAuthControl] = useState<any>(false);
+ const [filterData, updateFilterData] = useImmer<any>({});
+ const queryTopicInfo = useRequest<any>(
+ (
+ data: TopicQueryData = {
+ topicName: name,
+ }
+ ) => ({
+ url: '/api/op_query/admin_query_topic_authorize_control',
+ data: {
+ ...data,
+ },
+ })
+ );
+ const queryTopicConf = useRequest<any>(
+ (
+ data: TopicQueryData = {
+ topicName: name,
+ }
+ ) => ({
+ url: '/api/op_query/admin_query_topic_info',
+ data: {
+ ...data,
+ },
+ }),
+ {
+ onSuccess: data => {
+ setIsSrvAcceptPublish(data[0]['isSrvAcceptPublish']);
+ setIsSrvAcceptSubscribe(data[0]['isSrvAcceptSubscribe']);
+ setEnableAuthControl(data[0]['authData']['enableAuthControl']);
+ },
+ }
+ );
+
+ // render
+ const searchStyle = {
+ position: 'absolute',
+ top: '-40px',
+ right: '10px',
+ zIndex: 1,
+ width: '300px',
+ };
+ const renderBrokerList = (): ReactNode => {
+ const columns = [
+ {
+ title: 'Broker',
+ render: (t: string, r: TopicResultData) => {
+ return `${r.brokerId}#${r.brokerIp}:${r.brokerPort}`;
+ },
+ },
+ {
+ title: transParamsWithConstantsMap(
+ BROKER_INFO_ZH_MAP,
+ 'runInfo.acceptPublish'
+ ),
+ dataIndex: ['runInfo', 'acceptPublish'],
+ render: (t: string) => boolean2Chinese(t),
+ },
+ {
+ title: transParamsWithConstantsMap(
+ BROKER_INFO_ZH_MAP,
+ 'runInfo.acceptSubscribe'
+ ),
+ dataIndex: ['runInfo', 'acceptSubscribe'],
+ render: (t: string) => boolean2Chinese(t),
+ },
+ {
+ title: transParamsWithConstantsMap(
+ BROKER_INFO_ZH_MAP,
+ 'runInfo.numPartitions'
+ ),
+ dataIndex: ['runInfo', 'numPartitions'],
+ },
+ {
+ title: transParamsWithConstantsMap(
+ BROKER_INFO_ZH_MAP,
+ 'runInfo.brokerManageStatus'
+ ),
+ dataIndex: ['runInfo', 'brokerManageStatus'],
+ },
+ {
+ title: transParamsWithConstantsMap(BROKER_INFO_ZH_MAP,
'acceptPublish'),
+ dataIndex: 'acceptPublish',
+ render: (t: string) => boolean2Chinese(t),
+ },
+ {
+ title: transParamsWithConstantsMap(
+ BROKER_INFO_ZH_MAP,
+ 'acceptSubscribe'
+ ),
+ dataIndex: 'acceptSubscribe',
+ render: (t: string) => boolean2Chinese(t),
+ },
+ {
+ title: transParamsWithConstantsMap(BROKER_INFO_ZH_MAP,
'numPartitions'),
+ dataIndex: 'numPartitions',
+ },
+ {
+ title: transParamsWithConstantsMap(
+ BROKER_INFO_ZH_MAP,
+ 'unflushThreshold'
+ ),
+ dataIndex: 'unflushThreshold',
+ },
+ {
+ title: transParamsWithConstantsMap(
+ BROKER_INFO_ZH_MAP,
+ 'unflushInterval'
+ ),
+ dataIndex: 'unflushInterval',
+ },
+ {
+ title: transParamsWithConstantsMap(BROKER_INFO_ZH_MAP, 'deleteWhen'),
+ dataIndex: 'deleteWhen',
+ },
+ {
+ title: transParamsWithConstantsMap(BROKER_INFO_ZH_MAP, 'deletePolicy'),
+ dataIndex: 'deletePolicy',
+ },
+ {
+ title: '操作',
+ render: (t: string, r: TopicResultData) => {
+ return (
+ <span>
+ <a onClick={() => onEdit(r)}>编辑</a>
+ <a onClick={() => onReload(r)}>重载</a>
+ <a onClick={() => onDeleteBroker(r)}>删除</a>
+ </span>
+ );
+ },
+ },
+ ];
+ const { data } = queryTopicConf;
+ if (!data || !data[0]) return null;
+ const { topicInfo } = data[0];
+
+ return (
+ <Table
+ columns={columns}
+ dataSource={topicInfo}
+ rowKey={r => `${r.brokerId}#${r.brokerIp}:${r.brokerPort}`}
+ dataSourceX={filterData.topicInfoList}
+ searchPlaceholder="请输入brokerId,Ip,Port搜索"
+ searchStyle={searchStyle}
+ filterFnX={value =>
+ tableFilterHelper({
+ key: value,
+ srcArray: topicInfo,
+ targetArray: filterData.topicInfoList,
+ updateFunction: res =>
+ updateFilterData(filterData => {
+ filterData.topicInfoList = res;
+ }),
+ filterList: ['brokerId', 'brokerIp', 'brokerPort'],
+ })
+ }
+ />
+ );
+ };
+ const renderConsumeGroupList = (): ReactNode => {
+ const columns = [
+ {
+ title: '消费组',
+ dataIndex: 'groupName',
+ },
+ {
+ title: transParamsWithConstantsMap(PERSON_INFO_ZH_MAP, 'createUser'),
+ dataIndex: 'createUser',
+ },
+ {
+ title: transParamsWithConstantsMap(PERSON_INFO_ZH_MAP, 'createDate'),
+ dataIndex: 'createDate',
+ },
+ {
+ title: '操作',
+ render: (t: string, r: TopicResultData) => {
+ return (
+ <span>
+ <a onClick={() => onDeleteConsumeGroup(r)}>删除</a>
+ </span>
+ );
+ },
+ },
+ ];
+ const { data } = queryTopicInfo;
+ if (!data || !data[0]) return null;
+ const { authConsumeGroup } = data[0];
+
+ return (
+ <Table
+ columns={columns}
+ dataSource={authConsumeGroup}
+ rowKey={r => `${r.brokerId}#${r.brokerIp}:${r.brokerPort}`}
+ dataSourceX={filterData.list}
+ searchPlaceholder="请输入消费组名称搜索"
+ searchStyle={searchStyle}
+ filterFnX={value =>
+ tableFilterHelper({
+ key: value,
+ srcArray: authConsumeGroup,
+ targetArray: filterData.list,
+ updateFunction: res =>
+ updateFilterData(filterData => {
+ filterData.list = res;
+ }),
+ filterList: ['groupName'],
+ })
+ }
+ />
+ );
+ };
+
+ // event
+ // isSrvAcceptPublish && isSrvAcceptSubscribe event
+ const queryBrokerListByTopicNameQuery = useRequest<any, any>(
+ data => ({ url: '/api/op_query/admin_query_topic_info', ...data }),
+ { manual: true }
+ );
+ const onSwitchChange = (e: boolean, type: string) => {
+ let option = '';
+ const topicName = queryTopicConf.data[0].topicInfo[0].topicName;
+ if (type === 'isSrvAcceptPublish') {
+ option = e ? '发布' : '禁止可发布';
+ } else if (type === 'isSrvAcceptSubscribe') {
+ option = e ? '订阅' : '禁止可订阅';
+ }
+
+ queryBrokerListByTopicNameQuery
+ .run({
+ data: {
+ topicName,
+ brokerId: '',
+ },
+ })
+ .then((d: TopicResultData) => {
+ onOpenModal({
+ type: 'topicStateChange',
+ title: `请确认操作`,
+ updateFunction: updateModelParams,
+ params: {
+ option,
+ value: e,
+ topicName,
+ data: d[0].topicInfo,
+ type,
+ callback: () => {
+ if (type === 'isSrvAcceptPublish') {
+ setIsSrvAcceptPublish(e);
+ } else if (type === 'isSrvAcceptSubscribe') {
+ setIsSrvAcceptSubscribe(e);
+ }
+ },
+ },
+ });
+ });
+ };
+ // author
+ const onAuthorizeControl = (e: boolean) => {
+ const option = e ? '发布' : '禁止可发布';
+ const topicName = queryTopicConf.data[0].topicInfo[0].topicName;
+ onOpenModal({
+ type: 'authorizeControl',
+ title: `请确认操作`,
+ updateFunction: updateModelParams,
+ params: {
+ option,
+ value: e,
+ topicName,
+ callback: () => {
+ setEnableAuthControl(e);
+ },
+ },
+ });
+ };
+ // edit topic
+ const onEdit = (r?: TopicResultData) => {
+ const p = r || queryTopicConf.data[0].topicInfo[0];
+ onOpenModal({
+ type: 'editTopic',
+ title: '编辑Topic',
+ updateFunction: updateModelParams,
+ params: {
+ ...p,
+ callback: (d: any) => {
+ onOpenModal({
+ type: 'chooseBroker',
+ title: '选择【修改】broker列表',
+ updateFunction: updateModelParams,
+ params: {
+ data: d,
+ subType: 'edit',
+ callback: () => {
+ onOpenModal({
+ type: 'close',
+ updateFunction: updateModelParams,
+ });
+ },
+ },
+ });
+ },
+ },
+ });
+ };
+ // reload topic
+ const queryBrokerInfo = useRequest<any, any>(
+ data => ({ url: '/api/op_query/admin_query_broker_run_status', ...data }),
+ { manual: true }
+ );
+ const onReload = (r: TopicResultData) => {
+ queryBrokerInfo
+ .run({
+ data: {
+ brokerId: r.brokerId,
+ },
+ })
+ .then(data => {
+ onOpenBrokerModal({
+ type: 'reload',
+ title: `确认进行【重载】操作?`,
+ updateFunction: updateBrokerModalParams,
+ params: [data[0].brokerId],
+ });
+ });
+ };
+ // on delete broker
+ const onDeleteBroker = (r: TopicResultData) => {
+ queryBrokerListByTopicNameQuery
+ .run({
+ data: {
+ topicName: r.topicName,
+ brokerId: r.brokerId,
+ },
+ })
+ .then((d: TopicResultData) => {
+ onOpenModal({
+ type: 'deleteTopic',
+ title: `请确认操作`,
+ updateFunction: updateModelParams,
+ params: {
+ topicName: r.topicName,
+ data: d[0].topicInfo,
+ },
+ });
+ });
+ };
+ const onDeleteConsumeGroup = (r: TopicResultData) => {
+ onOpenModal({
+ type: 'deleteConsumeGroup',
+ title: `请确认消费组`,
+ updateFunction: updateModelParams,
+ params: {
+ topicName: r.topicName,
+ groupName: r.groupName,
+ },
+ });
+ };
+
+ return (
+ <Spin spinning={queryTopicConf.loading && queryTopicInfo.loading}>
+ <Breadcrumb
+ breadcrumbMap={breadMap}
+ appendParams={`Topic(${name})详情`}
+ />
+ <div className="main-container">
+ <TitleWrap
+ title="基本信息"
+ wrapperStyle={{ position: 'relative' }}
+ hasSplit={false}
+ >
+ <div className="topic-detail-options-wrapper">
+ <Switch
+ className="mr10"
+ checked={isSrvAcceptPublish}
+ checkedChildren="订阅"
+ unCheckedChildren="订阅"
+ onChange={e => onSwitchChange(e, 'isSrvAcceptPublish')}
+ />
+ <Switch
+ className="mr10"
+ checked={isSrvAcceptSubscribe}
+ checkedChildren="发布"
+ unCheckedChildren="发布"
+ onChange={e => onSwitchChange(e, 'isSrvAcceptSubscribe')}
+ />
+ <Switch
+ className="mr10"
+ checked={enableAuthControl}
+ checkedChildren="权限可控"
+ unCheckedChildren="权限可控"
+ onChange={e => onAuthorizeControl(e)}
+ />
+ </div>
+ <Form form={form}>
+ <Row gutter={24}>
+ {queryTopicConf.data &&
+ Object.keys(queryTopicConf.data[0]).map(
+ (t: string, index: number) => {
+ const label = transParamsWithConstantsMap(
+ TOPIC_INFO_ZH_MAP,
+ t
+ );
+ const ignoreList = [
+ 'isSrvAcceptPublish',
+ 'isSrvAcceptSubscribe',
+ ];
+ if (
+ queryTopicConf.data[0][t] instanceof Object ||
+ !label ||
+ ignoreList.includes(t)
+ )
+ return null;
+ return (
+ <Col span={12} key={'queryTopicConf' + index}>
+ <Form.Item labelCol={{ span: 12 }} label={label}>
+ {queryTopicConf.data[0][t] + ''}
+ </Form.Item>
+ </Col>
+ );
+ }
+ )}
+ </Row>
+ </Form>
+ </TitleWrap>
+ <TitleWrap title="缺省配置" wrapperStyle={{ position: 'relative' }}>
+ <div className="topic-detail-options-wrapper">
+ <Button
+ className="mr10"
+ type="primary"
+ size="small"
+ onClick={() => onEdit()}
+ >
+ 编辑
+ </Button>
+ </div>
+ <Form form={form}>
+ <Row gutter={24}>
+ {[
+ 'acceptPublish',
+ 'acceptSubscribe',
+ 'unflushThreshold',
+ 'unflushInterval',
+ 'deleteWhen',
+ 'deletePolicy',
+ 'numPartitions',
+ ].map((t: string, index: number) => {
+ if (
+ !queryTopicConf.data ||
+ !queryTopicConf.data[0].topicInfo[0]
+ )
+ return null;
+ const value = queryTopicConf.data[0].topicInfo[0][t];
+ return (
+ <Col span={12} key={'queryTopicConf' + index}>
+ <Form.Item labelCol={{ span: 12 }} label={t}>
+ {value + ''}
+ </Form.Item>
+ </Col>
+ );
+ })}
+ </Row>
+ </Form>
+ </TitleWrap>
+ <TitleWrap title="部署Broker列表">{renderBrokerList()}</TitleWrap>
+ <TitleWrap title="消费组列表">{renderConsumeGroupList()}</TitleWrap>
+ </div>
+ <CommonModal
+ modalParams={modalParams}
+ data={[queryTopicConf.data && queryTopicConf.data[0]]}
+ />
+ <BrokerModal
+ modalParams={brokerModalParams}
+ data={[queryBrokerInfo.data && queryBrokerInfo.data[0]]}
+ />
+ </Spin>
+ );
+};
+
+export default Detail;
diff --git a/web/src/pages/Topic/index.less b/web/src/pages/Topic/index.less
new file mode 100644
index 0000000..5efa767
--- /dev/null
+++ b/web/src/pages/Topic/index.less
@@ -0,0 +1,9 @@
+.topic-detail-options-wrapper {
+ position: absolute;
+ top: 15px;
+ right: 0;
+
+ .mr10 {
+ margin-right: 10px;
+ }
+}
\ No newline at end of file
diff --git a/web/src/pages/Topic/index.tsx b/web/src/pages/Topic/index.tsx
new file mode 100644
index 0000000..cd112f9
--- /dev/null
+++ b/web/src/pages/Topic/index.tsx
@@ -0,0 +1,279 @@
+import React, { useContext } from 'react';
+import GlobalContext from '@/context/globalContext';
+import Breadcrumb from '@/components/Breadcrumb';
+import Table from '@/components/Tablex';
+import { Form, Button, Spin, Switch } from 'antd';
+import { useImmer } from 'use-immer';
+import { useRequest } from '@/hooks';
+import tableFilterHelper from '@/components/Tablex/tableFilterHelper';
+import { transParamsWithConstantsMap } from '@/utils';
+import { TOPIC_INFO_ZH_MAP } from '@/constants/topic';
+import './index.less';
+import { Link } from 'react-router-dom';
+import CommonModal, {
+ onOpenModal,
+ TopicResultData,
+ TopicData,
+} from './commonModal';
+
+const Topic: React.FC = () => {
+ // column config
+ const columns = [
+ {
+ title: transParamsWithConstantsMap(TOPIC_INFO_ZH_MAP, 'topicName'),
+ dataIndex: 'topicName',
+ render: (t: Array<any>) => <Link to={'/topic/' + t}>{t}</Link>,
+ },
+ {
+ title: transParamsWithConstantsMap(TOPIC_INFO_ZH_MAP, 'infoCount'),
+ dataIndex: 'infoCount',
+ },
+ {
+ title: transParamsWithConstantsMap(TOPIC_INFO_ZH_MAP, 'totalCfgNumPart'),
+ dataIndex: 'totalCfgNumPart',
+ },
+ {
+ title: transParamsWithConstantsMap(
+ TOPIC_INFO_ZH_MAP,
+ 'totalRunNumPartCount'
+ ),
+ dataIndex: 'totalRunNumPartCount',
+ },
+ {
+ title: transParamsWithConstantsMap(
+ TOPIC_INFO_ZH_MAP,
+ 'isSrvAcceptPublish'
+ ),
+ dataIndex: 'isSrvAcceptPublish',
+ render: (t: boolean, r: TopicResultData) => {
+ return (
+ <Switch
+ checked={t}
+ onChange={e => onSwitchChange(e, r, 'isSrvAcceptPublish')}
+ />
+ );
+ },
+ },
+ {
+ title: transParamsWithConstantsMap(
+ TOPIC_INFO_ZH_MAP,
+ 'isSrvAcceptSubscribe'
+ ),
+ dataIndex: 'isSrvAcceptSubscribe',
+ render: (t: boolean, r: TopicResultData) => {
+ return (
+ <Switch
+ checked={t}
+ onChange={e => onSwitchChange(e, r, 'isSrvAcceptSubscribe')}
+ />
+ );
+ },
+ },
+ {
+ title: transParamsWithConstantsMap(
+ TOPIC_INFO_ZH_MAP,
+ 'enableAuthControl'
+ ),
+ dataIndex: 'authData.enableAuthControl',
+ render: (t: boolean, r: TopicResultData) => {
+ return (
+ <Switch
+ checked={r.authData.enableAuthControl}
+ onChange={e => onAuthorizeControl(e, r)}
+ />
+ );
+ },
+ },
+ {
+ title: '操作',
+ dataIndex: 'topicIp',
+ render: (t: string, r: any) => {
+ return <a onClick={() => onDelete(r)}>删除</a>;
+ },
+ },
+ ];
+ const { breadMap } = useContext(GlobalContext);
+ const [modalParams, updateModelParams] = useImmer<any>({});
+ const [filterData, updateFilterData] = useImmer<any>({});
+ const [topicList, updateTopicList] = useImmer<TopicData>([]);
+ const [form] = Form.useForm();
+ // init query
+ const { data, loading, run } = useRequest<any, TopicData>(
+ (data: TopicResultData) => ({
+ url: '/api/op_query/admin_query_topic_info',
+ data: data,
+ }),
+ {
+ cacheKey: 'topicList',
+ onSuccess: data => {
+ updateTopicList(d => {
+ Object.assign(d, data);
+ });
+ },
+ }
+ );
+
+ // table event
+ // acceptSubscribe && acceptPublish options
+ const queryBrokerListByTopicNameQuery = useRequest<any, any>(
+ data => ({ url: '/api/op_query/admin_query_topic_info', ...data }),
+ { manual: true }
+ );
+ const onSwitchChange = (e: boolean, r: TopicResultData, type: string) => {
+ let option = '';
+ if (type === 'isSrvAcceptPublish') {
+ option = e ? '发布' : '禁止可发布';
+ } else if (type === 'isSrvAcceptSubscribe') {
+ option = e ? '订阅' : '禁止可订阅';
+ }
+
+ queryBrokerListByTopicNameQuery
+ .run({
+ data: {
+ topicName: r.topicName,
+ brokerId: '',
+ },
+ })
+ .then((d: TopicResultData) => {
+ onOpenModal({
+ type: 'topicStateChange',
+ title: `请确认操作`,
+ updateFunction: updateModelParams,
+ params: {
+ option,
+ value: e,
+ topicName: r.topicName,
+ data: d[0].topicInfo,
+ type,
+ callback: () => {
+ const index = data.findIndex(
+ (t: TopicResultData) => t.topicName === r.topicName
+ );
+ updateTopicList(d => {
+ d[index][type] = e + '';
+ });
+ },
+ },
+ });
+ });
+ };
+ // author
+ const onAuthorizeControl = (e: boolean, r: TopicResultData) => {
+ const option = e ? '发布' : '禁止可发布';
+ onOpenModal({
+ type: 'authorizeControl',
+ title: `请确认操作`,
+ updateFunction: updateModelParams,
+ params: {
+ option,
+ value: e,
+ topicName: r.topicName,
+ callback: () => {
+ const index = data.findIndex(
+ (t: TopicResultData) => t.topicName === r.topicName
+ );
+ updateTopicList(d => {
+ d[index]['authData']['enableAuthControl'] = e + '';
+ });
+ },
+ },
+ });
+ };
+ // new topic
+ const onNewTopic = () => {
+ onOpenModal({
+ type: 'newTopic',
+ title: '新建Topic',
+ updateFunction: updateModelParams,
+ params: {
+ callback: (d: any) => {
+ onOpenModal({
+ type: 'chooseBroker',
+ title: '选择【新增】broker列表',
+ updateFunction: updateModelParams,
+ params: {
+ data: d,
+ callback: () => {
+ onOpenModal({
+ type: 'close',
+ updateFunction: updateModelParams,
+ });
+ },
+ },
+ });
+ },
+ },
+ });
+ };
+ // delete
+ const onDelete = (r: TopicResultData) => {
+ queryBrokerListByTopicNameQuery
+ .run({
+ data: {
+ topicName: r.topicName,
+ brokerId: '',
+ },
+ })
+ .then((d: TopicResultData) => {
+ onOpenModal({
+ type: 'deleteTopic',
+ title: `请确认操作`,
+ updateFunction: updateModelParams,
+ params: {
+ topicName: r.topicName,
+ data: d[0].topicInfo,
+ },
+ });
+ });
+ };
+
+ return (
+ <Spin spinning={loading}>
+ <Breadcrumb breadcrumbMap={breadMap} />
+ <div className="main-container">
+ <div
+ className="search-wrapper"
+ style={{ float: 'right', marginRight: '-16px' }}
+ >
+ <Form form={form} layout={'inline'}>
+ <Form.Item>
+ <Button
+ type="primary"
+ onClick={() => onNewTopic()}
+ style={{ margin: '0 10px 0 10px' }}
+ >
+ 新增
+ </Button>
+ <Button type="primary" onClick={() => run()}>
+ 刷新
+ </Button>
+ </Form.Item>
+ </Form>
+ </div>
+ <Table
+ columns={columns}
+ dataSource={topicList}
+ rowKey="topicName"
+ searchPlaceholder="请输入topicName查询"
+ searchWidth={12}
+ dataSourceX={filterData.list}
+ filterFnX={value =>
+ tableFilterHelper({
+ key: value,
+ srcArray: data,
+ targetArray: filterData.list,
+ updateFunction: res =>
+ updateFilterData(filterData => {
+ filterData.list = res;
+ }),
+ filterList: ['topicName'],
+ })
+ }
+ />
+ </div>
+ <CommonModal modalParams={modalParams} data={data} />
+ </Spin>
+ );
+};
+
+export default Topic;
diff --git a/web/src/pages/Topic/query.tsx b/web/src/pages/Topic/query.tsx
new file mode 100644
index 0000000..bf17545
--- /dev/null
+++ b/web/src/pages/Topic/query.tsx
@@ -0,0 +1,180 @@
+import * as React from 'react';
+import './index.less';
+import { OKProps } from '@/components/Modalx';
+import { useRequest } from '@/hooks';
+import { useContext, useEffect } from 'react';
+import GlobalContext from '@/context/globalContext';
+
+interface ComProps {
+ fire: string;
+ params: any;
+ type: string;
+}
+
+let newObjectTemp = '';
+let editObjectTemp = '';
+const Comp = (props: ComProps) => {
+ const { fire } = props;
+ const { userInfo } = useContext(GlobalContext);
+ // eslint-disable-next-line
+ useEffect(() => {
+ const { params, type } = props;
+ dispatchAction(type, params);
+ }, [fire]);
+
+ const dispatchAction = (type: string, p: OKProps) => {
+ if (!fire) return null;
+ let promise;
+ switch (type) {
+ case 'newTopic':
+ promise = newTopic(p);
+ break;
+ case 'endChooseBroker':
+ promise = endChooseBroker(p);
+ break;
+ case 'editTopic':
+ promise = editTopic(p);
+ break;
+ case 'endEditChooseBroker':
+ promise = endEditChooseBroker(p);
+ break;
+ case 'topicStateChange':
+ promise = topicStateChange(type, p);
+ break;
+ case 'authorizeControl':
+ promise = authorizeControl(type, p);
+ break;
+ case 'deleteTopic':
+ promise = deleteTopic(type, p);
+ break;
+ case 'deleteConsumeGroup':
+ promise = deleteConsumeGroup(type, p);
+ break;
+ }
+
+ promise &&
+ promise.then(t => {
+ const { callback } = p.params;
+ if (t.statusCode !== 0 && callback) callback(t);
+ });
+ };
+ const commonQuery = useRequest<any, any>((url, data) => ({ url, ...data }), {
+ manual: true,
+ });
+ const newTopicQuery = useRequest<any, any>(
+ data => ({
+ url: '/api/op_query/admin_query_broker_topic_config_info',
+ ...data,
+ }),
+ { manual: true }
+ );
+ const newTopic = (p: OKProps) => {
+ newObjectTemp = JSON.stringify(p.params);
+ return newTopicQuery.run({
+ data: {
+ topicName: '',
+ brokerId: '',
+ },
+ });
+ };
+
+ const endChooseBrokerQuery = useRequest<any, any>(
+ data => ({ url: '/api/op_modify/admin_add_new_topic_record', ...data }),
+ { manual: true }
+ );
+ const endChooseBroker = (p: OKProps) => {
+ const topicParams = JSON.parse(newObjectTemp);
+ const { params } = p;
+ return endChooseBrokerQuery.run({
+ data: {
+ borkerId: params.selectBroker.join(','),
+ confModAuthToken: p.psw,
+ ...topicParams,
+ },
+ });
+ };
+
+ const editTopic = (p: OKProps) => {
+ const { params } = p;
+ editObjectTemp = JSON.stringify(p.params);
+ return newTopicQuery.run({
+ data: {
+ topicName: params.topicName,
+ brokerId: '',
+ },
+ });
+ };
+ const endEditChooseBroker = (p: OKProps) => {
+ const topicParams = JSON.parse(editObjectTemp);
+ const { params } = p;
+ return commonQuery.run(`/api/op_modify/admin_modify_topic_info`, {
+ data: {
+ borkerId: params.selectBroker.join(','),
+ confModAuthToken: p.psw,
+ ...topicParams,
+ },
+ });
+ };
+
+ const deleteTopic = (type: string, p: OKProps) => {
+ const { params } = p;
+ return commonQuery.run(`/api/op_modify/admin_delete_topic_info`, {
+ data: {
+ brokerId: params.selectBroker.join(','),
+ confModAuthToken: p.psw,
+ modifyUser: userInfo.userName,
+ topicName: params.topicName,
+ },
+ });
+ };
+ const deleteConsumeGroup = (type: string, p: OKProps) => {
+ const { params } = p;
+ return commonQuery.run(
+ `/api/op_modify/admin_delete_allowed_consumer_group_info`,
+ {
+ data: {
+ groupName: params.groupName,
+ confModAuthToken: p.psw,
+ topicName: params.topicName,
+ },
+ }
+ );
+ };
+ const topicStateChange = (type: string, p: OKProps) => {
+ const { params } = p;
+ const data: any = {
+ brokerId: params.selectBroker.join([',']),
+ confModAuthToken: p.psw,
+ modifyUser: userInfo.userName,
+ topicName: params.topicName,
+ };
+ if (params.type === 'isSrvAcceptPublish') {
+ data.acceptPublish = params.value;
+ }
+ if (params.type === 'isSrvAcceptSubscribe') {
+ data.acceptSubscribe = params.value;
+ }
+
+ return commonQuery.run(`/api/op_modify/admin_modify_topic_info`, {
+ data,
+ });
+ };
+
+ const authorizeControl = (type: string, p: OKProps) => {
+ const { params } = p;
+ const data: any = {
+ confModAuthToken: p.psw,
+ topicName: params.topicName,
+ isEnable: params.value,
+ modifyUser: userInfo.userName,
+ };
+
+ return commonQuery.run(`/api/op_modify/admin_set_topic_authorize_control`,
{
+ data,
+ });
+ };
+
+ return <></>;
+};
+
+export default Comp;
diff --git a/web/src/react-app-env.d.ts b/web/src/react-app-env.d.ts
new file mode 100644
index 0000000..6431bc5
--- /dev/null
+++ b/web/src/react-app-env.d.ts
@@ -0,0 +1 @@
+/// <reference types="react-scripts" />
diff --git a/web/src/router.tsx b/web/src/router.tsx
new file mode 100644
index 0000000..46df97d
--- /dev/null
+++ b/web/src/router.tsx
@@ -0,0 +1,55 @@
+import React, { Suspense, lazy, useState } from 'react';
+import {
+ BrowserRouter as Router,
+ Switch,
+ Route,
+ Redirect,
+} from 'react-router-dom';
+import { PageLoading } from '@ant-design/pro-layout';
+import { hot } from 'react-hot-loader/root';
+import { Layout } from '@/components';
+import routes from '@/routes';
+import GlobalContext from '@/context/globalContext';
+
+const App = () => {
+ const [cluster, setCluster] = useState();
+ const [breadMap, setBreadMap] = useState();
+ // eslint-disable-next-line
+ const [userInfo, setUserInfo] = useState({
+ userName: 'webapi',
+ });
+
+ return (
+ <GlobalContext.Provider
+ value={{ cluster, setCluster, breadMap, setBreadMap, userInfo }}
+ >
+ <Router>
+ <Layout>
+ <Suspense fallback={<PageLoading />}>
+ <Switch>
+ {routes.map((route, index: number) => (
+ <Route
+ key={index}
+ path={route.path}
+ exact={route.exact}
+ strict={route.strict}
+ render={props => {
+ const LazyComponent = lazy(route.component);
+ return <LazyComponent {...props} />;
+ }}
+ />
+ ))}
+ </Switch>
+ <Route
+ exact
+ path="/"
+ render={() => <Redirect to="/issue" push />}
+ />
+ </Suspense>
+ </Layout>
+ </Router>
+ </GlobalContext.Provider>
+ );
+};
+
+export default process.env.NODE_ENV === 'development' ? hot(App) : App;
diff --git a/web/src/routes/index.tsx b/web/src/routes/index.tsx
new file mode 100644
index 0000000..ee36fa2
--- /dev/null
+++ b/web/src/routes/index.tsx
@@ -0,0 +1,37 @@
+import { RouteProps } from '@/typings';
+
+const routes: RouteProps[] = [
+ {
+ path: '/issue/:id',
+ component: () => import('@/pages/Issue/consumeGroupDetail'),
+ },
+ {
+ path: '/issue',
+ component: () => import('@/pages/Issue'),
+ },
+ {
+ path: '/broker/:id',
+ component: () => import('@/pages/Broker/detail'),
+ },
+ {
+ path: '/broker',
+ component: () => import('@/pages/Broker'),
+ },
+ {
+ path: '/topic/:name',
+ component: () => import('@/pages/Topic/detail'),
+ },
+ {
+ path: '/topic',
+ component: () => import('@/pages/Topic'),
+ },
+ {
+ path: '/cluster',
+ component: () => import('@/pages/Cluster'),
+ },
+ {
+ component: () => import('@/pages/NotFound'),
+ },
+];
+
+export default routes;
diff --git a/web/src/serviceWorker.ts b/web/src/serviceWorker.ts
new file mode 100644
index 0000000..109ab0e
--- /dev/null
+++ b/web/src/serviceWorker.ts
@@ -0,0 +1,146 @@
+// This optional code is used to register a service worker.
+// register() is not called by default.
+
+// This lets the app load faster on subsequent visits in production, and gives
+// it offline capabilities. However, it also means that developers (and users)
+// will only see deployed updates on subsequent visits to a page, after all the
+// existing tabs open on the page have been closed, since previously cached
+// resources are updated in the background.
+
+// To learn more about the benefits of this model and instructions on how to
+// opt-in, read https://bit.ly/CRA-PWA
+
+const isLocalhost = Boolean(
+ window.location.hostname === 'localhost' ||
+ // [::1] is the IPv6 localhost address.
+ window.location.hostname === '[::1]' ||
+ // 127.0.0.0/8 are considered localhost for IPv4.
+ window.location.hostname.match(
+ /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
+ )
+);
+
+type Config = {
+ onSuccess?: (registration: ServiceWorkerRegistration) => void;
+ onUpdate?: (registration: ServiceWorkerRegistration) => void;
+};
+
+export function register(config?: Config) {
+ if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
+ // The URL constructor is available in all browsers that support SW.
+ const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
+ if (publicUrl.origin !== window.location.origin) {
+ // Our service worker won't work if PUBLIC_URL is on a different origin
+ // from what our page is served on. This might happen if a CDN is used to
+ // serve assets; see
https://github.com/facebook/create-react-app/issues/2374
+ return;
+ }
+
+ window.addEventListener('load', () => {
+ const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
+
+ if (isLocalhost) {
+ // This is running on localhost. Let's check if a service worker still
exists or not.
+ checkValidServiceWorker(swUrl, config);
+
+ // Add some additional logging to localhost, pointing developers to the
+ // service worker/PWA documentation.
+ navigator.serviceWorker.ready.then(() => {
+ console.log(
+ 'This web app is being served cache-first by a service ' +
+ 'worker. To learn more, visit https://bit.ly/CRA-PWA'
+ );
+ });
+ } else {
+ // Is not localhost. Just register service worker
+ registerValidSW(swUrl, config);
+ }
+ });
+ }
+}
+
+function registerValidSW(swUrl: string, config?: Config) {
+ navigator.serviceWorker
+ .register(swUrl)
+ .then(registration => {
+ registration.onupdatefound = () => {
+ const installingWorker = registration.installing;
+ if (installingWorker == null) {
+ return;
+ }
+ installingWorker.onstatechange = () => {
+ if (installingWorker.state === 'installed') {
+ if (navigator.serviceWorker.controller) {
+ // At this point, the updated precached content has been fetched,
+ // but the previous service worker will still serve the older
+ // content until all client tabs are closed.
+ console.log(
+ 'New content is available and will be used when all ' +
+ 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
+ );
+
+ // Execute callback
+ if (config && config.onUpdate) {
+ config.onUpdate(registration);
+ }
+ } else {
+ // At this point, everything has been precached.
+ // It's the perfect time to display a
+ // "Content is cached for offline use." message.
+ console.log('Content is cached for offline use.');
+
+ // Execute callback
+ if (config && config.onSuccess) {
+ config.onSuccess(registration);
+ }
+ }
+ }
+ };
+ };
+ })
+ .catch(error => {
+ console.error('Error during service worker registration:', error);
+ });
+}
+
+function checkValidServiceWorker(swUrl: string, config?: Config) {
+ // Check if the service worker can be found. If it can't reload the page.
+ fetch(swUrl, {
+ headers: { 'Service-Worker': 'script' },
+ })
+ .then(response => {
+ // Ensure service worker exists, and that we really are getting a JS
file.
+ const contentType = response.headers.get('content-type');
+ if (
+ response.status === 404 ||
+ (contentType != null && contentType.indexOf('javascript') === -1)
+ ) {
+ // No service worker found. Probably a different app. Reload the page.
+ navigator.serviceWorker.ready.then(registration => {
+ registration.unregister().then(() => {
+ window.location.reload();
+ });
+ });
+ } else {
+ // Service worker found. Proceed as normal.
+ registerValidSW(swUrl, config);
+ }
+ })
+ .catch(() => {
+ console.log(
+ 'No internet connection found. App is running in offline mode.'
+ );
+ });
+}
+
+export function unregister() {
+ if ('serviceWorker' in navigator) {
+ navigator.serviceWorker.ready
+ .then(registration => {
+ registration.unregister();
+ })
+ .catch(error => {
+ console.error(error.message);
+ });
+ }
+}
diff --git a/web/src/setupProxy.js b/web/src/setupProxy.js
new file mode 100644
index 0000000..1340128
--- /dev/null
+++ b/web/src/setupProxy.js
@@ -0,0 +1,12 @@
+// eslint-disable-next-line @typescript-eslint/no-var-requires
+const { createProxyMiddleware } = require('http-proxy-middleware');
+
+module.exports = function(app) {
+ app.use(
+ createProxyMiddleware('/webapi.htm', {
+ target: 'http://127.0.0.1:8080', // should set api address
+ changeOrigin: true,
+ ws: true,
+ })
+ );
+};
diff --git a/web/src/store/global.ts b/web/src/store/global.ts
new file mode 100644
index 0000000..18d8693
--- /dev/null
+++ b/web/src/store/global.ts
@@ -0,0 +1,30 @@
+import { createStore } from '@reactseed/use-redux';
+
+export interface TState {
+ name: string;
+ age: number;
+}
+
+export interface TMethod {
+ updateName: (name: string) => void;
+ becomeOlder: () => void;
+}
+
+const store = createStore(() => ({
+ age: 20,
+ name: 'reactseed',
+}));
+
+const methods = (state: TState): TMethod => {
+ const { age } = state;
+ return {
+ updateName: (name: string) => {
+ state.name = name;
+ },
+ becomeOlder: () => {
+ state.age = age + 1;
+ },
+ };
+};
+
+export { store, methods };
diff --git a/web/src/typings/index.ts b/web/src/typings/index.ts
new file mode 100644
index 0000000..164ab50
--- /dev/null
+++ b/web/src/typings/index.ts
@@ -0,0 +1 @@
+export * from './router';
diff --git a/web/src/typings/router.ts b/web/src/typings/router.ts
new file mode 100644
index 0000000..38e855d
--- /dev/null
+++ b/web/src/typings/router.ts
@@ -0,0 +1,14 @@
+import { Route as LayoutRoute } from '@ant-design/pro-layout/lib/typings';
+import { RouteProps as ReactRouteProps } from 'react-router-dom';
+
+export interface Route extends LayoutRoute {
+ paths?: string[];
+}
+
+export type OmitRouteProps = Omit<ReactRouteProps, 'component'> & {
+ component: () => Promise<{ default: any }>;
+};
+
+export interface RouteProps extends OmitRouteProps {
+ component: () => Promise<{ default: any }>;
+}
diff --git a/web/src/utils/index.ts b/web/src/utils/index.ts
new file mode 100644
index 0000000..e410d91
--- /dev/null
+++ b/web/src/utils/index.ts
@@ -0,0 +1,45 @@
+import { isObject, isEmpty } from 'lodash';
+
+export const isDevelopEnv = () => {
+ return process.env.NODE_ENV === 'development';
+};
+
+export const isEmptyParam = (value: any): boolean => {
+ if (Array.isArray(value)) {
+ // value为数组
+ return !value.length;
+ }
+ if (isObject(value)) {
+ // value为对象
+ return isEmpty(value);
+ }
+ if (typeof value === 'undefined') {
+ // value为undefinded
+ return true;
+ }
+ if (Number.isFinite(value)) {
+ // value为数值
+ return false;
+ }
+ // value为默认值
+ return !value;
+};
+
+export const boolean2Chinese = (value: boolean | string): string => {
+ let v: boolean;
+ if (value === 'false') {
+ v = false;
+ } else if (value === 'true') {
+ v = true;
+ } else {
+ v = value as boolean;
+ }
+ return !v ? '否' : '是';
+};
+
+export const transParamsWithConstantsMap = (
+ map: any,
+ paramsName: string
+): string => {
+ return map[paramsName] || paramsName;
+};
diff --git a/web/tsconfig.json b/web/tsconfig.json
new file mode 100644
index 0000000..0124036
--- /dev/null
+++ b/web/tsconfig.json
@@ -0,0 +1,33 @@
+{
+ "extends": "./tsconfig.paths.json",
+ "compilerOptions": {
+ "typeRoots": [
+ "./typings"
+ ],
+ "target": "es5",
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "esnext"
+ ],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react"
+ },
+ "exclude": [
+ "build",
+ "node_modules"
+ ],
+ "include": [
+ "src"
+ ]
+}
diff --git a/web/tsconfig.paths.json b/web/tsconfig.paths.json
new file mode 100644
index 0000000..5879100
--- /dev/null
+++ b/web/tsconfig.paths.json
@@ -0,0 +1,8 @@
+{
+ "compilerOptions": {
+ "baseUrl": "src",
+ "paths": {
+ "@/*": ["*"]
+ }
+ }
+}