diff --git a/manager/build.sh b/build.sh
similarity index 64%
rename from manager/build.sh
rename to build.sh
index 6325a31..e44fe2c 100644
--- a/manager/build.sh
+++ b/build.sh
@@ -18,35 +18,37 @@
echo "build doris manager home path"
-#echo "build doris manager web start"
-#cd doris-manager
-#npm install
-#npm run build
-#echo "build doris manager web end"
-#
-#cd ../
-#echo "copy doris manager web resources to server"
-#rm -rf manager-server/src/main/resources/web-resource
-#mv doris-manager/dist manager-server/src/main/resources/web-resource
-#echo "copy doris manager web resources to server end"
+echo "build doris manager frontend start"
+cd frontend
+npm install
+npm run build
+echo "build doris manager frontend end"
+
+cd ../
+echo "copy doris manager web resources to server"
+rm -rf manager/manager-server/src/main/resources/web-resource
+mv frontend/dist manager/manager-server/src/main/resources/web-resource
+echo "copy doris manager web resources to server end"
echo "build doris manager server start"
+rm -rf output
+rm -rf doris-manager-1.0.0.tar.gz
+mkdir -p output
+mkdir -p output/server/lib
+cd manager
set -e
mvn clean install
echo "build doris manager server end"
echo "copy to output package start"
-rm -rf output
-rm -rf doris-manager-1.0.0.tar.gz
-mkdir -p output
-mkdir -p output/server/lib
-mv manager-server/target/manager-server-1.0.0.jar output/server/lib/doris-manager.jar
-cp -r conf output/server/
-cp -r manager-bin output/
+cd ../
+mv manager/manager-server/target/manager-server-1.0.0.jar output/server/lib/doris-manager.jar
+cp -r manager/conf output/server/
+cp -r manager/manager-bin output/
mv output/manager-bin/agent output/
mv output/manager-bin output/server/bin
mkdir -p output/agent/lib
-mv dm-agent/target/dm-agent-1.0.0.jar output/agent/lib/dm-agent.jar
-cp -r manager-server/src/main/resources/web-resource output/server/
+mv manager/dm-agent/target/dm-agent-1.0.0.jar output/agent/lib/dm-agent.jar
+cp -r manager/manager-server/src/main/resources/web-resource output/server/
tar -zcvf doris-manager-1.0.0.tar.gz output/
echo "copy to output package end"
\ No newline at end of file
diff --git a/manager/doc/Doris Manager Compiling & Deploying.md b/docs/Doris Manager Compiling & Deploying.md
similarity index 95%
rename from manager/doc/Doris Manager Compiling & Deploying.md
rename to docs/Doris Manager Compiling & Deploying.md
index 2aa6fe7..f4b090b 100644
--- a/manager/doc/Doris Manager Compiling & Deploying.md
+++ b/docs/Doris Manager Compiling & Deploying.md
@@ -2,7 +2,7 @@
## 一、代码编译和运行
### 一)编译
-直接运行manager路径下的build.sh脚本,会在manager路径下生成安装运行包——output,包中包括:
+直接运行build.sh脚本,会在manager路径下生成安装运行包——output,包中包括:
1、agent目录:Doris manager agent的安装包,是拷贝到部署doris服务的节点的安装包;
diff --git a/manager/doc/Doris Manger Cluster Managenent.md b/docs/Doris Manger Cluster Managenent.md
similarity index 100%
rename from manager/doc/Doris Manger Cluster Managenent.md
rename to docs/Doris Manger Cluster Managenent.md
diff --git a/manager/doc/Doris Manger Initializing.md b/docs/Doris Manger Initializing.md
similarity index 100%
rename from manager/doc/Doris Manger Initializing.md
rename to docs/Doris Manger Initializing.md
diff --git a/manager/doc/Doris Manger System Settings.md b/docs/Doris Manger System Settings.md
similarity index 100%
rename from manager/doc/Doris Manger System Settings.md
rename to docs/Doris Manger System Settings.md
diff --git a/manager/doc/Doris Manger User Space Management.md b/docs/Doris Manger User Space Management.md
similarity index 100%
rename from manager/doc/Doris Manger User Space Management.md
rename to docs/Doris Manger User Space Management.md
diff --git a/frontend/.babelrc b/frontend/.babelrc
new file mode 100644
index 0000000..2d33edd
--- /dev/null
+++ b/frontend/.babelrc
@@ -0,0 +1,43 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+{
+ "presets": [
+ [
+ "@babel/preset-env",
+ {
+ "targets": {
+ "esmodules": true
+ }
+ }
+ ],
+ "@babel/preset-react",
+ "@babel/preset-typescript"
+
+ ],
+ "plugins": [
+ [
+ "import",
+ {
+ "libraryName": "antd",
+ "style": "css"
+ }
+ ],
+ ["@babel/plugin-proposal-decorators", { "legacy": true }]
+ ],
+ "plugins": [["import", { "libraryName": "antd", "libraryDirectory": "lib", "style": true}, "antd"],]
+}
diff --git a/frontend/.editorconfig b/frontend/.editorconfig
new file mode 100644
index 0000000..ed72c26
--- /dev/null
+++ b/frontend/.editorconfig
@@ -0,0 +1,13 @@
+# http://editorconfig.org
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 4
+indent_style = space
+insert_final_newline = true
+max_line_length = 80
+trim_trailing_whitespace = true
+
+[*.md]
+max_line_length = 0
\ No newline at end of file
diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js
new file mode 100644
index 0000000..0b0af8c
--- /dev/null
+++ b/frontend/.eslintrc.js
@@ -0,0 +1,33 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+/** @format */
+
+module.exports = {
+ parser: '@typescript-eslint/parser',
+ extends: ['plugin:prettier/recommended', 'plugin:@typescript-eslint/recommended', "plugin:react/recommended", "plugin:react/jsx-runtime"],
+ parserOptions: {
+ ecmaVersion: 2019,
+ sourceType: 'module',
+ },
+ env: {
+ browser: true,
+ node: true,
+ },
+ rules: {
+ },
+};
diff --git a/frontend/.gitignore b/frontend/.gitignore
new file mode 100644
index 0000000..f30fe51
--- /dev/null
+++ b/frontend/.gitignore
@@ -0,0 +1,16 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+node_modules
+package-lock.json
+
+# production
+/dist
+
+#mac
+.DS_Store
+
+#npm && yarn
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
diff --git a/frontend/.lintstagedrc b/frontend/.lintstagedrc
new file mode 100644
index 0000000..cdf9678
--- /dev/null
+++ b/frontend/.lintstagedrc
@@ -0,0 +1,7 @@
+{
+ "src/**/*.js": [
+ "eslint --fix",
+ "prettier --write",
+ "git add"
+ ]
+}
\ No newline at end of file
diff --git a/frontend/.prettierrc.js b/frontend/.prettierrc.js
new file mode 100644
index 0000000..85e517c
--- /dev/null
+++ b/frontend/.prettierrc.js
@@ -0,0 +1,30 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+/** @format */
+
+module.exports = {
+ printWidth: 120,
+ semi: true,
+ singleQuote: true,
+ trailingComma: 'all',
+ bracketSpacing: true,
+ arrowParens: 'avoid',
+ insertPragma: false,
+ tabWidth: 4,
+ useTabs: false
+}
diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 0000000..a60ee0a
--- /dev/null
+++ b/frontend/README.md
@@ -0,0 +1,95 @@
+
+# Doris Manager Frontend Style Guide
+
+## 整体风格
+### 单一职责原则SRP(https://wikipedia.org/wiki/Single_responsibility_principle)
++ 每个文件只定义一个内容(减少心智负担)
++ 单文件要求不超过400行,超出进行拆分(易阅读)
++ 尽量定义内容小的函数,函数体不要过长
+
+### 文件/文件夹夹命名:
+- 字母全部小写
+- 不带空格
+- 用中划线(-)和 (.) 连接单词
+- 规范为[feature].[type].ts(如ab-cd.hooks.ts)
+- 建议使用英文单词全拼(易于理解,避免英文不好缩写错误),不好命名的可使用
+https://unbug.github.io/codelf/ 进行搜索看看大家怎么命名
+
+### Interface / Class 命名:
+- Upper Camel Case 大驼峰命名,强类型语言惯例
+- 文件名称为: [feature].interface.ts
+- 单文件原则,尽量为 Interface 独立一个文件
+- TypeScript Type 和 Interface 在同一个文件里
+
+### 常量、配置、枚举命名
+- 单文件原则,尽量为常量,枚举独立一个文件
+- 文件名称为:[feature].data.ts
+- 常量和配置都用 const 关键字,使用大写下划线命名
+```
+const PAGINATIONS = [10, 20, 50, 100, 200];
+const SERVER_URL = 'http://127.0.0.1:8001';
+```
+- 枚举使用 Upper Camel Case 大驼峰命名,且后面加上Enum与 Interface / Class 区分
+```
+enum ImportTaskStatusEnum {
+ Created: 0,
+ Running: 1,
+ Failed: 2,
+}
+```
+
+### 行尾逗号
+- 全部打开,便于在修改后在Git上看到清晰的比对(修改时追加 “,” 会让Git比对从一行变成2行)
+
+### 组件
+- 尽量不要在.tsx的组件里写太多逻辑,尽可能用hooks或工具类/服务(service)拆出去,保证视图层的干净清爽
+
+### 圈复杂度(Cyclomatic complexity, CC)
+
+参考:http://kaelzhang81.github.io/2017/06/18/%E8%AF%A6%E8%A7%A3%E5%9C%88%E5%A4%8D%E6%9D%82%E5%BA%A6/
+
+VS Code插件:CodeMetrics
+
+使用圈复杂度插件检查代码的负责度,可做参考,因为React本身负责度过高,心智负担重,圈复杂度插件检查主要还是在独立函数中可以作为参考
+
+### 单词拼写检查
+
+VS Code插件:Code Spell Checker
+
+使用Code Spell Checker检查单词拼写,错误单词会以绿色波浪线提示,遇到特殊名词可以右键点击添加到文件夹目录,到将其添加cspell.json中。
+
+### Import Sorter
+
+VS Code插件:Typescript Imports Sort
+设置 typescript.extension.sortImports.sortOnSave 为 true,在保存时自动sort import内容
+
+# HOW TO START
+require NodeJS > 10.0
+
+INSTALL DEPENDENCE
+
+```npm install```
+
+START SERVER
+
+```npm run start```
+
+
+# HOW TO BUILD
+
+```npm run build```
\ No newline at end of file
diff --git a/frontend/babel.config.js b/frontend/babel.config.js
new file mode 100644
index 0000000..419fe02
--- /dev/null
+++ b/frontend/babel.config.js
@@ -0,0 +1,24 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+"@babel/preset-react";
+module.exports = function (api) {
+ api.cache(true);
+ return {
+ presets: ["@babel/preset-react"],
+ };
+};
diff --git a/frontend/config/webpack.base.config.js b/frontend/config/webpack.base.config.js
new file mode 100644
index 0000000..312ae5a
--- /dev/null
+++ b/frontend/config/webpack.base.config.js
@@ -0,0 +1,164 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+const path = require('path');
+const HtmlWebpackPlugin = require('html-webpack-plugin');
+const MiniCssExtractPlugin = require('mini-css-extract-plugin');
+const devMode = process.env.NODE_ENV === 'dev' || process.env.NODE_ENV === 'development';
+const themes = require('../theme')();
+const ESLintPlugin = require('eslint-webpack-plugin');
+
+const postCssLoader = () => {
+ return {
+ loader: 'postcss-loader',
+ options: {
+ postcssOptions: {
+ plugins: [require('autoprefixer'), require('cssnano')],
+ },
+ },
+ };
+};
+
+module.exports = {
+ entry: {
+ index: ['react-hot-loader/patch'].concat([path.resolve(__dirname, '../src/index.tsx')]),
+ },
+ output: {
+ path: path.join(__dirname, '../dist/'),
+ publicPath: '/',
+ filename: 'assets/js/[name].[contenthash:8].js',
+ chunkFilename: 'assets/js/[name].[contenthash:8].js',
+ sourceMapFilename: 'assets/js/[name].[contenthash:8].js.map',
+ },
+ resolve: {
+ alias: {
+ '@src': path.resolve(__dirname, '../src'),
+ '@assets': path.resolve(__dirname, '../src/assets'),
+ '@components': path.resolve(__dirname, '../src/components'),
+ '@models': path.resolve(__dirname, '../src/models'),
+ '@router': path.resolve(__dirname, '../src/router'),
+ '@pages': path.resolve(__dirname, '../src/pages'),
+ '@utils': path.resolve(__dirname, '../src/utils'),
+ '@tools': path.resolve(__dirname, '../src/tools'),
+ 'bn.js': path.resolve(process.cwd(), 'node_modules', 'bn.js')
+ },
+ extensions: ['.ts', '.tsx', '.js', '.jsx'],
+ fallback: {
+ crypto: require.resolve('crypto-browserify'),
+ buffer: require.resolve('buffer'),
+ stream: require.resolve('stream-browserify'),
+ },
+ },
+ module: {
+ rules: [
+ {
+ test: /\.ts[x]?$/,
+ exclude: /node_modules/,
+ use: {
+ loader: 'babel-loader',
+ options: {
+ presets: [['@babel/preset-env', { targets: 'defaults' }]],
+ },
+ },
+ },
+ {
+ test: /\.css$/,
+ use: [
+ devMode ? { loader: 'style-loader' } : MiniCssExtractPlugin.loader,
+ {
+ loader: 'css-loader',
+ options: {
+ sourceMap: true,
+ },
+ },
+ {
+ loader: 'postcss-loader',
+ options: {
+ sourceMap: true,
+ },
+ },
+ ],
+ },
+ // For pure CSS (without CSS modules)
+ {
+ test: /\.less?$/,
+ include: /node_modules/,
+ use: [
+ devMode ? { loader: 'style-loader' } : MiniCssExtractPlugin.loader,
+ {
+ loader: 'css-loader',
+ },
+ postCssLoader(),
+ {
+ loader: 'less-loader',
+ options: {
+ lessOptions: {
+ modifyVars: {...themes},
+ javascriptEnabled: true,
+ },
+ implementation: require('less'),
+ },
+ },
+ ],
+ },
+ // For CSS modules
+ {
+ test: /\.less?$/,
+ exclude: /node_modules/,
+ use: [
+ devMode ? { loader: 'style-loader' } : MiniCssExtractPlugin.loader,
+ {
+ loader: 'css-loader',
+ options: {
+ modules: true,
+ },
+ },
+ postCssLoader(),
+ {
+ loader: 'less-loader',
+ options: {
+ lessOptions: {
+ modifyVars: {...themes},
+ javascriptEnabled: true,
+ },
+ implementation: require('less'),
+ },
+ },
+ ],
+ },
+ {
+ test: /\.(svg|png|jpg|gif|ttf|eot|otf|svg|woff(2)?)(\?[a-z0-9]+)?$/,
+ loader: 'url-loader',
+ options: {
+ limit: 2 * 1024,
+ name: '[path][name].[ext]',
+ },
+ },
+ ],
+ },
+ plugins: [
+ new HtmlWebpackPlugin({
+ filename: 'index.html',
+ template: 'public/index.html',
+ inject: 'body',
+ minify: false,
+ }),
+
+ new ESLintPlugin(),
+
+ ],
+};
diff --git a/frontend/config/webpack.dev.config.js b/frontend/config/webpack.dev.config.js
new file mode 100644
index 0000000..f158d52
--- /dev/null
+++ b/frontend/config/webpack.dev.config.js
@@ -0,0 +1,54 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+const webpackBaseConfig = require("./webpack.base.config");
+const { merge } = require("webpack-merge");
+const SERVER_RD = 'http://127.0.0.1:8880';
+
+module.exports = merge(webpackBaseConfig, {
+ mode: "development",
+ devtool: "eval-source-map",
+ // 开发服务配置
+ devServer: {
+ host: "localhost",
+ port: 8084,
+ publicPath: "/",
+ disableHostCheck: true,
+ useLocalIp: false,
+ open: true,
+ overlay: true,
+ hot: true,
+ stats: {
+ assets: false,
+ colors: true,
+ modules: false,
+ children: false,
+ chunks: false,
+ chunkModules: false,
+ entrypoints: false,
+ },
+ proxy: {
+ '/api': {
+ target: SERVER_RD,
+ changeOrigin: true,
+ secure: false,
+ // pathRewrite: {'^/api': '/api'}
+ },
+ },
+ historyApiFallback: true,
+ },
+});
diff --git a/frontend/config/webpack.prod.config.js b/frontend/config/webpack.prod.config.js
new file mode 100644
index 0000000..a992579
--- /dev/null
+++ b/frontend/config/webpack.prod.config.js
@@ -0,0 +1,93 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+const webpackBaseConfig = require("./webpack.base.config");
+const { merge } = require("webpack-merge");
+const path = require("path");
+const TerserWebpackPlugin = require('terser-webpack-plugin');
+const MiniCssExtractPlugin = require('mini-css-extract-plugin');
+const { CleanWebpackPlugin } = require('clean-webpack-plugin');
+// const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
+const CopyPlugin = require('copy-webpack-plugin');
+
+module.exports = merge(webpackBaseConfig, {
+ mode: 'production',
+ output: {
+ publicPath: '/',
+ },
+ optimization: {
+ minimizer: [
+ new TerserWebpackPlugin({
+ terserOptions: {
+ compress: {
+ drop_console: true,
+ },
+ sourceMap: true,
+ },
+ parallel: true,
+ }),
+ ],
+ chunkIds: 'named',
+ moduleIds: 'deterministic',
+ splitChunks: {
+ name: false,
+ chunks: 'all',
+ minChunks: 1,
+ // maxAsyncRequests: 5,
+ // maxInitialRequests: 5,
+ cacheGroups: {
+ vendor: {
+ test: /node_modules/,
+ name: 'vendor',
+ chunks: 'all',
+ enforce: true,
+ },
+ antd: {
+ test: /antd?/,
+ name: 'antd',
+ priority: 10,
+ chunks: 'initial',
+ enforce: true,
+ },
+ react: {
+ test: /react|react-dom|mobx|prop-type/,
+ name: 'react',
+ priority: 10,
+ chunks: 'initial',
+ enforce: true,
+ },
+ },
+ },
+ runtimeChunk: {
+ name: entrypoint => `runtime-${entrypoint.name}`,
+ },
+ },
+ plugins: [
+ new CleanWebpackPlugin(),
+ new MiniCssExtractPlugin({
+ filename: 'assets/css/[name].[contenthash].css',
+ chunkFilename: 'assets/css/[name].[contenthash].css',
+ }),
+ new CopyPlugin({
+ patterns: [
+ { from: path.join(__dirname, '../src/assets/'), to: path.join(__dirname, '../dist/src/assets/') },
+ { from: path.join(__dirname, '../favicon.ico'), to: path.join(__dirname, '../dist/') }
+ ],
+ }),
+ // new BundleAnalyzerPlugin()
+ ],
+});
diff --git a/frontend/favicon.ico b/frontend/favicon.ico
new file mode 100644
index 0000000..48dd93b
Binary files /dev/null and b/frontend/favicon.ico differ
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..3d04969
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,109 @@
+{
+ "name": "webpack5",
+ "version": "1.0.0-beta",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "start": "npm run dev",
+ "dev": "cross-env NODE_ENV=development webpack serve",
+ "build": "cross-env NODE_ENV=production webpack"
+ },
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "@ant-design/icons": "^4.6.3",
+ "@ant-design/pro-card": "^1.16.2",
+ "@ant-design/pro-descriptions": "^1.9.28",
+ "@ant-design/pro-form": "^1.46.0",
+ "@ant-design/pro-layout": "^7.0.1-alpha.5",
+ "@ant-design/pro-list": "^1.17.7",
+ "@ant-design/pro-table": "^2.56.7",
+ "@babel/core": "^7.12.10",
+ "ahooks": "^3.1.10",
+ "antd": "^4.16.11",
+ "axios": "^0.24.0",
+ "babel-loader": "^8.2.2",
+ "buffer": "^6.0.3",
+ "cross-env": "^7.0.3",
+ "crypto-browserify": "^3.12.0",
+ "echarts": "^5.0.2",
+ "echarts-for-react": "^3.0.0",
+ "echarts-liquidfill": "^3.1.0",
+ "highlight.js": "^11.0.1",
+ "i18next": "^20.4.0",
+ "i18next-browser-languagedetector": "^6.1.2",
+ "js-cookie": "^3.0.1",
+ "lodash-es": "^4.17.21",
+ "password-generator": "^2.3.2",
+ "path-to-regexp": "^6.2.0",
+ "postcss": "^8.3.6",
+ "re-resizable": "^6.9.0",
+ "react": "^17.0.1",
+ "react-css-modules": "^4.7.11",
+ "react-dom": "^17.0.1",
+ "react-hot-loader": "^4.13.0",
+ "react-i18next": "^11.11.4",
+ "react-router-cache-route": "^1.12.1",
+ "react-router-dom": "^5.2.0",
+ "recoil": "^0.5.0",
+ "stream-browserify": "^3.0.0",
+ "sweetalert2": "^11.1.9",
+ "sweetalert2-react-content": "^4.1.1",
+ "swr": "^0.5.6",
+ "ttag": "1.7.15",
+ "typescript": "^4.3.5",
+ "webpack": "^5.11.0",
+ "webpack-merge": "^5.7.3"
+ },
+ "devDependencies": {
+ "@babel/plugin-proposal-decorators": "^7.14.5",
+ "@babel/preset-env": "^7.12.11",
+ "@babel/preset-react": "^7.12.10",
+ "@babel/preset-typescript": "^7.12.7",
+ "@types/jest": "^26.0.20",
+ "@types/js-cookie": "^2.2.7",
+ "@types/lodash-es": "^4.17.6",
+ "@types/node": "^16.7.1",
+ "@types/react": "^17.0.17",
+ "@types/react-css-modules": "^4.6.4",
+ "@types/react-dom": "^17.0.9",
+ "@types/react-router": "^5.1.16",
+ "@types/react-router-config": "^5.0.1",
+ "@types/react-router-dom": "^5.1.8",
+ "@typescript-eslint/eslint-plugin": "^4.9.1",
+ "@typescript-eslint/parser": "^4.1.1",
+ "autoprefixer": "^10.3.2",
+ "babel-plugin-import": "^1.13.3",
+ "babel-plugin-ttag": "^1.7.26",
+ "clean-webpack-plugin": "^3.0.0",
+ "copy-webpack-plugin": "^9.0.1",
+ "css-loader": "^5.0.1",
+ "cssnano": "^5.0.8",
+ "eslint": "^8.11.0",
+ "eslint-config-prettier": "^8.5.0",
+ "eslint-plugin-prettier": "^4.0.0",
+ "eslint-plugin-react": "^7.29.4",
+ "eslint-webpack-plugin": "^3.1.1",
+ "file-loader": "^6.2.0",
+ "html-webpack-plugin": "^4.5.0",
+ "less": "^4.1.1",
+ "less-loader": "^10.0.1",
+ "lint-staged": "^12.3.5",
+ "mini-css-extract-plugin": "^2.2.0",
+ "postcss-flexbugs-fixes": "^5.0.2",
+ "postcss-loader": "^6.1.1",
+ "postcss-normalize": "^10.0.0",
+ "postcss-preset-env": "^6.7.0",
+ "prettier": "^2.3.2",
+ "react-router-config": "^5.1.1",
+ "style-loader": "^2.0.0",
+ "ts-jest": "^26.4.4",
+ "url-loader": "^4.1.1",
+ "webpack-bundle-analyzer": "^4.5.0",
+ "webpack-cli": "^4.3.0",
+ "webpack-dev-server": "^3.11.0"
+ },
+ "lint-staged": {
+ "*.js": "eslint --cache --fix"
+ }
+}
diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js
new file mode 100644
index 0000000..396ba2c
--- /dev/null
+++ b/frontend/postcss.config.js
@@ -0,0 +1,41 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+/**
+ * /* eslint-disable global-require, import/no-extraneous-dependencies, @typescript-eslint/no-var-requires
+ *
+ * @format
+ */
+
+module.exports = {
+ plugins: [
+ require('autoprefixer')(),
+ // 修复一些 flex 的 bug
+ require('postcss-flexbugs-fixes'),
+ // 支持一些现代浏览器 CSS 特性,支持 browserslist
+ require('postcss-preset-env')({
+ // 自动添加浏览器头
+ autoprefixer: {
+ // will add prefixes only for final and IE versions of specification
+ flexbox: 'no-2009'
+ },
+ stage: 3
+ }),
+ // 根据 browserslist 自动导入部分 normalize.css 的内容
+ require('postcss-normalize')
+ ]
+};
diff --git a/frontend/public/index.html b/frontend/public/index.html
new file mode 100644
index 0000000..ebb6464
--- /dev/null
+++ b/frontend/public/index.html
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+ Doris Manager
+
+
+
+
+
diff --git a/frontend/public/locales/en-us.json b/frontend/public/locales/en-us.json
new file mode 100644
index 0000000..418dd52
--- /dev/null
+++ b/frontend/public/locales/en-us.json
@@ -0,0 +1,318 @@
+{
+ "username": "Username",
+ "password": "Password",
+ "OldPassword":"Old Password",
+ "PleaseEnterTheOldPassword":"Please enter the old password",
+ "NewPassword":"New Password",
+ "ContainAtLeast2Types":"Contain at least 2 types of English uppercase, lowercase English, numbers and special symbols",
+ "PleaseEnterTheNewPassword":"Please enter the new password",
+ "NewPasswordCannotBeEmpty":"New password cannot be empty",
+ "OldPasswordCannotBeEmpty":"Old password cannot be empty",
+ "ConfirmPassword":"Confirm Password",
+ "ConfirmPasswordCanNotBeBlank":"Confirm password can not be blank!",
+ "TwoPasswordEntriesAreInconsistent":"Two password entries are inconsistent",
+ "PleaseConfirmYourNewPassword":"Please confirm your new password",
+ "Least6":"The password must be at least 6 characters in length",
+ "Save":"save",
+ "signOut": "Sign out",
+ "loginWarning":"Incorrect username or password",
+
+ "exitSuccessfully":"Exit successfully",
+ "editor": "Editor",
+ "format": "Format",
+ "clear": "Clear the editor",
+ "execute": "Execute",
+ "tableStructure": "Table Schema",
+ "dataPreview": "Data Preview",
+ "queryForm": "Query",
+ "runSuccessfully": "Run successfully",
+ "results": "Results",
+ "selectTheFile": "Select File",
+ "loadConfig": "Configurations",
+ "previous": "Previous",
+ "next": "Next",
+ "cancel": "Cancel",
+ "dataImport": "Data Import",
+ "table": "Table",
+ "delimiter": "Column Separator",
+ "importResults": "Import Results",
+ "errMsg":"Error request, please try again later",
+ "display10":"Display up to 10 lines",
+ "refresh":"Refresh",
+ "startingTime":"Starting Time",
+ "endTime":"End Time",
+ "currentDatabase":"Current Database",
+ "executionFailed":"Execution failed",
+ "uploadWarning":"Please select file",
+ "upload": "Upload",
+ "delimiterWarning":"Please select a separator",
+ "uploadedFiles":"Uploaded Files",
+ "filePreview":"File Preview",
+ "database":"Database",
+ "notice":"Notice",
+ "login":"Login",
+ "Mail":"Mail",
+ "loginExpirtMsg":"The login information has expired. Click OK to jump to the login page.",
+ "nextStep":"Next",
+ "previousStep":"Previous",
+ "pleaseEnter":"Please enter",
+ "cancelButton":"Cancel",
+ "loadButton":"Import",
+ "successfulOperation":"The operation was successful",
+ "tips":"Tips",
+ "fileSizeWarning": "File size cannot exceed 100M",
+ "selectWarning": "Please select a table",
+ "executionTime": "Execution Time",
+ "search":"Search",
+
+ "noData":"No data",
+ "data": "Data",
+ "queryProfileFirst":"please open Query Profile,then you can see the result list",
+ "namespace": "Current space name",
+ "backTo": "Back to",
+
+ "viewTime": "View time",
+ "clusterInformation": "Cluster Information",
+ "dataMonitoring": "Data Monitoring",
+ "QPS": "QPS (queries per second)",
+ "99th": "99th percentile query delay (ms)",
+ "queryErrorRate": "Query error rate",
+ "numberOfConnections": "Number of connections",
+ "importRate": "Import transaction commit / success / failure frequency",
+ "clusterScheduling":"Cluster scheduling",
+ "unhealthyFragmentation":"Unhealthy fragmentation",
+
+ "numberOfFeNodes":"Number of Fe nodes",
+ "FeNumberOfSurvivingNodes":"Fe number of surviving nodes",
+ "NumberOfBENodes":"Number of be nodes",
+ "BeNumberOfSurvivingNodes":"Fe number of surviving nodes",
+ "totalSpace":"Total space",
+ "usedSpace":"used space",
+ "CPUidleRate":"CPU idle rate (%)",
+ "nodeMemoryUsage":"Node memory usage (MB)",
+ "PleaseSelectNode":"Please select node",
+ "IOutilization":"IO utilization (%)",
+ "nodeSelection":"Node selection",
+
+ "required":"The field is required",
+ "confirmPassword": "Confirm password",
+ "inconsistentPasswords":"Inconsistent passwords",
+
+ "spaceInfo":"Space Information",
+ "clusterInfo":"cluster Information",
+ "spaceName":"Space Name",
+ "spaceIntroduction":"Space Introduction",
+ "adminName":"Admin Name",
+ "adminEmail":"Admin Email",
+ "adminpsw":"Admin Password",
+ "clusterAddr":"Cluster Address",
+ "httpPort":"HTTP Port",
+ "JDBCPort":"JDBC Port",
+ "userName":"User Name",
+ "userPwd":"User Password",
+ "linkTest":"Link Test",
+ "submit":"Submit",
+ "BENodeStatusMonitoring":"BE Node status monitoring",
+
+ "accountSettings":"Account Settings",
+ "Logout":"Logout",
+ "Help":"Help",
+ "IncrementalDataVersionConsolidation":"Incremental data version consolidation",
+
+ "SelectOnlyTheCurrentPage":"Select only the current page",
+ "SuccessfullyModified":"Successfully modified!",
+ "PleaseCheckTheConfigurationYouWantToModify":"Please check the configuration you want to modify!",
+ "operate":"Operate",
+ "edit":"Edit",
+ "ConfigurationItem":"Configuration item",
+ "SearchConfigurationItems":"Search configuration items",
+ "Node":"Node",
+ "SearchNode":"Search node",
+ "CurrentlySelected":"Currently selected",
+ "StripData":"data",
+ "BatchEditing":"Batch editing",
+ "EditConfigurationItems":"Edit configuration items",
+ "total":"A total of ",
+ "strip":"",
+ "ConfigurationValue":"Configuration value",
+ "PleaseEnterTheConfigurationValue":"Please enter the configuration value",
+ "EffectiveState":"Effective state",
+ "PleaseSelectAnEffectiveState":"Please select an effective state",
+ "TemporarilyEffective":"Temporarily effective",
+ "TemporarilyEffectiveTooltip":"Temporarily effective means that it only takes effect at the current time, and no longer takes effect after the node is restarted, and the previous configuration value is restored.",
+ "Permanent":"Permanent",
+ "PermanentTooltip":"Permanently effective means permanent effective after modification",
+ "Error":"Error",
+ "ConfigurationError":"The following configuration value modification failed, please check the editing content and corresponding node status",
+ "FailToEdit":"fail to edit",
+
+ "PleaseCheckTheConfiguration":"Please check the configuration you want to modify!",
+ "ForgetThePassword":"I seem to have forgotten my password",
+ "SignIn":"Sign in",
+ "Successfully":"The account information has been modified successfully!",
+ "PasswordResetComplete":"Password reset complete!",
+ "AccountSetting":"Account setting",
+ "AccountInformation":"Account information",
+ "Name":"Name",
+ "PleaseTypeInYourName":"Please type in your name",
+ "pleaseInputYourEmail":"Please input your email",
+ "update":"Update",
+ "Details":"Details",
+
+ "updateDoris": "Please Update Doris Cluster",
+ "httpInfo": "HTTP Connected Info",
+ "JDBCInfo": "JDBC Connected Info",
+
+ "DatabaseNum": "Database Number",
+ "DataTableNum": "Data Table Number",
+ "DiskUsage": "Disk Usage",
+ "RemainingDiskSpace": "Remaining Disk Space",
+ "dataWarehouse": "Data Warehouse",
+ "ClusterIinformationOverview": "Cluster information overview",
+ "ConnectionInformation": "Connection information",
+
+ "BasicInformationOfDatabase": "Basic information of database",
+ "DatabaseName": "Database name",
+ "DatabaseDescriptionInformation": "Database description information",
+ "CreationTime": "Creation time",
+ "TableType": "Table type (data model)",
+ "TableStructure": "Table structure",
+ "loading":"loading",
+
+ "clusterOverview": "Cluster Overview",
+ "nodeList": "Node List",
+ "parameterConf": "Parameter Conf",
+
+ "clusterId": "Cluster ID",
+ "normal": "normal",
+ "abnormal": "abnormal",
+ "clusterVersion": "Cluster Version",
+ "cpuUsage": "CPU Usage",
+ "ramUsage": "RAM Usage",
+ "diskUsage": "Disk Usage",
+ "start": "start",
+ "stop": "stop",
+ "restart": "restart",
+ "sourceUsage": "Source Usage",
+ "dataOverview": "Data Overview",
+
+ "nodeId": "Node ID",
+ "nodeType": "Node Type",
+ "hostIp": "Host IP",
+ "nodeStatus": "Node Status",
+ "nodeConf": "Node Conf",
+ "nodeVersion": "Node Version",
+
+ "paramSearch": "Param Search",
+ "paramName": "Param Name",
+ "paramType": "Param Type",
+ "paramImplication": "Param Implication",
+ "paramValueType": "Param Value Type",
+ "hot": "Hot",
+ "operation": "Operation",
+ "yes": "yes",
+ "no": "no",
+ "viewCurrentValue": "View Current Value",
+ "currentValue": "Current Value",
+ "confValue": "Conf Value",
+ "confValuePlaceholder": "Please fill in the configuration value",
+ "effectiveWay": "Effective Way",
+ "permanentEffective": "Permanent",
+ "onceEffective": "Effective This Time",
+ "effectiveWayRequiredMessage": "Please select the effective way",
+ "effectiveRange": "Effective Range",
+ "effectiveRangeRequiredMessage": "Please select the effective range",
+ "allNodes": "All Nodes",
+ "certainNodes": "Certain Nodes",
+ "effectiveNodes": "Effective Nodes",
+ "effectiveNodesPlaceholder": "Please select the effective nodes",
+
+ "userManagement": "User Management",
+ "addUser": "Add User",
+ "editUser": "Edit User",
+ "status": "Status",
+ "Status": "Status",
+ "superAdministrator": "Super Administrator",
+ "lastLogin": "Last Login",
+ "activated": "Activated",
+ "deactivated": "Deactivated",
+ "neverLoggedIn": "Never Logged in",
+ "resetPassword": "Reset Password",
+ "activateUser": "Activate User",
+ "deactivateUser": "Deactivate User",
+ "resetPasswordOrNot": "Whether to reset the account password?",
+ "pleaseSaveYourPassword": "Please save your password.",
+ "copySuccess": "Copy successfully.",
+ "copyError": "Copy failed.",
+ "resetPasswordFailed": "Reset password failed.",
+ "setupFailed": "Setup failed.",
+ "setupSuccess": "Setup successfully.",
+ "whetherToActivate": "Whether to activate",
+ "whetherToDeactivate": "Whether to deactivate",
+ "afterActivate": "After activating,",
+ "afterDeactivate": "After deactivating,",
+ "canNotLogin": "can not log in.",
+ "canLoginAgain": "can log in again.",
+ "createSuccess": "Create successfully.",
+ "editSuccess": "Edit successfully.",
+ "pleaseInputUsername": "Please input the username.",
+ "usernameLengthMessage": "Username length should be less than 20.",
+ "usernamePatternMessage": "Username can only contain uppercase and lowercase letters and numbers.",
+ "pleaseInputEmail": "Please input the user email.",
+ "emailTypeMessage": "Please input the email address in correct format.",
+
+ "pleaseSelectUsers": "Please select users.",
+ "members": "Members",
+ "roles": "Roles",
+ "removeMember": "Remove Member",
+ "removeMemberModalTitle": "Are you sure you want to remove this member from the space?",
+ "removeSuccess": "Remove successfully.",
+ "removeFailed": "Remove failed.",
+ "addMembers": "Add Members",
+ "users": "Users",
+ "addSuccess": "Add successfully",
+ "addFailed": "Add failed",
+ "fetchUserListFailed": "Fetch user list failed.",
+ "fetchSpaceMemberListFailed": "Fetch space member list failed.",
+
+ "roleName": "Role Name",
+ "deleteThisRole": "Delete this role",
+ "deleteThisRoleMessage": "Sure? All members of the role will lose the permission settings under the role. This operation is irreversible.",
+ "roleTopMessage": "Member permissions are managed through roles. Space Admin and Space Member are default roles and cannot be deleted.",
+ "create": "Create",
+ "createRole": "Create Role",
+ "editRole": "Edit Role",
+ "pleaseInputRoleName": "Please input the role name.",
+ "roleNameLengthMessage": "The role name length is 1-20.",
+ "roleNamePatternMessage": "Role names can only contain letters, numbers, Chinese, and underscores.",
+ "remove": "Remove",
+ "removeFromRoleMembers": "Remove from role members.",
+ "removeFromRoleMembersMessage": "Sure? The user will lose the permission settings under the role. This operation is irreversible.",
+ "Copy Successfully": "Copy Successfully",
+ "Platform Settings": "Platform Settings",
+ "Space List": "Space List",
+ "Finished": "Finished",
+ "Not Finished": "Not Finished",
+ "Space Name": "Space Name",
+ "Cluster": "Cluster",
+ "Query": "Query",
+ "Space Manager": "Space Manager",
+ "About": "About",
+ "Born for data analysis": "Born for data analysis",
+ "Contact": "Contact",
+ "Current Version": "Current Version",
+ "Notice": "Notice",
+ "New Cluster": "New Cluster",
+ "New Space": "New Space",
+ "Creator": "Creator",
+ "Actions": "Actions",
+ "Enter Space": "Enter Space",
+ "Delete": "Delete",
+ "Edit": "Edit",
+ "Cluster hosting": "Cluster hosting",
+ "Recover": "Recover",
+ "Space Register": "Space Register",
+ "Please Select Users...": "Please Select Users...",
+ "NewPasswordLengthRuleMessage": "Password length is 6-12",
+ "NewPasswordPatternRuleMessage": "Passwords only support uppercase letters, lowercase letters, numbers and underscores, including at least three types."
+}
diff --git a/frontend/public/locales/zh-cn.json b/frontend/public/locales/zh-cn.json
new file mode 100644
index 0000000..a4b3a83
--- /dev/null
+++ b/frontend/public/locales/zh-cn.json
@@ -0,0 +1,332 @@
+{
+ "username": "用户名",
+ "password": "密码",
+ "OldPassword":"旧密码",
+ "PleaseEnterTheOldPassword":"请输入旧密码",
+ "NewPassword":"新密码",
+ "ContainAtLeast2Types":"至少包含英文大写、英文小写、数字和特殊符号中的2种",
+ "PleaseEnterTheNewPassword":"请输入新密码",
+ "NewPasswordCannotBeEmpty":"新密码不能为空",
+ "OldPasswordCannotBeEmpty":"旧密码不能为空",
+ "Least6":"密码长度至少6个字符",
+ "ConfirmPassword":"确认密码",
+ "ConfirmPasswordCanNotBeBlank":"确认密码不能为空!",
+ "TwoPasswordEntriesAreInconsistent":"两次密码输入不一致",
+ "PleaseConfirmYourNewPassword":"请确认您新的密码",
+ "Save":"保存",
+ "exitSuccessfully": "退出成功",
+ "loginWarning":"账号或密码错误",
+
+ "editor": "编辑器",
+ "format": "格式化",
+ "clear": "清空编辑器",
+ "execute": "执行",
+ "tableStructure": "表结构",
+ "dataPreview": "数据预览",
+ "queryForm": "查询",
+ "runSuccessfully": "运行成功",
+ "results": "执行结果",
+ "selectTheFile": "文件选择",
+ "loadConfig": "导入配置",
+ "previous": "上一步",
+ "next": "下一步",
+ "cancel": "取消",
+ "dataImport": "数据导入",
+ "table": "表",
+ "delimiter": "列分隔符",
+ "importResults": "导入结果",
+ "errMsg": "请求出错,请稍后重试",
+ "display10": "最多显示10行",
+ "refresh": "刷新",
+ "startingTime": "开始时间",
+ "endTime":"结束时间",
+ "currentDatabase": "当前数据库",
+ "executionFailed": "上传失败",
+ "uploadWarning": "请选择文件",
+ "upload": "上传",
+ "delimiterWarning": "请选择分隔符",
+ "uploadedFiles": "已上传文件列表",
+ "filePreview": "文件预览",
+ "database": "数据库",
+ "notice": "注意",
+ "login": "登录",
+ "Mail":"邮箱",
+ "loginExpirtMsg": "登录信息已过期。点击确定跳转到登陆页面。",
+ "nextStep": "下一步",
+ "previousStep": "上一步",
+ "pleaseEnter": "请输入",
+ "cancelButton": "取消",
+ "loadButton": "导入",
+ "successfulOperation": "操作成功",
+ "tips": "提示",
+ "fileSizeWarning": "文件大小不能超过100m",
+ "selectWarning": "请选择表",
+ "executionTime": "执行时间",
+ "search":"查询",
+
+ "noData":"暂无数据",
+ "data": "数据",
+ "queryProfileFirst":"请先开启集群Query Profile,方可查看查询列表",
+ "namespace": "当前空间名称",
+ "backTo": "返回",
+
+
+ "viewTime": "查看时间",
+ "clusterInformation": "集群信息",
+ "dataMonitoring": "数据监控",
+ "QPS": "QPS(每秒查询次数)",
+ "99th": "99分位查询延迟(ms)",
+ "queryErrorRate": "查询错误率",
+ "numberOfConnections": "连接数",
+ "importRate": "导入事务提交/成功/失败频率",
+ "clusterScheduling":"集群调度情况",
+ "unhealthyFragmentation":"不健康分片",
+
+ "numberOfFeNodes":"FE节点数",
+ "FeNumberOfSurvivingNodes":"FE存活节点数",
+ "NumberOfBENodes":"BE节点数",
+ "BeNumberOfSurvivingNodes":"BE存活节点数",
+ "totalSpace":"总空间",
+ "usedSpace":"已用空间",
+
+ "CPUidleRate":"CPU空闲率(%)",
+ "nodeMemoryUsage":"节点内存使用量(MB)",
+ "IOutilization":"节点内存使用量(MB)",
+ "BaselineDataVersionConsolidation":"基线数据版本合并情况",
+ "IncrementalDataVersionConsolidation":"增量数据版本合并情况",
+ "PleaseSelectNode":"请选择节点",
+ "nodeSelection":"节点选择",
+ "BENodeStatusMonitoring":"BE节点监控",
+
+ "accountSettings":"账户设置",
+ "Logout":"注销",
+ "Help":"帮助",
+
+ "SelectOnlyTheCurrentPage":"只选当前页",
+ "SuccessfullyModified":"修改成功!",
+ "PleaseCheckTheConfigurationYouWantToModify":"请勾选要修改的配置!",
+ "operate":"操作",
+ "edit":"编辑",
+ "ConfigurationItem":"配置项",
+ "SearchConfigurationItems":"搜索配置项",
+ "Node":"节点",
+ "SearchNode":"搜索节点",
+ "CurrentlySelected":"当前选中",
+ "StripData":"数据",
+ "BatchEditing":"批量编辑",
+ "EditConfigurationItems":"编辑配置项",
+ "total":"共",
+ "strip":"条",
+ "ConfigurationValue":"配置值",
+ "PleaseEnterTheConfigurationValue":"请输入配置值",
+ "EffectiveState":"生效状态",
+ "PleaseSelectAnEffectiveState":"请选择生效状态",
+ "TemporarilyEffective":"暂时生效",
+ "TemporarilyEffectiveTooltip":"暂时生效,指只在当前生效,节点重启后不再生效,恢复修改之前的配置值",
+ "Permanent":"永久生效",
+ "PermanentTooltip":"永久生效,指修改后永久生效",
+ "Error":"错误",
+ "ConfigurationError":"以下配置值修改失败,请检查编辑内容和对应节点状态",
+ "FailToEdit":"配置出错",
+
+ "PleaseCheckTheConfiguration":"请勾选要修改的配置!",
+ "ForgetThePassword":"我好像忘记密码了",
+ "SignIn":"登录",
+ "Successfully":"The account information has been modified successfully!",
+ "PasswordResetComplete":"密码修改成功!",
+ "AccountSetting":"账户设置",
+ "AccountInformation":"帐户信息",
+ "Name":"姓名",
+ "PleaseTypeInYourName":"请输入姓名",
+ "pleaseInputYourEmail":"请输入邮箱",
+ "update":"更新",
+ "Details":"详情",
+
+
+
+ "required":"此项必填",
+ "confirmPassword": "确认密码",
+ "inconsistentPasswords":"密码不一致",
+
+
+
+ "spaceInfo":"空间信息",
+ "clusterInfo":"集群信息",
+ "spaceName":"空间名称",
+ "spaceIntroduction":"空间简介",
+ "adminName":"管理员姓名",
+ "adminEmail":"管理员邮箱",
+ "adminpsw":"管理员密码",
+ "clusterAddr":"集群地址",
+ "httpPort":"HTTP端口",
+ "JDBCPort":"JDBC端口",
+ "userName":"用户名",
+ "userPwd":"密码",
+ "linkTest":"链接测试",
+ "submit":"提交",
+ "SpaceDeleteTips": "连接空间的集群信息、空间内的文件夹(其中包括查询、仪表盘、定时任务等)、用户及权限信息等将被一并删除。确认删除空间?",
+ "DeleteSuccessTips": "删除成功",
+ "Delete Success": "删除成功",
+ "Failed": "失败",
+ "Delete": "删除",
+ "GoBack": "返回",
+ "NewSpace": "新建空间",
+
+ "updateDoris": "请升级Doris集群!",
+ "httpInfo": "HTTP连接信息",
+ "JDBCInfo": "JDBC连接信息",
+
+ "DatabaseNum": "数据库数量",
+ "DataTableNum": "数据表数量",
+ "DiskUsage": "磁盘占用量",
+ "RemainingDiskSpace": "剩余磁盘空间",
+ "dataWarehouse": "数据仓库",
+ "ClusterIinformationOverview": "集群信息概览",
+ "ConnectionInformation": "连接信息",
+
+ "BasicInformationOfDatabase": "数据库基本信息",
+ "DatabaseName": "数据库名称",
+ "DatabaseDescriptionInformation": "数据库描述信息",
+ "CreationTime": "创建时间",
+
+ "TableType": "表类型(数据模型)",
+ "TableStructure": "表结构",
+ "loading":"加载中",
+
+ "clusterOverview": "集群概览",
+ "nodeList": "节点列表",
+ "parameterConf": "参数配置",
+
+ "clusterId": "集群ID",
+ "normal": "正常",
+ "abnormal": "异常",
+ "clusterVersion": "集群版本",
+ "cpuUsage": "CPU 使用率",
+ "ramUsage": "内存使用率",
+ "diskUsage": "磁盘使用率",
+ "start": "启动",
+ "stop": "停止",
+ "restart": "重启",
+ "sourceUsage": "资源使用量",
+ "dataOverview": "数据概览",
+
+ "nodeId": "节点ID",
+ "nodeType": "节点类型",
+ "hostIp": "主机IP",
+ "nodeStatus": "节点状态",
+ "nodeConf": "节点配置",
+ "nodeVersion": "节点版本",
+
+ "paramSearch": "参数搜索",
+ "paramName": "参数名称",
+ "paramType": "参数类型",
+ "paramImplication": "参数含义",
+ "paramValueType": "参数值类型",
+ "hot": "热生效",
+ "operation": "操作",
+ "yes": "是",
+ "no": "否",
+ "viewCurrentValue": "查看当前值",
+ "currentValue": "当前值",
+ "confValue": "配置值",
+ "confValuePlaceholder": "请填写配置值",
+ "effectiveWay": "生效方式",
+ "permanentEffective": "永久生效",
+ "onceEffective": "本次生效",
+ "effectiveWayRequiredMessage": "请选择生效方式",
+ "effectiveRange": "生效范围",
+ "effectiveRangeRequiredMessage": "请选择生效范围",
+ "allNodes": "全部节点",
+ "certainNodes": "部分节点",
+ "effectiveNodes": "生效节点",
+ "effectiveNodesPlaceholder": "请选择生效节点",
+
+ "userManagement": "用户管理",
+ "addUser": "添加用户",
+ "editUser": "编辑用户",
+ "status": "状态",
+ "Status": "状态",
+ "superAdministrator": "超级管理员",
+ "lastLogin": "上次登录",
+ "activated": "启用",
+ "deactivated": "停用",
+ "neverLoggedIn": "尚未登录",
+ "resetPassword": "重置密码",
+ "activateUser": "激活用户",
+ "deactivateUser": "停用用户",
+ "resetPasswordOrNot": "是否重置该账号密码?",
+ "pleaseSaveYourPassword": "请保存您的密码。",
+ "copySuccess": "复制成功",
+ "copyError": "复制失败",
+ "resetPasswordFailed": "重置密码失败",
+ "setupFailed": "设置失败",
+ "setupSuccess": "设置成功",
+ "whetherToActivate": "是否激活",
+ "whetherToDeactivate": "是否停用",
+ "afterActivate": "激活后",
+ "afterDeactivate": "停用后",
+ "canNotLogin": "无法登录。",
+ "canLoginAgain": "可以重新登录。",
+ "createSuccess": "创建成功",
+ "editSuccess": "修改成功",
+ "pleaseInputUsername": "请输入用户名称。",
+ "usernameLengthMessage": "用户名长度应小于20。",
+ "usernamePatternMessage": "用户名只能包含大小写字母以及数字",
+ "pleaseInputEmail": "请输入用户邮箱。",
+ "emailTypeMessage": "请输入正确的邮箱地址",
+
+ "pleaseSelectUsers": "请选择用户。",
+ "members": "成员",
+ "roles": "角色",
+ "removeMember": "移除成员",
+ "removeMemberModalTitle": "确定移除此成员至空间外?",
+ "removeSuccess": "移除成功",
+ "removeFailed": "移除失败",
+ "addMembers": "添加成员",
+ "users": "用户",
+ "addSuccess": "添加成功",
+ "addFailed": "添加失败",
+ "fetchUserListFailed": "获取用户列表失败。",
+ "fetchSpaceMemberListFailed": "获取空间成员列表失败。",
+
+ "roleName": "角色名称",
+ "deleteThisRole": "删除这个角色",
+ "deleteThisRoleMessage": "确定吗?该角色所有成员都将丢失该角色下的权限设置。此操作不可逆。",
+ "roleTopMessage": "通过角色对成员权限进行管理。空间管理员和空间成员是默认角色,无法删除。",
+ "create": "新建",
+ "createRole": "创建角色",
+ "editRole": "编辑角色",
+ "pleaseInputRoleName": "请输入角色名称。",
+ "roleNameLengthMessage": "角色名称长度为1-20。",
+ "roleNamePatternMessage": "角色名称只能包含字母、数字、中文、下划线。",
+ "remove": "移除",
+ "removeFromRoleMembers": "从角色成员中移除。",
+ "removeFromRoleMembersMessage": "确定吗?该用户都将丢失该角色下的权限设置。此操作不可逆。",
+ "Copy Successfully": "复制成功",
+ "Platform Settings": "平台设置",
+ "Space List": "空间列表",
+ "Finished": "已完成",
+ "Not Finished": "未完成",
+ "Space Name": "空间名称",
+ "Cluster": "集群",
+ "Query": "查询",
+ "Space Manager": "空间管理",
+ "About": "关于",
+ "Born for data analysis": "为数据分析而生",
+ "Contact": "联系方式",
+ "Current Version": "当前版本",
+ "Notice": "注意",
+ "New Cluster": "新建集群",
+ "New Space": "新建空间",
+ "Creator": "创建人",
+ "Actions": "操作",
+ "Enter Space": "进入空间",
+ "Edit": "编辑",
+ "Cluster hosting": "集群托管",
+ "Recover": "恢复",
+ "Space Register": "注册空间",
+ "Please Select Users...": "请选择用户...",
+ "NewPasswordLengthRuleMessage": "密码长度为6-12",
+ "NewPasswordPatternRuleMessage": "密码仅支持大写字母、小写字母、数字、下划线,至少包含三种"
+}
+
diff --git a/frontend/src/app.tsx b/frontend/src/app.tsx
new file mode 100644
index 0000000..b603f7b
--- /dev/null
+++ b/frontend/src/app.tsx
@@ -0,0 +1,42 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React from 'react';
+import { hot } from 'react-hot-loader/root';
+import { BrowserRouter as Router, Switch } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+import { ConfigProvider } from 'antd';
+import zh from 'antd/lib/locale/zh_CN';
+import en from 'antd/lib/locale/en_US';
+import { RecoilRoot } from 'recoil';
+import routes from './routes';
+
+const languageMap = {
+ zh,
+ en,
+};
+
+const App = () => {
+ const { i18n } = useTranslation();
+ return (
+
+ {routes}
+
+ );
+};
+
+export default hot(App);
diff --git a/frontend/src/assets/404.png b/frontend/src/assets/404.png
new file mode 100644
index 0000000..c89f60f
Binary files /dev/null and b/frontend/src/assets/404.png differ
diff --git a/frontend/src/assets/background.jpg b/frontend/src/assets/background.jpg
new file mode 100644
index 0000000..501e900
Binary files /dev/null and b/frontend/src/assets/background.jpg differ
diff --git a/frontend/src/assets/doris.png b/frontend/src/assets/doris.png
new file mode 100644
index 0000000..e9a0426
Binary files /dev/null and b/frontend/src/assets/doris.png differ
diff --git a/frontend/src/assets/logo_manager.png b/frontend/src/assets/logo_manager.png
new file mode 100644
index 0000000..f25d9e9
Binary files /dev/null and b/frontend/src/assets/logo_manager.png differ
diff --git a/frontend/src/assets/space/u288.png b/frontend/src/assets/space/u288.png
new file mode 100644
index 0000000..e28440a
Binary files /dev/null and b/frontend/src/assets/space/u288.png differ
diff --git a/frontend/src/assets/space/u296.png b/frontend/src/assets/space/u296.png
new file mode 100644
index 0000000..617e2e7
Binary files /dev/null and b/frontend/src/assets/space/u296.png differ
diff --git a/frontend/src/assets/space/u919.png b/frontend/src/assets/space/u919.png
new file mode 100644
index 0000000..7a5413b
Binary files /dev/null and b/frontend/src/assets/space/u919.png differ
diff --git a/frontend/src/common/common.api.ts b/frontend/src/common/common.api.ts
new file mode 100644
index 0000000..34c5214
--- /dev/null
+++ b/frontend/src/common/common.api.ts
@@ -0,0 +1,47 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { http } from '@src/utils/http';
+import { IMember } from './common.interface';
+
+function getUserInfo() {
+ return http.get(`/api/v2/user/current`);
+}
+
+function getRoleMembersById(data: { roleId: string }) {
+ return http.get(`/api/permissions/group/${data.roleId}`);
+}
+
+function getRoles() {
+ return http.get('/api/permissions/group');
+}
+
+function getUsers(params?: { include_deactivated: boolean }) {
+ return http.get('/api/v2/user/', params);
+}
+
+function getSpaceUsers() {
+ return http.get('/api/v2/user/space');
+}
+
+export const CommonAPI = {
+ getUserInfo,
+ getRoleMembersById,
+ getRoles,
+ getUsers,
+ getSpaceUsers,
+};
diff --git a/frontend/src/common/common.context.ts b/frontend/src/common/common.context.ts
new file mode 100644
index 0000000..14000e0
--- /dev/null
+++ b/frontend/src/common/common.context.ts
@@ -0,0 +1,22 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React from 'react';
+import { UserInfo } from './common.interface';
+
+export const UserInfoContext = React.createContext(null);
+export const NewSpaceInfoContext = React.createContext(null);
diff --git a/frontend/src/common/common.data.ts b/frontend/src/common/common.data.ts
new file mode 100644
index 0000000..ac9f96f
--- /dev/null
+++ b/frontend/src/common/common.data.ts
@@ -0,0 +1,102 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+export enum FieldTypeEnum {
+ TINYINT = 'TINYINT',
+ SMALLINT = 'SMALLINT',
+ INT = 'INT',
+ BIGINT = 'BIGINT',
+ LARGEINT = 'LARGEINT',
+ BOOLEAN = 'BOOLEAN',
+ FLOAT = 'FLOAT',
+ DOUBLE = 'DOUBLE',
+ DECIMAL = 'DECIMAL',
+ DATE = 'DATE',
+ DATETIME = 'DATETIME',
+ CHAR = 'CHAR',
+ VARCHAR = 'VARCHAR',
+ // HLL = 'HLL',
+ BITMAP = 'BITMAP',
+}
+export enum ConfigurationTypeEnum {
+ FE = 'fe',
+ BE = 'be',
+}
+export enum TableTypeEnum {
+ PRIMARY_KEYS = 'PRIMARY_KEYS',
+ UNIQUE_KEYS = 'UNIQUE_KEYS',
+ DUP_KEYS = 'DUP_KEYS',
+ AGG_KEYS = 'AGG_KEYS',
+}
+
+
+export const TABLE_TYPE_KEYS = [
+ {
+ value: TableTypeEnum.PRIMARY_KEYS,
+ text: '',
+ },
+ {
+ value: TableTypeEnum.UNIQUE_KEYS,
+ text: '主键唯一表',
+ },
+ {
+ value: TableTypeEnum.DUP_KEYS,
+ text: '明细表',
+ },
+ {
+ value: TableTypeEnum.AGG_KEYS,
+ text: '聚合表',
+ },
+];
+
+export const FIELD_TYPES: string[] = [];
+for (const fieldType in FieldTypeEnum) {
+ if (typeof fieldType !== 'number') {
+ FIELD_TYPES.push(fieldType);
+ }
+}
+
+// 首列不能是以下类型
+const FIRST_COLUMN_FIELD_TYPE_CANNOT_BE = [
+ FieldTypeEnum.BITMAP,
+ // FieldTypeEnum.HLL,
+ FieldTypeEnum.FLOAT,
+ FieldTypeEnum.DOUBLE,
+];
+// 分桶列必须是以下类型
+export const BUCKET_MUST_BE = [
+ FieldTypeEnum.VARCHAR,
+ FieldTypeEnum.BIGINT,
+ FieldTypeEnum.INT,
+ FieldTypeEnum.LARGEINT,
+ FieldTypeEnum.SMALLINT,
+ FieldTypeEnum.TINYINT,
+];
+
+export const FIRST_COLUMN_FIELD_TYPES = FIELD_TYPES.filter(
+ (field: any) => !FIRST_COLUMN_FIELD_TYPE_CANNOT_BE.includes(field),
+);
+
+export const ANALYTICS_URL = '/login';
+export const STUDIO_INDEX_URL = `/meta/index`; //待确认
+export const MANAGE_INDEX_URL = `/super-admin/space/list`;
+
+
+export enum AuthTypeEnum {
+ LDAP = 'ldap',
+ STUDIO = 'studio',
+}
\ No newline at end of file
diff --git a/frontend/src/common/common.interface.ts b/frontend/src/common/common.interface.ts
new file mode 100644
index 0000000..f469b67
--- /dev/null
+++ b/frontend/src/common/common.interface.ts
@@ -0,0 +1,50 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+export interface UserInfo {
+ email: string;
+ name: string;
+ authType: string;
+ id: number;
+ collectionId: number;
+ ldap_auth: boolean;
+ last_login: Date;
+ updated_at: Date;
+ group_ids: number[];
+ date_joined: Date;
+ common_name: string;
+ google_auth: boolean;
+ space_id: number;
+ space_complete: boolean;
+ deploy_type: string;
+ manager_enable: boolean;
+ is_active: boolean;
+ is_admin: boolean;
+ is_qbnewb: boolean;
+ is_super_admin: boolean;
+}
+
+export interface IRole {
+ id: number;
+ member_count: number;
+ name: string;
+}
+
+export interface IMember {
+ name: string;
+ members: any[];
+ id: number;
+}
\ No newline at end of file
diff --git a/frontend/src/components/clipped-text/clipped-text.tsx b/frontend/src/components/clipped-text/clipped-text.tsx
new file mode 100644
index 0000000..d0ed37d
--- /dev/null
+++ b/frontend/src/components/clipped-text/clipped-text.tsx
@@ -0,0 +1,72 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { Tooltip } from 'antd';
+import React, { useRef, useEffect, useState } from 'react';
+import CopyText from '../copy-text';
+
+export function ClippedText(props: any) {
+ const refClip = useRef(null);
+ const [isShowTip, setShowTip] = useState(false);
+ let tempWidth = props.width;
+ if (props.inTable && typeof props.width === 'number') {
+ tempWidth = props.width - 40;
+ }
+ let width;
+ if (props.width) {
+ width = typeof props.width === 'number' ? `${tempWidth}px` : props.width;
+ } else {
+ width = 'calc(100% - 10px)';
+ }
+ const title = props.text ? props.text : props.children;
+
+ useEffect(() => {
+ const elem = refClip.current;
+ if (elem) {
+ if (elem.scrollWidth > elem.clientWidth) {
+ setShowTip(true);
+ }
+ }
+ }, []);
+
+ const content = (
+
+ {props.children}
+
+ );
+
+ return isShowTip ? (
+
+
+ {content}
+
+
+ ) : (
+ content
+ );
+}
diff --git a/frontend/src/components/common-header/header.api.ts b/frontend/src/components/common-header/header.api.ts
new file mode 100644
index 0000000..446e897
--- /dev/null
+++ b/frontend/src/components/common-header/header.api.ts
@@ -0,0 +1,28 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+/** @format */
+import { http } from '@src/utils/http';
+// import { GetDatabaseInfoByDbIdRequestParams, GetDatabaseRequestParams } from './header.interface';
+
+function refreshData() {
+ return http.post(`/api/meta/sync`, {});
+}
+
+export const HeaderAPI = {
+ refreshData,
+};
diff --git a/frontend/src/components/common-header/header.interface.ts b/frontend/src/components/common-header/header.interface.ts
new file mode 100644
index 0000000..cdc11f4
--- /dev/null
+++ b/frontend/src/components/common-header/header.interface.ts
@@ -0,0 +1,23 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+/** @format */
+export interface HeaderProps {
+ title: string;
+ icon: any;
+ callback: any;
+}
diff --git a/frontend/src/components/common-header/header.module.less b/frontend/src/components/common-header/header.module.less
new file mode 100644
index 0000000..9f2e764
--- /dev/null
+++ b/frontend/src/components/common-header/header.module.less
@@ -0,0 +1,48 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+/** @format */
+
+.common-header {
+ position: relative;
+ // height: 64px;
+ padding: 15px 38px;
+ // border-bottom: 1px solid #f0f0f0;
+ .common-header-title {
+ // font-size: 30px;
+ color: #555;
+
+ .common-header-icon {
+ font-size: 40px;
+ }
+
+ .common-header-name {
+ padding-left: 10px;
+ font-size: 30px;
+ font-weight: 600;
+ vertical-align: super;
+ }
+ }
+
+ .common-header-refresh {
+ position: absolute;
+ top: 25px;
+ right: 25px;
+ font-size: 20px;
+ color: @primary-color;
+ }
+}
diff --git a/frontend/src/components/common-header/header.tsx b/frontend/src/components/common-header/header.tsx
new file mode 100644
index 0000000..2fc9f81
--- /dev/null
+++ b/frontend/src/components/common-header/header.tsx
@@ -0,0 +1,61 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+/** @format */
+
+import React, { useState, useCallback, useEffect } from 'react';
+import styles from './header.module.less';
+import { HeaderProps } from './header.interface';
+import { SyncOutlined } from '@ant-design/icons';
+import { HeaderAPI } from './header.api';
+import CSSModules from 'react-css-modules';
+const EventEmitter = require('events').EventEmitter;
+const event = new EventEmitter();
+
+export function Header(props: HeaderProps) {
+ const [loading, setLoading] = useState(false);
+ // useEffect(() => {
+ // HeaderAPI.refreshData();
+ // }, []);
+ function refresh() {
+ HeaderAPI.refreshData();
+ event.emit('refreshData');
+ props.callback();
+ setTimeout(() => {
+ setLoading(false);
+ }, 300);
+ }
+ return (
+
+
+ {props.icon}
+ {props.title}
+
+
+ {
+ refresh();
+ setLoading(true);
+ }}
+ />
+
+
+ );
+}
+
+export const CommonHeader = CSSModules(styles)(Header);
\ No newline at end of file
diff --git a/frontend/src/components/copy-text/index.less b/frontend/src/components/copy-text/index.less
new file mode 100644
index 0000000..f80c7fd
--- /dev/null
+++ b/frontend/src/components/copy-text/index.less
@@ -0,0 +1,34 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+@import '~antd/lib/style/themes/default.less';
+
+.copy-wrap {
+ display: flex;
+ align-items: center;
+ width: 100%;
+}
+
+.copy-icon {
+ margin-left: 10px;
+ font-size: 16px;
+ cursor: pointer;
+
+ &:hover {
+ color: @primary-color;
+ }
+}
diff --git a/frontend/src/components/copy-text/index.tsx b/frontend/src/components/copy-text/index.tsx
new file mode 100644
index 0000000..18b55d6
--- /dev/null
+++ b/frontend/src/components/copy-text/index.tsx
@@ -0,0 +1,46 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React from 'react';
+import { CopyOutlined } from '@ant-design/icons';
+import { message } from 'antd';
+import './index.less';
+import { useTranslation } from 'react-i18next';
+
+export default function CopyText(props: any) {
+ const { t } = useTranslation();
+ function handleCopy() {
+ const input = document.createElement('input');
+ input.style.opacity = '0';
+ input.setAttribute('readonly', 'readonly');
+ input.setAttribute('value', props.text);
+ document.body.appendChild(input);
+ input.setSelectionRange(0, 9999);
+ input.select();
+ if (document.execCommand('copy')) {
+ document.execCommand('copy');
+ message.success(t`Copy Successfully`);
+ }
+ document.body.removeChild(input);
+ }
+ return (
+
+ {props.children}
+
+
+ );
+}
diff --git a/frontend/src/components/doris-modal/doris-modal.jsx b/frontend/src/components/doris-modal/doris-modal.jsx
new file mode 100644
index 0000000..59e1d3a
--- /dev/null
+++ b/frontend/src/components/doris-modal/doris-modal.jsx
@@ -0,0 +1,58 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import Swal from 'sweetalert2';
+import withReactContent from 'sweetalert2-react-content';
+
+const DorisSwal = withReactContent(Swal);
+
+class DorisModal {
+ message(message) {
+ DorisSwal.fire(message);
+ }
+
+ success(title, message = '') {
+ return Swal.fire(title, message, 'success');
+ }
+
+ error(title = '', message = '') {
+ return Swal.fire(title, message, 'error');
+ }
+
+ confirm(title, message = '', callback) {
+ Swal.fire({
+ title: title,
+ text: message,
+ icon: 'warning',
+ showCancelButton: true,
+ confirmButtonColor: '#3085d6',
+ cancelButtonColor: '#d33',
+ confirmButtonText: '确定',
+ cancelButtonText: '取消',
+ }).then(async result => {
+ if (result.isConfirmed && typeof callback === 'function') {
+ callback();
+ } else if (!result.isConfirmed) {
+ Swal.close();
+ } else {
+ this.success('操作成功');
+ }
+ });
+ }
+}
+
+export const modal = new DorisModal();
diff --git a/frontend/src/components/dot/dot.less b/frontend/src/components/dot/dot.less
new file mode 100644
index 0000000..f36e7d9
--- /dev/null
+++ b/frontend/src/components/dot/dot.less
@@ -0,0 +1,6 @@
+.dot {
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ background-color: @primary-color;
+}
\ No newline at end of file
diff --git a/frontend/src/components/dot/dot.tsx b/frontend/src/components/dot/dot.tsx
new file mode 100644
index 0000000..85c0a8b
--- /dev/null
+++ b/frontend/src/components/dot/dot.tsx
@@ -0,0 +1,8 @@
+import React from "react";
+import styles from "./dot.less";
+
+export function Dot() {
+ return (
+
+ )
+}
\ No newline at end of file
diff --git a/frontend/src/components/flatbtn/flat-btn-group.less b/frontend/src/components/flatbtn/flat-btn-group.less
new file mode 100644
index 0000000..9fdad90
--- /dev/null
+++ b/frontend/src/components/flatbtn/flat-btn-group.less
@@ -0,0 +1,59 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+@import '~antd/lib/style/themes/default.less';
+
+.flat-btn-group {
+ display: inline-block;
+
+ .anticon-down {
+ margin-left: 5px;
+ vertical-align: middle;
+ transition: 0.5s;
+ }
+
+ &:hover {
+ .anticon-down {
+ transition: 0.5s;
+ transform: rotate(180deg);
+ }
+ }
+
+ .flat-btn-more {
+ font-size: 14px;
+ color: @primary-color;
+ cursor: pointer;
+ }
+}
+
+.flat-menu {
+ &.ant-dropdown-menu {
+ min-width: 100px;
+
+ .ant-dropdown-menu-item > a,
+ .ant-dropdown-menu-submenu-title > a {
+ color: @primary-color;
+ }
+
+ .ant-dropdown-menu-item-disabled {
+ // background-color: #ddd;
+ .flat-btn--disabled {
+ color: rgba(0, 0, 0, 0.25);
+ }
+ }
+ }
+}
diff --git a/frontend/src/components/flatbtn/flat-btn-group.tsx b/frontend/src/components/flatbtn/flat-btn-group.tsx
new file mode 100644
index 0000000..f71dcb1
--- /dev/null
+++ b/frontend/src/components/flatbtn/flat-btn-group.tsx
@@ -0,0 +1,92 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { FunctionComponent, useRef } from 'react';
+import { DownOutlined } from '@ant-design/icons';
+import { Menu, Dropdown, Divider } from 'antd';
+import './flat-btn-group.less';
+
+interface FlatItemProps {
+ children?: React.ReactNode[];
+ showNum?: number;
+}
+
+const FlatBtnGroup: FunctionComponent = ({ showNum = 3, children = [] }) => {
+ let childList: React.ReactNode[] = [];
+ if (showNum <= 1) {
+ showNum = 3;
+ }
+ if (!Array.isArray(children)) {
+ childList.push(children);
+ } else {
+ childList = children;
+ }
+ const validChildren = childList.filter(child => !!child).flat(Infinity);
+ const newList = validChildren.slice(0, showNum - 1);
+ const dropList = validChildren.slice(showNum - 1);
+
+ const menu = (
+
+ {dropList.map((item: any, index) => {
+ return (
+
+ {item}
+
+ );
+ })}
+
+ );
+
+ const wrap = useRef(null);
+
+ return (
+
+ {newList.map((btn, key) => (
+
+ {btn}
+ {(key !== showNum - 1 && !(key < showNum && key === newList.length - 1)) || dropList.length ? (
+
+ ) : (
+ <>>
+ )}
+
+ ))}
+ {dropList.length ? (
+
{
+ const dom = wrap.current;
+ if (dom) {
+ return dom;
+ }
+ return document.body;
+ }}
+ >
+
+ 更多
+
+
+
+ ) : (
+ <>>
+ )}
+
+ );
+};
+
+export default FlatBtnGroup;
diff --git a/frontend/src/components/flatbtn/flat-btn.tsx b/frontend/src/components/flatbtn/flat-btn.tsx
new file mode 100644
index 0000000..5f62f4b
--- /dev/null
+++ b/frontend/src/components/flatbtn/flat-btn.tsx
@@ -0,0 +1,75 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { HTMLAttributes } from 'react';
+import classNames from 'classnames';
+import './style.less';
+import { Link } from 'react-router-dom';
+
+interface FlatBtnProps extends HTMLAttributes {
+ to?: string;
+ type?: '' | 'danger' | 'warn';
+ disabled?: boolean;
+ children?: string | JSX.Element;
+ className?: string;
+ default?: string;
+ key?: string | number;
+ href?: string;
+ [attr: string]: any;
+}
+
+const FlatBtn = (props: FlatBtnProps) => {
+ if (props.to) {
+ return (
+
+ {props.children}
+
+ );
+ }
+ return (
+ {
+ if (props.disabled) {
+ e.preventDefault();
+ e.stopPropagation();
+ } else {
+ props.onClick && props.onClick(e);
+ }
+ }}
+ className={classNames(
+ props.className && props.className,
+ { [`btn-${props.type}`]: props.type },
+ { 'flat-btn-disabled': props.disabled },
+ { 'flat-btn-default': props.default },
+ )}
+ >
+ {props.children}
+
+ );
+};
+
+export default FlatBtn;
diff --git a/frontend/src/components/flatbtn/index.tsx b/frontend/src/components/flatbtn/index.tsx
new file mode 100644
index 0000000..dc624c3
--- /dev/null
+++ b/frontend/src/components/flatbtn/index.tsx
@@ -0,0 +1,22 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import FlatBtn from './flat-btn';
+import FlatBtnGroup from './flat-btn-group';
+
+export { FlatBtn };
+export { FlatBtnGroup };
diff --git a/frontend/src/components/flatbtn/style.less b/frontend/src/components/flatbtn/style.less
new file mode 100644
index 0000000..3dfe55b
--- /dev/null
+++ b/frontend/src/components/flatbtn/style.less
@@ -0,0 +1,64 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+.flat-btn {
+ display: inline-block;
+ padding-right: 7px;
+ padding-left: 7px;
+ text-decoration: none !important;
+ text-shadow: none;
+ cursor: pointer;
+ background: none;
+ border: none;
+ border-right: 1px dashed #c4c4c4;
+ outline: none;
+ box-shadow: none;
+ // &.flat-btn--disabled {
+ // cursor: inherit !important;
+ // color: gray !important;
+ // }
+ &.flat-btn--default {
+ color: #414d5f;
+ cursor: not-allowed;
+ }
+
+ &:first-child {
+ padding-left: 0;
+ }
+
+ &:last-child {
+ border-right: 0;
+ }
+
+ &.btn-danger {
+ color: red;
+
+ &:hover {
+ background-color: inherit !important;
+ }
+ }
+
+ &.btn-warning {
+ color: #f0b64b;
+ }
+
+ .glyphicon-triangle-bottom {
+ margin-left: 5px;
+ font-size: 12px;
+ color: #666;
+ }
+}
diff --git a/frontend/src/components/header/header.api.ts b/frontend/src/components/header/header.api.ts
new file mode 100644
index 0000000..5333ee5
--- /dev/null
+++ b/frontend/src/components/header/header.api.ts
@@ -0,0 +1,37 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+/* eslint-disable prettier/prettier */
+/** @format */
+
+import { http } from '@src/utils/http';
+function getCurrentUser() {
+ return http.get(`/api/user/current`);
+}
+
+function getSpaceName(data: any) {
+ return http.get(`/api/space/${data}`);
+}
+
+function signOut() {
+ return http.delete(`/api/session/`);
+}
+export const LayoutAPI = {
+ getCurrentUser,
+ getSpaceName,
+ signOut,
+};
diff --git a/frontend/src/components/header/header.tsx b/frontend/src/components/header/header.tsx
new file mode 100644
index 0000000..f296e99
--- /dev/null
+++ b/frontend/src/components/header/header.tsx
@@ -0,0 +1,124 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { Menu, Col, Row, Dropdown, Button } from 'antd';
+import {SettingOutlined } from '@ant-design/icons';
+import React, { useContext, useState } from 'react';
+import { LayoutAPI } from './header.api';
+import { useHistory } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+import styles from './index.module.less';
+import { UserInfoContext } from '@src/common/common.context';
+import Swal from 'sweetalert2';
+const VERSION = require('../../../package.json').version;
+
+type HeaderMode = 'normal' | 'initialize' | 'super-admin';
+interface HeaderProps {
+ mode: HeaderMode;
+}
+
+export function Header(props: HeaderProps) {
+ const { t, i18n } = useTranslation();
+ const history = useHistory();
+ const [statisticInfo, setStatisticInfo] = useState({});
+ const user = JSON.parse(window.localStorage.getItem('user') as string);
+ const userInfo = useContext(UserInfoContext);
+ function getCurrentUser() {
+ LayoutAPI.getCurrentUser()
+ .then(res => {
+ window.localStorage.setItem('user', JSON.stringify(res.data))
+ LayoutAPI.getSpaceName(res.data.space_id).then(res1 => {
+ setStatisticInfo(res1.data || {});
+ })
+ })
+ .catch(err => {
+ console.log(err);
+ });
+ }
+ function onAccountSettings() {
+ history.push( `/user-setting`);
+ }
+ function onLogout() {
+ LayoutAPI.signOut()
+ .then(res => {
+ console.log(res)
+ if (res.code === 0) {
+ localStorage.removeItem('login');
+ history.push(`/login`);
+ }
+ })
+
+ }
+ const menu = (
+
+ {t`accountSettings`}
+ {
+ Swal.fire({
+ width: '480px',
+ title: `${t`Thanks for using`} Doris Manager! `,
+ html: `${t`Current Version`}: ${VERSION}
${t`Contact`}:dev@doris.apache.org
`,
+ footer: `Doris,${t`Born for data analysis`} `,
+ imageUrl: '/src/assets/doris.png',
+ imageHeight: 68,
+ showConfirmButton: false,
+ imageAlt: 'Doris Manager',
+ });
+ }}
+ style={{ padding: '10px 20px' }}
+ >
+ {t`About`} Doris Manager
+
+ {t`Logout`}
+
+ );
+ return (
+
+
+ {/* {
+ user && user.is_super_admin ? (
+
+ ) :(
+
+ {t`namespace`}:{(userInfo as UserInfo)?.space_name}
+
+ )
+ } */}
+ {
+ i18n.changeLanguage(i18n.language === 'zh' ? 'en' : 'zh');
+ }}
+ >
+ {i18n.language === 'zh' ? 'Switch to English' : '切换为中文'}
+
+
+
+ e.preventDefault()}>
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/header/index.module.less b/frontend/src/components/header/index.module.less
new file mode 100644
index 0000000..538e4eb
--- /dev/null
+++ b/frontend/src/components/header/index.module.less
@@ -0,0 +1,60 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+.logo {
+ width: 10em;
+ height: 4em;
+
+ background-image: url(../../assets/logo_manager.png);
+ background-size: 100% 100%;
+}
+
+#components-layout-demo-custom-trigger .trigger {
+ padding: 0 8px;
+ font-size: 18px;
+ line-height: 64px;
+ cursor: pointer;
+ transition: color 0.3s;
+}
+
+#components-layout-demo-custom-trigger .trigger:hover {
+ color: #1890ff;
+}
+
+.site-layout .site-layout-background {
+ background: #fff;
+ border-color: #a1a2a2;
+ border-width: 1px;
+ border-bottom-style: solid;
+}
+
+.data-builder-header-items {
+ padding: 8px;
+ margin-right: 16px;
+ border-radius: 8px;
+}
+.userStyle{
+ padding: 0;
+ background: white;
+ border-bottom: '1px solid #d9d9d9';
+}
+
+.adminStyle{
+ padding: 0;
+ background: white;
+ border-bottom: '1px solid #d9d9d9';
+}
\ No newline at end of file
diff --git a/frontend/src/components/helper/helper.less b/frontend/src/components/helper/helper.less
new file mode 100644
index 0000000..b5922eb
--- /dev/null
+++ b/frontend/src/components/helper/helper.less
@@ -0,0 +1,23 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+.palo-studio-helper {
+ font-size: 16px;
+ color: @primary-color;
+ text-align: left;
+ cursor: pointer;
+}
diff --git a/frontend/src/components/helper/helper.tsx b/frontend/src/components/helper/helper.tsx
new file mode 100644
index 0000000..8c4afa7
--- /dev/null
+++ b/frontend/src/components/helper/helper.tsx
@@ -0,0 +1,30 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { QuestionCircleFilled } from '@ant-design/icons';
+import { Tooltip } from 'antd';
+import React from 'react';
+import { TooltipProps } from 'antd/lib/tooltip';
+import './helper.less';
+
+export function Helper(props: TooltipProps) {
+ return (
+
+
+
+ );
+}
diff --git a/frontend/src/components/initialized-route/initialized-route.tsx b/frontend/src/components/initialized-route/initialized-route.tsx
new file mode 100644
index 0000000..04d0809
--- /dev/null
+++ b/frontend/src/components/initialized-route/initialized-route.tsx
@@ -0,0 +1,33 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { auth } from "@src/utils/auth";
+import React from "react";
+import { Redirect, Route } from "react-router";
+
+export function InitializedRoute({ children, ...rest }) {
+ const isPassportLogin = location.pathname.includes("/passport/login");
+ return (
+ {
+ if (!auth.checkInitialized()) {
+ return ;
+ } else {
+ return auth.checkLogin() || isPassportLogin ? children : ;
+ }
+ }} />
+ )
+}
diff --git a/frontend/src/components/loading-layout/index.tsx b/frontend/src/components/loading-layout/index.tsx
new file mode 100644
index 0000000..9866994
--- /dev/null
+++ b/frontend/src/components/loading-layout/index.tsx
@@ -0,0 +1,40 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { CSSProperties, PropsWithChildren } from 'react';
+import { Spin } from 'antd';
+
+interface LoadingLayoutProps {
+ loading?: boolean;
+ wrapperStyle?: CSSProperties;
+ tip?: string;
+}
+
+export default function LoadingLayout(props: PropsWithChildren) {
+ const { loading = false, wrapperStyle = {}, children, tip } = props;
+ return (
+
+ {loading ? (
+
+
+
+ ) : (
+ children
+ )}
+
+ );
+}
diff --git a/frontend/src/components/loading/index.tsx b/frontend/src/components/loading/index.tsx
new file mode 100644
index 0000000..8b4b73d
--- /dev/null
+++ b/frontend/src/components/loading/index.tsx
@@ -0,0 +1,30 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import * as React from 'react';
+import { useTranslation } from 'react-i18next';
+import './loading.less';
+export const Loading = () => {
+ const { t } = useTranslation();
+ return (
+ <>
+
+ {t`loading`}...
+
+ >
+ );
+};
diff --git a/frontend/src/components/loading/loading.less b/frontend/src/components/loading/loading.less
new file mode 100644
index 0000000..a21d47b
--- /dev/null
+++ b/frontend/src/components/loading/loading.less
@@ -0,0 +1,30 @@
+/* Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License. */
+
+.components-loading {
+ width: 100vw;
+ height: 100vh;
+ background: transparent;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ & span {
+ font-size: 30px;
+ color: yellowgreen;
+ }
+}
diff --git a/frontend/src/components/loadingwrapper/loadingwrapper.tsx b/frontend/src/components/loadingwrapper/loadingwrapper.tsx
new file mode 100644
index 0000000..42f2f5d
--- /dev/null
+++ b/frontend/src/components/loadingwrapper/loadingwrapper.tsx
@@ -0,0 +1,41 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { PropsWithChildren } from 'react';
+import { SpinProps } from 'antd/lib/spin';
+import { Spin } from 'antd';
+import { TABLE_DELAY } from '@src/config';
+
+type SpinWithoutSpinningProp = Omit;
+
+interface LoadingWrapperProps extends SpinWithoutSpinningProp {
+ loading?: boolean;
+}
+
+export function LoadingWrapper(props: PropsWithChildren) {
+ let loading = false;
+ if (props.loading) {
+ loading = true;
+ }
+ const spinProps = { ...props };
+ delete spinProps.loading;
+ return (
+
+ {props.children}
+
+ );
+}
diff --git a/frontend/src/components/metadata/index.tsx b/frontend/src/components/metadata/index.tsx
new file mode 100644
index 0000000..8091089
--- /dev/null
+++ b/frontend/src/components/metadata/index.tsx
@@ -0,0 +1,128 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { useState } from 'react';
+import { Tree } from 'antd';
+const TREE_DATA = [
+ {
+ title: '0-0',
+ key: '0-0',
+ children: [
+ {
+ title: '0-0-0',
+ key: '0-0-0',
+ children: [
+ {
+ title: '0-0-0-0',
+ key: '0-0-0-0',
+ },
+ {
+ title: '0-0-0-1',
+ key: '0-0-0-1',
+ },
+ {
+ title: '0-0-0-2',
+ key: '0-0-0-2',
+ },
+ ],
+ },
+ {
+ title: '0-0-1',
+ key: '0-0-1',
+ children: [
+ {
+ title: '0-0-1-0',
+ key: '0-0-1-0',
+ },
+ {
+ title: '0-0-1-1',
+ key: '0-0-1-1',
+ },
+ {
+ title: '0-0-1-2',
+ key: '0-0-1-2',
+ },
+ ],
+ },
+ {
+ title: '0-0-2',
+ key: '0-0-2',
+ },
+ ],
+ },
+ {
+ title: '0-1',
+ key: '0-1',
+ children: [
+ {
+ title: '0-1-0-0',
+ key: '0-1-0-0',
+ },
+ {
+ title: '0-1-0-1',
+ key: '0-1-0-1',
+ },
+ {
+ title: '0-1-0-2',
+ key: '0-1-0-2',
+ },
+ ],
+ },
+ {
+ title: '0-2',
+ key: '0-2',
+ },
+];
+
+export function MetaDataTree() {
+ const [expandedKeys, setExpandedKeys] = useState(['0-0-0', '0-0-1']);
+ const [checkedKeys, setCheckedKeys] = useState(['0-0-0']);
+ const [selectedKeys, setSelectedKeys] = useState([]);
+ const [autoExpandParent, setAutoExpandParent] = useState(true);
+
+ const onExpand = expandedKeysValue => {
+ console.log('onExpand', expandedKeysValue); // if not set autoExpandParent to false, if children expanded, parent can not collapse.
+ // or, you can remove all expanded children keys.
+
+ setExpandedKeys(expandedKeysValue);
+ setAutoExpandParent(false);
+ };
+
+ const onCheck = checkedKeysValue => {
+ console.log('onCheck', checkedKeysValue);
+ setCheckedKeys(checkedKeysValue);
+ };
+
+ const onSelect = (selectedKeysValue, info) => {
+ console.log('onSelect', info);
+ setSelectedKeys(selectedKeysValue);
+ };
+
+ return (
+
+ );
+}
diff --git a/frontend/src/components/not-found/index.tsx b/frontend/src/components/not-found/index.tsx
new file mode 100644
index 0000000..27b88ef
--- /dev/null
+++ b/frontend/src/components/not-found/index.tsx
@@ -0,0 +1,27 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import * as React from 'react';
+export const NotFound = () => {
+ return (
+ <>
+
+ 404
+
+ >
+ );
+};
diff --git a/frontend/src/components/sidebar/sidebar.less b/frontend/src/components/sidebar/sidebar.less
new file mode 100644
index 0000000..755c47c
--- /dev/null
+++ b/frontend/src/components/sidebar/sidebar.less
@@ -0,0 +1,47 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+.logo {
+ width: 10em;
+ height: 4em;
+
+ background-image: url(../../assets/logo_manager.png);
+ background-size: 100% 100%;
+}
+
+.logo-collapsed {
+ width: 34px;
+ height: 34px;
+ background-image: url(../../assets/doris.png);
+ background-size: 100% 100%;
+}
+
+.doris-manager-side {
+ position: fixed;
+ left: 0;
+ top: 0;
+ height: 100%;
+ z-index: 9999;
+ background-color: #002140 !important;
+}
+
+.line {
+ height: 1px;
+ margin: 14px 24px;
+ background-color: #fff;
+
+}
diff --git a/frontend/src/components/sidebar/sidebar.tsx b/frontend/src/components/sidebar/sidebar.tsx
new file mode 100644
index 0000000..2cebec4
--- /dev/null
+++ b/frontend/src/components/sidebar/sidebar.tsx
@@ -0,0 +1,169 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { Menu } from 'antd';
+import Sider from 'antd/lib/layout/Sider';
+import {
+ ClusterOutlined,
+ ConsoleSqlOutlined,
+ DatabaseOutlined,
+ SettingOutlined,
+ TableOutlined,
+ AppstoreOutlined,
+} from '@ant-design/icons';
+import { Link, useHistory } from 'react-router-dom';
+import React, { useState, useEffect, useContext } from 'react';
+
+import { useTranslation } from 'react-i18next';
+import styles from './sidebar.less';
+import { UserInfoContext } from '@src/common/common.context';
+
+const GLOBAL_PATHS = ['/settings', '/space'];
+
+export function Sidebar(props: any) {
+ const { t } = useTranslation();
+ const [selectedKeys, setSelectedKeys] = useState('/dashboard/overview');
+ const [collapsed, setCollapsed] = useState(true);
+ const { mode } = props;
+ const user = useContext(UserInfoContext);
+
+ const history = useHistory();
+ const isSuperAdmin = user?.is_super_admin;
+ const isSpaceAdmin = user?.is_admin;
+ const isInSpace = !GLOBAL_PATHS.includes(selectedKeys);
+ useEffect(() => {
+ if (history.location.pathname.includes('configuration')) {
+ setSelectedKeys('/configuration');
+ } else if (history.location.pathname.startsWith('/cluster')) {
+ setSelectedKeys('/cluster');
+ } else if (history.location.pathname.startsWith('/space')) {
+ setSelectedKeys('/space');
+ } else if (history.location.pathname.startsWith('/settings')) {
+ setSelectedKeys('/settings');
+ } else if (history.location.pathname.startsWith('/admin')) {
+ setSelectedKeys('/admin');
+ } else {
+ setSelectedKeys(history.location.pathname);
+ }
+ }, [history.location.pathname]);
+
+ function onCollapse() {
+ setCollapsed(!collapsed);
+ }
+
+ const handleMenuChange = (e: any) => {
+ setSelectedKeys(e.key);
+ };
+
+ if (mode === 'initialize') {
+ return (
+
+
+
+ history.push(`/meta/index`)}
+ />
+
+
+
+ );
+ } else {
+ return (
+
+
+
+
+
+ {isInSpace && (
+ <>
+ }>
+ {t`Cluster`}
+
+ }>
+ {t`data`}
+
+ }>
+ {t`Query`}
+
+ {(isSuperAdmin || isSpaceAdmin) && (
+ }>
+ {t`Space Manager`}
+
+ )}
+
+ >
+ )}
+ }>
+ {t`Space List`}
+
+ {isSuperAdmin && (
+ }>
+ {t`Platform Settings`}
+
+ )}
+
+
+ );
+ }
+}
diff --git a/frontend/src/components/status-mark/index.module.less b/frontend/src/components/status-mark/index.module.less
new file mode 100644
index 0000000..260375a
--- /dev/null
+++ b/frontend/src/components/status-mark/index.module.less
@@ -0,0 +1,42 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+.wrapper {
+ display: inline-flex;
+ align-items: center;
+ .mark {
+ width: 7px;
+ height: 7px;
+ border-radius: 50%;
+ margin-right: 5px;
+ &.mark-success {
+ background-color: #0cce0c;
+ }
+ &.mark-error {
+ background-color: red;
+ }
+ &.mark-info {
+ background-color: #91d5ff;
+ }
+ &.mark-warning {
+ background-color: #f7e568;
+ }
+ &.mark-deactivated {
+ background-color: #b7b3b3;
+ }
+ }
+}
diff --git a/frontend/src/components/status-mark/index.tsx b/frontend/src/components/status-mark/index.tsx
new file mode 100644
index 0000000..86dc747
--- /dev/null
+++ b/frontend/src/components/status-mark/index.tsx
@@ -0,0 +1,39 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { PropsWithChildren } from 'react';
+import classnames from 'classnames';
+import styles from './index.module.less';
+
+type Status = 'success' | 'warning' | 'error' | 'info' | 'deactivated';
+
+export interface StatusMarkProps {
+ status: Status;
+}
+
+export default function StatusMark(props: PropsWithChildren
) {
+ const { status, children } = props;
+
+ const markClassName = classnames(styles.mark, styles[`mark-${status}`]);
+
+ return (
+
+ );
+}
diff --git a/frontend/src/components/studio-header/header.api.ts b/frontend/src/components/studio-header/header.api.ts
new file mode 100644
index 0000000..5333ee5
--- /dev/null
+++ b/frontend/src/components/studio-header/header.api.ts
@@ -0,0 +1,37 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+/* eslint-disable prettier/prettier */
+/** @format */
+
+import { http } from '@src/utils/http';
+function getCurrentUser() {
+ return http.get(`/api/user/current`);
+}
+
+function getSpaceName(data: any) {
+ return http.get(`/api/space/${data}`);
+}
+
+function signOut() {
+ return http.delete(`/api/session/`);
+}
+export const LayoutAPI = {
+ getCurrentUser,
+ getSpaceName,
+ signOut,
+};
diff --git a/frontend/src/components/studio-header/header.tsx b/frontend/src/components/studio-header/header.tsx
new file mode 100644
index 0000000..c860a6d
--- /dev/null
+++ b/frontend/src/components/studio-header/header.tsx
@@ -0,0 +1,148 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import logo from '@assets/logo_nav.png';
+import React, { useEffect, useMemo, useState } from 'react';
+import styles from './index.module.less';
+// import queryString from 'query-string';
+import { Anchor, Col, Dropdown, Input, Layout, Menu, message, Row, Tooltip } from 'antd';
+import {
+ AppstoreAddOutlined,
+ LeftOutlined,
+ SearchOutlined,
+} from '@ant-design/icons';
+import { Link, RouteComponentProps, useHistory, useRouteMatch } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+import { SettingsIcon } from '../settings-icon/settings-icon';
+import { auth } from '@src/utils/auth';
+
+type HeaderMode = 'normal' | 'initialize' | 'super-admin';
+interface HeaderProps {
+ mode: HeaderMode;
+}
+
+export function Header(props: HeaderProps) {
+ const { t } = useTranslation();
+ const history = useHistory();
+ // const { q } = queryString.parse(history.location.search) as { q: string };
+ const { mode = 'normal' } = props;
+ const showHeaderFuncs = mode !== 'initialize' && mode !== 'super-admin';
+ const isHistoryQueryPage = history.location.pathname.includes('history-query');
+
+ const initialized = auth.checkInitialized();
+
+ // const searchBar = useMemo(
+ // () => (
+ // }
+ // defaultValue={q}
+ // onPressEnter={val => {
+ // history.push({ pathname: '/search', search: `?q=${val.target.value}` });
+ // }}
+ // />
+ // ),
+ // [q],
+ // );
+ return (
+
+
{
+ history.push(`/space`);
+ }}>
+
+
+ {/* {showHeaderFuncs &&
{searchBar}
} */}
+
+ {/* {showHeaderFuncs && (
+
+
+ {
+ history.push(`/collection`);
+ }}
+ />
+
+
+ )} */}
+ {/*
+ (window.location.href = `${window.location.origin}`)}
+ />
+ 创建查询
+
+
+
+ (window.location.href = `${window.location.origin}/new-studio/browse`)}
+ />
+
+
+
+
+ (window.location.href = `${window.location.origin}/new-studio/`)}
+ />
+
+
+ {statisticInfo.manager_enable && (
+
+
+ (window.location.href = `${window.location.origin}/d-stack`)}
+ />
+
+
+ )
+ }
+
+
+ {
+ const analyticsUrl = `${window.location.origin}/docs/pages/产品概述/产品介绍.html`;
+ window.open(analyticsUrl);
+ }}
+ />
+
+
*/}
+ {showHeaderFuncs && isHistoryQueryPage && (
+
+
+
+ (window.location.href = `${window.location.origin}/question#eyJkYXRhc2V0X3F1ZXJ5Ijp7ImRhdGFiYXNlIjpudWxsLCJuYXRpdmUiOnsicXVlcnkiOiIiLCJ0ZW1wbGF0ZS10YWdzIjp7fX0sInR5cGUiOiJOQVRJVkUifSwiZGlzcGxheSI6InRhYmxlIiwidmlzdWFsaXphdGlvbl9zZXR0aW5ncyI6e319`)
+ }
+ />
+
+
+ )}
+ {initialized && (
+
+
+
+ )}
+
+
+ );
+}
diff --git a/frontend/src/components/studio-header/index.module.less b/frontend/src/components/studio-header/index.module.less
new file mode 100644
index 0000000..d89dd29
--- /dev/null
+++ b/frontend/src/components/studio-header/index.module.less
@@ -0,0 +1,85 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+.palo-header {
+ width: 100%;
+ height: 65px;
+ background: #000;
+ color: #fff;
+ font-size: 14px;
+ display: flex;
+ align-items: center;
+ padding: 8px 16px;
+ justify-content: space-between;
+
+ .palo-logo{
+ width: 146px;
+ height: 60px;
+ margin-right: 8px;
+ background-image: url('../../assets/logo_nav.png');
+ img{
+ height: 100%;
+ }
+ }
+
+ .palo-search{
+ flex: 1;
+ .search-icon{
+ font-size: 16px;
+ color: #FFF;
+ }
+ &>span{
+ width: 480px;
+ height: 50px;
+ background: rgb(59, 59, 59);
+ border-radius: 6px;
+ border: none;
+ input{
+ background: transparent;
+ color: #fff;
+ font-size: 1em;
+ }
+ }
+ }
+
+ .palo-opt-box{
+ display: flex;
+ height: 50px;
+ align-items: center;
+ div{
+ margin-right: 16px;
+ padding: 10px;
+ cursor: pointer;
+ .icon{
+ font-size: 20px;
+ }
+ .icon-tip{
+ margin-left: 0.5rem;
+ vertical-align: text-bottom;
+ }
+ }
+ div:hover{
+ border-radius: 8px;
+ background: rgb(32,78,171);
+ }
+ div:last-child{
+ margin-right: 0;
+ }
+ }
+}
+
+
diff --git a/frontend/src/components/tabs-header/index.tsx b/frontend/src/components/tabs-header/index.tsx
new file mode 100644
index 0000000..3ec754d
--- /dev/null
+++ b/frontend/src/components/tabs-header/index.tsx
@@ -0,0 +1,51 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React from 'react';
+import { useLocation, useHistory } from 'react-router-dom';
+import { Tabs } from 'antd';
+
+const { TabPane } = Tabs;
+
+interface TabsHeaderProps {
+ routes: {
+ label: string;
+ path: string;
+ }[];
+}
+
+export default function TabsHeader(props: TabsHeaderProps) {
+ const { routes } = props;
+ const { pathname } = useLocation();
+ const history = useHistory();
+
+ const handleTabChange = (key: string) => {
+ history.replace(key);
+ };
+
+ const findActiveKey = (pathname: string) => {
+ return routes.find(route => pathname.startsWith(route.path))?.path;
+ };
+
+ return (
+
+ {routes.map(route => (
+
+ ))}
+
+ );
+}
diff --git a/frontend/src/config.ts b/frontend/src/config.ts
new file mode 100644
index 0000000..442dc5a
--- /dev/null
+++ b/frontend/src/config.ts
@@ -0,0 +1,28 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { getDefaultPageSize } from './utils/utils';
+import version from '../version.json';
+
+export const TABLE_DELAY = 150;
+export const PAGESIZE_OPTIONS = ['10', '20', '50', '100', '200'];
+export const PAGINATION = {
+ current: 1,
+ pageSize: getDefaultPageSize(),
+ // total: 0
+};
+export const VERSION = `v${version.version}`;
diff --git a/frontend/src/hooks/use-async.ts b/frontend/src/hooks/use-async.ts
new file mode 100644
index 0000000..4cbbf37
--- /dev/null
+++ b/frontend/src/hooks/use-async.ts
@@ -0,0 +1,107 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { useReducer, useCallback, useRef, useEffect } from 'react';
+import * as _ from 'lodash-es';
+
+export interface State {
+ loading: boolean;
+ data: T | undefined;
+ error: Error | null;
+}
+
+const defaultState = {
+ loading: false,
+ data: undefined,
+ error: null,
+};
+
+const defaultConfig = {
+ setStartLoading: true,
+ setEndLoading: true,
+};
+
+export type RunFunction = (promise: Promise, config?: Partial) => Promise;
+
+function useSafeDispatch(dispatch: (...args: T[]) => void) {
+ const isMountedRef = useRef(false);
+ useEffect(() => {
+ isMountedRef.current = true;
+ return () => {
+ isMountedRef.current = false;
+ };
+ }, []);
+ const safeDispatch = useCallback(
+ (...args: T[]) => {
+ if (isMountedRef.current) {
+ dispatch(...args);
+ }
+ },
+ [dispatch],
+ );
+ return {
+ safeDispatch,
+ isMountedRef,
+ };
+}
+
+export function useAsync(initialState?: Partial>) {
+ const finalState = {
+ ...defaultState,
+ ...initialState,
+ };
+ const [state, dispatch] = useReducer((state: State, action: Partial>) => ({ ...state, ...action }), {
+ ...finalState,
+ });
+
+ const { safeDispatch, isMountedRef } = useSafeDispatch(dispatch);
+
+ const run = useCallback>(
+ (promise, config) => {
+ const finalConfig = {
+ ...defaultConfig,
+ ...config,
+ };
+ const { setStartLoading, setEndLoading } = finalConfig;
+ if (setStartLoading) {
+ safeDispatch({ loading: true });
+ }
+ return promise
+ .then(data => {
+ safeDispatch({ data, error: _.cloneDeep(finalState.error) });
+ return data;
+ })
+ .catch(error => {
+ safeDispatch({ data: _.cloneDeep(finalState.data), error });
+ return Promise.reject(error);
+ })
+ .finally(() => {
+ if (setEndLoading) {
+ safeDispatch({ loading: false });
+ }
+ });
+ },
+ [safeDispatch],
+ );
+
+ return {
+ run,
+ dispatch: safeDispatch,
+ isMountedRef,
+ ...state,
+ };
+}
diff --git a/frontend/src/hooks/use-auth.ts b/frontend/src/hooks/use-auth.ts
new file mode 100644
index 0000000..b50fa06
--- /dev/null
+++ b/frontend/src/hooks/use-auth.ts
@@ -0,0 +1,53 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { InitializeAPI } from '@src/routes/initialize/initialize.api';
+import { isSuccess } from '@src/utils/http';
+import { useEffect, useState } from 'react';
+import { useHistory } from 'react-router';
+
+export function useAuth() {
+ const [initialized, setInitialized] = useState(false);
+ const history = useHistory();
+ const [initStep, setInitStep] = useState(0);
+ const [authType, setAuthType] = useState();
+ useEffect(() => {
+ getInitProperties();
+ }, []);
+
+ async function getInitProperties() {
+ const res = await InitializeAPI.getInitProperties();
+ if (isSuccess(res)) {
+ setInitStep(res.data.initStep);
+ setAuthType(res.data.auth_type);
+ if (res.data.completed) {
+ localStorage.setItem('initialized', 'true');
+ setInitialized(true);
+ } else {
+ localStorage.setItem('initialized', 'false');
+ setInitialized(false);
+ history.push('/initialize');
+ }
+ }
+ }
+ return {
+ initialized,
+ initStep,
+ authType,
+ getInitProperties,
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/hooks/use-pagination.ts b/frontend/src/hooks/use-pagination.ts
new file mode 100644
index 0000000..be6217a
--- /dev/null
+++ b/frontend/src/hooks/use-pagination.ts
@@ -0,0 +1,47 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { useState } from 'react';
+import { PAGESIZE_OPTIONS, PAGINATION } from '@src/config';
+
+function usePagination(config?: any) {
+ const defaultConfig = config || PAGINATION;
+ const [Pagination, setPagination] = useState(defaultConfig);
+ const PaginationForView = {
+ ...Pagination,
+ showQuickJumper: false,
+ size: 'small',
+ showSizeChanger: true,
+ pageSizeOptions: PAGESIZE_OPTIONS,
+ total: 0,
+ };
+ const PaginationForViewWithoutCurrent = {};
+ for (const [key, value] of Object.entries(PaginationForView)) {
+ if (key !== 'current') {
+ PaginationForViewWithoutCurrent[key] = value;
+ }
+ }
+
+ return {
+ Pagination,
+ PaginationForView,
+ PaginationForViewWithoutCurrent,
+ setPagination,
+ };
+}
+
+export { usePagination };
diff --git a/frontend/src/hooks/use-roles.hooks.ts b/frontend/src/hooks/use-roles.hooks.ts
new file mode 100644
index 0000000..5b8a558
--- /dev/null
+++ b/frontend/src/hooks/use-roles.hooks.ts
@@ -0,0 +1,81 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { CommonAPI } from "@src/common/common.api";
+import { IRole, IMember } from "@src/common/common.interface";
+import { isSuccess } from "@src/utils/http";
+import { Dispatch, SetStateAction, useState, useEffect } from "react";
+
+export function useRoles(): {
+ roles: IRole[];
+ setRoles: Dispatch>;
+ getRoles: () => void;
+ loading: boolean;
+} {
+ const [roles, setRoles] = useState([]);
+ const [loading, setLoading] = useState(false);
+ useEffect(() => {
+ getRoles();
+ }, []);
+
+ async function getRoles() {
+ setLoading(true);
+ const res = await CommonAPI.getRoles();
+ setLoading(false);
+ if (isSuccess(res)) {
+ setRoles(res.data);
+ }
+ }
+ return {
+ roles,
+ setRoles,
+ getRoles,
+ loading
+ };
+}
+
+
+export function useRoleMember(roleId: string): {
+ members: IMember;
+ setMembers: Dispatch>;
+ getRoleMembers: () => void;
+ loading: boolean;
+} {
+ const [members, setMembers] = useState({name: '', id: 0, members: []});
+ const [loading, setLoading] = useState(false);
+ useEffect(() => {
+ getRoleMembers();
+ }, []);
+
+ async function getRoleMembers() {
+ setLoading(true);
+ const res = await CommonAPI.getRoleMembersById({
+ roleId: roleId
+ });
+ console.log(res);
+ setLoading(false);
+ if (isSuccess(res)) {
+ setMembers(res.data);
+ }
+ }
+ return {
+ members,
+ setMembers,
+ getRoleMembers,
+ loading
+ };
+}
\ No newline at end of file
diff --git a/frontend/src/hooks/use-userinfo.hooks.ts b/frontend/src/hooks/use-userinfo.hooks.ts
new file mode 100644
index 0000000..d36bffb
--- /dev/null
+++ b/frontend/src/hooks/use-userinfo.hooks.ts
@@ -0,0 +1,35 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { CommonAPI } from "@src/common/common.api";
+import { isSuccess } from "@src/utils/http";
+import { Dispatch, SetStateAction, useState, useEffect } from "react";
+
+export function useUserInfo(): [any, Dispatch>] {
+ const [userInfo, setUserInfo] = useState({});
+ useEffect(() => {
+ getUserInfo();
+ }, []);
+
+ async function getUserInfo() {
+ const res = await CommonAPI.getUserInfo();
+ if (isSuccess(res)) {
+ setUserInfo(res.data);
+ }
+ }
+ return [userInfo, setUserInfo];
+}
\ No newline at end of file
diff --git a/frontend/src/hooks/use-users.hooks.ts b/frontend/src/hooks/use-users.hooks.ts
new file mode 100644
index 0000000..a33fb88
--- /dev/null
+++ b/frontend/src/hooks/use-users.hooks.ts
@@ -0,0 +1,58 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { CommonAPI } from '@src/common/common.api';
+import { isSuccess } from '@src/utils/http';
+import { Dispatch, SetStateAction, useState, useEffect } from 'react';
+
+export function useUsers(): {
+ users: any[];
+ setUsers: Dispatch>;
+ getUsers: () => void;
+} {
+ const [users, setUsers] = useState();
+ useEffect(() => {
+ getUsers();
+ }, []);
+
+ async function getUsers() {
+ const res = await CommonAPI.getUsers();
+ if (isSuccess(res)) {
+ setUsers(res.data);
+ }
+ }
+ return { users, getUsers, setUsers };
+}
+
+export function useSpaceUsers(): {
+ users: any[];
+ setUsers: Dispatch>;
+ getUsers: () => void;
+} {
+ const [users, setUsers] = useState();
+ useEffect(() => {
+ getUsers();
+ }, []);
+
+ async function getUsers() {
+ const res = await CommonAPI.getSpaceUsers()
+ if (isSuccess(res)) {
+ setUsers(res.data);
+ }
+ }
+ return { users, getUsers, setUsers };
+}
diff --git a/frontend/src/i18n.tsx b/frontend/src/i18n.tsx
new file mode 100644
index 0000000..0af2a13
--- /dev/null
+++ b/frontend/src/i18n.tsx
@@ -0,0 +1,50 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import LanguageDetector from 'i18next-browser-languagedetector';
+import i18n from 'i18next';
+import enUsTrans from '../public/locales/en-us.json';
+import zhCnTrans from '../public/locales/zh-cn.json';
+import {
+ initReactI18next
+} from 'react-i18next';
+i18n.use(LanguageDetector)
+ .use(initReactI18next)
+ .init({
+ detection: {
+ caches: ['localStorage', 'cookie']
+ },
+ resources: {
+ en: {
+ translation: enUsTrans
+ },
+ zh: {
+ translation: zhCnTrans
+ }
+ },
+ lng: localStorage.getItem('i18nextLng') || 'zh',
+ fallbackLng: 'zh',
+ debug: false,
+ interpolation: {
+ escapeValue: false
+ }
+ });
+
+export default i18n;
+
diff --git a/frontend/src/index.css b/frontend/src/index.css
new file mode 100644
index 0000000..ac4a59a
--- /dev/null
+++ b/frontend/src/index.css
@@ -0,0 +1,62 @@
+/* // Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License. */
+
+#root {
+ min-width: 100vw;
+ height: 100%;
+ min-height: 100vh;
+ background-color: white;
+}
+.ant-layout {
+ background-color: white !important;
+}
+ul,ol,li {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+h1,h2,h3,h4,h5,h6 {
+ margin: 0;
+ padding: 0;
+}
+.ant-pro-page-container-children-content {
+ margin: 16px 20px 0;
+}
+.editable-cell {
+ position: relative;
+}
+
+.editable-cell-value-wrap {
+ padding: 5px 12px;
+ cursor: pointer;
+}
+
+.editable-row:hover .editable-cell-value-wrap {
+ padding: 4px 11px;
+ border: 1px solid #d9d9d9;
+ border-radius: 2px;
+}
+
+[data-theme='dark'] .editable-row:hover .editable-cell-value-wrap {
+ border: 1px solid #434343;
+}
+[data-theme='dark'] .ant-menu-dark .ant-menu-sub, .ant-menu.ant-menu-dark, .ant-menu.ant-menu-dark .ant-menu-sub {
+ background: #001528 !important;
+}
+.ant-layout-sider {
+ position: fixed !important;
+}
\ No newline at end of file
diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx
new file mode 100644
index 0000000..936ef70
--- /dev/null
+++ b/frontend/src/index.tsx
@@ -0,0 +1,25 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import App from './app';
+import React from 'react';
+import ReactDOM from 'react-dom';
+import './index.css';
+import './i18n';
+
+
+ReactDOM.render( , document.querySelector("#root"));
diff --git a/frontend/src/interfaces/http.interface.ts b/frontend/src/interfaces/http.interface.ts
new file mode 100644
index 0000000..6f6f077
--- /dev/null
+++ b/frontend/src/interfaces/http.interface.ts
@@ -0,0 +1,22 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+export interface IResult {
+ msg: string;
+ code: number;
+ data: T;
+ count: number;
+}
diff --git a/frontend/src/layout/page-side/index.tsx b/frontend/src/layout/page-side/index.tsx
new file mode 100644
index 0000000..dbd6323
--- /dev/null
+++ b/frontend/src/layout/page-side/index.tsx
@@ -0,0 +1,51 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ * @format
+ */
+
+import React, { useState, useEffect, SyntheticEvent } from 'react';
+import { Resizable } from 're-resizable';
+import styles from './page-side.module.less';
+
+export function PageSide(props: any) {
+ const { children } = props;
+ const [sideBoxWidth, setSideBoxWidth] = useState(300);
+ const directionEnable = {
+ top: false,
+ right: true,
+ bottom: false,
+ left: false,
+ topRight: false,
+ bottomRight: false,
+ bottomLeft: false,
+ topLeft: false,
+ };
+ return (
+ {
+ setSideBoxWidth(sideBoxWidth + d.width);
+ }}
+ className={styles['build-page-side']}
+ >
+ {props.children}
+
+ );
+}
diff --git a/frontend/src/layout/page-side/page-side.module.less b/frontend/src/layout/page-side/page-side.module.less
new file mode 100644
index 0000000..bbc67a8
--- /dev/null
+++ b/frontend/src/layout/page-side/page-side.module.less
@@ -0,0 +1,27 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+/** @format */
+
+.build-page-side {
+ min-width: 300px;
+ max-width: 600px;
+ padding: 20px;
+ background-color: #edf2f5;
+ border-radius: 16px;
+ // box-shadow: 3px 3px 3px #bfbdbd;
+}
diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx
new file mode 100644
index 0000000..df577e4
--- /dev/null
+++ b/frontend/src/routes.tsx
@@ -0,0 +1,59 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React from 'react';
+import { Loading } from './components/loading';
+import { NotFound } from './components/not-found';
+import { Suspense } from 'react';
+
+
+import { Route, Switch, BrowserRouter as Router } from 'react-router-dom';
+import { InitializedRoute } from './components/initialized-route/initialized-route';
+import { Settings } from './routes/settings/settings';
+import { Initialize } from './routes/initialize/initialize.route';
+import { SuperAdminContainer } from './routes/super-admin-container';
+import { Admin } from './routes/admin/admin';
+import { Login } from './routes/passport/login';
+import { Container } from './routes/container';
+
+const routes = (
+ }>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+
+export default routes;
diff --git a/frontend/src/routes/admin/admin.module.less b/frontend/src/routes/admin/admin.module.less
new file mode 100644
index 0000000..064c9f9
--- /dev/null
+++ b/frontend/src/routes/admin/admin.module.less
@@ -0,0 +1,26 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+.container {
+ margin-left: 80px;
+ overflow-y: scroll;
+ height: calc(100% - 44px);
+}
+
+.card {
+ min-height: 100%;
+}
diff --git a/frontend/src/routes/admin/admin.tsx b/frontend/src/routes/admin/admin.tsx
new file mode 100644
index 0000000..ecfc2a2
--- /dev/null
+++ b/frontend/src/routes/admin/admin.tsx
@@ -0,0 +1,75 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { Suspense, useEffect, useMemo, useState } from 'react';
+import { Redirect, Switch, Route, useHistory } from 'react-router-dom';
+import { Card } from 'antd';
+import { useTranslation } from 'react-i18next';
+import styles from './admin.module.less';
+import { UserInfoContext } from '@src/common/common.context';
+import { Sidebar } from '@src/components/sidebar/sidebar';
+import { Header } from '@src/components/header/header';
+import TabsHeader from '@src/components/tabs-header';
+import { SpaceDetail } from '../space/detail/space-detail';
+import { People } from './people/people';
+import { useUserInfo } from '@src/hooks/use-userinfo.hooks';
+import LoadingLayout from '@src/components/loading-layout';
+
+export function Admin() {
+ const {t} = useTranslation()
+ const history = useHistory();
+ const [loading, setLoading] = useState(true);
+ const [userInfo] = useUserInfo();
+ useEffect(() => {
+ if (userInfo.id == null) return;
+ if (userInfo.id != null && !userInfo.is_super_admin && !userInfo.is_admin) {
+ history.push('/');
+ return;
+ }
+ setLoading(false);
+ }, [userInfo.id, userInfo.is_super_admin, userInfo.is_admin]);
+ const tabRoutes = useMemo(
+ () => [
+ { label: t`spaceInfo`, path: `/admin/space/${userInfo.space_id}` },
+ { label: t`members`, path: '/admin/people/user' },
+ { label: t`roles`, path: '/admin/people/role' },
+ ],
+ [userInfo.space_id, t],
+ );
+ return (
+
+
+
+
+
+
+ }
+ >
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/routes/admin/people/people.less b/frontend/src/routes/admin/people/people.less
new file mode 100644
index 0000000..edc11b1
--- /dev/null
+++ b/frontend/src/routes/admin/people/people.less
@@ -0,0 +1,31 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+.optionContent {
+ width: 100%;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+ span {
+ margin-left: 5px;
+ }
+}
+
+.optionTag {
+ position: absolute;
+ right: 30px;
+}
diff --git a/frontend/src/routes/admin/people/people.tsx b/frontend/src/routes/admin/people/people.tsx
new file mode 100644
index 0000000..1aa6728
--- /dev/null
+++ b/frontend/src/routes/admin/people/people.tsx
@@ -0,0 +1,35 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React from 'react';
+import { Role } from './role/role';
+import { Redirect, Route, Switch } from 'react-router-dom';
+import { User } from './user/user';
+
+export function People(props: any) {
+ const { match } = props;
+ console.log(match)
+ return (
+ <>
+
+
+
+
+
+ >
+ );
+}
diff --git a/frontend/src/routes/admin/people/role/list/create-or-edit-modal.tsx b/frontend/src/routes/admin/people/role/list/create-or-edit-modal.tsx
new file mode 100644
index 0000000..fe3c333
--- /dev/null
+++ b/frontend/src/routes/admin/people/role/list/create-or-edit-modal.tsx
@@ -0,0 +1,103 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { isSuccess } from '@src/utils/http';
+import { Form, Input, message, Modal } from 'antd';
+import React, { useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { RoleAPI } from '../role.api';
+
+export function CreateOrEditRoleModal(props: any) {
+ const { t } = useTranslation();
+ const { onCancel, role } = props;
+ const [loading, setLoading] = useState(false);
+ const title = role ? t`editRole` : t`createRole`;
+
+ useEffect(() => {
+ props.form.setFieldsValue({ name: props.role ? props.role.name : '' });
+ }, [props.role]);
+
+ async function handleCreate(values: any) {
+ setLoading(true);
+ try {
+ const res = await RoleAPI.createRole(values);
+ setLoading(false);
+ if (isSuccess(res)) {
+ message.success(t`createSuccess`);
+ props.onSuccess && props.onSuccess();
+ } else {
+ message.error(res.msg);
+ }
+ } catch (err) {
+ setLoading(false);
+ }
+ }
+ async function handleEdit(values: any) {
+ setLoading(true);
+ const res = await RoleAPI.updateRole({
+ id: (role as any).id,
+ name: values.name,
+ });
+ setLoading(false);
+ if (isSuccess(res)) {
+ message.success(t`SuccessfullyModified`);
+ props.onSuccess && props.onSuccess();
+ } else {
+ message.error(res.msg);
+ }
+ }
+ return (
+ {
+ props.form
+ .validateFields()
+ .then((values: any) => {
+ if (!role) {
+ handleCreate(values);
+ } else {
+ console.log(values);
+ handleEdit(values);
+ }
+ })
+ .catch((info: any) => {
+ console.log('Validate Failed:', info);
+ });
+ }}
+ >
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/routes/admin/people/role/list/list.tsx b/frontend/src/routes/admin/people/role/list/list.tsx
new file mode 100644
index 0000000..f5c62c9
--- /dev/null
+++ b/frontend/src/routes/admin/people/role/list/list.tsx
@@ -0,0 +1,153 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { ExclamationCircleOutlined } from '@ant-design/icons';
+import { IRole } from '@src/common/common.interface';
+import { FlatBtnGroup, FlatBtn } from '@src/components/flatbtn';
+import { useRoles } from '@src/hooks/use-roles.hooks';
+import { isSuccess } from '@src/utils/http';
+import { showName } from '@src/utils/utils';
+import { Button, Table, Modal, message, Row } from 'antd';
+import { useForm } from 'antd/lib/form/Form';
+import React, { useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useRouteMatch } from 'react-router';
+import { RoleAPI } from '../role.api';
+import { CreateOrEditRoleModal } from './create-or-edit-modal';
+
+export function RoleList(props: any) {
+ const { t } = useTranslation();
+ const { roles, getRoles, loading } = useRoles();
+ const [visible, setVisible] = useState(false);
+ const [currentRole, setCurrentRole] = useState();
+ const [modalLoading, setModalLoading] = useState(false);
+ const match = useRouteMatch();
+ const [form] = useForm();
+ const { confirm } = Modal;
+ const columns = [
+ {
+ title: t`roleName`,
+ key: 'name',
+ render: (record: IRole) => {showName(record.name)} ,
+ },
+ {
+ title: t`members`,
+ dataIndex: 'member_count',
+ key: 'member_count',
+ },
+ {
+ title: t`operation`,
+ key: 'actions',
+ render: (record: IRole) => {
+ const forbiddenEditRole = record.name.includes('Administrators') || record.name.includes('All Users');
+ return (
+
+ {
+ console.log(record);
+ setCurrentRole(record);
+ setVisible(true);
+ }}
+ >
+ {t`edit`}
+
+ handleDelete(record)}>
+ {t`Delete`}
+
+
+ );
+ },
+ },
+ ];
+ function handleDelete(record: IRole) {
+ confirm({
+ title: t`deleteThisRole`,
+ icon: ,
+ content: t`deleteThisRoleMessage`,
+ onOk() {
+ return RoleAPI.deleteRole({ roleId: record.id }).then(res => {
+ if (isSuccess(res)) {
+ message.success(t`DeleteSuccessTips`);
+ getRoles();
+ } else {
+ message.error(res.msg);
+ }
+ });
+ },
+ okType: 'danger',
+ onCancel() {
+ console.log('Cancel');
+ },
+ });
+ }
+ function onCancel() {
+ setVisible(false);
+ }
+ // async function handleCreate(values: any) {
+ // setCurrentRole(undefined);
+ // setModalLoading(true);
+ // const res = await RoleAPI.createRole(values);
+ // setModalLoading(false);
+ // if (isSuccess(res)) {
+ // message.success('创建成功');
+ // setVisible(false);
+ // getRoles();
+ // }
+ // }
+ async function handleEdit(values: any) {}
+ return (
+ <>
+
+
+ {t`roleTopMessage`}
+
+ {
+ setVisible(true);
+ setCurrentRole(undefined);
+ }}
+ >
+ {t`create`}
+
+
+
+
+ {visible && (
+ {
+ setVisible(false);
+ form.resetFields();
+ getRoles();
+ }}
+ form={form}
+ role={currentRole}
+ onCancel={() => setVisible(false)}
+ />
+ )}
+ >
+ );
+}
diff --git a/frontend/src/routes/admin/people/role/member/member.tsx b/frontend/src/routes/admin/people/role/member/member.tsx
new file mode 100644
index 0000000..e2ea2e8
--- /dev/null
+++ b/frontend/src/routes/admin/people/role/member/member.tsx
@@ -0,0 +1,204 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { ExclamationCircleOutlined } from '@ant-design/icons';
+import { UserInfoContext } from '@src/common/common.context';
+import { FlatBtn } from '@src/components/flatbtn';
+import { useRoleMember } from '@src/hooks/use-roles.hooks';
+import { useSpaceUsers } from '@src/hooks/use-users.hooks';
+import { isSuccess } from '@src/utils/http';
+import { showName } from '@src/utils/utils';
+import { Button, Table, Modal, Form, Select, message, Row } from 'antd';
+import { useForm } from 'antd/lib/form/Form';
+import { ColumnsType } from 'antd/lib/table';
+import React, { useContext, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useRouteMatch } from 'react-router';
+import { RoleAPI } from '../role.api';
+import commonStyles from '../../people.less';
+
+export function RoleMembers(props: any) {
+ const { t } = useTranslation();
+ const match = useRouteMatch<{ roleId: string }>();
+ const { users } = useSpaceUsers();
+ const { members, getRoleMembers, loading } = useRoleMember(match.params.roleId);
+ const [visible, setVisible] = useState(false);
+ const [confirmLoading, setConfirmLoading] = useState(false);
+ const userInfo = useContext(UserInfoContext)!;
+ const [form] = useForm();
+ const isAllUser = members?.name.includes('All Users');
+ const { confirm } = Modal;
+
+ const filteredUsers = users?.filter(user => {
+ const data = members.members.map(item => item.email);
+ if (data.includes(user.email)) {
+ return false;
+ }
+ return true;
+ });
+
+ const columns: ColumnsType = [
+ {
+ title: t`members`,
+ key: 'name',
+ dataIndex: 'name',
+ },
+ {
+ title: t`Mail`,
+ dataIndex: 'email',
+ key: 'email',
+ },
+ ];
+
+ if (!isAllUser) {
+ columns.push({
+ title: t`operation`,
+ key: 'actions',
+ render: (record: any) => {
+ const forbiddenEditRole =
+ members.name === 'All Users_1' ||
+ (members.name === 'Administrators_1' && members?.members.length <= 1) ||
+ userInfo.name === record.name;
+ return (
+ handleDelete(record)}>
+ {t`remove`}
+
+ );
+ },
+ });
+ }
+
+ async function handleCreate(values: any) {
+ setConfirmLoading(true);
+ const res = await RoleAPI.addMember({
+ user_ids: values.user,
+ group_id: +match.params.roleId,
+ });
+ setConfirmLoading(false);
+ if (isSuccess(res)) {
+ message.success(t`addSuccess`);
+ setVisible(false);
+ form.resetFields();
+ getRoleMembers();
+ } else {
+ message.error(res.msg);
+ }
+ }
+
+ function handleDelete(record: any) {
+ confirm({
+ title: t`removeFromRoleMembers`,
+ icon: ,
+ content: t`removeFromRoleMembersMessage`,
+ onOk() {
+ return RoleAPI.deleteMember({ membership_id: record.membership_id }).then(res => {
+ if (isSuccess(res)) {
+ message.success(t`DeleteSuccessTips`);
+ getRoleMembers();
+ } else {
+ message.error(res.msg);
+ }
+ });
+ },
+ okType: 'danger',
+ onCancel() {
+ console.log('Cancel');
+ },
+ });
+ }
+
+ return (
+ <>
+
+ {showName(members?.name)}
+ {
+ setVisible(true);
+ }}
+ >
+ {t`addMembers`}
+
+
+
+ {visible && (
+ setVisible(false)}
+ confirmLoading={confirmLoading}
+ onOk={() => {
+ form.validateFields()
+ .then(values => {
+ handleCreate(values);
+ })
+ .catch(info => {
+ console.log('Validate Failed:', info);
+ });
+ }}
+ >
+
+ {
+ return (option?.title as string).toLowerCase().indexOf(input.toLowerCase()) >= 0;
+ }}
+ >
+ {filteredUsers?.map((user, index) => {
+ return (
+
+
+
+ {user.common_name}
+ {user.email ? `(${user.email})` : ''}
+
+
+
+ );
+ })}
+
+
+
+
+ )}
+ >
+ );
+}
diff --git a/frontend/src/routes/admin/people/role/role.api.ts b/frontend/src/routes/admin/people/role/role.api.ts
new file mode 100644
index 0000000..7e26155
--- /dev/null
+++ b/frontend/src/routes/admin/people/role/role.api.ts
@@ -0,0 +1,46 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { http } from "@src/utils/http";
+
+function createRole(data: {name: string}) {
+ return http.post('/api/permissions/group', data)
+}
+
+function updateRole(data: {id: number, name: string}) {
+ return http.put(`/api/permissions/group/${data.id}`, {name: data.name})
+}
+
+function deleteRole(data: {roleId: number}) {
+ return http.delete(`/api/permissions/group/${data.roleId}`)
+}
+
+function deleteMember(data: {membership_id: number}) {
+ return http.delete(`/api/permissions/membership/${data.membership_id}`)
+}
+
+function addMember(data: {group_id: number, user_ids: number[]}) {
+ return http.post('/api/permissions/memberships', data)
+}
+
+export const RoleAPI = {
+ createRole,
+ updateRole,
+ deleteRole,
+ addMember,
+ deleteMember
+}
\ No newline at end of file
diff --git a/frontend/src/routes/admin/people/role/role.tsx b/frontend/src/routes/admin/people/role/role.tsx
new file mode 100644
index 0000000..37c941d
--- /dev/null
+++ b/frontend/src/routes/admin/people/role/role.tsx
@@ -0,0 +1,34 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React from 'react';
+import { Redirect, Route, Switch } from 'react-router-dom';
+import { RoleList } from './list/list';
+import { RoleMembers } from './member/member';
+
+export function Role(props: any) {
+ const { match } = props;
+ return (
+ <>
+
+
+
+
+
+ >
+ );
+}
diff --git a/frontend/src/routes/admin/people/user/create-modal.tsx b/frontend/src/routes/admin/people/user/create-modal.tsx
new file mode 100644
index 0000000..024fd76
--- /dev/null
+++ b/frontend/src/routes/admin/people/user/create-modal.tsx
@@ -0,0 +1,97 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React from 'react';
+import { Modal, Form, Select, message, Row } from 'antd';
+import { useTranslation } from 'react-i18next';
+import { IUser } from './user';
+import { useAsync } from '@src/hooks/use-async';
+import { addMemberToSpaceAPI } from './user.api';
+import commonStyles from '../people.less';
+
+const { Option } = Select;
+
+interface CreateModalProps {
+ visible: boolean;
+ users: IUser[];
+ onCancel: () => void;
+ getSpaceMembers: () => void;
+}
+
+interface FormInstanceProps {
+ users: number[];
+}
+
+export default function CreateModal(props: CreateModalProps) {
+ const { t } = useTranslation();
+ const [form] = Form.useForm();
+ const { loading: confirmLoading, run: runAddMembers } = useAsync();
+ const { visible, users, onCancel, getSpaceMembers } = props;
+
+ const handleOk = () => {
+ form.validateFields()
+ .then(values => {
+ runAddMembers(Promise.all(values.users.map(userId => addMemberToSpaceAPI(userId))))
+ .then(() => {
+ message.success(t`addSuccess`);
+ form.resetFields();
+ onCancel();
+ })
+ .catch(() => {
+ message.error(t`addFailed`);
+ })
+ .finally(() => {
+ getSpaceMembers();
+ });
+ })
+ .catch(console.log);
+ };
+
+ return (
+
+
+
+ {users.map(user => (
+
+
+
+ {user.name}
+ {user.email ? `(${user.email})` : ''}
+
+
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/frontend/src/routes/admin/people/user/user.api.tsx b/frontend/src/routes/admin/people/user/user.api.tsx
new file mode 100644
index 0000000..716c403
--- /dev/null
+++ b/frontend/src/routes/admin/people/user/user.api.tsx
@@ -0,0 +1,39 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { http, isSuccess } from '@src/utils/http';
+
+export function getSpaceMembersAPI() {
+ return http.get(`/api/v2/user/space`).then(res => {
+ if (isSuccess(res)) return res.data;
+ return Promise.reject(res);
+ });
+}
+
+export function removeMemberFromSpaceAPI(userId: number) {
+ return http.delete(`/api/v2/user/move/${userId}`).then(res => {
+ if (isSuccess(res)) return res.data;
+ return Promise.reject(res);
+ });
+}
+
+export function addMemberToSpaceAPI(userId: number) {
+ return http.post(`/api/v2/user/add/${userId}`).then(res => {
+ if (isSuccess(res)) return res.data;
+ return Promise.reject(res);
+ });
+}
\ No newline at end of file
diff --git a/frontend/src/routes/admin/people/user/user.hooks.ts b/frontend/src/routes/admin/people/user/user.hooks.ts
new file mode 100644
index 0000000..4c01460
--- /dev/null
+++ b/frontend/src/routes/admin/people/user/user.hooks.ts
@@ -0,0 +1,64 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { useCallback, useEffect } from 'react';
+import { message } from 'antd';
+import { useTranslation } from 'react-i18next';
+import { useAsync } from '@src/hooks/use-async';
+import { CommonAPI } from '@src/common/common.api';
+import { isSuccess } from '@src/utils/http';
+import { IUser } from './user';
+import { getSpaceMembersAPI } from './user.api';
+
+export function useSpaceMembers(userInfo: any) {
+ const { t } = useTranslation();
+ const { data: users, run: runGetUsers } = useAsync({ loading: true, data: [] });
+
+ const {
+ data: spaceMembers,
+ loading: loading,
+ run: runGetSpaceMembers,
+ } = useAsync({ loading: true, data: [] });
+
+ const getUsers = useCallback(() => {
+ runGetUsers(
+ CommonAPI.getUsers({ include_deactivated: false }).then(res => {
+ if (isSuccess(res)) return res.data;
+ return Promise.reject(res);
+ }),
+ ).catch(() => message.error(t`fetchUserListFailed`));
+ }, [runGetUsers]);
+
+ const getSpaceMembers = useCallback(() => {
+ runGetSpaceMembers(getSpaceMembersAPI()).catch(() => {
+ message.error(t`fetchSpaceMemberListFailed`);
+ });
+ }, [runGetSpaceMembers]);
+
+ useEffect(() => {
+ if (userInfo.space_id == null) return;
+ getSpaceMembers();
+ getUsers();
+ }, [getSpaceMembers, userInfo.space_id]);
+
+ return {
+ users,
+ spaceMembers,
+ getSpaceMembers,
+ loading,
+ };
+}
diff --git a/frontend/src/routes/admin/people/user/user.tsx b/frontend/src/routes/admin/people/user/user.tsx
new file mode 100644
index 0000000..e0c45fc
--- /dev/null
+++ b/frontend/src/routes/admin/people/user/user.tsx
@@ -0,0 +1,131 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { useContext, useState } from 'react';
+import { Button, Table, message, Modal, Row } from 'antd';
+import moment from 'moment';
+import { useTranslation } from 'react-i18next';
+import StatusMark from '@src/components/status-mark';
+import { UserInfoContext } from '@src/common/common.context';
+import CreateModal from './create-modal';
+import { removeMemberFromSpaceAPI } from './user.api';
+import { useSpaceMembers } from './user.hooks';
+import { FlatBtn } from '@src/components/flatbtn';
+
+export interface IUser {
+ id: number;
+ name: string;
+ email: string;
+ last_login: string;
+ is_active: boolean;
+}
+
+export function User() {
+ const {t} = useTranslation()
+ const userInfo = useContext(UserInfoContext)!;
+ const { users = [], spaceMembers = [], getSpaceMembers, loading } = useSpaceMembers(userInfo);
+ const [modalVisible, setModalVisible] = useState(false);
+
+ const filteredUsers = users.filter(user => !spaceMembers.find(member => member.id === user.id));
+
+ const columns = [
+ {
+ title: t`username`,
+ key: 'name',
+ dataIndex: 'name',
+ },
+ {
+ title: t`Mail`,
+ dataIndex: 'email',
+ key: 'email',
+ },
+ {
+ title: t`status`,
+ dataIndex: 'is_active',
+ filters: [
+ { text: t`activated`, value: true },
+ { text: t`deactivated`, value: false },
+ ],
+ render: (is_active: boolean) => (
+ {is_active ? t`activated` : t`deactivated`}
+ ),
+ onFilter: (value: any, record: any) => record.is_active === value,
+ },
+ {
+ title: t`lastLogin`,
+ dataIndex: 'last_login',
+ key: 'last_login',
+ render: (last_login: string) => {
+ return (
+ {last_login == null ? t`neverLoggedIn` : moment(last_login).format('YYYY-MM-DD HH:mm:ss')}
+ );
+ },
+ },
+ {
+ title: t`operation`,
+ key: 'actions',
+ render: (record: IUser) => {
+ const disabled = userInfo.id === record.id;
+ return (
+
+ {t`removeMember`}
+
+ );
+ },
+ },
+ ];
+
+ const handleRemove = (userId: number) => () => {
+ Modal.confirm({
+ title: t`removeMemberModalTitle`,
+ onOk: () => {
+ return removeMemberFromSpaceAPI(userId)
+ .then(() => {
+ message.success(t`removeSuccess`);
+ getSpaceMembers();
+ })
+ .catch(() => {
+ message.error('removeFailed');
+ });
+ },
+ });
+ };
+
+ return (
+ <>
+
+ setModalVisible(true)}>
+ {t`addMembers`}
+
+
+
+ setModalVisible(false)}
+ getSpaceMembers={getSpaceMembers}
+ />
+ >
+ );
+}
diff --git a/frontend/src/routes/cluster/cluster.api.ts b/frontend/src/routes/cluster/cluster.api.ts
new file mode 100644
index 0000000..b72fad1
--- /dev/null
+++ b/frontend/src/routes/cluster/cluster.api.ts
@@ -0,0 +1,73 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { http, isSuccess } from '@src/utils/http';
+
+export function getClusterOverview() {
+ return http.get('/api/cluster/overview').then(res => {
+ if (isSuccess(res)) return res.data;
+ return Promise.reject(res);
+ });
+}
+
+export function startCluster(cluster_id: number) {
+ return http.post('/api/control/cluster/start', { cluster_id }).then(res => {
+ if (isSuccess(res)) return res.data;
+ return Promise.reject(res);
+ });
+}
+
+export function stopCluster(cluster_id: number) {
+ return http.post('/api/control/cluster/stop', { cluster_id }).then(res => {
+ if (isSuccess(res)) return res.data;
+ return Promise.reject(res);
+ });
+}
+
+export function restartCluster(cluster_id: number) {
+ return http.post('/api/control/cluster/restart', { cluster_id }).then(res => {
+ if (isSuccess(res)) return res.data;
+ return Promise.reject(res);
+ });
+}
+
+export function getNodeList(clusterId: number) {
+ return http.get(`/api/control/cluster/${clusterId}/instances`).then(res => {
+ if (isSuccess(res)) return res.data;
+ return Promise.reject(res);
+ });
+}
+
+export function getConfigurationList(type: 'be' | 'fe') {
+ return http.post(`/api/rest/v2/manager/node/configuration_info?type=${type}`, { type }).then(res => {
+ if (isSuccess(res)) return res.data;
+ return Promise.reject(res);
+ });
+}
+
+interface ChangeConfigurationParams {
+ node: string[];
+ persist: 'true' | 'false';
+ value: string;
+}
+
+export function changeConfiguration(type: 'be' | 'fe', data: Record) {
+ return http.post(`/api/rest/v2/manager/node/set_config/${type}`, data).then(res => {
+ if (isSuccess(res)) return res.data;
+ return Promise.reject(res);
+ });
+}
diff --git a/frontend/src/routes/cluster/cluster.module.less b/frontend/src/routes/cluster/cluster.module.less
new file mode 100644
index 0000000..d59725e
--- /dev/null
+++ b/frontend/src/routes/cluster/cluster.module.less
@@ -0,0 +1,20 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+.container {
+ min-height: calc(100vh - 84px);
+}
diff --git a/frontend/src/routes/cluster/cluster.tsx b/frontend/src/routes/cluster/cluster.tsx
new file mode 100644
index 0000000..c5b43cc
--- /dev/null
+++ b/frontend/src/routes/cluster/cluster.tsx
@@ -0,0 +1,63 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { useEffect, useMemo, useState } from 'react';
+import { Steps } from 'antd';
+import { Switch, Route, Redirect, useRouteMatch } from 'react-router';
+import { useTranslation } from 'react-i18next';
+import styles from './cluster.module.less';
+import { UserInfoContext } from '@src/common/common.context';
+import ClusterOverview from './overview';
+import Nodes from './nodes';
+import Configuration from './configuration';
+import TabsHeader from '@src/components/tabs-header';
+import { useUserInfo } from '@src/hooks/use-userinfo.hooks';
+import LoadingLayout from '@src/components/loading-layout';
+
+export function Cluster(props: any) {
+ // const match = useRouteMatch();
+ const { t } = useTranslation();
+ const [loading, setLoading] = useState(true);
+ const [userInfo] = useUserInfo();
+ useEffect(() => {
+ if (userInfo.id == null) return;
+ setLoading(false);
+ }, [userInfo.id]);
+ const tabRoutes = useMemo(
+ () => [
+ { label: t`clusterOverview`, path: '/cluster/overview' },
+ { label: t`nodeList`, path: '/cluster/nodes' },
+ { label: t`parameterConf`, path: '/cluster/configuration' },
+ ],
+ [t],
+ );
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/routes/cluster/components/data-overview-item/index.module.less b/frontend/src/routes/cluster/components/data-overview-item/index.module.less
new file mode 100644
index 0000000..0e8258b
--- /dev/null
+++ b/frontend/src/routes/cluster/components/data-overview-item/index.module.less
@@ -0,0 +1,26 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+.container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ .label {
+ font-weight: 700;
+ margin-top: 40px;
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/routes/cluster/components/data-overview-item/index.tsx b/frontend/src/routes/cluster/components/data-overview-item/index.tsx
new file mode 100644
index 0000000..f107938
--- /dev/null
+++ b/frontend/src/routes/cluster/components/data-overview-item/index.tsx
@@ -0,0 +1,39 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React from 'react';
+import { Row, Col } from 'antd';
+import styles from './index.module.less';
+
+interface DataOverviewItemProps {
+ label: string;
+ value: number;
+ icon: React.ReactNode;
+}
+
+export default function DataOverviewItem(props: DataOverviewItemProps) {
+ const { icon, label, value } = props;
+ return (
+
+
+ {icon}
+ {value}
+
+
{label}
+
+ );
+}
diff --git a/frontend/src/routes/cluster/components/liquid-fill-chart/index.module.less b/frontend/src/routes/cluster/components/liquid-fill-chart/index.module.less
new file mode 100644
index 0000000..7555d94
--- /dev/null
+++ b/frontend/src/routes/cluster/components/liquid-fill-chart/index.module.less
@@ -0,0 +1,25 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+.container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ .label {
+ font-weight: 700;
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/routes/cluster/components/liquid-fill-chart/index.tsx b/frontend/src/routes/cluster/components/liquid-fill-chart/index.tsx
new file mode 100644
index 0000000..b37b028
--- /dev/null
+++ b/frontend/src/routes/cluster/components/liquid-fill-chart/index.tsx
@@ -0,0 +1,76 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React from 'react';
+import ReactEChartsCore from 'echarts-for-react/lib/core';
+import * as echarts from 'echarts/core';
+import 'echarts-liquidfill';
+import styles from './index.module.less'
+
+interface LiquidFillChartProps {
+ label?: string;
+ value: number;
+}
+
+export default function LiquidFillChart(props: LiquidFillChartProps) {
+ const { label = '', value } = props;
+ return (
+
+
{label}
+
{
+ return (param.value * 100).toFixed(2) + '%';
+ },
+ fontSize: 25,
+ },
+ },
+ ],
+ }}
+ />
+
+ );
+}
diff --git a/frontend/src/routes/cluster/configuration/check-modal.tsx b/frontend/src/routes/cluster/configuration/check-modal.tsx
new file mode 100644
index 0000000..b6209bd
--- /dev/null
+++ b/frontend/src/routes/cluster/configuration/check-modal.tsx
@@ -0,0 +1,58 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { useState } from 'react';
+import { Button, Modal, Table } from 'antd';
+import { useTranslation } from 'react-i18next';
+import { ConfigurationItem } from '.';
+
+interface CheckModalProps {
+ visible: boolean;
+ currentParameter: ConfigurationItem;
+ onCancel: () => void;
+}
+
+export default function CheckModal(props: CheckModalProps) {
+ const { t } = useTranslation();
+ const { visible, currentParameter, onCancel } = props;
+ const nodeList = currentParameter?.nodes?.map((node, index) => ({
+ host: node,
+ value: currentParameter.values[index],
+ }));
+
+ const columns = [
+ {
+ title: t`hostIp`,
+ dataIndex: 'host',
+ },
+ {
+ title: t`currentValue`,
+ dataIndex: 'value',
+ },
+ ];
+ return (
+ {t`cancel`}}
+ width={700}
+ onCancel={onCancel}
+ >
+
+
+ );
+}
diff --git a/frontend/src/routes/cluster/configuration/edit-modal.tsx b/frontend/src/routes/cluster/configuration/edit-modal.tsx
new file mode 100644
index 0000000..38f33f3
--- /dev/null
+++ b/frontend/src/routes/cluster/configuration/edit-modal.tsx
@@ -0,0 +1,130 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { useState } from 'react';
+import { Modal, Form, Input, Radio, Select, message } from 'antd';
+import { useTranslation } from 'react-i18next';
+import { ConfigurationItem } from '.';
+import { useAsync } from '@src/hooks/use-async';
+import * as ClusterAPI from '../cluster.api';
+
+interface EditModalProps {
+ visible: boolean;
+ currentParameter: ConfigurationItem;
+ onOk: () => void;
+ onCancel: () => void;
+}
+
+interface FormInstanceProps {
+ value: string;
+ persist: boolean;
+ range: 'all' | 'part';
+ nodes: number[];
+}
+
+export default function EditModal(props: EditModalProps) {
+ const { t } = useTranslation();
+ const { visible, currentParameter, onOk, onCancel } = props;
+ const { loading: confirmLoading, run: runChangeConfiguration } = useAsync();
+ const [form] = Form.useForm();
+
+ const handleOk = () => {
+ form.validateFields()
+ .then(values => {
+ runChangeConfiguration(
+ ClusterAPI.changeConfiguration(currentParameter.type === 'Frontend' ? 'fe' : 'be', {
+ [currentParameter.name]: {
+ node: [...currentParameter.nodes],
+ value: values.value,
+ persist: values.persist ? 'true' : 'false',
+ },
+ }),
+ )
+ .then(() => {
+ message.success('编辑成功');
+ onOk();
+ handleCancel();
+ })
+ .catch(res => {
+ message.error(res.msg);
+ });
+ })
+ .catch(console.log);
+ };
+
+ const handleCancel = () => {
+ form.resetFields();
+ // setRange('all');
+ onCancel();
+ };
+
+ return (
+
+
+
+
+
+
+ {t`permanentEffective`}
+ {t`onceEffective`}
+
+
+ {/*
+ {
+ setRange(e.target.value);
+ }}
+ >
+ {t`allNodes`}
+ {t`certainNodes`}
+
+ */}
+ {/* {range === 'part' && (
+
+
+ 127.0.0.1
+
+
+ )} */}
+
+
+ );
+}
diff --git a/frontend/src/routes/cluster/configuration/index.tsx b/frontend/src/routes/cluster/configuration/index.tsx
new file mode 100644
index 0000000..b9160bb
--- /dev/null
+++ b/frontend/src/routes/cluster/configuration/index.tsx
@@ -0,0 +1,184 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { useCallback, useEffect, useState } from 'react';
+import { Input, Row, Col, Table, message } from 'antd';
+import { useTranslation } from 'react-i18next';
+import CheckModal from './check-modal';
+import EditModal from './edit-modal';
+import { useAsync } from '@src/hooks/use-async';
+import * as ClusterAPI from '../cluster.api';
+import { FlatBtn, FlatBtnGroup } from '@src/components/flatbtn';
+
+export interface ConfigurationItem {
+ name: string;
+ nodes: string[];
+ type: 'Frontend' | 'Backend';
+ valueType: string;
+ values: string[];
+ hot: boolean;
+}
+
+const resolveConfigurationList = (configurationList: { rows: string[][] }) => {
+ return configurationList?.rows?.reduce((memo, current) => {
+ const index = memo.findIndex(item => item.name === current[0])
+ if(index < 0) {
+ memo.push({
+ name: current[0],
+ nodes: [current[1]],
+ type: current[2] === 'FE' ? 'Frontend' : 'Backend',
+ valueType: current[3],
+ values: [current[current.length - 2]],
+ hot: current[current.length - 1] === 'true'
+ })
+ }else {
+ memo[index].nodes.push(current[1])
+ memo[index].values.push(current[current.length - 2])
+ }
+ return memo
+ }, [] as ConfigurationItem[]) || []
+};
+
+export default function Configuration() {
+ const { t } = useTranslation();
+
+ const {
+ data: configurationList,
+ loading: configurationListLoading,
+ run: runGetConfigurationList,
+ } = useAsync({ loading: true, data: [] });
+
+ const [filteredConfigurationList, setFilteredConfigurationList] = useState();
+
+ const getConfigurationList = useCallback(
+ (setStartLoading: boolean = false) => {
+ return runGetConfigurationList(
+ Promise.all([ClusterAPI.getConfigurationList('fe'), ClusterAPI.getConfigurationList('be')]).then(
+ res => {
+ return [...resolveConfigurationList(res[0]), ...resolveConfigurationList(res[1])];
+ },
+ ),
+ { setStartLoading },
+ ).catch(res => {
+ message.error(res.msg);
+ });
+ },
+ [runGetConfigurationList],
+ );
+
+ useEffect(() => {
+ getConfigurationList();
+ }, [getConfigurationList]);
+
+ const [currentParameter, setCurrentParameter] = useState({} as ConfigurationItem);
+ const [editModalVisible, setEditModalVisible] = useState(false);
+ const [checkModalVisible, setCheckModalVisible] = useState(false);
+ const [searchString, setSearchString] = useState('');
+
+ const columns = [
+ {
+ title: t`paramName`,
+ dataIndex: 'name',
+ },
+ {
+ title: t`paramType`,
+ dataIndex: 'type',
+ filters: [
+ {
+ text: 'Frontend',
+ value: 'Frontend',
+ },
+ {
+ text: 'Backend',
+ value: 'Backend',
+ },
+ ],
+ onFilter: (value: any, record: ConfigurationItem) => record.type === value,
+ },
+ {
+ title: t`paramValueType`,
+ dataIndex: 'valueType',
+ },
+ {
+ title: t`hot`,
+ dataIndex: 'hot',
+ render: (hot: boolean) => (hot ? t`yes` : t`no`),
+ },
+ {
+ title: t`operation`,
+ key: 'operation',
+ render: (record: ConfigurationItem) => (
+
+ {t`viewCurrentValue`}
+
+ {t`edit`}
+
+
+ ),
+ },
+ ];
+
+ const handleCheck = (record: ConfigurationItem) => () => {
+ setCurrentParameter(record);
+ setCheckModalVisible(true);
+ };
+
+ const handleEdit = (record: ConfigurationItem) => () => {
+ setCurrentParameter(record);
+ setEditModalVisible(true);
+ };
+
+ const handleSearch = () => {
+ setFilteredConfigurationList(
+ configurationList?.filter(item => item.name.toLowerCase().includes(searchString.toLowerCase())),
+ );
+ };
+
+ return (
+ <>
+
+ {t`paramSearch`}
+
+ setSearchString(e.target.value)} onSearch={handleSearch} />
+
+
+
+ setCheckModalVisible(false)}
+ />
+ {
+ getConfigurationList(true).then(res => {
+ setFilteredConfigurationList(
+ res?.filter(item => item.name.toLowerCase().includes(searchString.toLowerCase())),
+ );
+ });
+ }}
+ onCancel={() => setEditModalVisible(false)}
+ />
+ >
+ );
+}
diff --git a/frontend/src/routes/cluster/list/list.tsx b/frontend/src/routes/cluster/list/list.tsx
new file mode 100644
index 0000000..65d92d4
--- /dev/null
+++ b/frontend/src/routes/cluster/list/list.tsx
@@ -0,0 +1,65 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { Button, PageHeader, Row, Space, Table } from 'antd';
+import React from 'react';
+import { useHistory, useRouteMatch } from 'react-router';
+
+export function ClusterList(props: any) {
+ const history = useHistory();
+ const match = useRouteMatch();
+ const columns = [
+ {
+ title: '集群',
+ dataIndex: 'cluster',
+ key: 'cluster',
+ },
+ {
+ title: '创建时间',
+ dataIndex: 'createTime',
+ key: 'createTime',
+ },
+ {
+ title: '状态',
+ dataIndex: 'status',
+ key: 'status',
+ },
+ {
+ title: '链接信息',
+ dataIndex: 'connect_info',
+ key: 'connect_info',
+ },
+ ];
+ const [clusterList, setClusterList] = React.useState([]);
+ return (
+
+
+
+
+ history.push(`/cluster/new`)}>新建集群
+ 接入集群
+
+
+
record[Object.keys(record).length - 1]}
+ size="middle"
+ />
+
+ );
+}
diff --git a/frontend/src/routes/cluster/monitor/monitor.api.ts b/frontend/src/routes/cluster/monitor/monitor.api.ts
new file mode 100644
index 0000000..0850b1a
--- /dev/null
+++ b/frontend/src/routes/cluster/monitor/monitor.api.ts
@@ -0,0 +1,70 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+/** @format */
+
+import { http } from '@src/utils/http';
+import { IResult } from 'src/interfaces/http.interface';
+
+function getStatisticInfo(data?: any): Promise> {
+ return http.get(`/api/rest/v2/manager/monitor/value/node_num`);
+}
+function getDisksCapacity(data?: any): Promise> {
+ return http.get(`/api/rest/v2/manager/monitor/value/disks_capacity`);
+}
+
+function getQPS(data?: any): Promise> {
+ return http.post(`/api/rest/v2/manager/monitor/timeserial/${data.type}?start=${data.start}&end=${data.end}`, data);
+}
+function getQueryLatency(data?: any): Promise> {
+ return http.post(`/api/rest/v2/manager/monitor/timeserial/query_latency?start=${data.start}&end=${data.end}`, data);
+}
+
+function getErrorRate(data?: any): Promise> {
+ return http.post(
+ `/api/rest/v2/manager/monitor/timeserial/query_err_rate?start=${data.start}&end=${data.end}`,
+ data,
+ );
+}
+function getConnectionTotal(data?: any): Promise> {
+ return http.post(`/api/rest/v2/manager/monitor/timeserial/conn_total?start=${data.start}&end=${data.end}`, data);
+}
+function getScheduled(data?: any): Promise> {
+ return http.post(
+ `/api/rest/v2/manager/monitor/timeserial/scheduled_tablet_num?start=${data.start}&end=${data.end}`,
+ data,
+ );
+}
+
+function getStatistic(): Promise> {
+ return http.get(`/api/rest/v2/manager/monitor/value/statistic`);
+}
+function getTxnStatus(data?: any): Promise> {
+ return http.post(`/api/rest/v2/manager/monitor/timeserial/txn_status?start=${data.start}&end=${data.end}`, data);
+}
+
+export const MonitorAPI = {
+ getStatisticInfo,
+ getDisksCapacity,
+ getQPS,
+ getQueryLatency,
+ getErrorRate,
+ getConnectionTotal,
+ getTxnStatus,
+ getScheduled,
+ getStatistic,
+};
diff --git a/frontend/src/routes/cluster/monitor/monitor.data.ts b/frontend/src/routes/cluster/monitor/monitor.data.ts
new file mode 100644
index 0000000..44bd283
--- /dev/null
+++ b/frontend/src/routes/cluster/monitor/monitor.data.ts
@@ -0,0 +1,111 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import dayjs from 'dayjs';
+export function getTimes(now: dayjs.Dayjs) {
+ return [
+ {
+ value: '1',
+ text: '最近半小时',
+ end: new Date().getTime(),
+ start: now.subtract(30, 'minute').valueOf(),
+ format: 'HH:mm',
+ },
+ {
+ value: '2',
+ text: '最近1小时',
+ end: new Date().getTime(),
+ start: now.subtract(1, 'hour').valueOf(),
+ format: 'HH:mm',
+ },
+ {
+ value: '3',
+ text: '最近6小时',
+ end: new Date().getTime(),
+ start: now.subtract(6, 'hour').valueOf(),
+ format: 'HH:mm',
+ },
+ {
+ value: '4',
+ text: '最近1天',
+ end: new Date().getTime(),
+ start: now.subtract(1, 'day').valueOf(),
+ format: 'HH:mm',
+ },
+ {
+ value: '5',
+ text: '最近三天',
+ end: new Date().getTime(),
+ start: now.subtract(3, 'day').valueOf(),
+ format: 'MM/DD HH:mm',
+ },
+ {
+ value: '6',
+ text: '最近一周',
+ end: new Date().getTime(),
+ start: now.subtract(1, 'week').valueOf(),
+ format: 'MM/DD HH:mm',
+ },
+ {
+ value: '7',
+ text: '最近半个月',
+ end: new Date().getTime(),
+ start: now.subtract(15, 'day').valueOf(),
+ format: 'MM/DD HH:mm',
+ },
+ {
+ value: '8',
+ text: '最近一个月',
+ end: new Date().getTime(),
+ start: now.subtract(1, 'month').valueOf(),
+ format: 'MM/DD HH:mm',
+ },
+ ];
+}
+
+export const CHARTS_OPTIONS = {
+ title: { text: '' },
+ grid: {
+ bottom: 100,
+ },
+ legend: {
+ data: [],
+ left: 0,
+ bottom: '0',
+ height: 80,
+ width: '100%',
+ type: 'scroll',
+ itemHeight: 10,
+ orient: 'vertical',
+ pageIconSize: 7,
+ textStyle: {
+ overflow: 'breakAll',
+ },
+ },
+ tooltip: {
+ trigger: 'axis',
+ },
+ xAxis: {
+ type: 'category',
+ boundaryGap: false,
+ data: [],
+ },
+ yAxis: {
+ type: 'value',
+ },
+ series: [],
+};
diff --git a/frontend/src/routes/cluster/monitor/monitor.less b/frontend/src/routes/cluster/monitor/monitor.less
new file mode 100644
index 0000000..8383000
--- /dev/null
+++ b/frontend/src/routes/cluster/monitor/monitor.less
@@ -0,0 +1,24 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+.monitor {
+ min-height: 100%;
+
+ h4 {
+ margin-left: 10px;
+ }
+}
diff --git a/frontend/src/routes/cluster/monitor/monitor.tsx b/frontend/src/routes/cluster/monitor/monitor.tsx
new file mode 100644
index 0000000..afb1321
--- /dev/null
+++ b/frontend/src/routes/cluster/monitor/monitor.tsx
@@ -0,0 +1,438 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+/* eslint-disable prettier/prettier */
+import { Row, Col, Select, Collapse, Card, Button, Divider } from 'antd';
+import React, { useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import ReactEChartsCore from 'echarts-for-react/lib/core';
+import * as echarts from 'echarts/core';
+import dayjs from 'dayjs';
+import './monitor.less';
+
+import { LineChart } from 'echarts/charts';
+import {
+ GridComponent,
+ PolarComponent,
+ RadarComponent,
+ GeoComponent,
+ SingleAxisComponent,
+ ParallelComponent,
+ CalendarComponent,
+ GraphicComponent,
+ ToolboxComponent,
+ TooltipComponent,
+ TitleComponent,
+ LegendComponent,
+} from 'echarts/components';
+import { CanvasRenderer } from 'echarts/renderers';
+import { MonitorAPI } from './monitor.api';
+import { CHARTS_OPTIONS, getTimes } from './monitor.data';
+import { deepClone, formatBytes } from '@src/utils/utils';
+import classNames from 'classnames';
+
+echarts.use([
+ LineChart,
+ CanvasRenderer,
+ TitleComponent,
+ GridComponent,
+ PolarComponent,
+ RadarComponent,
+ GeoComponent,
+ SingleAxisComponent,
+ ParallelComponent,
+ CalendarComponent,
+ GraphicComponent,
+ ToolboxComponent,
+ TooltipComponent,
+ LegendComponent,
+]);
+
+const { Panel } = Collapse;
+
+const gridStyle: React.CSSProperties = {
+ width: 'calc(100% / 6)',
+ textAlign: 'center',
+};
+export function ClusterMonitor() {
+ const { t } = useTranslation();
+ const [TIMES, setTIMES] = useState(() => getTimes(dayjs()));
+ const [statisticInfo, setStatisticInfo] = useState({});
+ const [disksCapacity, setDisksCapacity] = useState({});
+ // const [feNodes, setFENodes] = useState([]);
+ const [statistic, setStatistic] = useState([]);
+
+ const [currentTime, setCurrentTime] = useState(TIMES[1]);
+ const [qps, setQPS] = useState({});
+ const [errorRate, setErrorRate] = useState({});
+ const [queryLatency, setQueryLatency] = useState({});
+ const [connectionTotal, setConnectionTotal] = useState({});
+ const [txnStatus, setTxnStatus] = useState({});
+ const [pointNum] = useState('100');
+ const [scheduled, setScheduled] = useState({});
+
+ useEffect(() => {
+ init();
+ }, [currentTime.value, currentTime.start]);
+
+ function init() {
+ getStatisticInfo();
+ getDisksCapacity();
+ getStatistic();
+
+ getQPS();
+ getQueryLatency();
+ getErrorRate();
+ getConnectionTotal();
+ getTxnStatus();
+ getScheduled();
+ }
+
+ function getStatistic() {
+ MonitorAPI.getStatistic()
+ .then(res => {
+ setStatistic(res.data);
+ })
+ .catch(err => {
+ console.log(err);
+ });
+ }
+
+ function getScheduled() {
+ MonitorAPI.getScheduled({
+ start: currentTime.start,
+ end: currentTime.end,
+ point_num: pointNum,
+ })
+ .then(res => {
+ const option = formatData(res.data, '{t`clusterScheduling`}');
+ option.legend.left = 20
+ setScheduled(option);
+ })
+ .catch(err => {
+ console.log(err);
+ });
+ }
+
+ function getStatisticInfo() {
+ MonitorAPI.getStatisticInfo()
+ .then(res => {
+ setStatisticInfo(res.data || {});
+ })
+ .catch(err => {
+ console.log(err);
+ });
+ }
+ function getDisksCapacity() {
+ MonitorAPI.getDisksCapacity()
+ .then(res => {
+ setDisksCapacity(res.data);
+ })
+ .catch(err => {
+ console.log(err);
+ });
+ }
+
+ function formatData(response: any, title: string) {
+ const option = deepClone(CHARTS_OPTIONS);
+ // option.title.text = title;
+ const xAxisData = response?.x_value.map((item: string) => dayjs(item).format(currentTime.format));
+ option.xAxis.data = xAxisData;
+ const series: any[] = [];
+ const legendData: any[] = [];
+ function transformChartsData(y_value: string, formatter?: string) {
+ for (const [key, value] of Object.entries(y_value)) {
+ if (Array.isArray(value)) {
+ legendData.push(key);
+ series.push({
+ name: key,
+ data: value.map(item => {
+ if (formatter === 'MB') {
+ return typeof item === 'number' ? formatBytes(item, 2, false) : item;
+ }
+ return typeof item === 'number' ? item.toFixed(3) : item;
+ }),
+ type: 'line',
+ });
+ } else {
+ transformChartsData(value);
+ }
+ }
+ return series;
+ }
+ if (title === '节点内存(MB)') {
+ transformChartsData(response?.y_value || {}, 'MB');
+ } else {
+ transformChartsData(response?.y_value || {});
+ }
+
+ option.legend.data = legendData;
+ option.grid = {
+ bottom: 110,
+ };
+ option.series = series;
+ return option;
+ }
+ function getQPS() {
+ MonitorAPI.getQPS({
+ type: 'qps',
+ start: currentTime.start,
+ end: currentTime.end,
+ })
+ .then(res => {
+ const option = formatData(res.data, t`QPS`);
+ setQPS(option);
+ })
+ .catch(err => {
+ console.log(err);
+ });
+ }
+ function getErrorRate() {
+ MonitorAPI.getErrorRate({
+ start: currentTime.start,
+ end: currentTime.end,
+ })
+ .then(res => {
+ const option = formatData(res.data, t`queryErrorRate`);
+ setErrorRate(option);
+ })
+ .catch(err => {
+ console.log(err);
+ });
+ }
+ function getQueryLatency() {
+ MonitorAPI.getQueryLatency({
+ start: currentTime.start,
+ end: currentTime.end,
+ quantile: '0.99',
+ })
+ .then(res => {
+ const option = formatData(res.data, t`99th`);
+ option.grid.left = 60
+ option.legend.left = 40
+ setQueryLatency(option);
+ })
+ .catch(err => {
+ console.log(err);
+ });
+ }
+ function getConnectionTotal() {
+ MonitorAPI.getConnectionTotal({
+ start: currentTime.start,
+ end: currentTime.end,
+ })
+ .then(res => {
+ const option = formatData(res.data, t`numberOfConnections`);
+ option.legend.left = 20
+ option.grid = {
+ bottom: 120,
+ };
+ setConnectionTotal(option);
+ })
+ .catch(err => {
+ console.log(err);
+ });
+ }
+ function getTxnStatus() {
+ MonitorAPI.getTxnStatus({
+ start: currentTime.start,
+ end: currentTime.end,
+ })
+ .then(res => {
+ const option = formatData(res.data, t`importRate`);
+ function transformToZh_CN(text: string) {
+ switch (text) {
+ case 'success':
+ return '导入成功';
+ case 'failed':
+ return '导入失败';
+ case 'begin':
+ return '提交导入';
+ }
+ }
+ const series = option.series.map((item: any) => {
+ item.name = transformToZh_CN(item.name);
+ return item;
+ });
+ const legendData = option.legend.data.map((item: any) => {
+ return transformToZh_CN(item);
+ });
+ option.series = series;
+ option.legend.data = legendData;
+ option.legend.left = 20
+ option.grid = {
+ bottom: 120,
+ };
+ option.legend.bottom = 25
+ option.color = ['#91cc75','#d9363e','#fac858']
+ const txnStatus = option;
+ setTxnStatus(txnStatus);
+ })
+ .catch(err => {
+ console.log(err);
+ });
+ }
+
+ return (
+
+
+
+
+ {t`viewTime`}:
+
+ {
+ const time = TIMES.filter(item => item.value === v)[0];
+ setCurrentTime(time);
+ }}
+ >
+ {TIMES.map(time => {
+ return (
+
+ {time.text}
+
+ );
+ })}
+
+
+
+ {
+ const UPDATED_TIMES = getTimes(dayjs());
+ setTIMES(UPDATED_TIMES);
+ const time = UPDATED_TIMES.filter(item => item.value === currentTime.value)[0];
+ setCurrentTime(time);
+ }}
+ >
+ {t`refresh`}
+
+
+
+
+
+
+
+
+
+ {t`numberOfFeNodes`}
+ {statisticInfo?.fe_node_num_total}
+
+
+ {t`FeNumberOfSurvivingNodes`}
+ {statisticInfo?.fe_node_num_alive}
+
+
+ {t`NumberOfBENodes`}
+ {statisticInfo?.be_node_num_total}
+
+
+ {t`BeNumberOfSurvivingNodes`}
+ {statisticInfo?.be_node_num_alive}
+
+
+ {t`totalSpace`}
+ {formatBytes(disksCapacity?.be_disks_total) || t`noData`}
+
+
+ {t`usedSpace`}
+ {formatBytes(disksCapacity?.be_disks_used) || t`noData`}
+
+
+
+
+ {t`QPS`}
+
+
+
+ {t`99th`}
+
+
+
+ {t`queryErrorRate`}
+
+
+
+
+
+
+ {t`numberOfConnections`}
+
+
+
+ {t`importRate`}
+
+
+
+
+
+
+
+ {t`clusterScheduling`}
+
+
+
+
+ {t`unhealthyFragmentation`}
+ {statistic?.unhealthy_tablet_num}
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/routes/cluster/nodes/index.tsx b/frontend/src/routes/cluster/nodes/index.tsx
new file mode 100644
index 0000000..a3d3aea
--- /dev/null
+++ b/frontend/src/routes/cluster/nodes/index.tsx
@@ -0,0 +1,110 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { useContext, useEffect } from 'react';
+import { message, Table } from 'antd';
+import { useTranslation } from 'react-i18next';
+import StatusMark from '@src/components/status-mark';
+import { useAsync } from '@src/hooks/use-async';
+import * as ClusterAPI from '../cluster.api';
+import { UserInfoContext } from '@src/common/common.context';
+
+const enum ModuleNameEnum {
+ FRONTEND = 'fe',
+ BACKEND = 'be',
+ BROKER = 'broker',
+}
+
+const enum OperateStatusEnum {
+ SUCCESS = 'SUCCESS',
+ INIT = 'INIT',
+ PROCESSING = 'PROCESSING',
+ FAIL = 'FAIL',
+ CANCEL = 'CANCEL',
+}
+
+interface NodeListItem {
+ instanceId: number;
+ moduleName: ModuleNameEnum;
+ nodeHost: string;
+ operateStatus: OperateStatusEnum;
+}
+
+export default function Nodes() {
+ const { t } = useTranslation();
+ const userInfo = useContext(UserInfoContext)!;
+ const { data: nodeList, loading, run: runGetNodeList } = useAsync({ loading: true, data: [] });
+ useEffect(() => {
+ runGetNodeList(ClusterAPI.getNodeList(userInfo.space_id), { setStartLoading: false }).catch(res => {
+ message.error(res.msg);
+ });
+ }, [runGetNodeList, userInfo.space_id]);
+ const columns = [
+ {
+ title: t`nodeId`,
+ dataIndex: 'instanceId',
+ },
+ {
+ title: t`nodeType`,
+ dataIndex: 'moduleName',
+ filters: [
+ {
+ text: 'Frontend',
+ value: ModuleNameEnum.FRONTEND,
+ },
+ {
+ text: 'Backend',
+ value: ModuleNameEnum.BACKEND,
+ },
+ {
+ text: 'Broker',
+ value: ModuleNameEnum.BROKER,
+ },
+ ],
+ onFilter: (value: any, record: NodeListItem) => record.moduleName === value,
+ render: (moduleName: ModuleNameEnum) => {resolveModuleName(moduleName)} ,
+ },
+ {
+ title: t`hostIp`,
+ dataIndex: 'nodeHost',
+ },
+ {
+ title: t`nodeStatus`,
+ dataIndex: 'operateStatus',
+ render: (status: OperateStatusEnum) => (
+
+ {status === OperateStatusEnum.SUCCESS ? t`normal` : t`abnormal`}
+
+ ),
+ },
+ ];
+
+ const resolveModuleName = (moduleName: ModuleNameEnum) => {
+ switch (moduleName) {
+ case ModuleNameEnum.FRONTEND:
+ return 'Frontend';
+ case ModuleNameEnum.BACKEND:
+ return 'Backend';
+ case ModuleNameEnum.BROKER:
+ return 'Broker';
+ default:
+ return '';
+ }
+ };
+
+ return ;
+}
diff --git a/frontend/src/routes/cluster/overview/index.module.less b/frontend/src/routes/cluster/overview/index.module.less
new file mode 100644
index 0000000..1c73fbe
--- /dev/null
+++ b/frontend/src/routes/cluster/overview/index.module.less
@@ -0,0 +1,24 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+.infoRow {
+ width: 80%;
+ margin-bottom: 1.9vh;
+ .infoRowContent {
+ word-wrap: break-word;
+ }
+}
diff --git a/frontend/src/routes/cluster/overview/index.tsx b/frontend/src/routes/cluster/overview/index.tsx
new file mode 100644
index 0000000..aeb6215
--- /dev/null
+++ b/frontend/src/routes/cluster/overview/index.tsx
@@ -0,0 +1,190 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { useCallback, useContext, useEffect } from 'react';
+import { Button, Col, Divider, message, PageHeader, Row, Typography } from 'antd';
+import { DatabaseOutlined, TableOutlined } from '@ant-design/icons';
+import { useTranslation } from 'react-i18next';
+import moment from 'moment';
+import styles from './index.module.less';
+import StatusMark from '@src/components/status-mark';
+import LiquidFillChart from '../components/liquid-fill-chart';
+import DataOverviewItem from '../components/data-overview-item';
+import { UserInfoContext } from '@src/common/common.context';
+import { useAsync } from '@src/hooks/use-async';
+import * as ClusterAPI from '../cluster.api';
+import { SpaceAPI } from '../../space/space.api';
+import LoadingLayout from '@src/components/loading-layout';
+import { isSuccess } from '@src/utils/http';
+
+export default function ClusterOverview() {
+ const { t } = useTranslation();
+ const userInfo = useContext(UserInfoContext)!;
+ const { loading: startLoading, run: runClusterStart } = useAsync();
+ const { loading: stopLoading, run: runClusterStop } = useAsync();
+ const { loading: restartLoading, run: runClusterRestart } = useAsync();
+ const {
+ data: clusterInfo,
+ loading: clusterInfoLoading,
+ run: runGetClusterInfo,
+ } = useAsync<{
+ overview: Record;
+ space: Record;
+ }>({
+ loading: true,
+ data: {
+ overview: {},
+ space: {},
+ },
+ });
+ const getClusterInfo = useCallback(
+ (setStartLoading: boolean = false) => {
+ return runGetClusterInfo(
+ Promise.all([
+ SpaceAPI.spaceGet(userInfo.space_id + '').then(res => {
+ if (isSuccess(res)) return res.data;
+ return Promise.reject(res);
+ }),
+ ClusterAPI.getClusterOverview(),
+ ]).then(res => {
+ return {
+ space: res[0],
+ overview: res[1],
+ };
+ }),
+ { setStartLoading },
+ ).catch(res => {
+ message.error(res.msg);
+ });
+ },
+ [runGetClusterInfo, userInfo.space_id],
+ );
+
+ useEffect(() => {
+ getClusterInfo();
+ }, [getClusterInfo]);
+
+ const handleStart = () => {
+ runClusterStart(ClusterAPI.startCluster(userInfo.space_id))
+ .then(() => {
+ message.success('启动成功');
+ getClusterInfo(true);
+ })
+ .catch(res => message.error(res.msg));
+ };
+
+ const handleStop = () => {
+ runClusterStop(ClusterAPI.stopCluster(userInfo.space_id))
+ .then(() => {
+ message.success('停止成功');
+ getClusterInfo(true);
+ })
+ .catch(res => message.error(res.msg));
+ };
+
+ const handleRestart = () => {
+ runClusterRestart(ClusterAPI.restartCluster(userInfo.space_id))
+ .then(() => {
+ message.success('重启成功');
+ getClusterInfo(true);
+ })
+ .catch(res => message.error(res.msg));
+ };
+
+ return (
+
+
+ {clusterInfo?.space.status === 'NORMAL' ? t`normal` : t`abnormal`}
+
+ }
+ extra={
+ <>
+
+ {t`start`}
+
+ {t`stop`}
+ {t`restart`}
+ >
+ }
+ >
+
+ {t`clusterId`}:
+
+ {clusterInfo?.space.id}
+
+
+
+ {t`CreationTime`}:
+
+ {moment(clusterInfo?.space.createTime).format('YYYY-MM-DD')}
+
+
+
+ JDBC URL:
+
+ jdbc:mysql://{clusterInfo?.space.address}:{clusterInfo?.space.queryPort}
+ /DB_NAME?user=USER_NAME&password=PASSWORD
+
+
+
+
+ {t`sourceUsage`}
+
+
+
+
+
+
+
+
+ {t`dataOverview`}
+
+
+
+ }
+ />
+
+
+ }
+ />
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/routes/container.less b/frontend/src/routes/container.less
new file mode 100644
index 0000000..3d355ba
--- /dev/null
+++ b/frontend/src/routes/container.less
@@ -0,0 +1,28 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+.container {
+ width: calc(100% - 80px);
+ background: white;
+ height: 100%;
+ overflow-y: scroll;
+ margin-left: 80px;
+}
+.container-content {
+ padding: 20px;
+ min-height: calc(100% - 44px);
+}
diff --git a/frontend/src/routes/container.tsx b/frontend/src/routes/container.tsx
new file mode 100644
index 0000000..4849d8a
--- /dev/null
+++ b/frontend/src/routes/container.tsx
@@ -0,0 +1,74 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React from 'react';
+import styles from './container.less';
+
+import { Layout } from 'antd';
+import { Redirect, Route, Router, Switch, useHistory } from 'react-router-dom';
+import { Sidebar } from '@src/components/sidebar/sidebar';
+import { Header } from '@src/components/header/header';
+import { CommonAPI } from '@src/common/common.api';
+import { UserInfoContext } from '@src/common/common.context';
+import { UserInfo } from '@src/common/common.interface';
+import { Dashboard } from './dashboard/dashboard';
+import { Meta } from './meta/meta';
+import { NodeDashboard } from './node/dashboard';
+import {NodeList} from './node/list';
+import { Configuration } from './node/list/configuration';
+import { FEConfiguration } from './node/list/fe-configuration';
+import { BEConfiguration } from './node/list/be-configuration';
+import { Query } from './query';
+import { QueryDetails } from './query/query-details';
+import { Cluster } from './cluster/cluster';
+import { UserSetting } from './user-setting';
+import { useUserInfo } from '@src/hooks/use-userinfo.hooks';
+export function Container(props: any) {
+ const [userInfo] = useUserInfo();
+ const history = useHistory();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/routes/dashboard/components/dashboard-item/index.tsx b/frontend/src/routes/dashboard/components/dashboard-item/index.tsx
new file mode 100644
index 0000000..f6af493
--- /dev/null
+++ b/frontend/src/routes/dashboard/components/dashboard-item/index.tsx
@@ -0,0 +1,37 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import CSSModules from 'react-css-modules';
+import React, { useCallback, useState } from 'react';
+import styles from './message-item.less';
+import { messItemProps } from './message-item.interface';
+
+function MessageItem(props: messItemProps) {
+ return (
+
+
+
{props.icon}
+
{props.title}
+
+
+ {props.des}
+
+
+ );
+}
+
+export const DashboardItem = CSSModules(styles)(MessageItem);
diff --git a/frontend/src/routes/dashboard/components/dashboard-item/message-item.interface.ts b/frontend/src/routes/dashboard/components/dashboard-item/message-item.interface.ts
new file mode 100644
index 0000000..89bff2b
--- /dev/null
+++ b/frontend/src/routes/dashboard/components/dashboard-item/message-item.interface.ts
@@ -0,0 +1,23 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+/** @format */
+export interface messItemProps {
+ title: string;
+ icon: any;
+ des: number | string;
+}
diff --git a/frontend/src/routes/dashboard/components/dashboard-item/message-item.less b/frontend/src/routes/dashboard/components/dashboard-item/message-item.less
new file mode 100644
index 0000000..ed4f212
--- /dev/null
+++ b/frontend/src/routes/dashboard/components/dashboard-item/message-item.less
@@ -0,0 +1,57 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+/** @format */
+
+.mess-item {
+ position: relative;
+ display: flex;
+ // height: 64px;
+ width: 49%;
+ // height: 28vh;
+ padding: 5vh;
+ margin-top: 20px;
+ background: #edf2f5;
+ border-radius: 20px;
+ // box-shadow: 3px 3px 3px #e2dede;
+
+ .mess-item-title {
+ width: 35%;
+ text-align: center;
+
+ .mess-item-icon {
+ margin: 0;
+ font-size: 5vw;
+ color: #6536cc;
+ }
+
+ .mess-item-name {
+ margin: 0;
+ font-size: 1vw;
+ color: #7f7f7f;
+ }
+ }
+
+ .mess-item-content {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 65%;
+ font-size: 4vw;
+ // vertical-align: middle;
+ }
+}
diff --git a/frontend/src/routes/dashboard/connect-info/connect-info.less b/frontend/src/routes/dashboard/connect-info/connect-info.less
new file mode 100644
index 0000000..1b25c82
--- /dev/null
+++ b/frontend/src/routes/dashboard/connect-info/connect-info.less
@@ -0,0 +1,23 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+.connect-info-container {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ padding: 0 3px 3px 0;
+}
\ No newline at end of file
diff --git a/frontend/src/routes/dashboard/connect-info/connect-info.tsx b/frontend/src/routes/dashboard/connect-info/connect-info.tsx
new file mode 100644
index 0000000..6d6aef9
--- /dev/null
+++ b/frontend/src/routes/dashboard/connect-info/connect-info.tsx
@@ -0,0 +1,68 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import CSSModules from 'react-css-modules';
+import React, { useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import styles from './connect-info.less';
+import { Card, message } from 'antd';
+import { DashboardAPI } from '../dashboard.api';
+
+export function Component(props: any) {
+ const { t } = useTranslation();
+ const [spaceList, setSpaceList] = useState({});
+ const [https, setHttps] = useState([]);
+ const [mysqls, setMysqls] = useState([]);
+ useEffect(() => {
+ getOverviewInfo();
+ }, []);
+ async function getOverviewInfo() {
+ DashboardAPI.getSpaceList().then(res1 => {
+ const { msg, data, code } = res1;
+ let http = '';
+ let mysql = '';
+ if (code === 0) {
+ if (res1.data) {
+ setSpaceList(res1.data);
+ for (let i = 0; i < res1.data.http.length; i++) {
+ http += res1.data.http[i] + '; ';
+ }
+ for (let i = 0; i < res1.data.mysql.length; i++) {
+ mysql += res1.data.mysql[i] + '; ';
+ }
+ setHttps(http);
+ setMysqls(mysql);
+ }
+ } else if (code === 404) {
+ message.error(t`updateDoris`);
+ window.location.href = `${window.location.origin}`;
+ } else {
+ message.error(msg);
+ }
+ });
+ }
+ return (
+
+
+ {t`httpInfo`} {https}
+ {t`JDBCInfo`} {mysqls}
+
+
+ );
+}
+
+export const ConnectInfo = CSSModules(styles)(Component);
diff --git a/frontend/src/routes/dashboard/dashboard.api.ts b/frontend/src/routes/dashboard/dashboard.api.ts
new file mode 100644
index 0000000..6750f5b
--- /dev/null
+++ b/frontend/src/routes/dashboard/dashboard.api.ts
@@ -0,0 +1,31 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { http } from '@src/utils/http';
+import { IResult } from '@src/interfaces/http.interface';
+import { MetaInfoResponse } from './dashboard.interface';
+
+function getMetaInfo(): Promise> {
+ return http.get(`/api/cluster/overview`);
+}
+function getSpaceList() {
+ return http.get(`/api/rest/v2/manager/cluster/cluster_info/conn_info`);
+}
+export const DashboardAPI = {
+ getMetaInfo,
+ getSpaceList,
+};
diff --git a/frontend/src/routes/dashboard/dashboard.data.ts b/frontend/src/routes/dashboard/dashboard.data.ts
new file mode 100644
index 0000000..d225780
--- /dev/null
+++ b/frontend/src/routes/dashboard/dashboard.data.ts
@@ -0,0 +1,21 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+export enum DashboardTabEnum {
+ Overview = 'overview',
+ ConnectInfo = 'connect-info',
+}
\ No newline at end of file
diff --git a/frontend/src/routes/dashboard/dashboard.interface.ts b/frontend/src/routes/dashboard/dashboard.interface.ts
new file mode 100644
index 0000000..8522a6f
--- /dev/null
+++ b/frontend/src/routes/dashboard/dashboard.interface.ts
@@ -0,0 +1,33 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { RouteComponentProps } from 'react-router';
+
+export interface DashboardProps extends RouteComponentProps {
+
+}
+export interface GetMetaInfoRequestParams {
+ nsId: string;
+}
+export interface MetaInfoResponse {
+ beCount: number;
+ dbCount: number;
+ diskOccupancy: number;
+ feCount: number;
+ remainDisk: number;
+ tblCount: number;
+}
diff --git a/frontend/src/routes/dashboard/dashboard.less b/frontend/src/routes/dashboard/dashboard.less
new file mode 100644
index 0000000..27bd9ed
--- /dev/null
+++ b/frontend/src/routes/dashboard/dashboard.less
@@ -0,0 +1,27 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+/** @format */
+
+.home-main {
+ background: #f9fbfc;
+ border-radius: 16px;
+
+ .home-content {
+ padding: 0 38px 15px;
+ }
+}
diff --git a/frontend/src/routes/dashboard/dashboard.tsx b/frontend/src/routes/dashboard/dashboard.tsx
new file mode 100644
index 0000000..fd1c014
--- /dev/null
+++ b/frontend/src/routes/dashboard/dashboard.tsx
@@ -0,0 +1,75 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import CSSModules from 'react-css-modules';
+import React, { useState } from 'react';
+import styles from './dashboard.less';
+import { CommonHeader } from '@src/components/common-header/header';
+import { ConnectInfo } from './connect-info/connect-info';
+import { DashboardTabEnum } from './dashboard.data';
+import { HomeOutlined } from '@ant-design/icons';
+import { Link, match, Redirect, Route, Switch, useHistory, useLocation } from 'react-router-dom';
+import { Overview } from './overview/overview';
+import { Tabs } from 'antd';
+import { useTranslation } from 'react-i18next';
+
+const ICON_HOME = ;
+const { TabPane } = Tabs;
+
+function DashboardComponent(props: any) {
+ const { match } = props;
+ const { t } = useTranslation();
+ const [refreshToken, setRefreshToken] = useState(new Date().getTime());
+ const history = useHistory();
+ const location = useLocation();
+ const [activeKey, setTabsActiveKey] = useState(DashboardTabEnum.Overview);
+ React.useEffect( ()=>{
+ if(location.pathname.includes(DashboardTabEnum.ConnectInfo)) {
+ setTabsActiveKey(DashboardTabEnum.ConnectInfo);
+ } else {
+ setTabsActiveKey(DashboardTabEnum.Overview);
+ }
+ }, [])
+ return (
+
+
setRefreshToken(new Date().getTime())}
+ >
+
+ {
+ setTabsActiveKey(key);
+ if (key === DashboardTabEnum.Overview) {
+ history.push(`${match.path}/overview`);
+ } else {
+ history.push(`${match.path}/connect-info`);
+ }
+ }}>
+
+
+
+
+
+
+
+
+
+
+ );
+}
+export const Dashboard = CSSModules(styles)(DashboardComponent);
diff --git a/frontend/src/routes/dashboard/overview/overview.less b/frontend/src/routes/dashboard/overview/overview.less
new file mode 100644
index 0000000..93c5539
--- /dev/null
+++ b/frontend/src/routes/dashboard/overview/overview.less
@@ -0,0 +1,23 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+.overview-container {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ margin: 0 40px;
+}
\ No newline at end of file
diff --git a/frontend/src/routes/dashboard/overview/overview.tsx b/frontend/src/routes/dashboard/overview/overview.tsx
new file mode 100644
index 0000000..d8c0559
--- /dev/null
+++ b/frontend/src/routes/dashboard/overview/overview.tsx
@@ -0,0 +1,69 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import CSSModules from 'react-css-modules';
+import React, { useEffect, useState } from 'react';
+import styles from './overview.less';
+import { useTranslation } from 'react-i18next';
+import { DashboardAPI } from '../dashboard.api';
+import { DashboardItem } from '../components/dashboard-item';
+import { HddOutlined, LaptopOutlined, PieChartOutlined, TableOutlined } from '@ant-design/icons';
+import { message } from 'antd';
+import { MetaInfoResponse } from '../dashboard.interface';
+
+export function Component(props: any) {
+ const { t } = useTranslation();
+ const [metaInfo, setMetaInfo] = useState({
+ beCount: 0,
+ dbCount: 0,
+ diskOccupancy: 0,
+ feCount: 0,
+ remainDisk: 0,
+ tblCount: 0,
+ });
+ useEffect(() => {
+ getOverviewInfo();
+ }, []);
+ async function getOverviewInfo() {
+ const res = await DashboardAPI.getMetaInfo();
+ const { msg, data, code } = res;
+ if (code === 0) {
+ if (res.data) {
+ res.data.remainDisk = res.data.remainDisk / 1024;
+ setMetaInfo(res.data);
+ }
+ } else {
+ message.error(msg);
+ }
+ }
+ return (
+
+ } des={metaInfo.dbCount} />
+ } des={metaInfo.tblCount} />
+ }
+ des={
+ metaInfo.diskOccupancy == 0 ? metaInfo.diskOccupancy + '%' : metaInfo.diskOccupancy.toFixed(2) + '%'
+ }
+ />
+ } des={metaInfo.remainDisk.toFixed(2) + 'TB'} />
+
+ );
+}
+
+export const Overview = CSSModules(styles)(Component);
diff --git a/frontend/src/routes/database/database.api.ts b/frontend/src/routes/database/database.api.ts
new file mode 100644
index 0000000..d3767b7
--- /dev/null
+++ b/frontend/src/routes/database/database.api.ts
@@ -0,0 +1,29 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+/** @format */
+
+import { http } from '@src/utils/http';
+import { GetDatabaseInfoRequestParams, DatabaseInfoResponse } from './database.interface';
+import { IResult } from '@src/interfaces/http.interface';
+function getDatabaseInfo(params: GetDatabaseInfoRequestParams): Promise> {
+ return http.get(`/api/meta/dbId/${params.dbId}/info`);
+}
+
+export const DatabaseAPI = {
+ getDatabaseInfo,
+};
diff --git a/frontend/src/routes/database/database.interface.ts b/frontend/src/routes/database/database.interface.ts
new file mode 100644
index 0000000..fcea927
--- /dev/null
+++ b/frontend/src/routes/database/database.interface.ts
@@ -0,0 +1,28 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+/** @format */
+
+export interface GetDatabaseInfoRequestParams {
+ dbId: string;
+}
+export interface DatabaseInfoResponse {
+ createTime: string;
+ creator: string;
+ describe: string;
+ name: string;
+}
diff --git a/frontend/src/routes/database/database.module.less b/frontend/src/routes/database/database.module.less
new file mode 100644
index 0000000..12720fe
--- /dev/null
+++ b/frontend/src/routes/database/database.module.less
@@ -0,0 +1,42 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+/** @format */
+
+.database-main {
+ background: #fff;
+ border-radius: 16px;
+
+ .database-content {
+ padding: 0 38px 15px;
+
+ .items-container {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ }
+
+ .database-content-des {
+ padding: 50px;
+
+ :global .ant-form-item-control-input-content {
+ padding-left: 15px;
+ color: #948f8f;
+ }
+ }
+ }
+}
diff --git a/frontend/src/routes/database/index.tsx b/frontend/src/routes/database/index.tsx
new file mode 100644
index 0000000..2edf4a8
--- /dev/null
+++ b/frontend/src/routes/database/index.tsx
@@ -0,0 +1,91 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+/** @format */
+
+import React, { useState, useEffect } from 'react';
+import styles from './database.module.less';
+import CSSModules from 'react-css-modules';
+import { useHistory } from 'react-router-dom';
+import { Layout, Form, Tabs, Button, message } from 'antd';
+import { HddOutlined } from '@ant-design/icons';
+import { CommonHeader } from '@src/components/common-header/header';
+import { DatabaseAPI } from './database.api';
+import { DatabaseInfoResponse } from './database.interface';
+import { useTranslation } from 'react-i18next';
+import { getShowTime } from '@src/utils/utils';
+
+const { Content, Sider } = Layout;
+const iconDatabase = ;
+const { TabPane } = Tabs;
+const layout = {
+ labelCol: { span: 6 },
+ wrapperCol: { span: 18 },
+};
+
+function Database(props: any) {
+ const history = useHistory();
+ const { t } = useTranslation();
+ const [dbName, setDbName] = useState('');
+ const [databaseInfo, setDatabaseInfo] = useState({
+ createTime: '',
+ creator: '',
+ describe: '',
+ name: '',
+ });
+ useEffect(() => {
+ refresh();
+ }, [window.location.href]);
+ function refresh() {
+ const id = localStorage.getItem('database_id');
+ const name = localStorage.getItem('database_name');
+ if (!id) {
+ return;
+ }
+ setDbName(name);
+ DatabaseAPI.getDatabaseInfo({ dbId: id }).then(res => {
+ const { msg, code, data } = res;
+ if (code === 0) {
+ setDatabaseInfo(res.data);
+ } else {
+ message.error(msg);
+ }
+ });
+ }
+ return (
+
+
+
+
+
+
+
{databaseInfo.name}
+
+ {databaseInfo.describe ? databaseInfo.describe : '-'}
+
+ {getShowTime(databaseInfo.createTime) ? getShowTime(databaseInfo.createTime) : '-'}
+
+
+
+
+
+
+ );
+}
+
+export default CSSModules(styles)(Database);
diff --git a/frontend/src/routes/initialize/auths/auth.tsx b/frontend/src/routes/initialize/auths/auth.tsx
new file mode 100644
index 0000000..df04116
--- /dev/null
+++ b/frontend/src/routes/initialize/auths/auth.tsx
@@ -0,0 +1,32 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React from "react";
+import { Redirect, Route, Switch, useRouteMatch } from "react-router";
+import { AuthLDAP } from "./ldap/ldap";
+import { AuthStudio } from "./studio/studio";
+
+export function InitializeAuth(props: any) {
+ const match = useRouteMatch();
+ return (
+
+
+
+
+
+ )
+}
diff --git a/frontend/src/routes/initialize/auths/components/admin-user/admin-user.less b/frontend/src/routes/initialize/auths/components/admin-user/admin-user.less
new file mode 100644
index 0000000..c378431
--- /dev/null
+++ b/frontend/src/routes/initialize/auths/components/admin-user/admin-user.less
@@ -0,0 +1,19 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+.admin-user {
+}
\ No newline at end of file
diff --git a/frontend/src/routes/initialize/auths/components/admin-user/admin-user.tsx b/frontend/src/routes/initialize/auths/components/admin-user/admin-user.tsx
new file mode 100644
index 0000000..e42479d
--- /dev/null
+++ b/frontend/src/routes/initialize/auths/components/admin-user/admin-user.tsx
@@ -0,0 +1,123 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { InitializeAPI } from '@src/routes/initialize/initialize.api';
+import { isSuccess } from '@src/utils/http';
+import { Form, Input, Button, message } from 'antd';
+import { useForm } from 'antd/lib/form/Form';
+import React from 'react';
+import { RouteProps, useHistory, useRouteMatch } from 'react-router';
+import styles from './admin-user.less';
+interface AdminUserProps extends RouteProps {}
+
+export function AdminUser(props: AdminUserProps) {
+ const [form] = useForm();
+ const history = useHistory();
+ const match = useRouteMatch();
+ async function onFinish(values: any) {
+ const { password_confirm, username, ...params } = values;
+ const res = await InitializeAPI.setAdmin({ ...params, name: username });
+ if (isSuccess(res)) {
+ history.push('/initialize/auth/studio/finish');
+ } else {
+ message.error(res.msg);
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ ({
+ validator(_, value) {
+ if (!value || getFieldValue('password') === value) {
+ return Promise.resolve();
+ }
+ return Promise.reject(new Error('两次输入密码不一致'));
+ },
+ }),
+ ]}
+ label="确认密码"
+ >
+
+
+
+
+ 保存
+
+
+
+
+ );
+}
diff --git a/frontend/src/routes/initialize/auths/components/finish/finish.tsx b/frontend/src/routes/initialize/auths/components/finish/finish.tsx
new file mode 100644
index 0000000..725868b
--- /dev/null
+++ b/frontend/src/routes/initialize/auths/components/finish/finish.tsx
@@ -0,0 +1,39 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { Result, Button } from 'antd';
+import React from 'react';
+import { useHistory } from 'react-router';
+
+export function AuthFinish(props: any) {
+ const history = useHistory();
+ const authType = props.mode === 'ldap' ? 'LDAP' : '本地';
+ return (
+ {
+ localStorage.setItem('initialized', 'true');
+ history.push('/passport/login');
+ }}>
+ 跳转至登录页面
+ ,
+ ]}
+ />
+ );
+}
diff --git a/frontend/src/routes/initialize/auths/ldap/ldap-admin-user/ldap-admin-user.tsx b/frontend/src/routes/initialize/auths/ldap/ldap-admin-user/ldap-admin-user.tsx
new file mode 100644
index 0000000..8af8002
--- /dev/null
+++ b/frontend/src/routes/initialize/auths/ldap/ldap-admin-user/ldap-admin-user.tsx
@@ -0,0 +1,75 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { InitializeAPI } from '@src/routes/initialize/initialize.api';
+import { isSuccess } from '@src/utils/http';
+import { Form, Button, message, Select, Space } from 'antd';
+import React from 'react';
+import { RouteProps, useHistory } from 'react-router';
+import { useLDAPUsers } from './use-ldap-user.hooks';
+const { Option } = Select;
+
+interface AdminUserProps extends RouteProps {}
+
+export function LDAPAdminUser(props: AdminUserProps) {
+ const history = useHistory();
+ const { ldapUsers } = useLDAPUsers();
+ async function onFinish(values: any) {
+ const { username } = values;
+ const res = await InitializeAPI.setAdmin({ name: username });
+ if (isSuccess(res)) {
+ history.push('/initialize/auth/ldap/finish');
+ } else {
+ message.error(res.msg);
+ }
+ }
+
+ return (
+
+
+
+ {ldapUsers?.map(user => (
+
+
+ {user.name}
+ {user.email && ({user.email}) }
+
+
+ ))}
+
+
+
+
+
+ 保存
+
+
+
+
+ );
+}
diff --git a/frontend/src/routes/initialize/auths/ldap/ldap-admin-user/use-ldap-user.hooks.ts b/frontend/src/routes/initialize/auths/ldap/ldap-admin-user/use-ldap-user.hooks.ts
new file mode 100644
index 0000000..028297b
--- /dev/null
+++ b/frontend/src/routes/initialize/auths/ldap/ldap-admin-user/use-ldap-user.hooks.ts
@@ -0,0 +1,39 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { InitializeAPI } from '@src/routes/initialize/initialize.api';
+import { isSuccess } from '@src/utils/http';
+import { Dispatch, SetStateAction, useState, useEffect } from 'react';
+
+export function useLDAPUsers(): {
+ ldapUsers: any[];
+ setLDAPUsers: Dispatch>;
+ getLDAPUsers: () => void;
+} {
+ const [ldapUsers, setLDAPUsers] = useState();
+ useEffect(() => {
+ getLDAPUsers();
+ }, []);
+
+ async function getLDAPUsers() {
+ const res = await InitializeAPI.getLDAPUser();
+ if (isSuccess(res)) {
+ setLDAPUsers(res.data);
+ }
+ }
+ return { ldapUsers, getLDAPUsers, setLDAPUsers };
+}
diff --git a/frontend/src/routes/initialize/auths/ldap/ldap-config/ldap-config.tsx b/frontend/src/routes/initialize/auths/ldap/ldap-config/ldap-config.tsx
new file mode 100644
index 0000000..e1bb5fb
--- /dev/null
+++ b/frontend/src/routes/initialize/auths/ldap/ldap-config/ldap-config.tsx
@@ -0,0 +1,152 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { InitializeAPI } from '@src/routes/initialize/initialize.api';
+import { isSuccess } from '@src/utils/http';
+import { Form, Input, Button, message, Radio, InputNumber } from 'antd';
+import React, { useState } from 'react';
+import { useHistory } from 'react-router';
+
+export function LDAPConfig(props: any) {
+ const history = useHistory();
+ const [loading, setLoading] = useState(false);
+
+ async function onFinish(values: any) {
+ const { password_confirm, username, ...params } = values;
+ setLoading(true);
+ const res = await InitializeAPI.setLDAP({
+ authType: 'ldap',
+ ldapSetting: {
+ ...values,
+ ['ldap-user-base']: [values['ldap-user-base']],
+ },
+ });
+ setLoading(false);
+ if (isSuccess(res)) {
+ console.log(res);
+ history.push('/initialize/auth/ldap/ldap-info');
+ } else {
+ message.error(res.msg);
+ history.push('/initialize/auth/ldap/admin-user');
+ }
+ }
+ return (
+
+
服务器
+
+
+
+
+
+
+
+
+
+ None
+ SSL
+ StartTLS
+
+
+
+
+
+
+
+
+ {/* */}
+ 用户结构
+ {/*
+
+
+
+
+
+ {/* */}
+ 属性
+ {/*
+
+
+
+
+
+
+
+
+
+
+ 保存
+
+
+
+
+ );
+}
diff --git a/frontend/src/routes/initialize/auths/ldap/ldap.tsx b/frontend/src/routes/initialize/auths/ldap/ldap.tsx
new file mode 100644
index 0000000..f90c848
--- /dev/null
+++ b/frontend/src/routes/initialize/auths/ldap/ldap.tsx
@@ -0,0 +1,55 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { Steps } from "antd";
+import { pathToRegexp } from "path-to-regexp";
+import React, { useEffect, useState } from "react";
+import { Redirect, Route, Switch, useHistory, useRouteMatch } from "react-router";
+import { LDAPStepsEnum } from "../../initialize.data";
+import { AuthFinish } from "../components/finish/finish";
+import { LDAPAdminUser } from "./ldap-admin-user/ldap-admin-user";
+import { LDAPConfig } from "./ldap-config/ldap-config";
+const { Step } = Steps;
+
+export function AuthLDAP(props: any) {
+ const match = useRouteMatch();
+ const [step, setStep] = useState(LDAPStepsEnum['ldap-info']);
+ const history = useHistory();
+ useEffect(() => {
+ const regexp = pathToRegexp(`${match.path}/:step`);
+ const paths = regexp.exec(history.location.pathname);
+ const step = (paths as string[])[1];
+ setStep(LDAPStepsEnum[step]);
+ }, [history.location.pathname]);
+ return (
+
+ )
+}
diff --git a/frontend/src/routes/initialize/auths/studio/studio.tsx b/frontend/src/routes/initialize/auths/studio/studio.tsx
new file mode 100644
index 0000000..da2fbc5
--- /dev/null
+++ b/frontend/src/routes/initialize/auths/studio/studio.tsx
@@ -0,0 +1,55 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { Steps } from "antd";
+import React, { useEffect, useState } from "react";
+import { Redirect, Route, Switch, useHistory, useRouteMatch } from "react-router";
+import { pathToRegexp } from 'path-to-regexp';
+import { StudioStepsEnum } from "../../initialize.data";
+import { AdminUser } from "../components/admin-user/admin-user";
+import { AuthFinish } from "../components/finish/finish";
+
+const { Step } = Steps;
+
+export function AuthStudio(props: any) {
+ const match = useRouteMatch();
+ const [step, setStep] = useState(StudioStepsEnum['admin-user']);
+ const history = useHistory();
+ useEffect(() => {
+ const regexp = pathToRegexp(`${match.path}/:step`);
+ const paths = regexp.exec(history.location.pathname);
+ const step = (paths as string[])[1];
+ setStep(StudioStepsEnum[step]);
+ }, [history.location.pathname]);
+ return (
+
+ )
+}
+
+
diff --git a/frontend/src/routes/initialize/initialize.api.ts b/frontend/src/routes/initialize/initialize.api.ts
new file mode 100644
index 0000000..46a7c89
--- /dev/null
+++ b/frontend/src/routes/initialize/initialize.api.ts
@@ -0,0 +1,52 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { IResult } from "@src/interfaces/http.interface";
+import { http } from "@src/utils/http";
+
+function setAuthType(data?: any): Promise> {
+ return http.post(`/api/setting/init/authType`, data);
+}
+
+function resetAuthType(data?: any): Promise> {
+ return http.get(`/api/setting/reset`, data);
+}
+
+function setAdmin(data?: any): Promise> {
+ return http.post(`/api/setting/admin`, data);
+}
+
+function setLDAP(data?: any): Promise> {
+ return http.post(`/api/setting/init/ldapStudio`, data);
+}
+
+function getInitProperties(): Promise> {
+ return http.get(`/api/session/initProperties`);
+}
+
+function getLDAPUser(): Promise> {
+ return http.get(`/api/setting/ldapUsers`);
+}
+
+export const InitializeAPI = {
+ setAuthType,
+ resetAuthType,
+ setAdmin,
+ setLDAP,
+ getInitProperties,
+ getLDAPUser
+}
diff --git a/frontend/src/routes/initialize/initialize.data.ts b/frontend/src/routes/initialize/initialize.data.ts
new file mode 100644
index 0000000..243187d
--- /dev/null
+++ b/frontend/src/routes/initialize/initialize.data.ts
@@ -0,0 +1,27 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+export enum StudioStepsEnum {
+ 'admin-user',
+ 'finish'
+}
+
+export enum LDAPStepsEnum {
+ 'ldap-info',
+ 'admin-user',
+ 'finish'
+}
\ No newline at end of file
diff --git a/frontend/src/routes/initialize/initialize.less b/frontend/src/routes/initialize/initialize.less
new file mode 100644
index 0000000..1d4d635
--- /dev/null
+++ b/frontend/src/routes/initialize/initialize.less
@@ -0,0 +1,32 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+.initialize {
+ height: calc(100vh - 150px);
+ display: flex;
+ width: 600px;
+ margin: 40px auto;
+ .initialize-steps-content {
+ width: calc(100% - 32px);
+ }
+}
+.initialize-container {
+ padding: 40px;
+ width: 600px;
+ margin: 0 auto;
+ height: calc(100vh - 65px);
+}
\ No newline at end of file
diff --git a/frontend/src/routes/initialize/initialize.route.tsx b/frontend/src/routes/initialize/initialize.route.tsx
new file mode 100644
index 0000000..d131288
--- /dev/null
+++ b/frontend/src/routes/initialize/initialize.route.tsx
@@ -0,0 +1,73 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { AuthTypeEnum } from '@src/common/common.data';
+import { Sidebar } from '@src/components/sidebar/sidebar';
+import { useAuth } from '@src/hooks/use-auth';
+import React, { useEffect } from 'react';
+import { Route, Redirect, useRouteMatch, Switch, useHistory } from 'react-router';
+import { InitializeAuth } from './auths/auth';
+import { InitializePage } from './initialize';
+import { StudioStepsEnum, LDAPStepsEnum } from './initialize.data';
+import styles from './initialize.less';
+
+export function Initialize(props: any) {
+ const match = useRouteMatch();
+ const history = useHistory();
+ const {initStep, authType: currentAuthType, initialized} = useAuth();
+
+ useEffect(() => {
+ if (currentAuthType && initStep) {
+ const feStep = initStep ? initStep - 1 : 1;
+ let stepPage = '';
+ if (currentAuthType === AuthTypeEnum.STUDIO) {
+ stepPage = StudioStepsEnum[feStep];
+ } else if (currentAuthType === AuthTypeEnum.LDAP){
+ stepPage = LDAPStepsEnum[feStep];
+ }
+ if (initialized) {
+ if (currentAuthType === AuthTypeEnum.STUDIO) {
+ stepPage = StudioStepsEnum[feStep];
+ if (feStep === 1) {
+ history.push('/space');
+ }
+ } else if (currentAuthType === AuthTypeEnum.LDAP){
+ stepPage = LDAPStepsEnum[feStep];
+ if (feStep === 2) {
+ history.push('/space');
+ }
+ }
+ } else {
+ history.push(`${match.path}/auth/${currentAuthType}/${stepPage}`);
+ }
+ }
+ }, [currentAuthType, initialized]);
+ return (
+
+ );
+}
diff --git a/frontend/src/routes/initialize/initialize.tsx b/frontend/src/routes/initialize/initialize.tsx
new file mode 100644
index 0000000..50d783a
--- /dev/null
+++ b/frontend/src/routes/initialize/initialize.tsx
@@ -0,0 +1,57 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { AuthTypeEnum } from '@src/common/common.data';
+import { isSuccess } from '@src/utils/http';
+import { Button, Card, message, Radio, Row, Space, Steps } from 'antd';
+import React, { useState } from 'react';
+import { useHistory, useRouteMatch } from 'react-router';
+import { InitializeAPI } from './initialize.api';
+import styles from './initialize.less';
+
+export function InitializePage(props: any) {
+ const [authType, setAuthType] = useState(AuthTypeEnum.STUDIO);
+ const match = useRouteMatch();
+ const history = useHistory();
+ async function handleSetAuthType() {
+ const res = await InitializeAPI.setAuthType({authType});
+ if (isSuccess(res)) {
+ history.push(`${match.path}auth/${authType}`);
+ } else {
+ message.error(res.msg);
+ }
+ }
+
+ return (
+
+
+
+ setAuthType(e.target.value)} value={authType}>
+
+ 本地认证
+ {/* LDAP认证 */}
+
+
+ 注意,初始化选择好认证方式后不可再改变。
+
+ handleSetAuthType()}>去配置
+
+
+
+
+ );
+}
diff --git a/frontend/src/routes/meta/meta.less b/frontend/src/routes/meta/meta.less
new file mode 100644
index 0000000..f7985a5
--- /dev/null
+++ b/frontend/src/routes/meta/meta.less
@@ -0,0 +1,40 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+/** @format */
+.palo-new-main {
+ display: flex;
+ flex-direction: row;
+ min-height: 380px;
+
+ .new-main-sider {
+ flex: 0 0 300px;
+ height: calc(100vh - 95px);
+ margin-top: 30px;
+ border-radius: 16px;
+ }
+
+ .new-main-content {
+ flex: auto;
+ height: calc(100vh - 95px);
+ border-radius: 16px;
+ }
+}
+
+.site-layout-background {
+ background: none;
+}
diff --git a/frontend/src/routes/meta/meta.tsx b/frontend/src/routes/meta/meta.tsx
new file mode 100644
index 0000000..4d06a88
--- /dev/null
+++ b/frontend/src/routes/meta/meta.tsx
@@ -0,0 +1,52 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+/**
+ * @format
+ */
+import React, { useEffect, useState } from 'react';
+import styles from './meta.less';
+import { PageSide } from '@src/layout/page-side/index';
+import { MetaBaseTree } from '../tree/index';
+import { Redirect, Route, Router, Switch } from 'react-router-dom';
+import TableContent from '../table-content';
+import Database from '../database';
+
+export function Meta(props: any) {
+ return (
+
+ );
+}
diff --git a/frontend/src/routes/node/dashboard/index.module.less b/frontend/src/routes/node/dashboard/index.module.less
new file mode 100644
index 0000000..08e1fc5
--- /dev/null
+++ b/frontend/src/routes/node/dashboard/index.module.less
@@ -0,0 +1,20 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+.dashboard {
+ //
+}
diff --git a/frontend/src/routes/node/dashboard/index.tsx b/frontend/src/routes/node/dashboard/index.tsx
new file mode 100644
index 0000000..bd6f1b3
--- /dev/null
+++ b/frontend/src/routes/node/dashboard/index.tsx
@@ -0,0 +1,336 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { Row, Col, Select, Collapse, Button, Divider, AutoComplete } from 'antd';
+import React, { useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import ReactEChartsCore from 'echarts-for-react/lib/core';
+import * as echarts from 'echarts/core';
+import dayjs from 'dayjs';
+import './monitor.less';
+
+import { LineChart } from 'echarts/charts';
+import {
+ GridComponent,
+ PolarComponent,
+ RadarComponent,
+ GeoComponent,
+ SingleAxisComponent,
+ ParallelComponent,
+ CalendarComponent,
+ GraphicComponent,
+ ToolboxComponent,
+ TooltipComponent,
+ TitleComponent,
+ LegendComponent,
+} from 'echarts/components';
+import { CanvasRenderer } from 'echarts/renderers';
+import { MonitorAPI } from './monitor.api';
+import { CHARTS_OPTIONS, getTimes } from './monitor.data';
+import { deepClone, formatBytes } from '@src/utils/utils';
+
+echarts.use([
+ LineChart,
+ CanvasRenderer,
+ TitleComponent,
+ GridComponent,
+ PolarComponent,
+ RadarComponent,
+ GeoComponent,
+ SingleAxisComponent,
+ ParallelComponent,
+ CalendarComponent,
+ GraphicComponent,
+ ToolboxComponent,
+ TooltipComponent,
+ LegendComponent,
+]);
+
+const { Panel } = Collapse;
+const { Option } = Select;
+
+export function NodeDashboard() {
+ const { t } = useTranslation();
+ const [TIMES, setTIMES] = useState(() => getTimes(dayjs()));
+ const [beNodes, setBENodes] = useState([]);
+ const [selectedBENodes, setSelectedBENodes] = useState([]);
+ const [currentTime, setCurrentTime] = useState(TIMES[1]);
+ const [be_cpu_idle, setBE_CPU_IDLE] = useState({});
+ const [be_mem, setBE_Mem] = useState({});
+ const [be_disk_io, setBE_DISK_IO] = useState({});
+ const [be_base_compaction_score, setBE_base_compaction_score] = useState({});
+ const [be_cumu_compaction_score, setBE_cumu_compaction_score] = useState({});
+
+ useEffect(() => {
+ init();
+ }, [currentTime.value, currentTime.start]);
+
+ useEffect(() => {
+ updateBECharts();
+ }, [selectedBENodes, currentTime.value, currentTime.start]);
+
+ function handleBENodeChange(value: any) {
+ setSelectedBENodes(value);
+ }
+ function init() {
+ getBENodes();
+ }
+
+ function formatData(response: any, title: string) {
+ console.log(response);
+ const option = deepClone(CHARTS_OPTIONS);
+ // option.title.text = title;
+ const xAxisData = response?.x_value.map((item: string) => dayjs(item).format(currentTime.format));
+ option.xAxis.data = xAxisData;
+ const series: any[] = [];
+ const legendData: any[] = [];
+
+ function transformChartsData(y_value: string, formatter?: string) {
+ for (const [key, value] of Object.entries(y_value).sort()) {
+ if (Array.isArray(value)) {
+ legendData.push(key);
+ series.push({
+ name: key,
+ data: value.map(item => {
+ if (formatter === 'MB') {
+ return typeof item === 'number' ? formatBytes(item, 2, false) : item;
+ }
+ return typeof item === 'number' ? item.toFixed(3) : item;
+ }),
+ type: 'line',
+ });
+ } else {
+ transformChartsData(value);
+ }
+ }
+ return series;
+ }
+ if (title === '节点内存使用量') {
+ transformChartsData(response?.y_value, 'MB');
+ } else {
+ transformChartsData(response?.y_value);
+ }
+ option.legend = {
+ bottom: '0',
+ left: 0,
+ height: 80,
+ width: 'auto',
+ type: 'scroll',
+ itemHeight: 10,
+ data: legendData,
+ orient: 'vertical',
+ pageIconSize: 7,
+ textStyle: {
+ overflow: 'breakAll',
+ },
+ };
+ (option.grid = {
+ bottom: 110,
+ }),
+ (option.series = series);
+ return option;
+ }
+
+ // BE NODES REQUEST
+ function updateBECharts() {
+ getBE_CPU_IDLE();
+ getBE_Mem();
+ getBE_DiskIO();
+ getBE_base_compaction_score();
+ getBE_cumu_compaction_score();
+ }
+ function getBENodes() {
+ MonitorAPI.getBENodes().then(res => {
+ console.log(res.data);
+ setBENodes(res.data);
+ setSelectedBENodes(res.data);
+ });
+ }
+ function getBE_CPU_IDLE() {
+ MonitorAPI.getBE_CPU_IDLE(currentTime.start, currentTime.end, {
+ nodes: selectedBENodes,
+ }).then(res => {
+ const option = formatData(res.data, 'CPU空闲率(%)');
+ setBE_CPU_IDLE(option);
+ });
+ }
+ function getBE_Mem() {
+ MonitorAPI.getBE_Mem(currentTime.start, currentTime.end, {
+ nodes: selectedBENodes,
+ }).then(res => {
+ const option = formatData(res.data, '节点内存使用量');
+ setBE_Mem(option);
+ });
+ }
+ function getBE_DiskIO() {
+ MonitorAPI.getBE_DiskIO(currentTime.start, currentTime.end, {
+ nodes: selectedBENodes,
+ }).then(res => {
+ const option = formatData(res.data, 'IO利用率');
+ setBE_DISK_IO(option);
+ });
+ }
+ function getBE_base_compaction_score() {
+ MonitorAPI.getBE_base_compaction_score(currentTime.start, currentTime.end, {
+ nodes: selectedBENodes,
+ }).then(res => {
+ const option = formatData(res.data, '基线数据版本合并情况');
+ option.legend.left = 20;
+ option.yAxis = {
+ type: 'value',
+ minInterval: 1,
+
+ boundaryGap: [0, 0.1],
+ };
+ setBE_base_compaction_score(option);
+ });
+ }
+ function getBE_cumu_compaction_score() {
+ MonitorAPI.getBE_cumu_compaction_score(currentTime.start, currentTime.end, {
+ nodes: selectedBENodes,
+ }).then(res => {
+ const option = formatData(res.data, '增量数据版本合并情况');
+ option.legend.left = 20;
+ option.yAxis = {
+ type: 'value',
+ minInterval: 1,
+
+ boundaryGap: [0, 0.1],
+ };
+ setBE_cumu_compaction_score(option);
+ });
+ }
+ return (
+
+
+
+
+ {t`viewTime`}:
+
+ {
+ const time = TIMES.filter(item => item.value === v)[0];
+ setCurrentTime(time);
+ }}
+ >
+ {TIMES.map(time => {
+ return (
+
+ {time.text}
+
+ );
+ })}
+
+
+
+ {
+ const UPDATED_TIMES = getTimes(dayjs());
+ setTIMES(UPDATED_TIMES);
+ const time = UPDATED_TIMES.filter(item => item.value === currentTime.value)[0];
+ setCurrentTime(time);
+ }}
+ >
+ {t`refresh`}
+
+
+
+
+
+
+
+
+ {t`nodeSelection`}:
+
+ {beNodes.map(item => (
+
+ {item}
+
+ ))}
+
+
+
+
+ {t`CPUidleRate`}
+
+
+
+ {t`nodeMemoryUsage`}
+
+
+
+ {t`IOutilization`}
+
+
+
+
+
+
+ {t`BaselineDataVersionConsolidation`}
+
+
+
+ {t`IncrementalDataVersionConsolidation`}
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/routes/node/dashboard/monitor.api.ts b/frontend/src/routes/node/dashboard/monitor.api.ts
new file mode 100644
index 0000000..c1a4e01
--- /dev/null
+++ b/frontend/src/routes/node/dashboard/monitor.api.ts
@@ -0,0 +1,61 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+/** @format */
+
+import { http } from '@src/utils/http';
+import { IResult } from 'src/interfaces/http.interface';
+
+function getBENodes(): Promise> {
+ return http.get(`/api/rest/v2/manager/monitor/value/be_list`);
+}
+
+function getBE_CPU_IDLE(start?: any, end?: any, data?: any): Promise> {
+ return http.post(`/api/rest/v2/manager/monitor/timeserial/be_cpu_idle?start=${start}&end=${end}`, data);
+}
+
+function getBE_Mem(start?: any, end?: any, data?: any): Promise> {
+ return http.post(`/api/rest/v2/manager/monitor/timeserial/be_mem?start=${start}&end=${end}`, data);
+}
+
+function getBE_DiskIO(start?: any, end?: any, data?: any): Promise> {
+ return http.post(`/api/rest/v2/manager/monitor/timeserial/be_disk_io?start=${start}&end=${end}`, data);
+}
+
+function getBE_base_compaction_score(start?: any, end?: any, data?: any): Promise> {
+ console.log(data);
+ return http.post(
+ `/api/rest/v2/manager/monitor/timeserial/be_base_compaction_score?start=${start}&end=${end}`,
+ data,
+ );
+}
+
+function getBE_cumu_compaction_score(start?: any, end?: any, data?: any): Promise> {
+ return http.post(
+ `/api/rest/v2/manager/monitor/timeserial/be_cumu_compaction_score?start=${start}&end=${end}`,
+ data,
+ );
+}
+
+export const MonitorAPI = {
+ getBENodes,
+ getBE_CPU_IDLE,
+ getBE_Mem,
+ getBE_DiskIO,
+ getBE_base_compaction_score,
+ getBE_cumu_compaction_score,
+};
diff --git a/frontend/src/routes/node/dashboard/monitor.data.ts b/frontend/src/routes/node/dashboard/monitor.data.ts
new file mode 100644
index 0000000..e633fae
--- /dev/null
+++ b/frontend/src/routes/node/dashboard/monitor.data.ts
@@ -0,0 +1,96 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import dayjs from 'dayjs';
+export function getTimes(now: dayjs.Dayjs) {
+ return [
+ {
+ value: '1',
+ text: '最近半小时',
+ end: new Date().getTime(),
+ start: now.subtract(30, 'minute').valueOf(),
+ format: 'HH:mm',
+ },
+ {
+ value: '2',
+ text: '最近1小时',
+ end: new Date().getTime(),
+ start: now.subtract(1, 'hour').valueOf(),
+ format: 'HH:mm',
+ },
+ {
+ value: '3',
+ text: '最近6小时',
+ end: new Date().getTime(),
+ start: now.subtract(6, 'hour').valueOf(),
+ format: 'HH:mm',
+ },
+ {
+ value: '4',
+ text: '最近1天',
+ end: new Date().getTime(),
+ start: now.subtract(1, 'day').valueOf(),
+ format: 'HH:mm',
+ },
+ {
+ value: '5',
+ text: '最近三天',
+ end: new Date().getTime(),
+ start: now.subtract(3, 'day').valueOf(),
+ format: 'MM/DD HH:mm',
+ },
+ {
+ value: '6',
+ text: '最近一周',
+ end: new Date().getTime(),
+ start: now.subtract(1, 'week').valueOf(),
+ format: 'MM/DD HH:mm',
+ },
+ {
+ value: '7',
+ text: '最近半个月',
+ end: new Date().getTime(),
+ start: now.subtract(15, 'day').valueOf(),
+ format: 'MM/DD HH:mm',
+ },
+ {
+ value: '8',
+ text: '最近一个月',
+ end: new Date().getTime(),
+ start: now.subtract(1, 'month').valueOf(),
+ format: 'MM/DD HH:mm',
+ },
+ ];
+}
+
+export const CHARTS_OPTIONS = {
+ title: { text: '' },
+ legend: {
+ data: [],
+ left: 'center',
+ },
+ tooltip: {
+ trigger: 'axis',
+ },
+ xAxis: {
+ type: 'category',
+ boundaryGap: false,
+ data: [],
+ },
+ yAxis: { type: 'value' },
+ series: [],
+};
diff --git a/frontend/src/routes/node/dashboard/monitor.less b/frontend/src/routes/node/dashboard/monitor.less
new file mode 100644
index 0000000..8383000
--- /dev/null
+++ b/frontend/src/routes/node/dashboard/monitor.less
@@ -0,0 +1,24 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+.monitor {
+ min-height: 100%;
+
+ h4 {
+ margin-left: 10px;
+ }
+}
diff --git a/frontend/src/routes/node/list/be-configuration/be-config.api.ts b/frontend/src/routes/node/list/be-configuration/be-config.api.ts
new file mode 100644
index 0000000..9873cf6
--- /dev/null
+++ b/frontend/src/routes/node/list/be-configuration/be-config.api.ts
@@ -0,0 +1,37 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { http } from '@src/utils/http';
+
+function getConfigSelect() {
+ return http.get(`/api/rest/v2/manager/node/configuration_name`);
+}
+function getNodeSelect() {
+ return http.get(`/api/rest/v2/manager/node/node_list`);
+}
+function getConfigurationsInfo(data: any, type: string) {
+ return http.post(`/api/rest/v2/manager/node/configuration_info?type=${type}`, data);
+}
+function setConfig(data: any) {
+ return http.post(`/api/rest/v2/manager/node/set_config/be`, data);
+}
+export const BeConfigAPI = {
+ getConfigSelect,
+ getNodeSelect,
+ getConfigurationsInfo,
+ setConfig,
+};
diff --git a/frontend/src/routes/node/list/be-configuration/index.tsx b/frontend/src/routes/node/list/be-configuration/index.tsx
new file mode 100644
index 0000000..ddca4db
--- /dev/null
+++ b/frontend/src/routes/node/list/be-configuration/index.tsx
@@ -0,0 +1,534 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+/* eslint-disable prettier/prettier */
+import React, { useEffect, useState } from 'react';
+import { Descriptions, Radio, Select, Table, Space, Modal, Form, Input, Tooltip, message, Divider } from 'antd';
+import { InfoCircleOutlined } from '@ant-design/icons';
+const { Option } = Select;
+import { ConfigurationTypeEnum } from '@src/common/common.data';
+import { BeConfigAPI } from './be-config.api';
+import { useHistory } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+import { FILTERS } from '../config.data'
+
+export function BEConfiguration() {
+ const { t } = useTranslation();
+ const history = useHistory();
+ const [beColumn, setBeColumn] = useState([]);
+ const [tableData, setTableData] = useState([]);
+ const [configSelect, setConfigSelect] = useState([]);
+ const [nodeSelect, setNodeSelect] = useState([]);
+ const [confName, setConfName] = useState([]);
+ const [nodeName, setNodeName] = useState([]);
+ const [isModalVisible, setIsModalVisible] = useState(false);
+ const [selectedRowKeys, setSelectedRowKeys] = useState([]);
+ const [selectedRows, setSelectedRows] = useState([]);
+ const [isBatchEdit, setIsBatchEdit] = useState(true);
+ const [rowData, setRowsData] = useState({});
+ const [changeCurrent, setChangeCurrent] = useState();
+ const [nodevalue, setNodevalue] = useState([]);
+ function changeConfig(record: any) {
+ setRowsData(record);
+ setIsBatchEdit(true);
+ setIsModalVisible(true);
+ }
+
+ const [form] = Form.useForm();
+
+ const rowSelection = {
+ selectedRowKeys,
+ onChange: (selectedRowKeys: React.Key[], selectedRows: any) => {
+ console.log(selectedRows)
+ let newSelectedRowKeys = [];
+ let newSelectedRows: any[] = [];
+ newSelectedRowKeys = selectedRowKeys.filter((key, index) => {
+ if (selectedRows[index][Object.keys(selectedRows[index]).length - 2] === 'true') {
+ newSelectedRows.push(selectedRows[index]);
+ return true;
+ }
+ return false;
+ });
+ setSelectedRowKeys(newSelectedRowKeys);
+ setSelectedRows(newSelectedRows);
+ },
+ getCheckboxProps: (record: any) => {
+ const arr = Object.keys(record);
+ return {
+ disabled: record[arr.length - 2] === 'false',
+ key: record[arr.length - 1],
+ };
+ },
+ selections: [
+ Table.SELECTION_ALL,
+ Table.SELECTION_NONE,
+ {
+ text: t`SelectOnlyTheCurrentPage`,
+ onSelect(selectedRowKeys: React.Key[], selectedRows: any) {
+ setSelectedRowKeys(selectedRowKeys)
+ let newSelectedRows: any[] = [];
+ for (let i = 0; i < selectedRowKeys.length; i++) {
+ newSelectedRows.push(tableData[selectedRowKeys[i]])
+ }
+ setSelectedRows(newSelectedRows)
+ },
+ },
+ ],
+ };
+ const [typeValue, setTypeValue] = React.useState(ConfigurationTypeEnum.BE);
+ function onChange(e: any) {
+ setTypeValue(e.target.value);
+ if (e.target.value === ConfigurationTypeEnum.BE) {
+ localStorage.setItem("nodeText","");
+ history.push(`/configuration/be`);
+ } else {
+ localStorage.setItem("nodeText","");
+ history.push(`/configuration/fe`);
+ }
+ }
+ const handleOk = (values: any) => {
+ if (isBatchEdit) {
+ let name = rowData[0];
+ let data = {};
+ data[name] = {
+ node: [rowData[1]],
+ value: values.value,
+ persist: values.persist,
+ };
+
+ BeConfigAPI.setConfig(data).then(res => {
+ if (res.code === 0) {
+ setIsModalVisible(false);
+ getTable()
+ if (res.data.failed && res.data.failed.length !== 0) {
+ warning(res.data.failed)
+ } else {
+ message.success(t`SuccessfullyModified`);
+ }
+ } else {
+
+ message.error(res.msg);
+ }
+ });
+ } else {
+ let name = "";
+ let data = {};
+ for (let i = 0; i < selectedRows.length; i++) {
+ let nodes: any[] = [];
+ name = selectedRows[i][0]
+ selectedRows.map((item: any[], index: any) => {
+ if (item[0] === selectedRows[i][0]) {
+ nodes.push(item[1])
+ }
+ });
+ data[name] = {
+ node: nodes,
+ value: values.value,
+ persist: values.persist,
+ };
+ }
+ BeConfigAPI.setConfig(data).then(res => {
+ if (res.code === 0) {
+ setIsModalVisible(false);
+ getTable()
+ if (res.data.failed && res.data.failed.length !== 0) {
+ warning(res.data.failed)
+ } else {
+ message.success(t`SuccessfullyModified`);
+ }
+ } else {
+ message.error(res.msg);
+ }
+ });
+ }
+ console.log(values, isBatchEdit, rowData);
+ //setIsModalVisible(false);
+ };
+
+ const handleCancel = () => {
+ setIsModalVisible(false);
+ };
+
+ function getTable() {
+ let data = {};
+ if (confName.length === 0 && nodeName.length !== 0) {
+ data = { type: typeValue, node: nodeName };
+ }
+ if (confName.length !== 0 && nodeName.length === 0) {
+ data = { type: typeValue, conf_name: confName };
+ }
+ if (confName.length === 0 && nodeName.length === 0) {
+ data = { type: typeValue };
+ }
+ if (confName.length !== 0 && nodeName.length !== 0) {
+ data = { type: typeValue, conf_name: confName, node: nodeName };
+ }
+ setTable(data,typeValue)
+ }
+ function refresh() {
+ let data = {};
+ const nodeText = localStorage.getItem("nodeText")
+ if (nodeText !== "") {
+ setNodevalue(nodeText)
+ nodeName.push(nodeText)
+ const node = [];
+ node.push(nodeText)
+ if (confName.length === 0 && node.length !== 0) {
+ data = { type: typeValue, node: node };
+ }
+ if (confName.length !== 0 && node.length === 0) {
+ data = { type: typeValue, conf_name: confName };
+ }
+ if (confName.length === 0 && node.length === 0) {
+ data = { type: typeValue };
+ }
+ if (confName.length !== 0 && node.length !== 0) {
+ data = { type: typeValue, conf_name: confName, node: node };
+ }
+ } else {
+ if (confName.length === 0 && nodeName.length !== 0) {
+ data = { type: typeValue, node: nodeName };
+ }
+ if (confName.length !== 0 && nodeName.length === 0) {
+ data = { type: typeValue, conf_name: confName };
+ }
+ if (confName.length === 0 && nodeName.length === 0) {
+ data = { type: typeValue };
+ }
+ if (confName.length !== 0 && nodeName.length !== 0) {
+ data = { type: typeValue, conf_name: confName, node: nodeName };
+ }
+ }
+
+ setTable(data,typeValue)
+
+ BeConfigAPI.getConfigSelect().then((res: { data: any; code: any; msg: any }) => {
+ if (res.code === 0) {
+ const { data, code, msg } = res;
+ console.log(data);
+ const { backend, frontend } = data;
+ const configSelect = backend.map((item: string, index: number) => {
+ return (
+
+ {item}
+
+ );
+ });
+ setConfigSelect(configSelect);
+ } else {
+ message.warning(res.msg);
+ }
+ });
+ BeConfigAPI.getNodeSelect().then((res: { data: any; code: any; msg: any }) => {
+ if (res.code === 0) {
+ const { data, code, msg } = res;
+ const { backend, frontend } = data;
+ const nodeSelect = backend.map((item: string, index: number) => {
+ return (
+
+ {item}
+
+ );
+ });
+ setNodeSelect(nodeSelect);
+ } else {
+ message.warning(res.msg);
+ }
+ });
+ }
+
+ function batchEditing() {
+ if (selectedRowKeys.length === 0) {
+ message.warning(t`PleaseCheckTheConfigurationYouWantToModify`);
+ } else {
+ setIsModalVisible(true);
+ setIsBatchEdit(false);
+ }
+ }
+ useEffect(() => {
+ refresh();
+ }, []);
+ function configSelectChange(_value: any) {
+ setSelectedRowKeys([])
+ setSelectedRows([])
+ const newArray = _value.map(x => x.trim())
+ setConfName(newArray);
+ let data = {};
+ if (newArray.length === 0 && nodeName.length !== 0) {
+ data = { type: typeValue, node: nodeName };
+ }
+ if (newArray.length !== 0 && nodeName.length === 0) {
+ data = { type: typeValue, conf_name: newArray };
+ }
+ if (newArray.length === 0 && nodeName.length === 0) {
+ data = { type: typeValue };
+ }
+ if (newArray.length !== 0 && nodeName.length !== 0) {
+ data = { conf_name: newArray, node: nodeName };
+ }
+ setTable(data,typeValue)
+ }
+
+ function setTable(data: any,typeValue: any) {
+ BeConfigAPI.getConfigurationsInfo(data, typeValue).then(res => {
+ if (res.code == 0) {
+ const { column_names, rows } = res.data;
+ const columns = column_names.map((item: string, index: number) => {
+ if (item === '可修改') {
+ return {
+ title: t`operate`,
+ dataIndex: item,
+ render: (text: any, record: any, index: any) => (
+
+ {record[Object.keys(record).length - 2] === 'true' ? (
+ changeConfig(record)}>{ t`edit`}
+ ) : (
+ <>>
+ )}
+
+ ),
+ fixed: 'right',
+ width: 100,
+ };
+ }
+ if (item === 'MasterOnly') {
+ return {
+ title: item,
+ dataIndex: `${index}`,
+ key: item,
+ columnWidth: 10,
+ filters: FILTERS,
+ onFilter: (value: any, record: any) => onFilterCols(value,record),
+ };
+ }
+ else {
+ return {
+ title: item,
+ dataIndex: index,
+ columnWidth: 10,
+ };
+ }
+ });
+ setBeColumn(columns);
+
+ for (let i = 0; i < rows.length; i++) {
+ rows[i].push('' + i + '');
+ }
+ setTableData(rows.map((item: string[]) => ({ ...item })));
+ if (data.conf_name) {
+ setChangeCurrent(1)
+ }
+ } else {
+ message.warning(res.msg);
+ }
+ });
+ }
+
+ function onFilterCols(value: any, record: any) {
+ return record[6].indexOf(value) === 0
+ }
+
+ function nodeSelectChange(value: any) {
+ setSelectedRowKeys([])
+ setSelectedRows([])
+ if (value.length === 0) {
+ setNodevalue([])
+
+ } else {
+ setNodevalue(value)
+ }
+ localStorage.setItem("nodeText", "");
+ const newArray = value.map(x => x.trim())
+ setNodeName(newArray);
+ let data = {};
+ if (confName.length === 0 && newArray.length !== 0) {
+ data = { type: typeValue, node: newArray };
+ }
+ if (confName.length !== 0 && newArray.length === 0) {
+ data = { type: typeValue, conf_name: confName };
+ }
+ if (confName.length === 0 && newArray.length === 0) {
+ data = { type: typeValue };
+ }
+ if (confName.length !== 0 && newArray.length !== 0) {
+ data = { type: typeValue, conf_name: confName, node: newArray };
+ }
+ setTable(data,typeValue)
+ }
+
+ function pageSizeChange(current: React.SetStateAction, pageSize: number | undefined) {
+ setChangeCurrent(current)
+ }
+ const [configvalue, setConfigvalue] = React.useState([]);
+ const configSelectProps = {
+ mode: 'multiple' as const,
+ style: { width: '100%' },
+ configvalue,
+ onChange: (newValue: string[]) => {
+ setConfigvalue(newValue);
+ },
+ maxTagCount: 'responsive' as const,
+ };
+ const nodeSelectProps = {
+ mode: 'multiple' as const,
+ style: { width: '100%' },
+ nodevalue,
+ onChange: (newValue: string[]) => {
+ setNodevalue(newValue);
+ },
+ maxTagCount: 'responsive' as const,
+ };
+ return (
+
+
+
+ {t`nodeSelection`}:
+
+ FE节点
+ BE节点
+
+
+
+ { t`ConfigurationItem`}:
+
+ {configSelect}
+
+
+
+ { t`Node`}:
+
+ {nodeSelect}
+
+
+
+
+
+
+ { t`CurrentlySelected`} {selectedRowKeys.length} {t`StripData`}
+
+
+
+
+
+ {t`BatchEditing`}
+
+
+
+
+
record[Object.keys(record).length - 1]}
+ size="middle"
+ scroll={{ x: 1300 }}
+ pagination={{
+ position: ['bottomCenter'],
+ total: beColumn,
+ current:changeCurrent,
+ showSizeChanger: true,
+ showQuickJumper: true,
+ onChange: (current, pageSize) => pageSizeChange(current, pageSize),
+ showTotal: (total: any) => {t`total`+`${total}` +t`strip`},
+ }}
+ >
+
+
{
+ form.validateFields()
+ .then((values: any) => {
+ form.resetFields();
+ handleOk(values);
+ })
+ .catch((info: any) => {
+ console.log('Validate Failed:', info);
+ });
+ }}
+ >
+
+
+
+
+
+
+ {t`TemporarilyEffective`}
+
+
+
+
+
+ { t`Permanent`}
+
+
+
+
+
+
+
+
+
+ );
+ function warning(failed: []) {
+ const arr = failed.map((failedMsg:any) => {
+ return <>{failedMsg.node} { t`Node`} {failedMsg.config_name} {t`Error`+":"} {failedMsg.err_info} >
+ })
+ Modal.error({
+ content: <>
+
+ { t`ConfigurationError`}
+
+ {arr}
+
+ >
+ ,
+ title: { t`FailToEdit`} ,
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
+ onOk() { },
+ bodyStyle: { height: '250px' },
+ width: '520px',
+ closable:true
+
+ });
+ }
+}
diff --git a/frontend/src/routes/node/list/config.data.ts b/frontend/src/routes/node/list/config.data.ts
new file mode 100644
index 0000000..7227a88
--- /dev/null
+++ b/frontend/src/routes/node/list/config.data.ts
@@ -0,0 +1,26 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+export enum MASTER_MAP {
+ TRUE = 'true',
+ FALSE = 'false',
+}
+
+export const FILTERS = [
+ { text: MASTER_MAP.TRUE, value: MASTER_MAP.TRUE },
+ { text: MASTER_MAP.FALSE, value: MASTER_MAP.FALSE },
+];
\ No newline at end of file
diff --git a/frontend/src/routes/node/list/configuration/index.tsx b/frontend/src/routes/node/list/configuration/index.tsx
new file mode 100644
index 0000000..8f7f881
--- /dev/null
+++ b/frontend/src/routes/node/list/configuration/index.tsx
@@ -0,0 +1,106 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { useState } from 'react';
+import { Descriptions, Radio, Select, Table, message } from 'antd';
+import { ConfigurationTypeEnum } from '@src/common/common.data';
+import { useHistory } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+
+export function Configuration() {
+ const history = useHistory();
+ const { t } = useTranslation();
+ const [feColumn, setFeColumn] = useState([]);
+
+ const [tableData, setTableData] = useState([]);
+
+ const [configSelect, setConfigSelect] = useState([]);
+
+ const [nodeSelect, setNodeSelect] = useState([]);
+
+ const [selectedRowKeys, setSelectedRowKeys] = useState([]);
+
+ const rowSelection = {};
+
+ function batchEditing() {
+ if (selectedRowKeys.length === 0) {
+ message.warning( t`PleaseCheckTheConfiguration`);
+ }
+ }
+ function onChange(e: any) {
+ if (e.target.value === ConfigurationTypeEnum.BE) {
+ localStorage.setItem("nodeText","");
+ history.push(`/configuration/be`);
+ } else {
+ localStorage.setItem("nodeText","");
+ history.push(`/configuration/fe`);
+ }
+ }
+
+ return (
+ <>
+
+
+ {t`nodeSelection`+":"}
+
+ FE{t`Node`}
+ BE{t`Node`}
+
+
+
+ { t`ConfigurationItem`}:
+
+ {configSelect}
+
+
+
+ { t`Node`}:
+
+ {nodeSelect}
+
+
+
+ {' '}
+
+ { t`BatchEditing`}
+
+
+
+
+
record[Object.keys(record).length - 1]}
+ size="middle"
+ scroll={{ x: 1300 }}
+ pagination={{
+ position: ['bottomCenter'],
+ total: tableData.length,
+ showSizeChanger: true,
+ showQuickJumper: true,
+ showTotal: (total: any) => {t`total`+`${total}` +t`strip`},
+ }}
+ >
+
+ >
+ );
+}
+
diff --git a/frontend/src/routes/node/list/fe-configuration/fe-config.api.ts b/frontend/src/routes/node/list/fe-configuration/fe-config.api.ts
new file mode 100644
index 0000000..28419f8
--- /dev/null
+++ b/frontend/src/routes/node/list/fe-configuration/fe-config.api.ts
@@ -0,0 +1,37 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { http } from '@src/utils/http';
+
+function getConfigSelect() {
+ return http.get(`/api/rest/v2/manager/node/configuration_name`);
+}
+function getNodeSelect() {
+ return http.get(`/api/rest/v2/manager/node/node_list`);
+}
+function getConfigurationsInfo(data: any, type: string) {
+ return http.post(`/api/rest/v2/manager/node/configuration_info?type=${type}`, data);
+}
+function setConfig(data: any) {
+ return http.post(`/api/rest/v2/manager/node/set_config/fe`, data);
+}
+export const FeConfigAPI = {
+ getConfigSelect,
+ getNodeSelect,
+ getConfigurationsInfo,
+ setConfig,
+};
diff --git a/frontend/src/routes/node/list/fe-configuration/index.tsx b/frontend/src/routes/node/list/fe-configuration/index.tsx
new file mode 100644
index 0000000..b5fc695
--- /dev/null
+++ b/frontend/src/routes/node/list/fe-configuration/index.tsx
@@ -0,0 +1,535 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+/* eslint-disable prettier/prettier */
+import React, { useEffect, useState } from 'react';
+import { Descriptions, Radio, Select, Table, Space, Modal, Form, Input, Tooltip, message, Divider } from 'antd';
+import { FallOutlined, InfoCircleOutlined } from '@ant-design/icons';
+const { Option } = Select;
+import { ConfigurationTypeEnum } from '@src/common/common.data';
+import { FeConfigAPI } from './fe-config.api';
+import { useHistory } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+import { FILTERS } from '../config.data'
+
+export function FEConfiguration() {
+ const { t } = useTranslation();
+ const [modal, contextHolder] = Modal.useModal();
+ const history = useHistory();
+ const [feColumn, setFeColumn] = useState([]);
+ const [tableData, setTableData] = useState([]);
+ const [configSelect, setConfigSelect] = useState([]);
+ const [nodeSelect, setNodeSelect] = useState([]);
+ const [confName, setConfName] = useState([]);
+ const [nodeName, setNodeName] = useState([]);
+ const [isModalVisible, setIsModalVisible] = useState(false);
+ const [selectedRowKeys, setSelectedRowKeys] = useState([]);
+ const [selectedRows, setSelectedRows] = useState([]);
+ const [isBatchEdit, setIsBatchEdit] = useState(true);
+ const [rowData, setRowsData] = useState({});
+ const [changeCurrent, setChangeCurrent] = useState();
+ const [nodevalue, setNodevalue] = useState([]);
+ function changeConfig(record: any) {
+ setRowsData(record);
+ setIsBatchEdit(true);
+ setIsModalVisible(true);
+ }
+
+ const [form] = Form.useForm();
+
+ const rowSelection = {
+ selectedRowKeys,
+ onChange: (selectedRowKeys: React.Key[], selectedRows: any) => {
+ console.log(selectedRows)
+ let newSelectedRowKeys = [];
+ const newSelectedRows: any[] = [];
+ newSelectedRowKeys = selectedRowKeys.filter((key, index) => {
+ if (selectedRows[index][Object.keys(selectedRows[index]).length - 2] === 'true') {
+ newSelectedRows.push(selectedRows[index]);
+ return true;
+ }
+ return false;
+ });
+ setSelectedRowKeys(newSelectedRowKeys);
+ setSelectedRows(newSelectedRows);
+ },
+ getCheckboxProps: (record: any) => {
+ const arr = Object.keys(record);
+ return {
+ disabled: record[arr.length - 2] === 'false',
+ key: record[arr.length - 1],
+ };
+ },
+ selections: [
+ Table.SELECTION_ALL,
+ Table.SELECTION_NONE,
+ {
+ text: t`SelectOnlyTheCurrentPage`,
+ onSelect(selectedRowKeys: React.Key[], selectedRows: any) {
+ setSelectedRowKeys(selectedRowKeys)
+ const newSelectedRows: any[] = [];
+ for (let i = 0; i < selectedRowKeys.length; i++) {
+ newSelectedRows.push(tableData[selectedRowKeys[i]])
+ }
+ setSelectedRows(newSelectedRows)
+ },
+ },
+ ],
+ };
+ const [typeValue, setTypeValue] = React.useState(ConfigurationTypeEnum.FE);
+ function onChange(e: any) {
+ setTypeValue(e.target.value);
+ if (e.target.value === ConfigurationTypeEnum.BE) {
+ localStorage.setItem("nodeText","");
+ history.push(`/configuration/be`);
+ } else {
+ localStorage.setItem("nodeText","");
+ history.push(`/configuration/fe`);
+ }
+ }
+ const handleOk = (values: any) => {
+ if (isBatchEdit) {
+ const name = rowData[0];
+ const data = {};
+ data[name] = {
+ node: [rowData[1]],
+ value: values.value,
+ persist: values.persist,
+ };
+
+ FeConfigAPI.setConfig(data).then(res => {
+ if (res.code === 0) {
+ setIsModalVisible(false);
+ getTable()
+ if (res.data.failed && res.data.failed.length !== 0) {
+ warning(res.data.failed)
+ } else {
+ message.success(t`SuccessfullyModified`);
+ }
+ } else {
+ message.error(res.msg);
+ }
+ });
+ } else {
+ let name = "";
+ const data = {};
+ for (let i = 0; i < selectedRows.length; i++) {
+ const nodes: any[] = [];
+ name = selectedRows[i][0]
+ selectedRows.map((item: any[], index: any) => {
+ if (item[0] === selectedRows[i][0]) {
+ nodes.push(item[1])
+ }
+ });
+ data[name] = {
+ node: nodes,
+ value: values.value,
+ persist: values.persist,
+ };
+ }
+ FeConfigAPI.setConfig(data).then(res => {
+ if (res.code === 0) {
+ setIsModalVisible(false);
+ getTable()
+ if (res.data.failed && res.data.failed.length !== 0) {
+ warning(res.data.failed)
+ } else {
+ message.success(t`SuccessfullyModified`);
+ }
+ } else {
+ message.error(res.msg);
+ }
+ });
+ }
+ console.log(values, isBatchEdit, rowData);
+ //setIsModalVisible(false);
+ };
+
+ const handleCancel = () => {
+ setIsModalVisible(false);
+ };
+
+ function getTable() {
+ let data = {};
+ if (confName.length === 0 && nodeName.length !== 0) {
+ data = { type: typeValue, node: nodeName };
+ }
+ if (confName.length !== 0 && nodeName.length === 0) {
+ data = { type: typeValue, conf_name: confName };
+ }
+ if (confName.length === 0 && nodeName.length === 0) {
+ data = { type: typeValue };
+ }
+ if (confName.length !== 0 && nodeName.length !== 0) {
+ data = { type: typeValue, conf_name: confName, node: nodeName };
+ }
+ setTable(data,typeValue)
+ }
+ function refresh() {
+ let data = {};
+ const nodeText = localStorage.getItem("nodeText")
+ if (nodeText !== "") {
+ setNodevalue(nodeText)
+ nodeName.push(nodeText)
+ const node = [];
+ node.push(nodeText)
+ if (confName.length === 0 && node.length !== 0) {
+ data = { type: typeValue, node: node };
+ }
+ if (confName.length !== 0 && node.length === 0) {
+ data = { type: typeValue, conf_name: confName };
+ }
+ if (confName.length === 0 && node.length === 0) {
+ data = { type: typeValue };
+ }
+ if (confName.length !== 0 && node.length !== 0) {
+ data = { type: typeValue, conf_name: confName, node: node };
+ }
+ } else {
+ if (confName.length === 0 && nodeName.length !== 0) {
+ data = { type: typeValue, node: nodeName };
+ }
+ if (confName.length !== 0 && nodeName.length === 0) {
+ data = { type: typeValue, conf_name: confName };
+ }
+ if (confName.length === 0 && nodeName.length === 0) {
+ data = { type: typeValue };
+ }
+ if (confName.length !== 0 && nodeName.length !== 0) {
+ data = { type: typeValue, conf_name: confName, node: nodeName };
+ }
+ }
+
+ setTable(data,typeValue)
+
+ FeConfigAPI.getConfigSelect().then((res: { data: any; code: any; msg: any }) => {
+ if (res.code === 0) {
+ const { data, code, msg } = res;
+ console.log(data);
+ const { backend, frontend } = data;
+ const configSelect = frontend.map((item: string, index: number) => {
+ return (
+
+ {item}
+
+ );
+ });
+ setConfigSelect(configSelect);
+ } else {
+ message.warning(res.msg);
+ }
+ });
+ FeConfigAPI.getNodeSelect().then((res: { data: any; code: any; msg: any }) => {
+ if (res.code === 0) {
+ const { data, code, msg } = res;
+ const { backend, frontend } = data;
+ const nodeSelect = frontend.map((item: string, index: number) => {
+ return (
+
+ {item}
+
+ );
+ });
+ setNodeSelect(nodeSelect);
+ } else {
+ message.warning(res.msg);
+ }
+ });
+ }
+
+ function batchEditing() {
+ if (selectedRowKeys.length === 0) {
+ message.warning(t`PleaseCheckTheConfigurationYouWantToModify`);
+ } else {
+ setIsModalVisible(true);
+ setIsBatchEdit(false);
+ }
+ }
+ useEffect(() => {
+ refresh();
+ }, []);
+
+ function configSelectChange(_value: any) {
+ setSelectedRowKeys([])
+ setSelectedRows([])
+ const newArray = _value.map(x => x.trim())
+ setConfName(newArray);
+ let data = {};
+ if (newArray.length === 0 && nodeName.length !== 0) {
+ data = { type: typeValue, node: nodeName };
+ }
+ if (newArray.length !== 0 && nodeName.length === 0) {
+ data = { type: typeValue, conf_name: newArray };
+ }
+ if (newArray.length === 0 && nodeName.length === 0) {
+ data = { type: typeValue };
+ }
+ if (newArray.length !== 0 && nodeName.length !== 0) {
+ data = { conf_name: newArray, node: nodeName };
+ }
+ setTable(data,typeValue)
+ }
+
+ function setTable(data: any,typeValue: any) {
+ FeConfigAPI.getConfigurationsInfo(data, typeValue).then(res => {
+ if (res.code == 0) {
+ const { column_names, rows } = res.data;
+ const columns = column_names.map((item: string, index: number) => {
+ if (item === '可修改') {
+ return {
+ title: t`operate`,
+ dataIndex: item,
+ render: (text: any, record: any, index: any) => (
+
+ {record[Object.keys(record).length - 2] === 'true' ? (
+ changeConfig(record)}>{ t`edit`}
+ ) : (
+ <>>
+ )}
+
+ ),
+ fixed: 'right',
+ width: 100,
+ };
+ }
+ if (item === 'MasterOnly') {
+ return {
+ title: item,
+ dataIndex: `${index}`,
+ key: item,
+ columnWidth: 10,
+ filters: FILTERS,
+ onFilter: (value: any, record: any) => onFilterCols(value,record),
+ };
+ }
+ else {
+ return {
+ title: item,
+ dataIndex: index,
+ columnWidth: 10,
+ };
+ }
+ });
+ setFeColumn(columns);
+
+ for (let i = 0; i < rows.length; i++) {
+ rows[i].push('' + i + '');
+ }
+ setTableData(rows.map((item: string[]) => ({ ...item })));
+ if (data.conf_name) {
+ setChangeCurrent(1)
+ }
+ } else {
+ message.warning(res.msg);
+ }
+ });
+ }
+
+ function onFilterCols(value: any, record: any) {
+ return record[4].indexOf(value) === 0
+ }
+
+ function nodeSelectChange(value: any) {
+ setSelectedRowKeys([])
+ setSelectedRows([])
+ if (value.length === 0) {
+ setNodevalue([])
+ } else {
+ setNodevalue(value)
+ }
+
+ localStorage.setItem("nodeText", "");
+ const newArray = value.map(x => x.trim())
+ setNodeName(newArray);
+ let data = {};
+ if (confName.length === 0 && newArray.length !== 0) {
+ data = { type: typeValue, node: newArray };
+ }
+ if (confName.length !== 0 && newArray.length === 0) {
+ data = { type: typeValue, conf_name: confName };
+ }
+ if (confName.length === 0 && newArray.length === 0) {
+ data = { type: typeValue };
+ }
+ if (confName.length !== 0 && newArray.length !== 0) {
+ data = { type: typeValue, conf_name: confName, node: newArray };
+ }
+ setTable(data,typeValue)
+ }
+
+ function pageSizeChange(current: React.SetStateAction, pageSize: number | undefined) {
+ setChangeCurrent(current)
+ }
+ const [configvalue, setConfigvalue] = React.useState([]);
+ const configSelectProps = {
+ mode: 'multiple' as const,
+ style: { width: '100%' },
+ configvalue,
+ onChange: (newValue: string[]) => {
+ setConfigvalue(newValue);
+ },
+ maxTagCount: 'responsive' as const,
+ };
+ const nodeSelectProps = {
+ mode: 'multiple' as const,
+ style: { width: '100%' },
+ nodevalue,
+ onChange: (newValue: string[]) => {
+ setNodevalue(newValue);
+ },
+ maxTagCount: 'responsive' as const,
+ };
+ return (
+
+
+
+ {t`nodeSelection`}:
+
+ FE节点
+ BE节点
+
+
+
+ { t`ConfigurationItem`}:
+
+ { configSelect}
+
+
+
+ { t`Node`}:
+
+ {nodeSelect}
+
+
+
+
+
+
+ { t`CurrentlySelected`} {selectedRowKeys.length} {t`StripData`}
+
+
+
+
+ {t`BatchEditing`}
+
+
+
+
+
record[Object.keys(record).length - 1]}
+ size="middle"
+ scroll={{ x: 1300 }}
+ pagination={{
+ position: ['bottomCenter'],
+ total: feColumn,
+ current:changeCurrent,
+ showSizeChanger: true,
+ showQuickJumper: true,
+ onChange: (current, pageSize) => pageSizeChange(current, pageSize),
+ showTotal: (total: any) => {t`total`+`${total}` +t`strip`},
+ }}
+ >
+
+
{
+ form.validateFields()
+ .then((values: any) => {
+ form.resetFields();
+ handleOk(values);
+ })
+ .catch((info: any) => {
+ console.log('Validate Failed:', info);
+ });
+ }}
+ >
+
+
+
+
+
+
+ {t`TemporarilyEffective`}
+
+
+
+
+
+ { t`Permanent`}
+
+
+
+
+
+
+
+
+
+ );
+ function warning(failed: []) {
+ const arr = failed.map((failedMsg:any) => {
+ return <>{failedMsg.node} { t`Node`} {failedMsg.config_name} {t`Error`+":"} {failedMsg.err_info} >
+ })
+ Modal.error({
+ content: <>
+
+ { t`ConfigurationError`}
+
+ {arr}
+
+ >
+ ,
+ title: { t`FailToEdit`} ,
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
+ onOk() { },
+ bodyStyle: { height: '250px' },
+ width: '520px',
+ closable:true
+
+ });
+ }
+}
+
+
diff --git a/frontend/src/routes/node/list/index.tsx b/frontend/src/routes/node/list/index.tsx
new file mode 100644
index 0000000..25b9eea
--- /dev/null
+++ b/frontend/src/routes/node/list/index.tsx
@@ -0,0 +1,190 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+/* eslint-disable prettier/prettier */
+import React, { useEffect, useState } from 'react';
+import { Table, Space, message } from 'antd';
+import { NodeAPI } from './node.api';
+import { useHistory } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+export function NodeList() {
+ const history = useHistory();
+ const { t } = useTranslation();
+ const [feColumns, setFeColumns] = useState([]);
+ const [beColumns, setBeColumns] = useState([]);
+ const [brokersColumns, setBrokersColumns] = useState([]);
+ const [beTableData, setBeTableData] = useState([]);
+ const [feTableData, setFeTableData] = useState([]);
+ const [brokersTableData, setBrokersTableData] = useState([]);
+ function refresh() {
+ NodeAPI.getFrontends().then(res => {
+ const { msg, data, code } = res;
+ const { column_names, rows } = data;
+ if (code === 0) {
+ const column = column_names.map((item: string, index: number) => {
+ if (index === 0) {
+ return {
+ title: 'FE '+t`Node`,
+ dataIndex: index,
+ key: item,
+ width: 200,
+ };
+ } else {
+ return {
+ title: item,
+ dataIndex: index,
+ key: item,
+ width:200,
+ };
+ }
+ });
+ column.push({
+ title: 'CONF',
+ key: 'action',
+ render: (text: any, record: any, index: any) => (
+
+ toFeDetailsPage(record)}>{t`Details`}
+
+ ),
+ fixed: 'right',
+ width:100,
+ });
+ setFeColumns(column);
+ setFeTableData(rows.map((item: string[]) => ({ ...item })));
+ } else {
+ message.error(msg);
+ }
+ });
+ NodeAPI.getBrokers().then(res => {
+ const { msg, data, code } = res;
+ const { column_names, rows } = data;
+ if (code === 0) {
+ const column = column_names.map((item: string, index: number) => {
+ if (index === 0) {
+ return {
+ title: 'Brokers',
+ dataIndex: index,
+ key: item,
+ width: 200,
+ };
+ } else {
+ return {
+ title: item,
+ dataIndex: index,
+ key: item,
+ width:200,
+ };
+ }
+ });
+ setBrokersColumns(column);
+ setBrokersTableData(rows.map((item: string[]) => ({ ...item })));
+ } else {
+ message.error(msg);
+ }
+ });
+ NodeAPI.getBackends().then(res => {
+ const { msg, data, code } = res;
+ const { column_names, rows } = data;
+ if (code === 0) {
+ const column = column_names.map((item: string, index: number) => {
+ if (index === 0) {
+ return {
+ title: 'BE '+t`Node`,
+ dataIndex: index,
+ key: item,
+ width: 200,
+ };
+ } else {
+ return {
+ title: item,
+ dataIndex: index,
+ key: item,
+ width:200,
+ };
+ }
+ }
+ );
+ column.push({
+ title: 'CONF',
+ key: 'CONF',
+ render: (text: any, record: any, index: any) => (
+
+ toBeDetailsPage(record)} >{ t`Details`}
+
+ ),
+ fixed: 'right',
+ width:100,
+ });
+ setBeColumns(column);
+ setBeTableData(rows.map((item: string[]) => ({ ...item })));
+ } else {
+ message.error(msg);
+ }
+ });
+ }
+ useEffect(() => {
+ refresh();
+ }, []);
+
+ function toBeDetailsPage(record: any) {
+ let node = "";
+ node += record[2]+":"+record[6]
+ localStorage.setItem("nodeText", node);
+ history.push(`/configuration/be`)
+ }
+
+ function toFeDetailsPage(record: any) {
+ let node = "";
+ node += record[1]+":"+record[4]
+ localStorage.setItem("nodeText", node);
+ history.push(`/configuration/fe`) ;
+ }
+ return (
+ <>
+
+
+
+ >
+ );
+}
diff --git a/frontend/src/routes/node/list/node.api.ts b/frontend/src/routes/node/list/node.api.ts
new file mode 100644
index 0000000..4241e6b
--- /dev/null
+++ b/frontend/src/routes/node/list/node.api.ts
@@ -0,0 +1,33 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { http } from '@src/utils/http';
+function getFrontends() {
+ return http.get(`/api/rest/v2/manager/node/frontends`);
+}
+function getBrokers() {
+ return http.get(`/api/rest/v2/manager/node/brokers`);
+}
+function getBackends() {
+ return http.get(`/api/rest/v2/manager/node/backends`);
+}
+
+export const NodeAPI = {
+ getFrontends,
+ getBrokers,
+ getBackends,
+};
diff --git a/frontend/src/routes/passport/forgot.login.tsx b/frontend/src/routes/passport/forgot.login.tsx
new file mode 100644
index 0000000..4393c46
--- /dev/null
+++ b/frontend/src/routes/passport/forgot.login.tsx
@@ -0,0 +1,37 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+/** @format */
+import Link from 'antd/lib/typography/Link';
+import React, { useState } from 'react';
+import styles from './index.module.less';
+
+function ForgotLogin(props: any) {
+ return (
+
+
+
Please contact an administrator to have them reset your password
+
+ props.history.push(`/login`)}>
+ Back to login
+
+
+
+ );
+}
+
+export default ForgotLogin;
diff --git a/frontend/src/routes/passport/index.module.less b/frontend/src/routes/passport/index.module.less
new file mode 100644
index 0000000..7f591fa
--- /dev/null
+++ b/frontend/src/routes/passport/index.module.less
@@ -0,0 +1,44 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+.login-container {
+ width: 420px;
+ padding: 32px;
+ margin-top: 32px;
+ line-height: 24px;
+ background-color: #fff;
+ border: 1px solid rgb(215 211 212);
+ border-radius: 6px;
+ box-shadow: rgb(0 0 0 / 8%) 0 7px 20px;
+ transition: all 0.2s linear 0s;
+
+ input {
+ width: 100%;
+ padding: 0.75em;
+ border-radius: 4px;
+ }
+}
+
+.not-found {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ background: url('../../assets/background.jpg');
+ background-size: cover;
+}
diff --git a/frontend/src/routes/passport/login.tsx b/frontend/src/routes/passport/login.tsx
new file mode 100644
index 0000000..c3aabc6
--- /dev/null
+++ b/frontend/src/routes/passport/login.tsx
@@ -0,0 +1,83 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { useEffect, useState } from 'react';
+import { Form, Input, Button, Radio, Checkbox, message } from 'antd';
+import styles from './index.module.less';
+import { PassportAPI } from './passport.api';
+import { useTranslation } from 'react-i18next';
+import { useHistory } from 'react-router-dom';
+import { useAuth } from '@src/hooks/use-auth';
+export function Login(props: any) {
+ const [form] = Form.useForm();
+ const { t } = useTranslation();
+ const history = useHistory();
+ const {initialized} = useAuth();
+ function handleLogin(_value: any) {
+ PassportAPI.SessionLogin(_value).then(res => {
+ if (res.code === 0) {
+ PassportAPI.getCurrentUser().then(user => {
+ window.localStorage.setItem('login', 'true')
+ window.localStorage.setItem('user', JSON.stringify(user.data));
+ history.push('/space/list');
+ })
+ } else {
+ message.warn(res.msg);
+ }
+ });
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+ 记住我
+
+
+
+ { t`SignIn`}
+
+
+ {/*
+ props.history.push(`/forgot`)}
+ >
+ { t`ForgetThePassword`}
+
+ */}
+
+
+
+ );
+};
diff --git a/frontend/src/routes/passport/passport.api.ts b/frontend/src/routes/passport/passport.api.ts
new file mode 100644
index 0000000..4537343
--- /dev/null
+++ b/frontend/src/routes/passport/passport.api.ts
@@ -0,0 +1,30 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+/** @format */
+
+import { http } from '@src/utils/http';
+function SessionLogin(_value: any) {
+ return http.post(`/api/session/`, _value);
+}
+function getCurrentUser() {
+ return http.get(`/api/user/current`);
+}
+export const PassportAPI = {
+ SessionLogin,
+ getCurrentUser,
+};
diff --git a/frontend/src/routes/query/index.tsx b/frontend/src/routes/query/index.tsx
new file mode 100644
index 0000000..0c36147
--- /dev/null
+++ b/frontend/src/routes/query/index.tsx
@@ -0,0 +1,147 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+/* eslint-disable prettier/prettier */
+import React, { useState, useEffect } from 'react';
+import { Table, Card, Space, Input } from 'antd';
+import { useTranslation } from 'react-i18next';
+import { getQueryInfo } from './query.api';
+import { ColumnsType } from 'antd/es/table';
+import { FILTERS } from './query.data';
+import { useHistory } from 'react-router-dom';
+import { ClippedText } from '@src/components/clipped-text/clipped-text';
+const { Search } = Input;
+
+interface ICol {
+ [propName: string]: any;
+}
+
+export function Query() {
+ const { t } = useTranslation();
+ const history = useHistory();
+ const [tableData, setTableData] = useState([]);
+ const [tableColumn, setTableColumn] = useState>([]);
+
+ function queryDetails(record: any) {
+ console.log(record)
+ history.push(`/details/${record[0]}`)
+ }
+ useEffect(() => {
+ getQueryInfo().then(res => {
+ const { column_names, rows } = res.data;
+ handleResult(column_names, rows);
+ });
+ }, []);
+ const onSearch = (value: string) => {
+ let newValue = encodeURI(value)
+ getQueryInfo({
+ search: newValue,
+ }).then(res => {
+ const { column_names, rows } = res.data;
+ handleResult(column_names, rows);
+ });
+ };
+
+ const handleResult = (column_names: string[], rows: [][]) => {
+ const columns = column_names.map((item: string, index: number) => {
+ if (item === 'Query ID') {
+ return {
+ title: item,
+ dataIndex: `${index}`,
+ key: item,
+ columnWidth: 10,
+ render: (text: any, record: any, index: any) => (
+
+
+ queryDetails(record)}>{text}
+
+
+
+ ),
+ };
+ }
+ if (item === '状态') {
+ return {
+ title: item,
+ dataIndex: `${index}`,
+ key: item,
+ columnWidth: 10,
+ filters: FILTERS,
+ onFilter: (value: any, record: any) => record[index].indexOf(value) === 0,
+ };
+ }
+ if (item === 'FE节点') {
+ return {
+ title: item,
+ dataIndex: index,
+ key: item,
+ width: 150,
+ ellipsis: true,
+ };
+ }
+ if (item === '开始时间') {
+ return {
+ title: item,
+ dataIndex: index,
+ key: item,
+ width: 150,
+ ellipsis: true,
+ };
+ }
+ if (item === '结束时间') {
+ return {
+ title: item,
+ dataIndex: index,
+ key: item,
+ width: 150,
+ ellipsis: true,
+ };
+ }
+ return {
+ title: item,
+ dataIndex: index,
+ key: item,
+ columnWidth: 10,
+ ellipsis: true,
+ };
+ });
+ setTableColumn(columns);
+ setTableData(rows.map((item: string[]) => ({ ...item })));
+ };
+
+ return (
+ <>
+ }
+ >
+
+ columns={tableColumn}
+ dataSource={tableData}
+ bordered
+ scroll={{ x: 1300 }}
+ size="middle"
+ pagination={{
+ pageSizeOptions: ['10', '20', '50'],
+ }}
+ locale={{emptyText: t`noDate` }}
+ >
+
+ >
+ );
+}
diff --git a/frontend/src/routes/query/query-details/code.css b/frontend/src/routes/query/query-details/code.css
new file mode 100644
index 0000000..844b528
--- /dev/null
+++ b/frontend/src/routes/query/query-details/code.css
@@ -0,0 +1,144 @@
+
+/*!
+ Theme: GitHub
+ Description: Light theme as seen on github.com
+ Author: github.com
+ Maintainer: @Hirse
+ Updated: 2021-05-15
+
+ Outdated base version: https://github.com/primer/github-syntax-light
+ Current colors taken from GitHub's CSS
+
+ // Licensed to the Apache Software Foundation (ASF) under one
+ // or more contributor license agreements. See the NOTICE file
+ // distributed with this work for additional information
+ // regarding copyright ownership. The ASF licenses this file
+ // to you under the Apache License, Version 2.0 (the
+ // "License"); you may not use this file except in compliance
+ // with the License. You may obtain a copy of the License at
+ //
+ // http://www.apache.org/licenses/LICENSE-2.0
+ //
+ // Unless required by applicable law or agreed to in writing,
+ // software distributed under the License is distributed on an
+ // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ // KIND, either express or implied. See the License for the
+ // specific language governing permissions and limitations
+ // under the License.
+*/
+
+.hljs {
+ color: #24292e;
+ background: #ffffff;
+ }
+
+ .hljs-doctag,
+ .hljs-keyword,
+ .hljs-meta .hljs-keyword,
+ .hljs-template-tag,
+ .hljs-template-variable,
+ .hljs-type,
+ .hljs-variable.language_ {
+ /* prettylights-syntax-keyword */
+ color: #d73a49;
+ }
+
+ .hljs-title,
+ .hljs-title.class_,
+ .hljs-title.class_.inherited__,
+ .hljs-title.function_ {
+ /* prettylights-syntax-entity */
+ color: #6f42c1;
+ }
+
+ .hljs-attr,
+ .hljs-attribute,
+ .hljs-literal,
+ .hljs-meta,
+ .hljs-number,
+ .hljs-operator,
+ .hljs-variable,
+ .hljs-selector-attr,
+ .hljs-selector-class,
+ .hljs-selector-id {
+ /* prettylights-syntax-constant */
+ color: #005cc5;
+ }
+
+ .hljs-regexp,
+ .hljs-string,
+ .hljs-meta .hljs-string {
+ /* prettylights-syntax-string */
+ color: #032f62;
+ }
+
+ .hljs-built_in,
+ .hljs-symbol {
+ /* prettylights-syntax-variable */
+ color: #e36209;
+ }
+
+ .hljs-comment,
+ .hljs-code,
+ .hljs-formula {
+ /* prettylights-syntax-comment */
+ color: #6a737d;
+ }
+
+ .hljs-name,
+ .hljs-quote,
+ .hljs-selector-tag,
+ .hljs-selector-pseudo {
+ /* prettylights-syntax-entity-tag */
+ color: #22863a;
+ }
+
+ .hljs-subst {
+ /* prettylights-syntax-storage-modifier-import */
+ color: #24292e;
+ }
+
+ .hljs-section {
+ /* prettylights-syntax-markup-heading */
+ color: #005cc5;
+ font-weight: bold;
+ }
+
+ .hljs-bullet {
+ /* prettylights-syntax-markup-list */
+ color: #735c0f;
+ }
+
+ .hljs-emphasis {
+ /* prettylights-syntax-markup-italic */
+ color: #24292e;
+ font-style: italic;
+ }
+
+ .hljs-strong {
+ /* prettylights-syntax-markup-bold */
+ color: #24292e;
+ font-weight: bold;
+ }
+
+ .hljs-addition {
+ /* prettylights-syntax-markup-inserted */
+ color: #22863a;
+ background-color: #f0fff4;
+ }
+
+ .hljs-deletion {
+ /* prettylights-syntax-markup-deleted */
+ color: #b31d28;
+ background-color: #ffeef0;
+ }
+
+ .hljs-char.escape_,
+ .hljs-link,
+ .hljs-params,
+ .hljs-property,
+ .hljs-punctuation,
+ .hljs-tag {
+ /* purposely ignored */
+ }
+
\ No newline at end of file
diff --git a/frontend/src/routes/query/query-details/index.tsx b/frontend/src/routes/query/query-details/index.tsx
new file mode 100644
index 0000000..56d45dc
--- /dev/null
+++ b/frontend/src/routes/query/query-details/index.tsx
@@ -0,0 +1,122 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { useState, useEffect, useCallback } from 'react';
+import { useParams } from 'react-router-dom';
+import { Table, Card } from 'antd';
+import { getSQLText, getProfileText, getQueryInfo } from './../query.api';
+import Profile from './profile';
+import styles from './query.module.less';
+import { ColumnsType } from 'antd/es/table';
+import { ClippedText } from '@src/components/clipped-text/clipped-text';
+import hljs from 'highlight.js'
+import './code.css';
+type tabType = 'sql' | 'text' | 'profile';
+interface ICol {
+ [propName: string]: any;
+}
+export function QueryDetails() {
+ const [currentTab, setCurrentTab] = useState('sql');
+ const [currentSQL, setCurrentSQL] = useState('sql');
+ const [currentText, setCurrentText] = useState('sql');
+ const [tableData, setTableData] = useState([]);
+ const [tableColumn, setTableColumn] = useState>([]);
+ const params = useParams<{ queryId: string }>();
+ const queryId = params.queryId;
+ const tabListNoTitle = [
+ {
+ key: 'sql',
+ tab: 'SQL语句',
+ },
+ {
+ key: 'text',
+ tab: 'Text',
+ },
+ {
+ key: 'profile',
+ tab: 'Visualization',
+ },
+ ];
+
+ const contentListNoTitle = {
+ sql: (
+
+ ),
+ text: (
+
+ ),
+ profile: ,
+ };
+
+ useEffect(() => {
+ getQueryInfo({ query_id: queryId }).then(res => {
+ const { column_names, rows } = res.data;
+ const columns = column_names.map((item: string, index: number) => {
+ if (item === 'Query ID') {
+ return {
+ title: item,
+ dataIndex: `${index}`,
+ key: item,
+ columnWidth: 10,
+ render: (text: any, record: any, index: any) => (
+ {text}
+ ),
+ };
+ }
+ return {
+ title: item,
+ dataIndex: index,
+ key: item,
+ columnWidth: '10px',
+ ellipsis: true,
+ };
+ });
+ setTableColumn(columns);
+ setTableData(rows.map((item: string[]) => ({ ...item })));
+ });
+ }, []);
+ useEffect(() => {
+ getSQLText({ queryId }).then(res => {
+ setCurrentSQL(res.data?.sql || '--');
+ });
+ getProfileText({ queryId }).then(res => {
+ setCurrentText(res.data?.profile || '--');
+ });
+ }, []);
+
+ return (
+ <>
+
+
+
+ {
+ setCurrentTab(key as tabType);
+ }}
+ >
+ {contentListNoTitle[currentTab]}
+
+ >
+ );
+}
diff --git a/frontend/src/routes/query/query-details/profile.tsx b/frontend/src/routes/query/query-details/profile.tsx
new file mode 100644
index 0000000..de32091
--- /dev/null
+++ b/frontend/src/routes/query/query-details/profile.tsx
@@ -0,0 +1,93 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+/* eslint-disable prettier/prettier */
+import React, { FC, useState, useEffect, useCallback } from 'react';
+import { useParams } from 'react-router-dom';
+import { Tree, Tag, Space, Card, Tabs } from 'antd';
+import { getProfileFragments, getProfileGraph } from '../query.api';
+import styles from './query.module.less'
+
+interface IFragment {
+ fragment_id: string;
+ instance_id: string[];
+ time: string;
+}
+const Profile: FC = () => {
+ const [value, setValue] = useState('');
+ const [treeData, setTreeData] = useState();
+ const params = useParams<{ queryId: string }>();
+ const queryId = params.queryId;
+
+ useEffect(() => {
+ getProfileFragments({ queryId }).then(res => {
+ const temp = res.data.map((item: IFragment) => ({
+
+ title: `Fragment${item.fragment_id} -- ${item.time}`,
+ key: item.fragment_id,
+ disabled: true,
+ children: Object.keys(item.instance_id).map(instance => ({
+ title: `${instance} -- ${item.instance_id[instance]}` ,
+ key: instance,
+ isLeaf: true,
+ parent: item.fragment_id,
+ })),
+ }));
+ setTreeData(temp);
+ });
+ getProfileGraph({
+ queryId,
+ }).then(res => {
+ setValue(res.data.graph);
+ });
+ }, []);
+
+ const treeChange = (keys: React.Key[], info: any) => {
+ setValue('');
+ const { key, parent } = info.node;
+ getProfileGraph({
+ queryId,
+ fragmentId: parent,
+ instanceId: key,
+ }).then(res => {
+ setValue(res.data.graph);
+ });
+ };
+ function overview() {
+ getProfileGraph({
+ queryId,
+ }).then(res => {
+ setValue(res.data.graph);
+ });
+ }
+
+ return (
+
+ );
+};
+
+export default Profile;
diff --git a/frontend/src/routes/query/query-details/query.module.less b/frontend/src/routes/query/query-details/query.module.less
new file mode 100644
index 0000000..61eb866
--- /dev/null
+++ b/frontend/src/routes/query/query-details/query.module.less
@@ -0,0 +1,56 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+.textBox {
+ width: 100%;
+ min-height: 500px;
+ max-height: 70vh;
+ overflow-y: scroll;
+ border: 1px solid #ddd;
+ }
+
+.profileBox {
+ display: flex;
+ :global {
+ .ant-tree .ant-tree-treenode-disabled .ant-tree-node-content-wrapper{
+ color: #666 !important;
+ }
+ }
+
+ .fragment {
+ flex-shrink: 0;
+ width: 270px;
+ height: 70vh;
+ overflow: scroll;
+ border: 1px solid #ddd;
+ }
+
+ .graph {
+ flex: 1;
+ max-height: 70vh;
+ overflow-y: scroll;
+ font-size: 14px;
+ color: #fff;
+ background-color: #000;
+ }
+}
+
+:global {
+ .ant-tree .ant-tree-treenode-disabled .ant-tree-node-content-wrapper{
+ color: #666 !important;
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/routes/query/query.api.ts b/frontend/src/routes/query/query.api.ts
new file mode 100644
index 0000000..49ba9f3
--- /dev/null
+++ b/frontend/src/routes/query/query.api.ts
@@ -0,0 +1,49 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+/** @format */
+
+import { http } from '@src/utils/http';
+import { IResult } from '@src/interfaces/http.interface';
+
+// 查询主页
+export function getQueryInfo(data?: any): Promise> {
+ return http.get(`/api/rest/v2/manager/query/query_info`, data);
+}
+
+// sql语句
+export function getSQLText(data?: any): Promise> {
+ return http.get(`/api/rest/v2/manager/query/sql/${data.queryId}`, {}, { responseType: 'json' });
+}
+
+// profile text
+export function getProfileText(data?: any): Promise> {
+ return http.get(`/api/rest/v2/manager/query/profile/text/${data.queryId}`);
+}
+
+// profile fragments
+export function getProfileFragments(data?: any): Promise> {
+ return http.get(`/api/rest/v2/manager/query/profile/fragments/${data.queryId}`);
+}
+
+// profile graph
+export function getProfileGraph(data?: any): Promise> {
+ return http.get(`/api/rest/v2/manager/query/profile/graph/${data.queryId}`, {
+ fragment_id: data.fragmentId,
+ instance_id: data.instanceId,
+ });
+}
diff --git a/frontend/src/routes/query/query.data.ts b/frontend/src/routes/query/query.data.ts
new file mode 100644
index 0000000..d613380
--- /dev/null
+++ b/frontend/src/routes/query/query.data.ts
@@ -0,0 +1,28 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+export enum STATUS_MAP {
+ RUNNING = 'RUNNING',
+ EOF = 'EOF',
+ ERR = 'ERR',
+}
+
+export const FILTERS = [
+ { text: STATUS_MAP.RUNNING, value: STATUS_MAP.RUNNING },
+ { text: STATUS_MAP.EOF, value: STATUS_MAP.EOF },
+ { text: STATUS_MAP.ERR, value: STATUS_MAP.ERR },
+];
diff --git a/frontend/src/routes/settings/components/settings-header/settings-header.less b/frontend/src/routes/settings/components/settings-header/settings-header.less
new file mode 100644
index 0000000..24b55c7
--- /dev/null
+++ b/frontend/src/routes/settings/components/settings-header/settings-header.less
@@ -0,0 +1,53 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+.admin-header {
+ display: flex;
+ width: 100%;
+ background-color: #001529;
+}
+.admin-header-logo {
+ color: white;
+ width: 240px;
+ padding-left: 40px;
+ line-height: 48px;
+ cursor: pointer;
+}
+.palo-opt-box{
+ display: flex;
+ height: 50px;
+ align-items: center;
+ div{
+ margin-right: 16px;
+ padding: 10px;
+ cursor: pointer;
+ .icon{
+ font-size: 20px;
+ }
+ .icon-tip{
+ margin-left: 0.5rem;
+ vertical-align: text-bottom;
+ }
+ }
+ div:hover{
+ border-radius: 8px;
+ background: rgb(32,78,171);
+ }
+ div:last-child{
+ margin-right: 0;
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/routes/settings/components/settings-header/settings-header.tsx b/frontend/src/routes/settings/components/settings-header/settings-header.tsx
new file mode 100644
index 0000000..0ec08c2
--- /dev/null
+++ b/frontend/src/routes/settings/components/settings-header/settings-header.tsx
@@ -0,0 +1,72 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { SettingOutlined } from '@ant-design/icons';
+import { UserInfoContext } from '@src/common/common.context';
+import { Menu, Space } from 'antd';
+import React, { useContext, useState } from 'react';
+import { useHistory, useRouteMatch } from 'react-router';
+import { SettingsIcon } from '../../../../components/settings-icon/settings-icon';
+import styles from './settings-header.less';
+
+export function SettingsHeader(props: any) {
+ const history = useHistory();
+ const match = useRouteMatch();
+ const [current, setCurrent] = useState(() => {
+ let tab = 'user';
+ ['global', 'user'].map(key => {
+ if (history.location.pathname.includes(key)) {
+ tab = key;
+ }
+ });
+ return tab;
+ });
+ const userInfo = useContext(UserInfoContext);
+ function handleClick(e) {
+ setCurrent(e.key);
+ if (e.key === 'space') {
+ history.push(`${match.path}/space/${userInfo.space_id}`);
+ return;
+ }
+ history.push(`${match.path}/${e.key}`);
+ }
+ return (
+
+
{
+ history.push(`/space`);
+ }}>
+
+
+ Palo Studio平台管理
+
+
+
+
+ 用户
+
+
+ 平台设置
+
+
+
+
+ );
+}
diff --git a/frontend/src/routes/settings/components/tabs-header/index.tsx b/frontend/src/routes/settings/components/tabs-header/index.tsx
new file mode 100644
index 0000000..d9bbe05
--- /dev/null
+++ b/frontend/src/routes/settings/components/tabs-header/index.tsx
@@ -0,0 +1,40 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React from 'react';
+import { useLocation, useHistory } from 'react-router-dom';
+import { Tabs } from 'antd';
+import { useTranslation } from 'react-i18next';
+
+const { TabPane } = Tabs;
+
+export default function TabsHeader() {
+ const { pathname } = useLocation();
+ const history = useHistory();
+
+ const {t} = useTranslation()
+
+ const handleTabChange = (key: string) => {
+ history.replace(key);
+ };
+
+ return (
+
+
+
+ );
+}
diff --git a/frontend/src/routes/settings/global/components/loading-layout/index.tsx b/frontend/src/routes/settings/global/components/loading-layout/index.tsx
new file mode 100644
index 0000000..9866994
--- /dev/null
+++ b/frontend/src/routes/settings/global/components/loading-layout/index.tsx
@@ -0,0 +1,40 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { CSSProperties, PropsWithChildren } from 'react';
+import { Spin } from 'antd';
+
+interface LoadingLayoutProps {
+ loading?: boolean;
+ wrapperStyle?: CSSProperties;
+ tip?: string;
+}
+
+export default function LoadingLayout(props: PropsWithChildren) {
+ const { loading = false, wrapperStyle = {}, children, tip } = props;
+ return (
+
+ {loading ? (
+
+
+
+ ) : (
+ children
+ )}
+
+ );
+}
diff --git a/frontend/src/routes/settings/global/components/setting-item-layout/index.tsx b/frontend/src/routes/settings/global/components/setting-item-layout/index.tsx
new file mode 100644
index 0000000..121e82b
--- /dev/null
+++ b/frontend/src/routes/settings/global/components/setting-item-layout/index.tsx
@@ -0,0 +1,40 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { Typography, Row } from 'antd';
+import React, { PropsWithChildren } from 'react';
+
+interface SettingItemLayoutProps {
+ title?: string;
+ description?: string;
+}
+
+export default function SettingItemLayout(props: PropsWithChildren) {
+ const { title = '', description = '', children } = props;
+
+ return (
+
+ {title && (
+
+ {title}
+
+ )}
+ {description && {description} }
+ {children}
+
+ );
+}
diff --git a/frontend/src/routes/settings/global/components/sidebar/index.tsx b/frontend/src/routes/settings/global/components/sidebar/index.tsx
new file mode 100644
index 0000000..34b0cc7
--- /dev/null
+++ b/frontend/src/routes/settings/global/components/sidebar/index.tsx
@@ -0,0 +1,52 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { useContext } from 'react';
+import { Menu } from 'antd';
+import { useHistory, useLocation, useRouteMatch } from 'react-router';
+import { UserInfoContext } from '@src/common/common.context';
+import { getGlobalRoutes } from '../../global.utils';
+
+interface MenuInfo {
+ key: string;
+}
+
+export default function Sidebar() {
+ const userInfo = useContext(UserInfoContext);
+ const location = useLocation();
+ const match = useRouteMatch();
+ const history = useHistory();
+
+ const handleClick = (menuInfo: MenuInfo) => {
+ history.replace(menuInfo.key);
+ };
+
+ const globalRoutes = getGlobalRoutes(userInfo.authType === 'ldap');
+
+ return (
+
+ {globalRoutes.map(route => (
+ {route.label}
+ ))}
+
+ );
+}
diff --git a/frontend/src/routes/settings/global/constants.ts b/frontend/src/routes/settings/global/constants.ts
new file mode 100644
index 0000000..a60cc0b
--- /dev/null
+++ b/frontend/src/routes/settings/global/constants.ts
@@ -0,0 +1,26 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { CSSProperties } from 'react';
+
+export const LOADING_WRAPPER_STYLE: CSSProperties = {
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ width: 600,
+ height: 600,
+};
diff --git a/frontend/src/routes/settings/global/context/global-settings-context.tsx b/frontend/src/routes/settings/global/context/global-settings-context.tsx
new file mode 100644
index 0000000..b3769c8
--- /dev/null
+++ b/frontend/src/routes/settings/global/context/global-settings-context.tsx
@@ -0,0 +1,72 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { PropsWithChildren, useCallback, useEffect } from 'react';
+import { message } from 'antd';
+import { GlobalSettingItem } from '../types';
+import { fetchGlobalSettingsApi } from '../global.api';
+import { useAsync } from '@src/hooks/use-async';
+
+interface GlobalSettingsContextProps {
+ globalSettings: GlobalSettingItem[] | undefined;
+ loading: boolean;
+ error: Error | null;
+ fetchGlobalSettings: () => Promise;
+}
+
+export const GlobalSettingsContext = React.createContext({
+ globalSettings: [],
+ loading: true,
+ error: null,
+ fetchGlobalSettings: () => Promise.resolve(),
+});
+
+const ERROR_MESSAGE = '获取平台设置失败';
+
+export default function GlobalSettingsContextProvider(props: PropsWithChildren<{}>) {
+ const {
+ data: globalSettings,
+ loading,
+ error,
+ run,
+ } = useAsync({
+ loading: true,
+ data: [],
+ });
+ const fetchGlobalSettings = useCallback(() => {
+ return run(fetchGlobalSettingsApi(), { setStartLoading: false }).catch(() => {
+ message.error(ERROR_MESSAGE);
+ });
+ }, [run]);
+
+ useEffect(() => {
+ fetchGlobalSettings();
+ }, [run, fetchGlobalSettings]);
+
+ return (
+
+ {props.children}
+
+ );
+}
diff --git a/frontend/src/routes/settings/global/context/index.tsx b/frontend/src/routes/settings/global/context/index.tsx
new file mode 100644
index 0000000..439c44f
--- /dev/null
+++ b/frontend/src/routes/settings/global/context/index.tsx
@@ -0,0 +1,25 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { PropsWithChildren } from 'react';
+import GlobalSettingsContextProvider from './global-settings-context';
+
+export default function GlobalSettingProvider(props: PropsWithChildren<{}>) {
+ return {props.children} ;
+}
+
+export * from './global-settings-context';
diff --git a/frontend/src/routes/settings/global/global.api.ts b/frontend/src/routes/settings/global/global.api.ts
new file mode 100644
index 0000000..e8dd032
--- /dev/null
+++ b/frontend/src/routes/settings/global/global.api.ts
@@ -0,0 +1,65 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { http } from '@src/utils/http';
+import { GlobalSettingItem } from './types';
+
+export function fetchGlobalSettingsApi() {
+ return http.get('/api/setting/global').then(res => {
+ if (res.code === 0) return res.data;
+ return Promise.reject(res);
+ }) as Promise;
+}
+
+export interface RemoteSettingParams extends GlobalSettingItem {
+ type?: string;
+}
+
+export function changeSettingApi(key: string, params: RemoteSettingParams) {
+ return http.put(`/api/setting/${key}`, params).then(res => {
+ if (res.code === 0) return res.data;
+ return Promise.reject(res);
+ });
+}
+
+export function changeEmailSettingApi(params: Record) {
+ return http.put('/api/email/', params).then(res => {
+ if (res.code === 0) return res.data;
+ return Promise.reject(res);
+ });
+}
+
+export function deleteEmailSettingApi() {
+ return http.delete('/api/email/').then(res => {
+ if (res.code === 0) return res.data;
+ return Promise.reject(res);
+ });
+}
+
+export function sendTestEmailApi(params: { email: string }) {
+ return http.post('/api/email/test/', params).then(res => {
+ if (res.code === 0) return res.data;
+ return Promise.reject(res);
+ });
+}
+
+export function getLdapSettingsApi() {
+ return http.get('/api/ldap/setting').then(res => {
+ if (res.code === 0) return res.data;
+ return Promise.reject(res);
+ });
+}
diff --git a/frontend/src/routes/settings/global/global.hooks.ts b/frontend/src/routes/settings/global/global.hooks.ts
new file mode 100644
index 0000000..1f16746
--- /dev/null
+++ b/frontend/src/routes/settings/global/global.hooks.ts
@@ -0,0 +1,82 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { useState, useContext, useEffect, useCallback } from 'react';
+import * as _ from 'lodash-es';
+import { message } from 'antd';
+import { GlobalSettingItem } from './types';
+import { GlobalSettingsContext } from './context';
+import { changeSettingApi, RemoteSettingParams } from './global.api';
+
+function getSettings(settings: GlobalSettingItem[] | null, settingKeys: T[]) {
+ return (settings
+ ?.filter(item => settingKeys.includes(item.key as T))
+ .reduce((memo, current) => {
+ memo[current.key] = current;
+ return memo;
+ }, {}) || {}) as Record;
+}
+
+export function useSettings(settingKeys: T[]) {
+ const { globalSettings, fetchGlobalSettings } = useContext(GlobalSettingsContext);
+ const initialSettings = getSettings(globalSettings, settingKeys);
+ const [settings, setSettings] = useState({ ...initialSettings });
+
+ useEffect(() => {
+ const settings = getSettings(globalSettings, settingKeys);
+ setSettings({ ...settings });
+ }, [globalSettings, settingKeys]);
+
+ const remoteSetting = useCallback(
+ _.debounce((key: T, params: RemoteSettingParams) => {
+ changeSettingApi(key, params)
+ .then(() => {
+ message.success('设置成功');
+ })
+ .catch(() => {
+ message.error('设置失败');
+ })
+ .finally(() => fetchGlobalSettings());
+ }, 200),
+ [fetchGlobalSettings],
+ );
+
+ const changeSettingItem =
+ (key: T, type?: string, changeRemote: boolean = true) =>
+ (e: any) => {
+ const value = e && e.target ? e.target.value : e;
+ const targetSetting = settings[key]!;
+ const newTargetSetting = {
+ ...targetSetting,
+ value,
+ };
+ setSettings({
+ ...settings,
+ [key]: newTargetSetting,
+ });
+ if (!changeRemote) return;
+ remoteSetting(key, {
+ ...newTargetSetting,
+ type,
+ });
+ };
+
+ return {
+ settings,
+ changeSettingItem,
+ };
+}
diff --git a/frontend/src/routes/settings/global/global.routes.ts b/frontend/src/routes/settings/global/global.routes.ts
new file mode 100644
index 0000000..2860462
--- /dev/null
+++ b/frontend/src/routes/settings/global/global.routes.ts
@@ -0,0 +1,50 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import PublicSharing from './routes/public-sharing';
+import Localization from './routes/localization';
+import Email from './routes/email';
+import General from './routes/general';
+
+export interface Route {
+ path: string;
+ label: string;
+ component: () => JSX.Element;
+}
+
+export const DEFAULT_GLOBAL_ROUTES: Route[] = [
+ {
+ path: 'public_sharing',
+ label: '公开分享',
+ component: PublicSharing,
+ },
+ {
+ path: 'localization',
+ label: '本土化',
+ component: Localization,
+ },
+ {
+ path: 'email',
+ label: '邮箱',
+ component: Email,
+ },
+ {
+ path: 'general',
+ label: '访问与帮助',
+ component: General,
+ },
+];
diff --git a/frontend/src/routes/settings/global/global.tsx b/frontend/src/routes/settings/global/global.tsx
new file mode 100644
index 0000000..2eff657
--- /dev/null
+++ b/frontend/src/routes/settings/global/global.tsx
@@ -0,0 +1,66 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { useContext, useEffect, useState } from 'react';
+import { Typography, Row } from 'antd';
+import { Switch, Route, Redirect, useRouteMatch } from 'react-router-dom';
+import styles from './style.module.less';
+import Sidebar from './components/sidebar';
+import GlobalSettingProvider from './context';
+import { UserInfoContext } from '@src/common/common.context';
+import LoadingLayout from './components/loading-layout';
+import { getGlobalRoutes } from './global.utils';
+
+export function Global() {
+ const [loading, setLoading] = useState(true);
+ const match = useRouteMatch();
+ const userInfo = useContext(UserInfoContext);
+ const isLdap = userInfo.authType === 'ldap';
+
+ useEffect(() => {
+ if (userInfo.id != null) {
+ setLoading(false);
+ }
+ }, [userInfo.id]);
+
+ const globalRoutes = getGlobalRoutes(isLdap);
+
+ return (
+
+
+
+
+ 设置
+
+
+
+
+ {globalRoutes.map(route => (
+
+ ))}
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/routes/settings/global/global.utils.ts b/frontend/src/routes/settings/global/global.utils.ts
new file mode 100644
index 0000000..fe985df
--- /dev/null
+++ b/frontend/src/routes/settings/global/global.utils.ts
@@ -0,0 +1,48 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { DEFAULT_GLOBAL_ROUTES, Route } from './global.routes';
+import Certificate from './routes/certificate';
+
+export function getValueFromJson(key: string, defaultValue?: any) {
+ let res = defaultValue;
+ try {
+ res = JSON.parse(key);
+ } catch (e) {}
+ return res;
+}
+
+export function getProtocol(url: string) {
+ const match = /^https?:\/\//.exec(url);
+ return match ? match[0] : '';
+}
+
+export function getAddress(url: string) {
+ const match = /^https?:\/\//.exec(url);
+ return match ? url.slice(match[0].length) : url || '';
+}
+
+export function getGlobalRoutes(isLdap: boolean) {
+ return [
+ isLdap && {
+ path: 'certificate',
+ label: '认证',
+ component: Certificate,
+ },
+ ...DEFAULT_GLOBAL_ROUTES,
+ ].filter(Boolean) as Route[];
+}
diff --git a/frontend/src/routes/settings/global/routes/certificate/index.tsx b/frontend/src/routes/settings/global/routes/certificate/index.tsx
new file mode 100644
index 0000000..fec9306
--- /dev/null
+++ b/frontend/src/routes/settings/global/routes/certificate/index.tsx
@@ -0,0 +1,99 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { useEffect } from 'react';
+import { Form, Input, message, Radio } from 'antd';
+import { useAsync } from '@src/hooks/use-async';
+import LoadingLayout from '../../components/loading-layout';
+import { LOADING_WRAPPER_STYLE } from '../../constants';
+import { getLdapSettingsApi } from '../../global.api';
+
+export default function Certificate() {
+ const [form] = Form.useForm();
+ const { data, loading, run } = useAsync({ loading: true, data: {} });
+ useEffect(() => {
+ run(getLdapSettingsApi()).catch(() => {
+ message.error('获取ldap设置失败');
+ });
+ }, [run]);
+ useEffect(() => {
+ form.setFieldsValue(data);
+ }, [data]);
+ return (
+
+
+
+
+
+
+
+
+
+ None
+ SSL
+ StartTLS
+
+
+
+
+
+
+
+
+ 用户结构
+
+
+
+
+
+
+ 属性
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/routes/settings/global/routes/email/email-modal.tsx b/frontend/src/routes/settings/global/routes/email/email-modal.tsx
new file mode 100644
index 0000000..78772dc
--- /dev/null
+++ b/frontend/src/routes/settings/global/routes/email/email-modal.tsx
@@ -0,0 +1,49 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { useState } from 'react';
+import { Input, Modal } from 'antd';
+
+interface EmailModalProps {
+ visible: boolean;
+ confirmLoading: boolean;
+ onOk: (email: string) => () => void;
+ onCancel: () => void;
+}
+
+export default function EmailModal(props: EmailModalProps) {
+ const { visible, onOk, onCancel, confirmLoading } = props;
+ const [emailContent, setEmailContent] = useState('');
+
+ return (
+
+ setEmailContent(e.target.value)}
+ placeholder="请输入目标邮箱地址"
+ />
+
+ );
+}
diff --git a/frontend/src/routes/settings/global/routes/email/hooks.ts b/frontend/src/routes/settings/global/routes/email/hooks.ts
new file mode 100644
index 0000000..f84c6ba
--- /dev/null
+++ b/frontend/src/routes/settings/global/routes/email/hooks.ts
@@ -0,0 +1,85 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { useState } from 'react';
+import { message } from 'antd';
+import * as _ from 'lodash-es';
+import { useAsync } from '@src/hooks/use-async';
+import { changeEmailSettingApi, deleteEmailSettingApi, sendTestEmailApi } from '../../global.api';
+import { SETTING_KEYS, SettingKeysTypes } from '.';
+import { GlobalSettingItem } from '../../types';
+
+export function useEmailSettings(
+ settings: Record,
+ fetchGlobalSettings: () => Promise,
+) {
+ const { loading: buttonLoading, run: runRemoteApi } = useAsync();
+ const [modalVisible, setModalVisible] = useState(false);
+ const [isChanged, setIsChanged] = useState(false);
+
+ const handleSaveEmailSettings = _.debounce(() => {
+ const params = Object.keys(settings).reduce((memo, current) => {
+ if (SETTING_KEYS.includes(current as SettingKeysTypes)) {
+ memo[current] = settings[current]?.value || null;
+ }
+ return memo;
+ }, {} as Record);
+ runRemoteApi(changeEmailSettingApi(params))
+ .then(() => {
+ message.success('保存成功');
+ })
+ .catch(() => {
+ message.error('保存失败');
+ })
+ .finally(() => fetchGlobalSettings())
+ .then(() => setIsChanged(false));
+ }, 200);
+
+ const handleDeleteEmailSettings = _.debounce(() => {
+ runRemoteApi(deleteEmailSettingApi())
+ .then(() => {
+ message.success('清除成功');
+ })
+ .catch(() => {
+ message.error('清除失败');
+ })
+ .finally(() => fetchGlobalSettings())
+ .then(() => setIsChanged(false));
+ });
+
+ const handleSendTestEmail = (email: string) => () => {
+ runRemoteApi(sendTestEmailApi({ email }))
+ .then(() => {
+ message.success('发送成功');
+ })
+ .catch(() => {
+ message.error('发送失败');
+ })
+ .finally(() => setModalVisible(false));
+ };
+
+ return {
+ isChanged,
+ modalVisible,
+ buttonLoading,
+ setModalVisible,
+ setIsChanged,
+ handleSaveEmailSettings,
+ handleDeleteEmailSettings,
+ handleSendTestEmail,
+ };
+}
diff --git a/frontend/src/routes/settings/global/routes/email/index.tsx b/frontend/src/routes/settings/global/routes/email/index.tsx
new file mode 100644
index 0000000..71368f4
--- /dev/null
+++ b/frontend/src/routes/settings/global/routes/email/index.tsx
@@ -0,0 +1,169 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { useContext } from 'react';
+import { Input, InputNumber, Radio, Row, Button, Col } from 'antd';
+import EmailModal from './email-modal';
+import LoadingLayout from '../../components/loading-layout';
+import SettingItemLayout from '../../components/setting-item-layout';
+import { GlobalSettingsContext } from '../../context';
+import { useSettings } from '../../global.hooks';
+import { getValueFromJson } from '../../global.utils';
+import { useEmailSettings } from './hooks';
+import { LOADING_WRAPPER_STYLE } from '../../constants';
+
+export const SETTING_KEYS = [
+ 'email-smtp-host',
+ 'email-smtp-port',
+ 'email-smtp-security',
+ 'email-smtp-username',
+ 'email-smtp-password',
+ 'email-from-address',
+] as const;
+
+export type SettingKeysTypes = typeof SETTING_KEYS[number];
+
+const SMTP_SECURITY_OPTIONS = [
+ { value: 'none', label: 'None' },
+ { value: 'ssl', label: 'SSL' },
+ { value: 'tls', label: 'TLS' },
+ { value: 'starttls', label: 'STARTTLS' },
+];
+
+export default function Email() {
+ const { loading, fetchGlobalSettings } = useContext(GlobalSettingsContext);
+ const { settings, changeSettingItem } = useSettings(SETTING_KEYS as any);
+ const {
+ buttonLoading,
+ modalVisible,
+ setModalVisible,
+ isChanged,
+ setIsChanged,
+ handleSaveEmailSettings,
+ handleDeleteEmailSettings,
+ handleSendTestEmail,
+ } = useEmailSettings(settings, fetchGlobalSettings);
+
+ const buttonDisabled =
+ !settings['email-smtp-host']?.value ||
+ !settings['email-smtp-port']?.value ||
+ !settings['email-from-address']?.value ||
+ settings['email-smtp-security']?.value == null;
+
+ const sendEmailButtonVisible = !isChanged && settings['email-from-address']?.value;
+
+ const changeEmailSettingItem = (key: SettingKeysTypes) => (e: any) => {
+ setIsChanged(true);
+ changeSettingItem(key, undefined, false)(e);
+ };
+
+ return (
+
+
+
+
+
+ changeEmailSettingItem('email-smtp-port')(v + '')}
+ />
+
+
+
+ {SMTP_SECURITY_OPTIONS.map(options => (
+
+ {options.label}
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+ {
+ setModalVisible(false);
+ }}
+ />
+
+
+
+ 保存修改
+
+
+ {sendEmailButtonVisible && (
+
+ setModalVisible(true)}>
+ 发送邮件测试
+
+
+ )}
+
+
+ 清除
+
+
+
+
+ );
+}
diff --git a/frontend/src/routes/settings/global/routes/general/index.tsx b/frontend/src/routes/settings/global/routes/general/index.tsx
new file mode 100644
index 0000000..7c8b421
--- /dev/null
+++ b/frontend/src/routes/settings/global/routes/general/index.tsx
@@ -0,0 +1,91 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { useContext } from 'react';
+import { Input, Select } from 'antd';
+import LoadingLayout from '../../components/loading-layout';
+import SettingItemLayout from '../../components/setting-item-layout';
+import { GlobalSettingsContext } from '../../context';
+import { useSettings } from '../../global.hooks';
+import { LOADING_WRAPPER_STYLE } from '../../constants';
+import { getProtocol, getAddress } from '../../global.utils';
+
+const { Option } = Select;
+
+const SETTING_KEYS = ['site-name', 'site-url', 'admin-email'] as const;
+
+type SettingsKeysTypes = typeof SETTING_KEYS[number];
+
+const PROTOCOL_OPTIONS = [{ value: 'http://' }, { value: 'https://' }];
+
+export default function General() {
+ const { loading } = useContext(GlobalSettingsContext);
+ const { settings, changeSettingItem } = useSettings(SETTING_KEYS as any);
+
+ return (
+
+
+
+
+
+ {
+ const address = getAddress(settings['site-url']?.value);
+ changeSettingItem('site-url', 'string')(v + address);
+ }}
+ >
+ {PROTOCOL_OPTIONS.map(options => (
+
+ {options.value}
+
+ ))}
+
+ }
+ value={getAddress(settings['site-url']?.value)}
+ placeholder="请输入网站地址"
+ defaultValue={getAddress(settings['site-url']?.default)}
+ onChange={e => {
+ const protocol = getProtocol(settings['site-url']?.value);
+ changeSettingItem('site-url', 'string')(protocol + e.target.value);
+ }}
+ />
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/routes/settings/global/routes/localization/constants.ts b/frontend/src/routes/settings/global/routes/localization/constants.ts
new file mode 100644
index 0000000..dfcc349
--- /dev/null
+++ b/frontend/src/routes/settings/global/routes/localization/constants.ts
@@ -0,0 +1,43 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+export const DEFAULT_DATE_STYLE_OPTIONS = [
+ { value: 'MMMM D, YYYY', completeLabel: '一月 7, 2018', compressedLabel: '1月, 7, 2018' },
+ { value: 'D MMMM, YYYY', completeLabel: '7 一月, 2018', compressedLabel: '7 1月, 2018' },
+ { value: 'dddd, MMMM D, YYYY', completeLabel: '星期日, 一月 7, 2018', compressedLabel: '周日, 1月 7, 2018' },
+ { value: 'M/D/YYYY', label: [1, 7, 2018] },
+ { value: 'D/M/YYYY', label: [7, 1, 2018] },
+ { value: 'YYYY/M/D', label: [2018, 1, 7] },
+];
+
+export const DATE_SEPARATOR_OPTIONS = [
+ { value: '/', label: 'M/D/YYYY' },
+ { value: '-', label: 'M-D-YYYY' },
+ { value: '.', label: 'M.D.YYYY' },
+];
+
+export const TIME_STYLE_OPTIONS = [
+ { value: 'h:mm A', label: '5:24 下午 (12小时制)' },
+ { value: 'k:mm', label: '17:24 (24小时制)' },
+];
+
+export const NUMBER_SEPARATORS_OPTIONS = [
+ { value: '.,', label: '100,000.00' },
+ { value: ', ', label: '100 000,00' },
+ { value: ',.', label: '100.000,00' },
+ { value: '.', label: '100000.00' },
+];
diff --git a/frontend/src/routes/settings/global/routes/localization/form-item-layout.tsx b/frontend/src/routes/settings/global/routes/localization/form-item-layout.tsx
new file mode 100644
index 0000000..07ac54b
--- /dev/null
+++ b/frontend/src/routes/settings/global/routes/localization/form-item-layout.tsx
@@ -0,0 +1,35 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { Typography } from 'antd';
+import React, { PropsWithChildren } from 'react';
+
+interface FormItemLayoutProps {
+ title: string;
+}
+
+export default function FormItemLayout(props: PropsWithChildren) {
+ const { title, children } = props;
+ return (
+
+
+ {title}
+
+ {children}
+
+ );
+}
diff --git a/frontend/src/routes/settings/global/routes/localization/form-layout.tsx b/frontend/src/routes/settings/global/routes/localization/form-layout.tsx
new file mode 100644
index 0000000..c3f08b8
--- /dev/null
+++ b/frontend/src/routes/settings/global/routes/localization/form-layout.tsx
@@ -0,0 +1,35 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { Typography } from 'antd';
+import React, { PropsWithChildren } from 'react';
+
+interface FormLayoutProps {
+ title: string;
+}
+
+export default function FormLayout(props: PropsWithChildren) {
+ const { title, children } = props;
+ return (
+
+
+ {title}
+
+ {children}
+
+ );
+}
diff --git a/frontend/src/routes/settings/global/routes/localization/index.tsx b/frontend/src/routes/settings/global/routes/localization/index.tsx
new file mode 100644
index 0000000..a4efa03
--- /dev/null
+++ b/frontend/src/routes/settings/global/routes/localization/index.tsx
@@ -0,0 +1,69 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { useContext } from 'react';
+import { useSettings } from '../../global.hooks';
+import LoadingLayout from '../../components/loading-layout';
+import TemporalForm from './temporal-form';
+import NumberForm from './number-form';
+import { GlobalSettingsContext } from '../../context';
+import { LOADING_WRAPPER_STYLE } from '../../constants';
+import { Divider } from 'antd';
+import { getValueFromJson } from '../../global.utils';
+import { GlobalSettingItem } from '../../types';
+
+const SETTING_KEYS = ['custom-formatting'] as const;
+
+type SettingKeysTypes = typeof SETTING_KEYS[number];
+
+type LocalizationNamspace = 'type/Temporal' | 'type/Number';
+
+export default function Localization() {
+ const { loading } = useContext(GlobalSettingsContext);
+ const { settings, changeSettingItem } = useSettings(SETTING_KEYS as any);
+ const targetSettings = settings['custom-formatting'] || ({} as GlobalSettingItem);
+ const defaultSettings = getValueFromJson(targetSettings?.default, {});
+
+ const changeLocalizationSetting = (namespace: LocalizationNamspace) => (key: string) => (e: any) => {
+ const value = e && e.target ? e.target.value : e;
+ changeSettingItem('custom-formatting')({
+ ...targetSettings.value,
+ [namespace]: {
+ ...targetSettings.value?.[namespace],
+ [key]: value,
+ },
+ });
+ };
+
+ return (
+
+
+
+ );
+}
diff --git a/frontend/src/routes/settings/global/routes/localization/number-form.tsx b/frontend/src/routes/settings/global/routes/localization/number-form.tsx
new file mode 100644
index 0000000..2303694
--- /dev/null
+++ b/frontend/src/routes/settings/global/routes/localization/number-form.tsx
@@ -0,0 +1,56 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React from 'react';
+import { Select } from 'antd';
+import FormItemLayout from './form-item-layout';
+import FormLayout from './form-layout';
+import { NUMBER_SEPARATORS_OPTIONS } from './constants';
+
+const { Option } = Select;
+
+interface NumberSettings {
+ number_separators: string;
+}
+
+interface NumberFormProps {
+ settings: NumberSettings;
+ defaultSettings: NumberSettings;
+ changeLocalizationSettingItem: (key: string) => (e: any) => void;
+}
+
+export default function NumberForm(props: NumberFormProps) {
+ const { settings, defaultSettings, changeLocalizationSettingItem } = props;
+ return (
+
+
+
+ {NUMBER_SEPARATORS_OPTIONS.map(options => (
+
+ {options.label}
+
+ ))}
+
+
+
+ );
+}
diff --git a/frontend/src/routes/settings/global/routes/localization/temporal-form.tsx b/frontend/src/routes/settings/global/routes/localization/temporal-form.tsx
new file mode 100644
index 0000000..13e9fa3
--- /dev/null
+++ b/frontend/src/routes/settings/global/routes/localization/temporal-form.tsx
@@ -0,0 +1,109 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { useMemo } from 'react';
+import { Select, Radio, Space, Switch } from 'antd';
+import FormLayout from './form-layout';
+import FormItemLayout from './form-item-layout';
+import { DEFAULT_DATE_STYLE_OPTIONS, DATE_SEPARATOR_OPTIONS, TIME_STYLE_OPTIONS } from './constants';
+
+const { Option } = Select;
+
+interface TemporalSettings {
+ date_abbreviate: boolean;
+ date_separator: string;
+ date_style: string;
+ time_style: string;
+}
+
+interface TemporalFormProps {
+ settings: TemporalSettings;
+ defaultSettings: TemporalSettings;
+ changeLocalizationSettingItem: (key: string) => (e: any) => void;
+}
+
+export default function TemporalForm(props: TemporalFormProps) {
+ const { settings, defaultSettings, changeLocalizationSettingItem } = props;
+
+ const DATE_STYLE_OPTIONS = useMemo(() => {
+ return DEFAULT_DATE_STYLE_OPTIONS.map(options => ({
+ value: options.value,
+ label: options.label
+ ? options.label.join(settings.date_separator)
+ : settings.date_abbreviate
+ ? options.compressedLabel
+ : options.completeLabel,
+ }));
+ }, [settings.date_separator, settings.date_abbreviate]);
+
+ return (
+
+
+
+ {DATE_STYLE_OPTIONS.map(item => (
+
+ {item.label}
+
+ ))}
+
+
+
+
+
+ {DATE_SEPARATOR_OPTIONS.map(item => (
+
+ {item.label}
+
+ ))}
+
+
+
+
+
+
+
+
+
+ {TIME_STYLE_OPTIONS.map(item => (
+
+ {item.label}
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/frontend/src/routes/settings/global/routes/public-sharing/index.tsx b/frontend/src/routes/settings/global/routes/public-sharing/index.tsx
new file mode 100644
index 0000000..2d656ff
--- /dev/null
+++ b/frontend/src/routes/settings/global/routes/public-sharing/index.tsx
@@ -0,0 +1,49 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { useContext } from 'react';
+import { Switch } from 'antd';
+import LoadingLayout from '../../components/loading-layout';
+import SettingItemLayout from '../../components/setting-item-layout';
+import { GlobalSettingsContext } from '../../context';
+import { useSettings } from '../../global.hooks';
+import { LOADING_WRAPPER_STYLE } from '../../constants';
+import { getValueFromJson } from '../../global.utils';
+
+const SETTING_KEYS = ['enable-public-sharing'] as const;
+
+type SettingKeysTypes = typeof SETTING_KEYS[number];
+
+export default function PublicSharing() {
+ const { loading } = useContext(GlobalSettingsContext);
+ const { settings, changeSettingItem } = useSettings(SETTING_KEYS as any);
+
+ return (
+
+
+
+
+ {getValueFromJson(settings['enable-public-sharing']?.value) ? '启用' : '取消'}
+
+
+
+ );
+}
diff --git a/frontend/src/routes/settings/global/style.module.less b/frontend/src/routes/settings/global/style.module.less
new file mode 100644
index 0000000..f81cd52
--- /dev/null
+++ b/frontend/src/routes/settings/global/style.module.less
@@ -0,0 +1,23 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+.container {
+ padding: 32px;
+ .main {
+ display: flex;
+ }
+}
diff --git a/frontend/src/routes/settings/global/types/index.ts b/frontend/src/routes/settings/global/types/index.ts
new file mode 100644
index 0000000..ed7f190
--- /dev/null
+++ b/frontend/src/routes/settings/global/types/index.ts
@@ -0,0 +1,23 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+export interface GlobalSettingItem {
+ key: string;
+ default: any;
+ value: any;
+ description: string;
+}
diff --git a/frontend/src/routes/settings/settings.module.less b/frontend/src/routes/settings/settings.module.less
new file mode 100644
index 0000000..064c9f9
--- /dev/null
+++ b/frontend/src/routes/settings/settings.module.less
@@ -0,0 +1,26 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+.container {
+ margin-left: 80px;
+ overflow-y: scroll;
+ height: calc(100% - 44px);
+}
+
+.card {
+ min-height: 100%;
+}
diff --git a/frontend/src/routes/settings/settings.tsx b/frontend/src/routes/settings/settings.tsx
new file mode 100644
index 0000000..abdebd6
--- /dev/null
+++ b/frontend/src/routes/settings/settings.tsx
@@ -0,0 +1,62 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { useEffect, useState } from 'react';
+import { Card } from 'antd';
+import { Redirect, Route, Switch, useRouteMatch, useHistory } from 'react-router-dom';
+import styles from './settings.module.less';
+import { UserInfoContext } from '@src/common/common.context';
+import { Sidebar } from '@src/components/sidebar/sidebar';
+import { Header } from '@src/components/header/header';
+import TabsHeader from './components/tabs-header';
+import { User } from './user/user';
+import { useUserInfo } from '@src/hooks/use-userinfo.hooks';
+import LoadingLayout from './global/components/loading-layout';
+
+export function Settings(props: any) {
+ const match = useRouteMatch();
+ const history = useHistory();
+ const [loading, setLoading] = useState(true);
+ const [userInfo] = useUserInfo();
+ useEffect(() => {
+ if (userInfo.id == null) return;
+ if (userInfo.id != null && !userInfo.is_super_admin) {
+ history.push('/space');
+ return;
+ }
+ setLoading(false);
+ }, [userInfo.id]);
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/frontend/src/routes/settings/user/list/create-or-edit-modal.tsx b/frontend/src/routes/settings/user/list/create-or-edit-modal.tsx
new file mode 100644
index 0000000..d1bb87a
--- /dev/null
+++ b/frontend/src/routes/settings/user/list/create-or-edit-modal.tsx
@@ -0,0 +1,145 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { useEffect, useState } from 'react';
+import { Form, Input, message, Modal } from 'antd';
+import { CopyOutlined } from '@ant-design/icons';
+import { useTranslation } from 'react-i18next';
+import { isSuccess } from '@src/utils/http';
+import { UserAPI } from '../user.api';
+import { copyText, generatePassword } from '../user.utils';
+import styles from './list.module.less';
+
+export function CreateOrEditRoleModal(props: any) {
+ const { t, i18n } = useTranslation();
+ const { onCancel, user } = props;
+ const [loading, setLoading] = useState(false);
+ const title = user ? t`editUser` : t`addUser`;
+
+ useEffect(() => {
+ props.form.setFieldsValue(user ? user : { name: '', email: '' });
+ }, [user]);
+
+ async function handleCreate(values: any) {
+ setLoading(true);
+ try {
+ const password = generatePassword();
+ const res = await UserAPI.createUser({ ...values, password });
+ setLoading(false);
+ if (isSuccess(res)) {
+ message.success(t`createSuccess`);
+ props.onSuccess && props.onSuccess();
+ Modal.confirm({
+ title: t`pleaseSaveYourPassword`,
+ content: (
+
+ {
+ copyText(password);
+ message.success(t`copySuccess`);
+ }}
+ />
+ }
+ />
+
+ ),
+ });
+ } else {
+ message.error(res.msg);
+ }
+ } catch (err) {
+ setLoading(false);
+ }
+ }
+ async function handleEdit(values: any) {
+ setLoading(true);
+ const res = await UserAPI.updateUser({
+ user_id: user.id,
+ name: values.name,
+ email: values.email,
+ });
+ setLoading(false);
+ if (isSuccess(res)) {
+ message.success(t`editSuccess`);
+ props.onSuccess && props.onSuccess();
+ } else {
+ message.error(res.msg);
+ }
+ }
+ return (
+ {
+ props.form
+ .validateFields()
+ .then((values: any) => {
+ if (!user) {
+ handleCreate(values);
+ } else {
+ handleEdit(values);
+ }
+ })
+ .catch((info: any) => {
+ console.log('Validate Failed:', info);
+ });
+ }}
+ >
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/routes/settings/user/list/list.module.less b/frontend/src/routes/settings/user/list/list.module.less
new file mode 100644
index 0000000..a82477d
--- /dev/null
+++ b/frontend/src/routes/settings/user/list/list.module.less
@@ -0,0 +1,24 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+.clipInput {
+ :global {
+ .copy-icon {
+ color: #1890ff!important;
+ }
+ }
+}
diff --git a/frontend/src/routes/settings/user/list/list.tsx b/frontend/src/routes/settings/user/list/list.tsx
new file mode 100644
index 0000000..e41d49a
--- /dev/null
+++ b/frontend/src/routes/settings/user/list/list.tsx
@@ -0,0 +1,231 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { useContext, useState } from 'react';
+import moment from 'moment';
+import { Button, Table, Input, Modal, message, Switch, Row } from 'antd';
+import { CopyOutlined } from '@ant-design/icons';
+import { useForm } from 'antd/lib/form/Form';
+import { useTranslation } from 'react-i18next';
+import StatusMark from '@src/components/status-mark';
+import { FlatBtnGroup, FlatBtn } from '@src/components/flatbtn';
+import { UserInfoContext } from '@src/common/common.context';
+import { isSuccess } from '@src/utils/http';
+import { UserAPI } from '../user.api';
+import { useGlobalUsers } from '../user.hooks';
+import { generatePassword, copyText } from '../user.utils';
+import { CreateOrEditRoleModal } from './create-or-edit-modal';
+import styles from './list.module.less';
+import { UserInfo } from '@src/common/common.interface';
+
+export function UserList() {
+ const { t } = useTranslation();
+ const userInfo = useContext(UserInfoContext) as UserInfo;
+ const { users, getUsers, loading, setUsers } = useGlobalUsers({
+ include_deactivated: true,
+ });
+ const [visible, setVisible] = useState(false);
+ const [currentUser, setCurrentUser] = useState();
+ const [form] = useForm();
+ const columns = [
+ {
+ title: t`username`,
+ key: 'name',
+ dataIndex: 'name',
+ },
+ {
+ title: t`Mail`,
+ dataIndex: 'email',
+ key: 'email',
+ },
+ {
+ title: t`status`,
+ dataIndex: 'is_active',
+ filters: [
+ { text: t`enabled`, value: true },
+ { text: t`disabled`, value: false },
+ ],
+ render: (is_active: boolean) => (
+
+ {is_active ? t`activated` : t`deactivated`}
+
+ ),
+ onFilter: (value: any, record: any) => record.is_active === value,
+ },
+ {
+ title: t`superAdministrator`,
+ dataIndex: 'is_super_admin',
+ key: 'is_super_admin',
+ render: (is_super_admin: boolean, record: any, index: number) => (
+
+ ),
+ },
+ {
+ title: t`lastLogin`,
+ dataIndex: 'last_login',
+ key: 'last_login',
+ render: (last_login: string) => {
+ return (
+
+ {last_login == null ? t`neverLoggedIn` : moment(last_login).format('YYYY-MM-DD HH:mm:ss')}
+
+ );
+ },
+ },
+ {
+ title: t`operation`,
+ key: 'actions',
+ render: (record: any) => {
+ const disabled = userInfo.id === record.id;
+ return (
+
+ {
+ setCurrentUser(record);
+ setVisible(true);
+ }}
+ >
+ {t`edit`}
+
+
+ handleResetPassword(record)}>{t`resetPassword`}
+
+ toggleActivate(record)} disabled={disabled}>
+ {record.is_active ? t`deactivateUser` : t`activateUser`}
+
+
+ );
+ },
+ },
+ ];
+
+ const handleResetPassword = (record: any) => {
+ Modal.confirm({
+ title: t`resetPasswordOrNot`,
+ onOk: () => {
+ const password = generatePassword();
+ UserAPI.resetPassword({ user_id: record.id, password })
+ .then(res => {
+ if (isSuccess(res)) {
+ Modal.confirm({
+ title: t`pleaseSaveYourPassword`,
+ content: (
+
+ {
+ try {
+ copyText(password);
+ message.success(t`copySuccess`);
+ } catch (e) {
+ message.error(t`copyError`);
+ }
+ }}
+ />
+ }
+ />
+
+ ),
+ });
+ return;
+ }
+ message.error(res.msg);
+ })
+ .catch(() => message.error(t`resetPasswordFailed`));
+ },
+ });
+ };
+
+ const changeSuperAdmin = (user_id: number, index: number) => async (checked: boolean) => {
+ users.splice(index, 1, {
+ ...users[index],
+ is_super_admin: !users[index].is_super_admin,
+ });
+ setUsers([...users]);
+ UserAPI.updateUserAdmin({ admin: checked, user_id })
+ .then(res => {
+ if (!isSuccess(res)) {
+ message.error(res.msg);
+ getUsers();
+ }
+ })
+ .catch(() => {
+ message.error(t`setupFailed`);
+ getUsers();
+ });
+ };
+ const toggleActivate = (record: any) => {
+ const { is_active } = record;
+ Modal.confirm({
+ title: `${is_active ? t`whetherToDeactivate` : t`whetherToActivate`} ${record.name} ?`,
+ content: `${is_active ? t`afterDeactivate` : t`afterActivate`} ${record.name} ${
+ is_active ? t`canNotLogin` : t`canLoginAgain`
+ }`,
+ onOk() {
+ const operator = is_active ? UserAPI.deactivateUser : UserAPI.activateUser;
+ operator({ user_id: record.id })
+ .then(res => {
+ if (isSuccess(res)) {
+ message.success(t`setupSuccess`);
+ getUsers();
+ } else {
+ message.error(t`setupFailed`);
+ }
+ })
+ .catch(() => {
+ message.error(t`setupFailed`);
+ });
+ },
+ });
+ };
+
+ return (
+ <>
+
+ {
+ setVisible(true);
+ setCurrentUser(undefined);
+ }}
+ >
+ {t`addUser`}
+
+
+
+ {visible && (
+ {
+ setVisible(false);
+ form.resetFields();
+ getUsers();
+ }}
+ form={form}
+ user={currentUser}
+ onCancel={() => setVisible(false)}
+ />
+ )}
+ >
+ );
+}
diff --git a/frontend/src/routes/settings/user/user.api.ts b/frontend/src/routes/settings/user/user.api.ts
new file mode 100644
index 0000000..4240ad9
--- /dev/null
+++ b/frontend/src/routes/settings/user/user.api.ts
@@ -0,0 +1,62 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { http, isSuccess } from '@src/utils/http';
+
+function getUsers(data: { include_deactivated: boolean; cluster_id?: number }) {
+ return http.get('/api/v2/user/', data);
+}
+function createUser(data: { name: string; email: string; password: string }) {
+ return http.post('/api/v2/user/', data);
+}
+function updateUser(data: { name: string; user_id: number; email: string }) {
+ return http.put(`/api/v2/user/${data.user_id}`, data);
+}
+
+function updateUserAdmin(data: { admin: boolean; user_id: number }) {
+ return http.put(`/api/v2/user/${data.user_id}/admin`, { admin: data.admin });
+}
+
+function deactivateUser(data: { user_id: number }) {
+ return http.delete(`/api/v2/user/${data.user_id}`);
+}
+
+function activateUser(data: { user_id: number }) {
+ return http.put(`/api/v2/user/${data.user_id}/reactivate`);
+}
+
+function resetPassword(data: { user_id: number; password: string }) {
+ return http.put(`/api/v2/user/${data.user_id}/password`, { password: data.password });
+}
+
+function syncLdapUser() {
+ return http.get('/api/setting/syncLdapUser').then(res => {
+ if (isSuccess(res)) return res.data;
+ return Promise.reject(res);
+ });
+}
+
+export const UserAPI = {
+ getUsers,
+ createUser,
+ updateUser,
+ updateUserAdmin,
+ deactivateUser,
+ activateUser,
+ resetPassword,
+ syncLdapUser
+};
diff --git a/frontend/src/routes/settings/user/user.hooks.ts b/frontend/src/routes/settings/user/user.hooks.ts
new file mode 100644
index 0000000..868c60a
--- /dev/null
+++ b/frontend/src/routes/settings/user/user.hooks.ts
@@ -0,0 +1,56 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { isSuccess } from '@src/utils/http';
+import { Dispatch, SetStateAction, useState, useEffect } from 'react';
+import { UserAPI } from './user.api';
+
+interface GetUsersParams {
+ include_deactivated: boolean;
+ cluster_id?: number;
+}
+
+export function useGlobalUsers(params: GetUsersParams): {
+ users: any[];
+ setUsers: Dispatch>;
+ getUsers: (extraParams?: GetUsersParams) => Promise;
+ loading: boolean;
+} {
+ const [users, setUsers] = useState([]);
+ const [loading, setLoading] = useState(false);
+ useEffect(() => {
+ getUsers();
+ }, []);
+
+ async function getUsers(extraParams?: GetUsersParams) {
+ setLoading(true);
+ const res = await UserAPI.getUsers({
+ ...params,
+ ...extraParams,
+ });
+ setLoading(false);
+ if (isSuccess(res)) {
+ setUsers(res.data);
+ }
+ }
+ return {
+ users,
+ setUsers,
+ getUsers,
+ loading,
+ };
+}
diff --git a/frontend/src/routes/settings/user/user.less b/frontend/src/routes/settings/user/user.less
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/src/routes/settings/user/user.tsx b/frontend/src/routes/settings/user/user.tsx
new file mode 100644
index 0000000..db6dbfc
--- /dev/null
+++ b/frontend/src/routes/settings/user/user.tsx
@@ -0,0 +1,32 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React from 'react';
+import { Redirect, Route, Switch } from 'react-router-dom';
+import { UserList } from './list/list';
+
+export function User(props: any) {
+ const { match } = props;
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
diff --git a/frontend/src/routes/settings/user/user.utils.ts b/frontend/src/routes/settings/user/user.utils.ts
new file mode 100644
index 0000000..c6fc521
--- /dev/null
+++ b/frontend/src/routes/settings/user/user.utils.ts
@@ -0,0 +1,42 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import passwordGenerator from 'password-generator';
+
+function isStrongEnough(password: string) {
+ return /^(?![a-zA-Z]+$)(?![A-Z\d]+$)(?![A-Z_]+$)(?![a-z\d]+$)(?![a-z_]+$)(?![\d_]+$)[a-zA-Z\d_]{6,12}$/.test(
+ password,
+ );
+}
+
+export function generatePassword() {
+ let password = passwordGenerator(12, false, /[a-zA-Z\d_]/);
+ while (!isStrongEnough(password)) {
+ password = passwordGenerator(12, false, /[a-zA-Z\d_]/);
+ }
+ return password;
+}
+
+export function copyText(text: string) {
+ const textArea = document.createElement('textarea');
+ textArea.style.opacity = '0';
+ document.body.appendChild(textArea);
+ textArea.value = text;
+ textArea.select();
+ document.execCommand('copy');
+ document.body.removeChild(textArea);
+}
diff --git a/frontend/src/routes/space/access-cluster/access-cluster.data.ts b/frontend/src/routes/space/access-cluster/access-cluster.data.ts
new file mode 100644
index 0000000..32284ee
--- /dev/null
+++ b/frontend/src/routes/space/access-cluster/access-cluster.data.ts
@@ -0,0 +1,30 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+export enum AccessClusterStepsEnum {
+ 'space-register',
+ 'connect-cluster',
+ 'managed-options',
+ 'node-verify',
+ 'cluster-verify',
+ 'finish',
+}
+
+export const ACCESS_CLUSTER_REQUEST_INIT_PARAMS = {
+ clusterId: '0',
+ requestId: '0',
+ currentEventType: '1',
+}
diff --git a/frontend/src/routes/space/access-cluster/access-cluster.recoil.ts b/frontend/src/routes/space/access-cluster/access-cluster.recoil.ts
new file mode 100644
index 0000000..2bc7dbf
--- /dev/null
+++ b/frontend/src/routes/space/access-cluster/access-cluster.recoil.ts
@@ -0,0 +1,60 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { isSuccess } from "@src/utils/http";
+import { atom, selector } from "recoil";
+import { SpaceAPI } from "../space.api";
+import { ACCESS_CLUSTER_REQUEST_INIT_PARAMS } from "./access-cluster.data";
+
+export const nextStepDisabledState = atom({
+ key: 'nextStepDisabled',
+ default: false,
+});
+
+export const requestInfoState = atom({
+ key: 'requestInfoState',
+ default: ACCESS_CLUSTER_REQUEST_INIT_PARAMS,
+});
+
+export const stepDisabledState = atom({
+ key: 'stepDisabledState',
+ default: {
+ next: false,
+ prev: false,
+ },
+});
+
+// export const requestInfoQuery = selector({
+// key: 'requestInfoQuery',
+// get: async ({get}) => {
+// const requestInfo = get(requestInfoState);
+// console.log(requestInfo);
+// // if (+requestInfo.clusterId === 0) {
+// // return JSON.parse(localStorage.getItem('requestInfo') || JSON.stringify(ACCESS_CLUSTER_REQUEST_INIT_PARAMS));
+// // }
+// const res = await SpaceAPI.spaceGet(requestInfo.clusterId);
+// console.log(res);
+// if (isSuccess(res)) {
+// localStorage.setItem('requestInfo', JSON.stringify(res.data));
+// return res.data;
+// }
+// return ACCESS_CLUSTER_REQUEST_INIT_PARAMS;
+// },
+// set: ({set}, newValue: any) => {
+// set(requestInfoState, newValue);
+// }
+// });
diff --git a/frontend/src/routes/space/access-cluster/access-cluster.tsx b/frontend/src/routes/space/access-cluster/access-cluster.tsx
new file mode 100644
index 0000000..0fa5bf0
--- /dev/null
+++ b/frontend/src/routes/space/access-cluster/access-cluster.tsx
@@ -0,0 +1,209 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import ProCard from '@ant-design/pro-card';
+import { Button, message, Row, Space, Steps } from 'antd';
+import React, { useEffect, useState } from 'react';
+import { Redirect, useRouteMatch, useHistory } from 'react-router';
+import CacheRoute, { CacheSwitch } from 'react-router-cache-route';
+import { pathToRegexp } from 'path-to-regexp';
+import { NewSpaceInfoContext } from '@src/common/common.context';
+import { useForm } from 'antd/lib/form/Form';
+import { AccessClusterStepsEnum } from './access-cluster.data';
+import { SpaceRegister } from '../components/space-register/space-register';
+import { ConnectCluster } from './steps/connect-cluster/connect-cluster';
+import { ManagedOptions } from './steps/managed-options/managed-options';
+import { NodeVerify } from '../components/node-verify/node-verify';
+import { isSuccess } from '@src/utils/http';
+import { SpaceAPI } from '../space.api';
+import { ClusterAccessParams } from '../space.interface';
+import { useRecoilState, useRecoilValue } from 'recoil';
+import { requestInfoState, stepDisabledState } from './access-cluster.recoil';
+import { ClusterVerify } from './steps/cluster-verify/cluster-verify';
+import { SpaceAccessFinish } from './steps/finish/finish';
+const { Step } = Steps;
+
+export function AccessCluster(props: any) {
+ const match = useRouteMatch<{requestId: string}>();
+ const history = useHistory();
+ const [step, setStep] = React.useState(0);
+ const [loading, setLoading] = useState(false);
+ const [requestInfo, setRequestInfo] = useRecoilState(requestInfoState);
+ const [stepDisabled, setStepDisabled] = useRecoilState(stepDisabledState);
+ const hidePrevSteps = [AccessClusterStepsEnum['space-register'], AccessClusterStepsEnum['node-verify'], AccessClusterStepsEnum['cluster-verify'], AccessClusterStepsEnum.finish];
+
+ useEffect(() => {
+ if (history.location.pathname === '/space/list') {
+ return;
+ }
+ const regexp = pathToRegexp(`${match.path}/:step`);
+ const paths = regexp.exec(history.location.pathname);
+ const step = (paths as string[])[2];
+ setStep(AccessClusterStepsEnum[step]);
+
+ setStepDisabled({...stepDisabled, next: false});
+
+ if (match.params.requestId && +match.params.requestId !== 0) {
+ getRequestInfo();
+ }
+ }, [history.location.pathname]);
+
+ const [form] = useForm();
+
+ async function getRequestInfo() {
+ const requestId = match.params.requestId;
+ const res = await SpaceAPI.getRequestInfo(requestId);
+ if (isSuccess(res)) {
+ setRequestInfo(res.data);
+ }
+ }
+
+ async function nextStep() {
+ const value = form.getFieldsValue();
+ const newStep = step + 1;
+ setLoading(true);
+ const params: ClusterAccessParams = {
+ ...requestInfo.reqInfo,
+ cluster_id: requestInfo.clusterId,
+ request_id: requestInfo.requestId,
+ event_type: (step + 1).toString(),
+ }
+ if (value && step === AccessClusterStepsEnum['space-register']) {
+ params.spaceInfo = {
+ describe: value.describe,
+ name: value.name,
+ spaceAdminUsers: value.spaceAdminUsers,
+ }
+ }
+ if (value && step === AccessClusterStepsEnum['connect-cluster']) {
+ params.clusterAccessInfo = {
+ address: value.address,
+ httpPort: value.httpPort,
+ passwd: value.passwd || '',
+ queryPort: value.queryPort,
+ type: value.type,
+ user: value.user,
+ }
+ }
+
+ if (value && step === AccessClusterStepsEnum['managed-options']) {
+ params.authInfo = {
+ sshKey: value.sshKey,
+ sshPort: value.sshPort,
+ sshUser: value.sshUser,
+ }
+ params.installInfo = value.installInfo
+ }
+
+ const res = await SpaceAPI.accessCluster(params);
+ setLoading(false);
+ if (isSuccess(res)) {
+ setRequestInfo(res.data);
+ setStep(newStep);
+ setStepDisabled({...stepDisabled, next: false});
+ setTimeout(() => {
+ history.push(`/space/access/${res.data.requestId}/${AccessClusterStepsEnum[newStep]}`);
+ }, 0)
+
+ } else {
+ message.error(res.msg);
+ }
+ }
+
+
+ function prevStep() {
+ const newStep = step - 1;
+ setStep(newStep);
+ setStepDisabled({...stepDisabled, prev: false});
+ history.push(`/space/access/${requestInfo.requestId}/${AccessClusterStepsEnum[newStep]}`);
+ }
+
+ function finish() {
+ history.push('/space/list');
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {hidePrevSteps.includes(step) ? (
+ <>>
+ ) : (
+ {
+ prevStep();
+ }}
+ >
+ 上一步
+
+ )}
+ {step === AccessClusterStepsEnum['finish'] ? (
+ {
+ finish();
+ }}
+ >
+ 完成
+
+ ) : (
+ {
+ nextStep();
+ }}
+ loading={loading}
+ disabled={stepDisabled.next}
+ >
+ 下一步
+
+ )}
+
+
+
+
+
+ >
+ );
+}
diff --git a/frontend/src/routes/space/access-cluster/steps/cluster-verify/cluster-verify.tsx b/frontend/src/routes/space/access-cluster/steps/cluster-verify/cluster-verify.tsx
new file mode 100644
index 0000000..e46e891
--- /dev/null
+++ b/frontend/src/routes/space/access-cluster/steps/cluster-verify/cluster-verify.tsx
@@ -0,0 +1,150 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { useContext, useEffect, useLayoutEffect, useState } from 'react';
+import { PageContainer } from '@ant-design/pro-layout';
+import ProCard from '@ant-design/pro-card';
+import { Button, message, Row, Space, Steps, Table, Tabs } from 'antd';
+import { useHistory, useRouteMatch } from 'react-router';
+import TabPane from '@ant-design/pro-card/lib/components/TabPane';
+import { isSuccess } from '@src/utils/http';
+import { NewSpaceInfoContext } from '@src/common/common.context';
+import { DorisNodeTypeEnum } from '@src/routes/space/new-cluster/types/params.type';
+import { SpaceAPI } from '@src/routes/space/space.api';
+import { useRequest } from 'ahooks';
+import { IResult } from '@src/interfaces/http.interface';
+import { OperateStatusEnum } from '@src/routes/space/space.data';
+import { useRecoilState } from 'recoil';
+import { stepDisabledState } from '../../access-cluster.recoil';
+const Step = Steps.Step;
+
+export function ClusterVerify(props: any) {
+ const [activeKey, setActiveKey] = useState(DorisNodeTypeEnum.FE);
+ const {reqInfo} = useContext(NewSpaceInfoContext);
+ const match = useRouteMatch<{spaceId: string}>();
+ const [instance, setInstance] = useState([]);
+ const [nodeTypes, setNodeTypes] = useState([]);
+ const [feNodes, setFENodes] = useState([]);
+ const [beNodes, setBENodes] = useState([]);
+ const [brokerNodes, setBrokerNodes] = useState([]);
+ const [stepDisabled, setStepDisabled] = useRecoilState(stepDisabledState);
+
+
+ const columns = [
+ {
+ title: '序号',
+ dataIndex: 'instanceId',
+ key: 'instanceId',
+ },
+ {
+ title: '节点IP',
+ dataIndex: 'nodeHost',
+ key: 'nodeHost',
+ },
+ {
+ title: '校验结果',
+ key: 'operateStatus',
+ render: (record: any) => {
+ return (
+
+
+
+ )
+ }
+ },
+ ];
+ const getClusterInstance = useRequest, any>(
+ (clusterId: string) => {
+ return SpaceAPI.getClusterInstance({clusterId});
+ },
+ {
+ manual: true,
+ pollingInterval: 2000,
+ onSuccess: (res: any) => {
+ if (isSuccess(res)) {
+ const data: any[] = res.data;
+ setInstance(res.data);
+ const types = [];
+ const feNodes = res.data.filter(item => item.moduleName?.toUpperCase() === DorisNodeTypeEnum.FE);
+ const beNodes = res.data.filter(item => item.moduleName?.toUpperCase() === DorisNodeTypeEnum.BE);
+ const brokerNodes = res.data.filter(item => item.moduleName?.toUpperCase() === DorisNodeTypeEnum.BROKER);
+ setFENodes(feNodes);
+ setBENodes(beNodes);
+ setBrokerNodes(brokerNodes);
+ if (feNodes.length > 0) {
+ types.push({key: DorisNodeTypeEnum.FE, tab: 'FE节点', moduleName: DorisNodeTypeEnum.FE });
+ }
+ if (beNodes.length > 0) {
+ types.push({key: DorisNodeTypeEnum.BE, tab: 'BE节点', moduleName: DorisNodeTypeEnum.BE });
+ }
+ if (brokerNodes.length > 0) {
+ types.push({key: DorisNodeTypeEnum.BROKER, tab: 'Broker节点', moduleName: DorisNodeTypeEnum.BROKER });
+ }
+ setNodeTypes(types);
+ const CANCEL_STATUS = [OperateStatusEnum.PROCESSING, OperateStatusEnum.INIT];
+ if (data.filter(item => CANCEL_STATUS.includes(item.operateStatus)).length === 0) {
+ getClusterInstance.cancel();
+ }
+ if (data.filter(item => item.operateStatus !== OperateStatusEnum.SUCCESS).length > 0) {
+ setStepDisabled({...stepDisabled, next: true});
+ } else {
+ setStepDisabled({...stepDisabled, next: false});
+ }
+ }
+ },
+ onError: () => {
+ if (reqInfo.cluster_id) {
+ message.error('请求出错');
+ getClusterInstance.cancel();
+ }
+ },
+ },
+ );
+
+
+ useEffect(() => {
+ if (reqInfo.cluster_id) {
+ console.log(reqInfo.cluster_id)
+ getClusterInstance.run(reqInfo.cluster_id);
+ }
+ }, [reqInfo.cluster_id]);
+
+
+ return (
+ 校验集群,
+ }}
+ >
+ setActiveKey(key)} type="card">
+ {nodeTypes.map(item => (
+
+
+ ))}
+
+ {activeKey === DorisNodeTypeEnum.FE && (
+
+ )}
+ {activeKey === DorisNodeTypeEnum.BE && (
+
+ )}
+ {activeKey === DorisNodeTypeEnum.BROKER && (
+
+ )}
+
+ );
+}
diff --git a/frontend/src/routes/space/access-cluster/steps/connect-cluster/connect-cluster.tsx b/frontend/src/routes/space/access-cluster/steps/connect-cluster/connect-cluster.tsx
new file mode 100644
index 0000000..8dc7d9d
--- /dev/null
+++ b/frontend/src/routes/space/access-cluster/steps/connect-cluster/connect-cluster.tsx
@@ -0,0 +1,130 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { useContext, useEffect, useState } from 'react';
+import { PageContainer } from '@ant-design/pro-layout';
+import { Button, Form, Input, Modal, Space } from 'antd';
+import { NewSpaceInfoContext } from '@src/common/common.context';
+import { SpaceAPI } from '@src/routes/space/space.api';
+import styles from '../../../space.less';
+import { stepDisabledState } from '../../access-cluster.recoil';
+import { useRecoilState } from 'recoil';
+
+const tip = {
+ default: '请进行链接测试',
+ fault: '链接测试未通过'
+}
+
+export function ConnectCluster(props: any) {
+ const { form, reqInfo, step } = useContext(NewSpaceInfoContext);
+ const [testFlag, setTestFlag] = useState('none');
+ const [stepDisabled, setStepDisabled] = useRecoilState(stepDisabledState);
+
+ useEffect(() => {
+ form.setFieldsValue({...reqInfo.clusterAccessInfo});
+ setStepDisabled({...stepDisabled, next: true});
+ }, [reqInfo.cluster_id, step]);
+
+
+ const handleLinkTest = () => {
+ const values = form.getFieldsValue();
+ SpaceAPI.spaceValidate({
+ address: values.address.trim(),
+ httpPort: values.httpPort,
+ passwd: values.passwd || '',
+ queryPort: values.queryPort,
+ user: values.user.trim(),
+ }).then(res => {
+ const { msg, data, code } = res;
+ if (code === 0) {
+ Modal.success({
+ title: "集群连接成功",
+ content: msg,
+ });
+ setStepDisabled({...stepDisabled, next: false});
+ setTestFlag('success')
+ } else {
+ Modal.error({
+ title: "集群连接失败",
+ content: msg,
+ });
+ setStepDisabled({...stepDisabled, next: true});
+ setTestFlag('failed');
+ }
+ });
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ handleLinkTest()}>链接测试
+
+ {
+ (testFlag !== 'success') &&
+
+ {testFlag === 'failed' ? tip.fault: tip.default}
+
+ }
+
+
+ );
+}
+
diff --git a/frontend/src/routes/space/access-cluster/steps/finish/finish.tsx b/frontend/src/routes/space/access-cluster/steps/finish/finish.tsx
new file mode 100644
index 0000000..d1e9c08
--- /dev/null
+++ b/frontend/src/routes/space/access-cluster/steps/finish/finish.tsx
@@ -0,0 +1,41 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { PageContainer } from '@ant-design/pro-layout';
+import { Result, Button, Checkbox, Form, Input, Row, Space } from 'antd';
+import React from 'react';
+import { useHistory } from 'react-router';
+
+export function SpaceAccessFinish(props: any) {
+ return (
+ 完成创建,
+ }}
+ >
+
+ 空间接管成功
+
+ }
+ />
+ ,
+
+ );
+}
diff --git a/frontend/src/routes/space/access-cluster/steps/managed-options/managed-options.tsx b/frontend/src/routes/space/access-cluster/steps/managed-options/managed-options.tsx
new file mode 100644
index 0000000..50ade76
--- /dev/null
+++ b/frontend/src/routes/space/access-cluster/steps/managed-options/managed-options.tsx
@@ -0,0 +1,78 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { Divider, Form, Input, PageHeader } from 'antd';
+import React, { useContext, useEffect } from 'react';
+import ProCard from '@ant-design/pro-card';
+import { NewSpaceInfoContext } from '@src/common/common.context';
+import TextArea from 'antd/lib/input/TextArea';
+
+export function ManagedOptions(props: any) {
+ const { form, reqInfo } = useContext(NewSpaceInfoContext);
+ useEffect(() => {
+ form.setFieldsValue({...reqInfo.authInfo});
+ }, [reqInfo.cluster_id]);
+ return (
+ 托管选项} headerBordered>
+
+
+ 请提前完成Manager节点与其他节点间SSH信任,并在下方填入Manager节点的SSH信息。如何进行SSH信任?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Doris与Doris Manager Agent将安装至该目录下。请确保该目录为Doris及相关组件专用。
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/routes/space/components/cluster-verify/cluster-verify.tsx b/frontend/src/routes/space/components/cluster-verify/cluster-verify.tsx
new file mode 100644
index 0000000..5e11214
--- /dev/null
+++ b/frontend/src/routes/space/components/cluster-verify/cluster-verify.tsx
@@ -0,0 +1,132 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { useContext, useEffect, useState } from 'react';
+import { PageContainer } from '@ant-design/pro-layout';
+import ProCard from '@ant-design/pro-card';
+import { Button, Row, Space, Table, Tabs } from 'antd';
+import { useHistory, useRouteMatch } from 'react-router';
+import TabPane from '@ant-design/pro-card/lib/components/TabPane';
+import { DorisNodeTypeEnum } from '../../new-cluster/types/params.type';
+import { SpaceAPI } from '../../space.api';
+import { isSuccess } from '@src/utils/http';
+import { NewSpaceInfoContext } from '@src/common/common.context';
+
+export function ClusterVerify(props: any) {
+ const history = useHistory();
+ const [activeKey, setActiveKey] = useState(DorisNodeTypeEnum.FE);
+ const {reqInfo} = useContext(NewSpaceInfoContext);
+ const match = useRouteMatch<{spaceId: string}>();
+ const clusterId = match.params.spaceId;
+ const [instance, setInstance] = useState([]);
+ const [nodeTypes, setNodeTypes] = useState([]);
+ const [feNodes, setFENodes] = useState([]);
+ const [beNodes, setBENodes] = useState([]);
+ const [brokerNodes, setBrokerNodes] = useState([]);
+
+
+ const columns = [
+ {
+ title: '序号',
+ dataIndex: 'instanceId',
+ key: 'instanceId',
+ },
+ {
+ title: '节点IP',
+ dataIndex: 'nodeHost',
+ key: 'nodeHost',
+ },
+ {
+ title: '安装进度',
+ dataIndex: 'operateStatus',
+ key: 'operateStatus',
+ },
+ {
+ title: '操作',
+ key: 'option',
+ render: () => (
+
+ 重试
+ {/* 跳过
+ 查看日志 */}
+
+ ),
+ },
+ ];
+
+ useEffect(() => {
+ if (reqInfo && reqInfo.cluster_id) {
+ getClusterInstance();
+ }
+ }, [reqInfo.cluster_id]);
+
+ async function getClusterInstance() {
+ const res = await SpaceAPI.getClusterInstance({clusterId: reqInfo.cluster_id});
+ if (isSuccess(res)) {
+ setInstance(res.data);
+ const types = [];
+ const feNodes = res.data.filter(item => item.moduleName?.toUpperCase() === DorisNodeTypeEnum.FE);
+ const beNodes = res.data.filter(item => item.moduleName?.toUpperCase() === DorisNodeTypeEnum.BE);
+ const brokerNodes = res.data.filter(item => item.moduleName?.toUpperCase() === DorisNodeTypeEnum.BROKER);
+ setFENodes(feNodes);
+ setBENodes(beNodes);
+ setBrokerNodes(brokerNodes);
+ if (feNodes.length > 0) {
+ types.push({key: DorisNodeTypeEnum.FE, tab: 'FE节点', moduleName: DorisNodeTypeEnum.FE });
+ }
+ if (beNodes.length > 0) {
+ types.push({key: DorisNodeTypeEnum.BE, tab: 'BE节点', moduleName: DorisNodeTypeEnum.BE });
+ }
+ if (brokerNodes.length > 0) {
+ types.push({key: DorisNodeTypeEnum.BROKER, tab: 'Broker节点', moduleName: DorisNodeTypeEnum.BROKER });
+ }
+ setNodeTypes(types);
+ }
+ }
+
+ // async function getClusterModule() {
+ // const res = await SpaceAPI.getClusterModule({clusterId: reqInfo.cluster_id});
+ // if (isSuccess(res)) {
+ // setNodeTypes(res.data);
+ // }
+ // }
+
+
+ return (
+ 校验集群,
+ }}
+ >
+ setActiveKey(key)} type="card">
+ {nodeTypes.map(item => (
+
+
+ ))}
+
+ {activeKey === DorisNodeTypeEnum.FE && (
+
+ )}
+ {activeKey === DorisNodeTypeEnum.BE && (
+
+ )}
+ {activeKey === DorisNodeTypeEnum.BROKER && (
+
+ )}
+
+ );
+}
diff --git a/frontend/src/routes/space/components/finish/finish.tsx b/frontend/src/routes/space/components/finish/finish.tsx
new file mode 100644
index 0000000..4eb8874
--- /dev/null
+++ b/frontend/src/routes/space/components/finish/finish.tsx
@@ -0,0 +1,69 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { PageContainer } from '@ant-design/pro-layout';
+import { NewSpaceInfoContext } from '@src/common/common.context';
+import { Result, Button, Checkbox, Form, Input, Row, Space } from 'antd';
+import React, { useContext } from 'react';
+import { useHistory } from 'react-router';
+
+export function SpaceCreateFinish(props: any) {
+ const { form } = useContext(NewSpaceInfoContext);
+ const history = useHistory();
+ const onFinish = (values: any) => {
+ console.log('Success:', values);
+ };
+ return (
+ 完成创建,
+ }}
+ >
+
+ 空间创建成功
+ 请设定集群root密码
+
+ }
+ extra={[
+ <>
+
+
+
+
+ >,
+ ]}
+ />
+ ,
+
+ );
+}
diff --git a/frontend/src/routes/space/components/node-verify/node-verify.data.ts b/frontend/src/routes/space/components/node-verify/node-verify.data.ts
new file mode 100644
index 0000000..86cb1e3
--- /dev/null
+++ b/frontend/src/routes/space/components/node-verify/node-verify.data.ts
@@ -0,0 +1,47 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { StepsProps } from "antd";
+
+export enum NodeVerifyStepEnum {
+ ACCESS_AUTH,
+ INSTALL_DIR_CHECK,
+ JDK_CHECK,
+ AGENT_DEPLOY,
+ AGENT_START,
+ AGENT_REGISTER,
+}
+
+export namespace NodeVerifyStepEnum {
+ export function getTitle(step: NodeVerifyStepEnum) {
+ switch(step) {
+ case NodeVerifyStepEnum.ACCESS_AUTH:
+ return 'SSH校验';
+ case NodeVerifyStepEnum.INSTALL_DIR_CHECK:
+ return '安装路径校验';
+ case NodeVerifyStepEnum.JDK_CHECK:
+ return 'JDK依赖检查';
+ case NodeVerifyStepEnum.AGENT_DEPLOY:
+ return 'Agent安装';
+ case NodeVerifyStepEnum.AGENT_START:
+ return 'Agent启动';
+ case NodeVerifyStepEnum.AGENT_REGISTER:
+ return 'Agent注册';
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/routes/space/components/node-verify/node-verify.tsx b/frontend/src/routes/space/components/node-verify/node-verify.tsx
new file mode 100644
index 0000000..6b0837a
--- /dev/null
+++ b/frontend/src/routes/space/components/node-verify/node-verify.tsx
@@ -0,0 +1,110 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { useContext, useLayoutEffect, useState } from 'react';
+import { PageContainer } from '@ant-design/pro-layout';
+import { Button, message, Steps, Table } from 'antd';
+import { NewSpaceInfoContext } from '@src/common/common.context';
+import { isSuccess } from '@src/utils/http';
+import { SpaceAPI } from '../../space.api';
+import { NodeVerifyStepEnum } from './node-verify.data';
+import { useRequest } from 'ahooks';
+import { IResult } from '@src/interfaces/http.interface';
+import { OperateStatusEnum } from '../../space.data';
+import { useRecoilState } from 'recoil';
+import { stepDisabledState } from '../../access-cluster/access-cluster.recoil';
+const Step = Steps.Step;
+
+export function NodeVerify(props: any) {
+ const {reqInfo} = useContext(NewSpaceInfoContext);
+ const [nodes, setNodes] = useState([]);
+ const [stepDisabled, setStepDisabled] = useRecoilState(stepDisabledState);
+
+ const getClusterNodes = useRequest, any>(
+ (clusterId: string) => {
+ return SpaceAPI.getClusterNodes({clusterId});
+ },
+ {
+ manual: true,
+ pollingInterval: 2000,
+ onSuccess: (res: any) => {
+ if (isSuccess(res)) {
+ const data: any[] = res.data;
+ setNodes(data);
+ if (data.filter(item => item.operateStatus === OperateStatusEnum.PROCESSING).length === 0) {
+ getClusterNodes.cancel();
+ }
+ if (data.filter(item => item.operateStatus !== OperateStatusEnum.SUCCESS).length > 0) {
+ setStepDisabled({...stepDisabled, next: true});
+ } else {
+ setStepDisabled({...stepDisabled, next: false});
+ }
+ }
+ },
+ onError: () => {
+ if (reqInfo.cluster_id) {
+ message.error('请求出错');
+ }
+ },
+ },
+ );
+
+
+ useLayoutEffect(() => {
+ if (reqInfo.cluster_id) {
+ getClusterNodes.run(reqInfo.cluster_id);
+ }
+ }, [reqInfo.cluster_id]);
+
+ const columns = [
+ {
+ title: '序号',
+ dataIndex: 'nodeId',
+ key: 'nodeId',
+ },
+ {
+ title: '节点IP',
+ dataIndex: 'host',
+ key: 'host',
+ },
+ {
+ title: '校验进度',
+ key: 'operateStage',
+ render: (record: any) => {
+ return (
+
+
+
+
+
+
+
+
+ )
+ }
+ }
+ ];
+ return (
+ 校验主机,
+ }}
+ >
+
+
+ );
+}
diff --git a/frontend/src/routes/space/components/result-modal.tsx b/frontend/src/routes/space/components/result-modal.tsx
new file mode 100644
index 0000000..48a3612
--- /dev/null
+++ b/frontend/src/routes/space/components/result-modal.tsx
@@ -0,0 +1,174 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { useEffect, useState } from 'react';
+import { PageContainer } from '@ant-design/pro-layout';
+import { Button, Row, Space, Table, Modal, message } from 'antd';
+import { useHistory } from 'react-router';
+import { nodeStatusQuery, fresh , modalState, stepState, processId} from '../new-cluster/recoils/index'
+import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
+import API from '../new-cluster/new-cluster.api';
+import { isSuccess } from '@src/utils/http';
+import * as types from '../new-cluster/types/index.type';
+import { NewClusterStepsEnum } from '../new-cluster/new-cluster.data'
+import ProCard from '@ant-design/pro-card';
+import { DorisNodeTypeEnum } from '../new-cluster/types/index.type';
+
+export function ResultModal(props: any) {
+ const history = useHistory();
+ // const nodeStatus = useRecoilValue(nodeStatusQuery);
+ const [modal, setModal] = useRecoilState(modalState);
+ const [refresh, setFresh] = useRecoilState(fresh);
+ const [step, setStep] = useRecoilState(stepState);
+ const processID = useRecoilValue(processId);
+ const [data, setData] = useState([]);
+ const [loading, setloading] = useState(false)
+ const columns = [
+ {
+ title: '序号',
+ dataIndex: 'id',
+ key: 'id',
+ },
+ {
+ title: '节点IP',
+ dataIndex: 'host',
+ key: 'host',
+ },
+ {
+ title: '状态信息',
+ dataIndex: 'status',
+ key: 'status',
+ render: (_, record:any) => (
+
+ {record.status} {record.result}
+
+ ),
+ },
+ {
+ title: '操作',
+ key: 'option',
+ render: (_, record:any) => (
+
+ reTry(record)}>重试
+ {/* doSkip(record)}>跳过
+ viewLog(record)}>查看日志 */}
+
+ ),
+ },
+ ];
+
+ const handleOk = (() => {
+ setStep(step + 1)
+ setModal({
+ ...modal,
+ visible: false
+ })
+ console.log(NewClusterStepsEnum[step + 1])
+ // window.location.reload() //todo
+ history.replace(`/cluster/new/${NewClusterStepsEnum[step + 1]}`)
+ })
+ const handleCancel = (() => {
+ setModal({
+ ...modal,
+ visible: false
+ })
+ })
+
+ const reTry = (record: any) => {
+ API.reTryTask(record.id).then(res => {
+ if(isSuccess(res)){
+ message.success(res.msg)
+ setFresh(refresh + 1)
+ setData([])
+ fetchNode();
+ }else{
+ message.warn(res.msg)
+ }
+ })
+ }
+ const doSkip = (record: any) => {
+ API.skipTask(record.id).then(res => {
+ if(isSuccess(res)){
+ message.success(res.msg)
+ setFresh(refresh + 1)
+ setData([])
+ fetchNode();
+ }else{
+ message.warn(res.msg)
+ }
+ })
+ }
+ const viewLog = (record: any) => {
+ history.push({
+ pathname: `/cluster/logs/${record.id}`
+ })
+ }
+
+ const fetchNode = () => {
+ setloading(true);
+ API.getTaskStatus(processID).then(res => {
+ setData(res.data)
+ setloading(false);
+ })
+ }
+
+ useEffect(() => {
+ fetchNode();
+ }, [modal.visible])
+
+ useEffect(() => {
+ let timer: any = null;
+ console.log('00000')
+ let len = data.filter(item => (item.status === types.taskStatusEnum.SUCCESS) || (item.status === types.taskStatusEnum.FAILURE)).length;
+ if(modal.visible && (len < data.length || data.length === 0)){
+ timer = window.setTimeout(() => {
+ setFresh(refresh + 1)
+ fetchNode()
+ }, 5000)
+ }
+ return () => {
+ window.clearTimeout(timer)
+ }
+ }, [refresh, modal.visible])
+
+ return (
+ 取消,
+ item.status === types.taskStatusEnum.SUCCESS).length) !== data.length}>下一步
+ // 下一步
+ ]}>
+ {
+ step === 0 ?
+
+ :
+ <>
+
+ (item.taskRole === DorisNodeTypeEnum.FE))} rowKey="id" pagination={false} loading={loading}/>
+
+
+ (item.taskRole === DorisNodeTypeEnum.BE))} rowKey="id" pagination={false} loading={loading}/>
+
+
+ (item.taskRole === DorisNodeTypeEnum.BROKER))} rowKey="id" pagination={false} loading={loading}/>
+
+ >
+ }
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/routes/space/components/space-register/space-register.tsx b/frontend/src/routes/space/components/space-register/space-register.tsx
new file mode 100644
index 0000000..530945b
--- /dev/null
+++ b/frontend/src/routes/space/components/space-register/space-register.tsx
@@ -0,0 +1,119 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { Col, Divider, InputNumber, message, Modal, Row, Select, Space, Tag } from 'antd';
+import React, { useContext, useEffect, useState } from 'react';
+import { Form, Input, Button, Radio } from 'antd';
+import styles from '../../space.less';
+type RequiredMark = boolean | 'optional';
+type testStatus = 'success' | 'fault' | 'none';
+import { useHistory } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+import { useRecoilValue } from 'recoil';
+import { SpaceAPI } from '@src/routes/space/space.api';
+import { usersQuery } from '@src/routes/space/space.recoil';
+import TextArea from 'antd/lib/input/TextArea';
+import { PageContainer } from '@ant-design/pro-layout';
+import { NewSpaceInfoContext } from '@src/common/common.context';
+
+export function SpaceRegister() {
+ const { t } = useTranslation();
+ const { form, reqInfo } = useContext(NewSpaceInfoContext);
+ const allUsers = useRecoilValue(usersQuery);
+
+ useEffect(() => {
+ form.setFieldsValue({...reqInfo.spaceInfo});
+ }, [reqInfo.cluster_id]);
+
+ return (
+ {t`Space Register`},
+ }}
+ >
+ {
+ if (!value) {
+ return Promise.reject(new Error(''));
+ }
+ let resData = await SpaceAPI.spaceCheck(value);
+ if (resData.code === 0) {
+ return Promise.resolve();
+ }
+ return Promise.reject(new Error(resData.msg));
+ },
+ },
+ ]}
+ validateTrigger="onBlur"
+ >
+
+
+
+
+
+
+ {
+ return (option?.title as string).toLowerCase().indexOf(input.toLowerCase()) >= 0;
+ }}
+ >
+ {allUsers
+ .filter(user => user.is_active)
+ .map(user => {
+ return (
+
+
+
+ {user.name}
+ {user.email ? `(${user.email})` : ''}
+
+
+
+ );
+ })}
+ {!allUsers.length && 无可选用户 }
+
+
+
+
+ );
+}
diff --git a/frontend/src/routes/space/detail/space-detail.tsx b/frontend/src/routes/space/detail/space-detail.tsx
new file mode 100644
index 0000000..a8a0bbc
--- /dev/null
+++ b/frontend/src/routes/space/detail/space-detail.tsx
@@ -0,0 +1,225 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { useEffect, useState } from 'react';
+import styles from '../space.less';
+import { Button, Form, Input, Row, Space, Select, Col, InputNumber, Tag } from 'antd';
+import { Divider, message } from 'antd';
+import { SpaceAPI } from '../space.api';
+import { useHistory, useParams } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+import { RequiredMark } from 'antd/lib/form/Form';
+import { modal } from '@src/components/doris-modal/doris-modal';
+import { useRecoilValue } from 'recoil';
+import { usersQuery } from '../space.recoil';
+import { useUserInfo } from '@src/hooks/use-userinfo.hooks';
+
+export function SpaceDetail() {
+ const { t } = useTranslation();
+ const [form] = Form.useForm();
+ const [userInfo, setUserInfo] = useUserInfo();
+ const params = useParams<{ spaceId: string }>();
+ const [buttonType, setButtonType] = useState<'primary' | 'default'>('default');
+ const [saveButtonDisable, setSaveButtonDisable] = useState(true);
+ const [initForm, setInitForm] = useState({});
+ const history = useHistory();
+ const allUsers = useRecoilValue(usersQuery);
+ function getSpaceInfo() {
+ SpaceAPI.spaceGet(params.spaceId).then(res => {
+ const { msg, data, code } = res;
+ if (code === 0) {
+ if (res.data) {
+ form.setFieldsValue({ ...res.data });
+ setInitForm(res.data);
+ if (allUsers && allUsers?.length) {
+ setInitForm({
+ ...res.data,
+ spaceAdminUser: allUsers,
+ });
+ }
+ }
+ } else {
+ message.error(msg);
+ }
+ });
+ }
+ useEffect(() => {
+ getSpaceInfo();
+ }, []);
+
+ function handleDelete() {
+ const spaceId = params.spaceId;
+ modal.confirm(t`notice`, t`SpaceDeleteTips`, async () => {
+ SpaceAPI.spaceDelete(spaceId).then(result => {
+ if (result && result.code !== 0) {
+ modal.error('空间删除失败', result.msg);
+ } else {
+ modal.success('空间删除成功').then(result => {
+ if (result.isConfirmed) {
+ history.push(`/space/list`);
+ }
+ });
+ }
+ });
+ });
+ }
+
+ const handleSave = () => {
+ form.validateFields().then(value => {
+ SpaceAPI.spaceUpdate({
+ describe: value.describe,
+ name: value.name.trim(),
+ spaceAdminUsers: value.spaceAdminUserId,
+ spaceId: params.spaceId,
+ }).then(res => {
+ console.log(res);
+ if (res.code === 0) {
+ message.success(res.msg);
+ setSaveButtonDisable(true)
+ // history.push('/')
+ } else {
+ message.error(res.msg);
+ }
+ });
+ });
+ };
+
+ const handleMeta = () => {
+ SpaceAPI.metaOption().then(res => {
+ if (res.code === 0) {
+ setButtonType('primary');
+ window.setTimeout(() => {
+ setButtonType('default');
+ }, 2000);
+ } else {
+ message.error(res.msg);
+ }
+ });
+ };
+
+ return (
+
+
+
+ {
+ if (!value) {
+ return Promise.reject(new Error(t`required`));
+ }
+ if (value === initForm?.name) {
+ return Promise.resolve();
+ }
+ let resData = await SpaceAPI.spaceCheck(value);
+ if (resData.code === 0) {
+ return Promise.resolve();
+ }
+ return Promise.reject(new Error(resData.msg));
+ },
+ },
+ ]}
+ validateTrigger="onBlur"
+ >
+
+
+
+
+
+
+ {
+ return (option?.title as string).toLowerCase().indexOf(input.toLowerCase()) >= 0;
+ }}
+ onChange={() => {
+ if (!saveButtonDisable) return;
+ setSaveButtonDisable(false);
+ }}
+ >
+ {initForm?.spaceAdminUser?.map((user: any, index: number) => {
+ return (
+
+
+
+ {user.name}
+ {user.email ? `(${user.email})` : ''}
+
+
+
+ );
+ })}
+
+
+
+
+
+
+
+
+ {/* {
+ history.push(`/space/list`);
+ }}
+ >
+ 取消
+
+ {
+ userInfo?.is_super_admin && (
+
+ 删除
+
+ )
+ } */}
+ {/* {
+ userInfo?.is_admin && ( */}
+
+ {t`Save`}
+
+ {/* )
+ } */}
+
+
+
+ );
+}
diff --git a/frontend/src/routes/space/list/list.less b/frontend/src/routes/space/list/list.less
new file mode 100644
index 0000000..448b186
--- /dev/null
+++ b/frontend/src/routes/space/list/list.less
@@ -0,0 +1,46 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+.noSpace{
+ display: flex;
+ width: 100%;
+ height: 80vh;
+ .noSpace-user, .noSpace-space {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ .img{
+ width: 30%;
+ height: 30%;
+ margin-bottom: 30px;
+ img{
+ width: 100%;
+ height: 100%;
+ }
+ }
+ .title{
+ font-size: 16px;
+ margin-bottom: 10px;
+ }
+ .desc{
+ font-size: 10px;
+ margin-bottom: 10px;
+ }
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/routes/space/list/list.tsx b/frontend/src/routes/space/list/list.tsx
new file mode 100644
index 0000000..3c06cc1
--- /dev/null
+++ b/frontend/src/routes/space/list/list.tsx
@@ -0,0 +1,239 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+import React, { useContext, useEffect, useState } from 'react';
+import { Button, Dropdown, Menu, Row, Table, Tabs } from 'antd';
+import { Space } from 'antd';
+import { SpaceAPI } from '../space.api';
+import moment from 'moment';
+import { FlatBtn, FlatBtnGroup } from '@src/components/flatbtn';
+import { TABLE_DELAY } from '@src/config';
+import { useHistory } from 'react-router';
+import { PageContainer } from '@ant-design/pro-layout';
+import { useRequest } from 'ahooks';
+import { DownOutlined } from '@ant-design/icons';
+import { modal } from '@src/components/doris-modal/doris-modal';
+import { AccessClusterStepsEnum, ACCESS_CLUSTER_REQUEST_INIT_PARAMS } from '../access-cluster/access-cluster.data';
+import { useRecoilState } from 'recoil';
+import { requestInfoState } from '../access-cluster/access-cluster.recoil';
+import { UserInfoContext } from '@src/common/common.context';
+import { useTranslation } from 'react-i18next';
+import { NewClusterStepsEnum } from '../new-cluster/new-cluster.data';
+const { TabPane } = Tabs;
+type SpaceListType = 'finished' | 'draft';
+
+export const SpaceList = () => {
+ const userInfo = useContext(UserInfoContext);
+ const history = useHistory();
+ const [activeKey, setActiveKey] = useState('finished');
+ const [requestInfo, setRequestInfo] = useRecoilState(requestInfoState);
+ const { t } = useTranslation();
+
+ const {
+ data: spaceList,
+ loading,
+ run: getSpaceList,
+ } = useRequest(
+ async () => {
+ const res = await SpaceAPI.spaceList();
+ return res.data;
+ },
+ { manual: true },
+ );
+
+ const {
+ data: draftSpaceList,
+ loading: draftLoading,
+ run: getDraftSpaceList,
+ } = useRequest(
+ async () => {
+ const res = await SpaceAPI.spaceList();
+ return res.data;
+ },
+ { manual: true },
+ );
+
+ useEffect(() => {
+ refresh();
+ }, [activeKey]);
+
+ function refresh() {
+ if (activeKey === 'finished') {
+ getSpaceList();
+ } else {
+ getDraftSpaceList();
+ }
+ }
+
+ async function recover(record: any) {
+ record.requestInfo.type === 'CREATION'
+ ? history.push(`/space/new/${record.requestId}/${NewClusterStepsEnum[record.eventType]}`)
+ : history.push(`/space/access/${record.requestId}/${AccessClusterStepsEnum[record.eventType]}`);
+ }
+
+ const enterSpace = (record: any) => {
+ SpaceAPI.switchSpace(record.id).then(res => {
+ if (res.code === 0) {
+ history.push('/');
+ }
+ });
+ };
+
+ function deleteSpace(record: any) {
+ modal.confirm(t`Notice`, t`SpaceDeleteTips`, async () => {
+ SpaceAPI.spaceDelete(record.id).then(result => {
+ if (result && result.code !== 0) {
+ modal.error('失败', result.msg);
+ } else {
+ modal.success(t`Delete Success`).then(result => {
+ if (result.isConfirmed) {
+ refresh();
+ }
+ });
+ }
+ });
+ });
+ }
+
+ const columns = [
+ {
+ title: t`Space Name`,
+ dataIndex: 'name',
+ key: 'name',
+ },
+ {
+ title: t`CreationTime`,
+ dataIndex: 'createTime',
+ key: 'createTime',
+ render: (createTime: string) => {moment(new Date(createTime)).format('YYYY-MM-DD HH:mm:ss')} ,
+ },
+ {
+ title: t`Status`,
+ dataIndex: 'status',
+ key: 'status',
+ },
+ {
+ title: t`Actions`,
+ dataIndex: 'status',
+ key: 'status',
+ width: '240px',
+ render: (text: string, record: any) => (
+
+ enterSpace(record)}>{t`Enter Space`}
+ deleteSpace(record)}>{t`Delete`}
+ {t`Edit`}
+
+ ),
+ },
+ ];
+
+ const draftColumns = [
+ {
+ title: t`Space Name`,
+ dataIndex: 'name',
+ key: 'name',
+ },
+ {
+ title: t`CreationTime`,
+ dataIndex: 'createTime',
+ key: 'createTime',
+ render: (createTime: string) => {moment(new Date(createTime)).format('YYYY-MM-DD HH:mm:ss')} ,
+ },
+ {
+ title: t`Creator`,
+ dataIndex: 'creator',
+ key: 'creator',
+ },
+ {
+ title: t`Actions`,
+ dataIndex: 'status',
+ key: 'status',
+ width: '200px',
+ render: (text: string, record: any) => (
+
+ recover(record)}>{t`Recover`}
+ deleteSpace(record)}>{t`Delete`}
+
+ ),
+ },
+ ];
+ const menu = (
+
+ {
+ setRequestInfo(ACCESS_CLUSTER_REQUEST_INIT_PARAMS);
+ history.push('/space/new/0');
+ }}
+ key="1"
+ >{t`New Cluster`}
+ {
+ setRequestInfo(ACCESS_CLUSTER_REQUEST_INIT_PARAMS);
+ history.push('/space/access/0');
+ }}
+ key="2"
+ >{t`Cluster hosting`}
+
+ );
+
+ return (
+ {t`Space List`},
+ }}
+ >
+ setActiveKey(key)}
+ type="card"
+ tabBarExtraContent={
+ <>
+ {userInfo?.is_super_admin && (
+
+
+
+
+ {t`New Space`}
+
+
+
+
+ )}
+ >
+ }
+ >
+
+ {userInfo?.is_super_admin && }
+
+ {activeKey === 'finished' && (
+ list.requestCompleted)}
+ rowKey="name"
+ loading={{ spinning: loading, delay: TABLE_DELAY }}
+ />
+ )}
+ {activeKey === 'draft' && (
+ !list.requestCompleted)}
+ rowKey="name"
+ loading={{ spinning: loading, delay: TABLE_DELAY }}
+ />
+ )}
+
+ );
+};
diff --git a/frontend/src/routes/space/new-cluster/logs/logs.tsx b/frontend/src/routes/space/new-cluster/logs/logs.tsx
new file mode 100644
index 0000000..8d0d391
--- /dev/null
+++ b/frontend/src/routes/space/new-cluster/logs/logs.tsx
@@ -0,0 +1,48 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, {useEffect, useState} from 'react';
+import { PageContainer } from '@ant-design/pro-layout';
+import { useHistory, useParams } from 'react-router';
+import API from '../new-cluster.api';
+import { Button } from 'antd';
+
+export function Logs(props: any) {
+ const history = useHistory();
+ const params = useParams<{taskId: string}>();
+ const [logText, setlogText] = useState('');
+
+ useEffect(() => {
+ API.getTaskLog(params.taskId).then(res => {
+ setlogText(res.data.log)
+ })
+ }, [])
+ return (
+ {history.goBack()}}>返回,
+ ]}
+ >
+
+ {logText}
+
+
+ );
+}
diff --git a/frontend/src/routes/space/new-cluster/new-cluster.api.ts b/frontend/src/routes/space/new-cluster/new-cluster.api.ts
new file mode 100644
index 0000000..c869bdc
--- /dev/null
+++ b/frontend/src/routes/space/new-cluster/new-cluster.api.ts
@@ -0,0 +1,91 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { IResult } from "@src/interfaces/http.interface";
+import { API_SERVER_PREFIX } from "@src/utils/api";
+import { http } from "@src/utils/http";
+import * as types from "./types/index.type";
+
+
+function getTaskStatus(data: number | string ): Promise> {
+ return http.get(`${API_SERVER_PREFIX}/process/${data}/currentTasks`);
+}
+function getTaskLog(taskId: number | string ): Promise> {
+ return http.get(`${API_SERVER_PREFIX}/process/task/log/${taskId}`);
+}
+function reTryTask(taskId: number | string ): Promise> {
+ return http.post(`${API_SERVER_PREFIX}/process/task/retry/${taskId}`);
+}
+function skipTask(taskId: number | string ): Promise> {
+ return http.post(`${API_SERVER_PREFIX}/process/task/skip/${taskId}`);
+}
+
+function getRoleList(data: {clusterId: number | string}): Promise> {
+ return http.get(`${API_SERVER_PREFIX}/server/roleList`, data);
+}
+
+
+function createCluster(data?: types.CreateClusterRequestParams): Promise> {
+ return http.post(`${API_SERVER_PREFIX}/server/installAgent`, data);
+}
+
+function installService(data?: types.InstallServiceRequestParams): Promise> {
+ return http.post(`${API_SERVER_PREFIX}/agent/installService`, data);
+}
+function getNodeHardware(data: {clusterId: number | string}): Promise> {
+ return http.get(`${API_SERVER_PREFIX}/agent/hardware/0`);
+}
+
+function deployConfig(data?: types.DeployConfigRequestParams): Promise> {
+ return http.post(`${API_SERVER_PREFIX}/agent/deployConfig`, data);
+}
+
+function startService(data?: types.StartServiceRequestParams): Promise> {
+ return http.post(`${API_SERVER_PREFIX}/agent/startService`, data);
+}
+function startCluster(data?: types.StartClusterRequestParams): Promise> {
+ return http.post(`${API_SERVER_PREFIX}/agent/buildCluster`, data);
+}
+function installComplete(processId: number): Promise> {
+ return http.post(`/api/process/installComplete/${processId}`);
+}
+
+function getCurrentProcess(): Promise> {
+ return http.get(`/api/process/currentProcess`);
+}
+
+
+function goBackProcess(processId: number): Promise> {
+ return http.post(`/api/process/back/${processId}`);
+}
+export default {
+ getTaskStatus,
+ getTaskLog,
+ reTryTask,
+ skipTask,
+ getRoleList,
+
+ createCluster,
+ installService,
+ getNodeHardware,
+ deployConfig,
+ startService,
+ startCluster,
+ getCurrentProcess,
+ goBackProcess,
+ installComplete
+}
\ No newline at end of file
diff --git a/frontend/src/routes/space/new-cluster/new-cluster.data.ts b/frontend/src/routes/space/new-cluster/new-cluster.data.ts
new file mode 100644
index 0000000..6a081b4
--- /dev/null
+++ b/frontend/src/routes/space/new-cluster/new-cluster.data.ts
@@ -0,0 +1,27 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+export enum NewClusterStepsEnum {
+ 'register-space',
+ 'add-node',
+ 'install-options',
+ 'verify-node',
+ 'cluster-plan',
+ 'node-config',
+ 'cluster-deploy',
+ 'finish',
+}
\ No newline at end of file
diff --git a/frontend/src/routes/space/new-cluster/new-cluster.tsx b/frontend/src/routes/space/new-cluster/new-cluster.tsx
new file mode 100644
index 0000000..830c2e8
--- /dev/null
+++ b/frontend/src/routes/space/new-cluster/new-cluster.tsx
@@ -0,0 +1,237 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import ProCard from '@ant-design/pro-card';
+import { Button, message, Row, Space, Steps } from 'antd';
+import React, { useEffect, useState } from 'react';
+import { Switch, Route, Redirect, useRouteMatch, useHistory } from 'react-router';
+import { NewClusterStepsEnum } from './new-cluster.data';
+import { ClusterPlan } from './steps/cluster-plan/cluster-plan';
+import { NodeConfig } from './steps/node-config/node-config';
+import { RunCluster } from './steps/run-cluster/run-cluster';
+import { stepState, CurrentProcessQuery, processId } from './recoils/index';
+import { useRecoilState, useRecoilValue } from 'recoil';
+import CacheRoute, { CacheSwitch } from 'react-router-cache-route';
+import { pathToRegexp } from 'path-to-regexp';
+import { AddNode } from './steps/add-node/add-node';
+import { InstallOptions } from './steps/install-options/install-options';
+import { ClusterDeploy } from './steps/cluster-deploy/cluster-deploy';
+import { SpaceCreateFinish } from '../components/finish/finish';
+import { NewSpaceInfoContext } from '@src/common/common.context';
+import { useForm } from 'antd/lib/form/Form';
+import { SpaceRegister } from '../components/space-register/space-register';
+import { NodeVerify } from '../components/node-verify/node-verify';
+import { SpaceAPI } from '../space.api';
+import { isSuccess } from '@src/utils/http';
+import { requestInfoState, stepDisabledState } from '../access-cluster/access-cluster.recoil';
+const { Step } = Steps;
+
+const PREV_DISABLED_STEPS = [NewClusterStepsEnum[3], NewClusterStepsEnum[6], NewClusterStepsEnum[7]];
+const NEXT_DISABLED_STEPS = [NewClusterStepsEnum[3], NewClusterStepsEnum[6]];
+
+export function NewCluster(props: any) {
+ const match = useRouteMatch<{ requestId: string }>();
+ const history = useHistory();
+ const [step, setStep] = React.useState(0);
+ const [stepDisabled, setStepDisabled] = useRecoilState(stepDisabledState);
+ // const [curStep, setStepState] = useRecoilState(stepState);
+ const [curProcessId, setProcessId] = useRecoilState(processId);
+ const [requestInfo, setRequestInfo] = useRecoilState(requestInfoState);
+ const [loading, setLoading] = useState(false);
+
+ useEffect(() => {
+ if (history.location.pathname === '/space/list') {
+ return;
+ }
+ const regexp = pathToRegexp(`${match.path}/:step`);
+ const paths = regexp.exec(history.location.pathname);
+ const step = (paths as string[])[2];
+ setStep(NewClusterStepsEnum[step]);
+ setStepDisabled({
+ ...stepDisabled,
+ next: NEXT_DISABLED_STEPS.includes(step),
+ prev: PREV_DISABLED_STEPS.includes(step),
+ });
+ if (match.params.requestId && +match.params.requestId !== 0) {
+ getRequestInfo();
+ }
+ }, [history.location.pathname, step]);
+
+ const [form] = useForm();
+
+ async function getRequestInfo() {
+ const requestId = match.params.requestId;
+ const res = await SpaceAPI.getRequestInfo(requestId);
+ if (isSuccess(res)) {
+ setRequestInfo(res.data);
+ }
+ }
+
+ async function nextStep() {
+ const value = form.getFieldsValue();
+ const newStep = step + 1;
+ setLoading(true);
+ const params = {
+ ...requestInfo.reqInfo,
+ cluster_id: requestInfo.clusterId,
+ request_id: requestInfo.requestId,
+ event_type: (step + 1).toString(),
+ };
+ if (value && step === NewClusterStepsEnum['register-space']) {
+ params.spaceInfo = {
+ describe: value.describe,
+ name: value.name,
+ spaceAdminUsers: value.spaceAdminUsers,
+ };
+ }
+ if (value && step === NewClusterStepsEnum['add-node']) {
+ params.authInfo = {
+ sshKey: value.sshKey,
+ sshPort: parseInt(value.sshPort),
+ sshUser: value.sshUser,
+ };
+ params.hosts = value.hosts;
+ }
+ if (value && step === NewClusterStepsEnum['install-options']) {
+ params.installInfo = value.installDir;
+ params.packageInfo = value.packageUrl;
+ }
+ if (value && step === NewClusterStepsEnum['cluster-plan']) {
+ params.nodeConfig = value.nodeConfig;
+ }
+ if (value && step === NewClusterStepsEnum['node-config']) {
+ params.deployConfigs = value.deployConfigs;
+ }
+ console.log(params);
+ const res = await SpaceAPI.createCluster(params);
+ setLoading(false);
+ if (isSuccess(res)) {
+ setRequestInfo(res.data);
+ setStep(newStep);
+ setTimeout(() => {
+ history.push(`/space/new/${res.data.requestId}/${NewClusterStepsEnum[newStep]}`);
+ }, 0);
+ } else {
+ message.error(res.msg);
+ }
+ }
+
+ function prevStep() {
+ const newStep = step - 1;
+ setStep(newStep);
+ history.push(`/space/new/${requestInfo.requestId}/${NewClusterStepsEnum[newStep]}`);
+ }
+
+ async function finish() {
+ const value = form.getFieldsValue();
+ setLoading(true);
+ const params = {
+ ...requestInfo.reqInfo,
+ cluster_id: requestInfo.clusterId,
+ request_id: requestInfo.requestId,
+ event_type: (step + 1).toString(),
+ };
+ params.clusterPassword = value.clusterPassword;
+ console.log(params);
+ const res = await SpaceAPI.createCluster(params);
+ setLoading(false);
+ if (isSuccess(res)) {
+ history.push(`/space/list`);
+ } else {
+ message.error(res.msg);
+ }
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {step === 0 ? (
+ <>>
+ ) : (
+ {
+ prevStep();
+ }}
+ disabled={stepDisabled.prev}
+ >
+ 上一步
+
+ )}
+ {step === NewClusterStepsEnum['finish'] ? (
+ {
+ finish();
+ }}
+ >
+ 完成
+
+ ) : (
+ {
+ nextStep();
+ }}
+ disabled={stepDisabled.next}
+ loading={loading}
+ >
+ 下一步
+
+ )}
+
+
+
+
+
+ >
+ );
+}
diff --git a/frontend/src/routes/space/new-cluster/recoils/index.ts b/frontend/src/routes/space/new-cluster/recoils/index.ts
new file mode 100644
index 0000000..bf45149
--- /dev/null
+++ b/frontend/src/routes/space/new-cluster/recoils/index.ts
@@ -0,0 +1,20 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+export * from "./result.recoil"
+export * from "./node.recoil"
+export * from "./step.recoil"
\ No newline at end of file
diff --git a/frontend/src/routes/space/new-cluster/recoils/node.recoil.ts b/frontend/src/routes/space/new-cluster/recoils/node.recoil.ts
new file mode 100644
index 0000000..6bdbd5b
--- /dev/null
+++ b/frontend/src/routes/space/new-cluster/recoils/node.recoil.ts
@@ -0,0 +1,33 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import {atom, selector} from 'recoil'
+import API from '../new-cluster.api'
+import * as types from '../types/index.type'
+import {} from './result.recoil'
+
+export const nodeHardwareQuery = selector({
+ key: "NodeHardwareQuery",
+ get: async ( {get} ) => {
+ const response = await API.getNodeHardware({clusterId: 0}); // todo
+ if(response.code === 0){
+ return response.data
+ }else{
+ return []
+ }
+ }
+})
\ No newline at end of file
diff --git a/frontend/src/routes/space/new-cluster/recoils/result.recoil.ts b/frontend/src/routes/space/new-cluster/recoils/result.recoil.ts
new file mode 100644
index 0000000..18f64c6
--- /dev/null
+++ b/frontend/src/routes/space/new-cluster/recoils/result.recoil.ts
@@ -0,0 +1,73 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import {atom, selector} from 'recoil'
+import API from '../new-cluster.api'
+import * as types from '../types/index.type'
+import { DorisNodeTypeEnum } from '../types/index.type';
+
+export const fresh = atom({
+ key: 'fresh',
+ default: 1,
+});
+
+export const modalState = atom({
+ key: 'modalState',
+ default: {
+ visible: false
+ },
+});
+
+export const processId = atom({
+ key: 'processId',
+ default: 1,
+});
+
+export const nodeStatusQuery = selector({
+ key: "NodeStatusQuery",
+ get: async ( { get } ) => {
+ const f = get(fresh);
+ const g = get(modalState);
+ const response = await API.getTaskStatus(get(processId));
+ if(response.code === 0){
+ return response.data
+ }else{
+ return []
+ }
+ }
+})
+
+
+export const roleListQuery = selector<{fe:string[], be: string[]}>({
+ key: "RoleListQuery",
+ get: async ( {get} ) => {
+ const response = await API.getRoleList({ clusterId: 0});
+ if(response.code === 0){
+ const FE = response.data.filter(item => item.role === DorisNodeTypeEnum.FE).map(item => item.host);
+ const BE = response.data.filter(item => item.role === DorisNodeTypeEnum.BE).map(item => item.host);
+ return {
+ fe: FE,
+ be: BE
+ }
+ }else{
+ return {
+ fe: [],
+ be: []
+ }
+ }
+ }
+})
diff --git a/frontend/src/routes/space/new-cluster/recoils/step.recoil.ts b/frontend/src/routes/space/new-cluster/recoils/step.recoil.ts
new file mode 100644
index 0000000..794fb94
--- /dev/null
+++ b/frontend/src/routes/space/new-cluster/recoils/step.recoil.ts
@@ -0,0 +1,39 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import {atom, selector} from 'recoil'
+import API from '../new-cluster.api'
+import * as types from '../types/index.type'
+
+
+export const stepState = atom({
+ key: 'stepState',
+ default: 0,
+});
+
+export const CurrentProcessQuery = selector({
+ key: "CurrentProcessQuery",
+ get: async ( { get } ) => {
+ const curstep = get(stepState);
+ const response = await API.getCurrentProcess();
+ if(response.code === 0){
+ return response.data
+ }else{
+ return {}
+ }
+ }
+})
diff --git a/frontend/src/routes/space/new-cluster/steps/add-node/add-node.interface.ts b/frontend/src/routes/space/new-cluster/steps/add-node/add-node.interface.ts
new file mode 100644
index 0000000..c3a60cf
--- /dev/null
+++ b/frontend/src/routes/space/new-cluster/steps/add-node/add-node.interface.ts
@@ -0,0 +1,26 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+export interface CreateClusterRequestParams {
+ clusterId: string;
+ packageUrl: string;
+ installDir: string;
+ hosts: string[];
+ user: string;
+ sshPort: number;
+ sshKey: string;
+}
diff --git a/frontend/src/routes/space/new-cluster/steps/add-node/add-node.tsx b/frontend/src/routes/space/new-cluster/steps/add-node/add-node.tsx
new file mode 100644
index 0000000..1d370de
--- /dev/null
+++ b/frontend/src/routes/space/new-cluster/steps/add-node/add-node.tsx
@@ -0,0 +1,66 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { Button, Divider, Form, Input, message, Modal, PageHeader, Row, Space, Steps, Table } from 'antd';
+import React, { useContext, useMemo, useRef, useState } from 'react';
+import ProCard from '@ant-design/pro-card';
+import { NewSpaceInfoContext, UserInfoContext } from '@src/common/common.context';
+import {NodeList} from './node-list/node-list';
+
+const { TextArea } = Input;
+
+export function AddNode(props: any) {
+ const userInfo = useContext(UserInfoContext);
+ const {form} = useContext(NewSpaceInfoContext);
+ return (
+ 添加节点} headerBordered>
+
+ 请提前完成Manager节点与其他节点间SSH信任,并在下方填入Manager节点的SSH信息。如何进行SSH信任?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/routes/space/new-cluster/steps/add-node/node-list/node-list.tsx b/frontend/src/routes/space/new-cluster/steps/add-node/node-list/node-list.tsx
new file mode 100644
index 0000000..2e028d4
--- /dev/null
+++ b/frontend/src/routes/space/new-cluster/steps/add-node/node-list/node-list.tsx
@@ -0,0 +1,187 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { EditableProTable, ProColumns } from '@ant-design/pro-table';
+import { Row, Button, Input, message, Modal } from 'antd';
+import TextArea from 'antd/lib/input/TextArea';
+import React, { useEffect, useRef, useState } from 'react';
+type DataSourceType = {
+ id: React.Key;
+ ip: string;
+ order: number;
+};
+const reg =
+ /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/;
+
+export function NodeList(props: { value?: any; onChange?: any }) {
+ const [dataSource, setDataSource] = useState([]);
+ const [position, setPosition] = useState<'top' | 'bottom' | 'hidden'>('bottom');
+ const [isModalVisible, setIsModalVisible] = useState(false);
+ const [editableKeys, setEditableRowKeys] = useState([]);
+ const [ipList, setIpList] = useState('');
+
+ const columns: ProColumns[] = [
+ {
+ title: '序号',
+ dataIndex: 'order',
+ editable: false,
+ width: '30%',
+ render: (dom, rowData, index) => {
+ return {`${index}`} ;
+ },
+ },
+ {
+ title: 'IP地址',
+ dataIndex: 'ip',
+ renderFormItem: (dom, rowData, index) => ,
+ render: (dom, rowData, index) => {
+ return {`${rowData.ip}`} ;
+ },
+ },
+ {
+ title: '操作',
+ valueType: 'option',
+ width: 200,
+ render: (text, record, _, action) => [
+ {
+ action?.startEditable?.(record.id);
+ }}
+ >
+ 编辑
+ ,
+ {
+ setDataSource(dataSource.filter(item => item.id !== record.id));
+ }}
+ >
+ 删除
+ ,
+ ],
+ },
+ ];
+
+ const IpInput: React.FC<{
+ value?: string;
+ onChange?: (value: string) => void;
+ }> = ({ value, onChange }) => {
+ const ref = useRef (null);
+ const handleInputConfirm = (val: string) => {
+ if (reg.test(val)) {
+ if (dataSource.filter(item => item.ip === val).length) {
+ message.error('此ip已存在!');
+ } else {
+ onChange?.(val);
+ }
+ } else {
+ message.error('请确认ip输入是否正确');
+ }
+ };
+
+ return (
+ handleInputConfirm(e.target.value)}
+ />
+ );
+ };
+
+ useEffect(() => {
+ props?.onChange(dataSource.map(item => item.ip));
+ }, [dataSource.length]);
+
+ function handleOk() {
+ let baseLength = dataSource.length || 0;
+ let hosts = [];
+ let ips = [...new Set(ipList.split(/[,,\s\n]/))]
+ .filter(item => reg.test(item))
+ .map((item, index) => ({
+ id: (Math.random() * 1000000).toFixed(0),
+ order: baseLength + index + 1,
+ ip: item,
+ }));
+ if (baseLength) {
+ let tmp = ips.reduce((cur: { id: string; order: number; ip: string }[], next) => {
+ if (!dataSource.filter(item => item.ip === next.ip).length) {
+ return [...cur, next];
+ }
+ return [...cur];
+ }, []);
+ hosts = [...dataSource, ...tmp];
+ setDataSource(hosts);
+ } else {
+ hosts = [...ips];
+ setDataSource(hosts);
+ }
+ setIpList('');
+ setIsModalVisible(false);
+ }
+ return (
+
+
+ {
+ setIsModalVisible(true);
+ }}
+ >
+ 批量添加
+
+
+
+ rowKey="id"
+ maxLength={5}
+ recordCreatorProps={{
+ position: position as 'top',
+ record: () => ({
+ id: (Math.random() * 1000000).toFixed(0),
+ order: dataSource.length + 1,
+ ip: '',
+ }),
+ }}
+ columns={columns}
+ value={dataSource}
+ onChange={value => {
+ const hosts = value.map(item => item.ip);
+ setDataSource(value.filter(item => item.ip));
+ }}
+ editable={{
+ type: 'multiple',
+ editableKeys,
+ onSave: async (rowKey, data, row) => {
+ console.log(rowKey, data, row);
+ },
+ onChange: setEditableRowKeys,
+ }}
+ />
+ setIsModalVisible(false)}>
+
+
+ );
+}
diff --git a/frontend/src/routes/space/new-cluster/steps/cluster-deploy/cluster-deploy.tsx b/frontend/src/routes/space/new-cluster/steps/cluster-deploy/cluster-deploy.tsx
new file mode 100644
index 0000000..4efae58
--- /dev/null
+++ b/frontend/src/routes/space/new-cluster/steps/cluster-deploy/cluster-deploy.tsx
@@ -0,0 +1,183 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { useContext, useEffect, useState } from 'react';
+import { PageContainer } from '@ant-design/pro-layout';
+import ProCard from '@ant-design/pro-card';
+import { Button, Row, Space, Table, Tabs, message, Steps } from 'antd';
+import { LoadingOutlined, PlayCircleFilled } from '@ant-design/icons';
+import { useHistory } from 'react-router';
+import { useRequest } from 'ahooks';
+import TabPane from '@ant-design/pro-card/lib/components/TabPane';
+import { DorisNodeTypeEnum } from '../../types/params.type';
+import { IResult } from '@src/interfaces/http.interface';
+import { SpaceAPI } from '@src/routes/space/space.api';
+import { OperateStatusEnum } from '@src/routes/space/space.data';
+import { isSuccess } from '@src/utils/http';
+import { NewSpaceInfoContext } from '@src/common/common.context';
+import { useDidCache, useDidRecover } from 'react-router-cache-route';
+import { useRecoilState } from 'recoil';
+import { stepDisabledState } from '@src/routes/space/access-cluster/access-cluster.recoil';
+
+const { Step } = Steps;
+
+const ERROR_STATUS = [OperateStatusEnum.FAIL, OperateStatusEnum.CANCEL];
+
+const STEP_MAP = {
+ GET_INSTALL_PACKAGE: '获取安装包',
+ NODE_CONF: '下发节点配置',
+ START_UP_NODE: '启动节点',
+};
+
+export function ClusterDeploy(props: any) {
+ const { reqInfo, step } = useContext(NewSpaceInfoContext);
+ const history = useHistory();
+ const [activeKey, setActiveKey] = useState(DorisNodeTypeEnum.FE);
+ const [instances, setInstances] = useState([]);
+ const [stepDisabled, setStepDisabled] = useRecoilState(stepDisabledState);
+ const [readyLoading, setReadyLoading] = useState(true);
+ const getJDBCReady = useRequest, any>(
+ (clusterId: string) => {
+ return SpaceAPI.getJDBCReady({ clusterId });
+ },
+ {
+ manual: true,
+ pollingInterval: 1000,
+ onSuccess: (res: any) => {
+ if (isSuccess(res)) {
+ const data: boolean = res.data;
+ if (data) {
+ getJDBCReady.cancel();
+ setReadyLoading(false);
+ setStepDisabled({ ...stepDisabled, next: false });
+ }
+ }
+ },
+ onError: () => {
+ if (reqInfo.cluster_id) {
+ message.error('请求出错');
+ }
+ },
+ },
+ );
+ const getClusterInstances = useRequest, any>(
+ (clusterId: string) => {
+ return SpaceAPI.getClusterInstance({ clusterId });
+ },
+ {
+ manual: true,
+ pollingInterval: 2000,
+ onSuccess: (res: any) => {
+ if (isSuccess(res)) {
+ const data: any[] = res.data;
+ setInstances(data);
+ if (data.some(item => ERROR_STATUS.includes(item.operateStatus))) {
+ getClusterInstances.cancel();
+ }
+ if (
+ data.every(item => item.operateStatus === OperateStatusEnum.SUCCESS && item.operateStage === 3)
+ ) {
+ getClusterInstances.cancel();
+ getJDBCReady.run(reqInfo.cluster_id)
+ }
+ }
+ },
+ onError: () => {
+ if (reqInfo.cluster_id) {
+ message.error('请求出错');
+ }
+ },
+ },
+ );
+
+ useDidCache(() => {
+ getClusterInstances.cancel();
+ getClusterInstances.cancel();
+ });
+
+ useDidRecover(() => {
+ if (reqInfo.cluster_id && step === 6) {
+ getClusterInstances.run(reqInfo.cluster_id);
+ }
+ });
+
+ useEffect(() => {
+ if (reqInfo.cluster_id && step === 6) {
+ getClusterInstances.run(reqInfo.cluster_id);
+ }
+ return () => {
+ getClusterInstances.cancel();
+ getClusterInstances.cancel();
+ };
+ }, [reqInfo.cluster_id, step]);
+
+ const getStepStatus = (record: any) => {
+ const currentStepStatus = OperateStatusEnum.getStepStatus(record.operateStatus);
+ if (currentStepStatus === 'error') return 'error';
+ return readyLoading ? 'process' : 'finish';
+ };
+
+ const columns = [
+ {
+ title: '序号',
+ dataIndex: 'instanceId',
+ },
+ {
+ title: '节点IP',
+ dataIndex: 'nodeHost',
+ },
+ {
+ title: '安装进度',
+ dataIndex: 'operateStage',
+ render: (operateStage: number, record: any) => (
+ {
+ if (status === 'process') return ;
+ return iconDot;
+ }}
+ status={getStepStatus(record)}
+ current={record.operateStage - 1}
+ size="small"
+ style={{ marginLeft: -50 }}
+ >
+ {Object.keys(STEP_MAP).map((stepKey, index) => (
+
+ ))}
+
+ ),
+ },
+ ];
+
+ const getTableDataSource = (activeKey: string) => {
+ return instances.filter(item => item.moduleName === activeKey.toLowerCase());
+ };
+
+ return (
+ 部署集群,
+ }}
+ >
+ setActiveKey(key)} type="card">
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/routes/space/new-cluster/steps/cluster-plan/cluster-plan.tsx b/frontend/src/routes/space/new-cluster/steps/cluster-plan/cluster-plan.tsx
new file mode 100644
index 0000000..4d2db95
--- /dev/null
+++ b/frontend/src/routes/space/new-cluster/steps/cluster-plan/cluster-plan.tsx
@@ -0,0 +1,254 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { useState, useContext, useEffect } from 'react';
+import { PageContainer } from '@ant-design/pro-layout';
+import { Alert, Button, Modal, Row, Space, Switch, Table, Radio, message, Tabs, Form } from 'antd';
+import { ExclamationCircleOutlined } from '@ant-design/icons';
+import { useHistory } from 'react-router';
+import * as types from '../../types/index.type';
+import API from '../../new-cluster.api';
+import { SpaceAPI } from '../../../space.api';
+import { ResultModal } from '../../../components/result-modal';
+import { modalState, processId, nodeHardwareQuery, stepState } from '../../recoils/index';
+import { useRecoilState, useRecoilValue } from 'recoil';
+import { isSuccess } from '@src/utils/http';
+import { DorisNodeTypeEnum } from '../../types/index.type';
+import { NewSpaceInfoContext } from '@src/common/common.context';
+import { FlatBtn } from '@src/components/flatbtn';
+import { useAsync } from '@src/hooks/use-async';
+const { confirm } = Modal;
+const { TabPane } = Tabs;
+
+export function ClusterPlan() {
+ const { form } = useContext(NewSpaceInfoContext);
+ return (
+
+
+
+
+ );
+}
+
+export function ClusterPlanContent(props: any) {
+ const { reqInfo } = useContext(NewSpaceInfoContext);
+ const [visible, setVisible] = useState(false);
+ const [confirmLoading, setConfirmLoading] = useState(false);
+ const [selectedRowKeys, setSelectedRowKeys] = useState([]);
+ const [feChecked, setFeChecked] = useState([]);
+ const [feData, setFeData] = useState([]);
+ const [beChecked, setBeChecked] = useState([]);
+ const [beData, setBeData] = useState([]);
+ const [roleType, setRoleType] = useState(DorisNodeTypeEnum.FE);
+ const [mixMode, setMixMode] = useState(false);
+ const [modal, setModal] = useRecoilState(modalState);
+ const processID = useRecoilValue(processId);
+ const { data, run: runGetClusterNodes } = useAsync({ data: [] });
+ const [modalAllData, setModalAllData] = useState(data || []);
+ const [activeKey, setActiveKey] = useState(DorisNodeTypeEnum.FE);
+
+ useEffect(() => {
+ if (!reqInfo.cluster_id) return;
+ runGetClusterNodes(
+ SpaceAPI.getClusterNodes({ clusterId: reqInfo.cluster_id }).then(res => {
+ if (isSuccess(res)) return res.data;
+ return Promise.reject(res);
+ }),
+ ).catch(res => {
+ message.error(res.msg);
+ });
+ }, [runGetClusterNodes, reqInfo.cluster_id]);
+
+ useEffect(() => {
+ props?.onChange([
+ {
+ moduleName: 'fe',
+ nodeIds: feData.map(item => item.nodeId),
+ },
+ {
+ moduleName: 'be',
+ nodeIds: beData.map(item => item.nodeId),
+ },
+ ]);
+ }, [feData, beData]);
+
+ const changeFeNodeType = (record: any, index: number, val: types.feNodeType) => {
+ let tempFeData = [...feData];
+ tempFeData[index] = {
+ ...tempFeData[index],
+ feNodeType: val,
+ };
+ console.log(tempFeData);
+ setFeData(tempFeData);
+ };
+ const nodeColumns = [
+ {
+ title: '节点ID',
+ dataIndex: 'nodeId',
+ },
+ {
+ title: '节点IP',
+ dataIndex: 'host',
+ key: 'host',
+ },
+ ];
+ const feColumns = [
+ ...nodeColumns,
+ {
+ title: '操作',
+ key: 'action',
+ render: (e: any, record: any) => handleRemove(record, 'fe')}>删除 ,
+ },
+ ];
+ const beColumns = [
+ ...nodeColumns,
+ {
+ title: '操作',
+ key: 'action',
+ render: (e: any, record: any) => handleRemove(record, 'be')}>删除 ,
+ },
+ ];
+
+ const handleRemove = (record: any, type: 'fe' | 'be') => {
+ switch (type) {
+ case 'fe':
+ const feIndex = feData.findIndex(item => item.nodeId === record.nodeId);
+ feData.splice(feIndex, 1);
+ setFeData([...feData]);
+ setFeChecked(feChecked.filter(item => item !== record.host));
+ break;
+ case 'be':
+ const beIndex = beData.findIndex(item => item.nodeId === record.nodeId);
+ beData.splice(beIndex, 1);
+ setBeData([...beData]);
+ setBeChecked(beChecked.filter(item => item !== record.host));
+ break;
+ default:
+ break;
+ }
+ };
+
+ const showModal = (val: DorisNodeTypeEnum) => {
+ setVisible(true);
+ setRoleType(val);
+ setModalAllData(data || []);
+ val === DorisNodeTypeEnum.FE ? setSelectedRowKeys(feChecked) : setSelectedRowKeys(beChecked);
+ };
+
+ const handleOk = () => {
+ setConfirmLoading(true);
+ setTimeout(() => {
+ setVisible(false);
+ setConfirmLoading(false);
+ }, 0);
+
+ if (roleType === DorisNodeTypeEnum.FE) {
+ setFeChecked(selectedRowKeys);
+ let tempFeData = data?.filter(item => selectedRowKeys.includes(item.host));
+ setFeData(tempFeData || []);
+ }
+ if (roleType === DorisNodeTypeEnum.BE) {
+ setBeChecked(selectedRowKeys);
+ let tempBeData = data?.filter(item => selectedRowKeys.includes(item.host));
+ setBeData(tempBeData || []);
+ }
+ };
+
+ const handleCancel = () => {
+ setSelectedRowKeys([]);
+ setVisible(false);
+ };
+ const onSelectChange = (selectedRowKeys: any[]) => {
+ setSelectedRowKeys(selectedRowKeys);
+ };
+ const rowSelection = {
+ selectedRowKeys,
+ onChange: onSelectChange,
+ };
+ // const handleInstallService = () => {
+ // // setModal({...modal, visible: true})
+ // // return;
+ // const temp1 = feData.map(item => ({
+ // host: item.host,
+ // role: DorisNodeTypeEnum.FE,
+ // feNodeType: item.feNodeType || types.feNodeType.OBSERVER,
+ // }));
+ // const temp2 = beData.map(item => ({
+ // host: item.host,
+ // role: DorisNodeTypeEnum.BE,
+ // }));
+ // const params = {
+ // processId: processID,
+ // installInfos: [...temp1, ...temp2],
+ // };
+ // API.installService(params).then(res => {
+ // if (isSuccess(res)) {
+ // setModal({ ...modal, visible: true });
+ // } else {
+ // message.info(res.msg);
+ // }
+ // });
+ // };
+ return (
+ 规划节点,
+ }}
+ >
+ setActiveKey(key)}
+ type="card"
+ tabBarExtraContent={
+ showModal(activeKey)}>
+ 分配节点
+
+ }
+ >
+
+
+
+ {activeKey === DorisNodeTypeEnum.FE && }
+ {activeKey === DorisNodeTypeEnum.BE && }
+ {visible && (
+
+ <>
+
+
+ >
+
+ )}
+ {modal.visible && }
+
+ );
+}
diff --git a/frontend/src/routes/space/new-cluster/steps/install-options/install-options.tsx b/frontend/src/routes/space/new-cluster/steps/install-options/install-options.tsx
new file mode 100644
index 0000000..a238108
--- /dev/null
+++ b/frontend/src/routes/space/new-cluster/steps/install-options/install-options.tsx
@@ -0,0 +1,73 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { Form, Input, PageHeader } from 'antd';
+import React, { useContext } from 'react';
+import ProCard from '@ant-design/pro-card';
+import { NewSpaceInfoContext } from '@src/common/common.context';
+
+export function InstallOptions(props: any) {
+ const {form} = useContext(NewSpaceInfoContext);
+ return (
+ 安装选项} headerBordered>
+
+
+
Doris Manager将从提供的http地址直接获取安装包。
+
+ 若Manager节点可访问公网,推荐直接使用预编译安装包地址;若Manager节点不可访问公网,推荐自行搭建http服务提供安装包。
+
+
+
+
+
+
+
+
+
Doris与Doris Manager Agent将安装至该目录下。请确保该目录为Doris及相关组件专用。
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/routes/space/new-cluster/steps/node-config/components/custom-config.interface.ts b/frontend/src/routes/space/new-cluster/steps/node-config/components/custom-config.interface.ts
new file mode 100644
index 0000000..bfe20b9
--- /dev/null
+++ b/frontend/src/routes/space/new-cluster/steps/node-config/components/custom-config.interface.ts
@@ -0,0 +1,27 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { ClassAttributes } from "react";
+
+export interface CustomConfigProps extends ClassAttributes{
+ onChange?: (value: CustomConfigValue) => void;
+}
+
+export interface CustomConfigValue {
+ showCustomConfig: boolean;
+ value: string;
+}
\ No newline at end of file
diff --git a/frontend/src/routes/space/new-cluster/steps/node-config/components/custom-config.tsx b/frontend/src/routes/space/new-cluster/steps/node-config/components/custom-config.tsx
new file mode 100644
index 0000000..e9047a4
--- /dev/null
+++ b/frontend/src/routes/space/new-cluster/steps/node-config/components/custom-config.tsx
@@ -0,0 +1,53 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { Input, Row, Space, Switch } from "antd";
+import React, { useState } from "react";
+import { CustomConfigProps } from "./custom-config.interface";
+
+export function CustomConfig(props: CustomConfigProps) {
+ const [showCustomConfig, setShowCustomConfig] = useState(false);
+ return (
+ <>
+
+
+ 自定义配置
+ {
+ setShowCustomConfig(checked);
+ }}
+ />
+
+
+ {
+ showCustomConfig && (
+ {
+ props.onChange && props.onChange({
+ value: e.target.value,
+ showCustomConfig,
+ });
+ }} rows={5} placeholder="请输入您的自定义配置项,若存在冲突,自定义配置将覆盖上方默认配置 格式: xxx=xxx" />
+ )
+ }
+
+ >
+ );
+}
diff --git a/frontend/src/routes/space/new-cluster/steps/node-config/data.ts b/frontend/src/routes/space/new-cluster/steps/node-config/data.ts
new file mode 100644
index 0000000..8d5fcf5
--- /dev/null
+++ b/frontend/src/routes/space/new-cluster/steps/node-config/data.ts
@@ -0,0 +1,107 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+export const BASE_FE_CONFIG = [
+ {
+ key: '0',
+ config_item: 'JVM_XMX',
+ default_value: '8g',
+ desc: '待补充',
+ },
+ {
+ key: '1',
+ config_item: 'http_port',
+ default_value: '8030',
+ desc: '待补充',
+ },
+ {
+ key: '2',
+ config_item: 'rpc_port',
+ default_value: '9020',
+ desc: '待补充',
+ },
+ {
+ key: '3',
+ config_item: 'query_port',
+ default_value: '9030',
+ desc: '待补充',
+ },
+ {
+ key: '4',
+ config_item: 'edit_log_port',
+ default_value: '9010',
+ desc: '待补充',
+ },
+ {
+ key: '5',
+ config_item: 'priority_networks',
+ default_value: '10.10.10.0/24',
+ desc: '待补充',
+ },
+ {
+ key: '6',
+ config_item: 'meta_dir',
+ default_value: '/usr/local/doris/fe/doris-meta',
+ desc: '待补充',
+ },
+]
+export const BASE_BE_CONFIG = [
+ {
+ key: '0',
+ config_item: 'be_port',
+ default_value: '9060',
+ desc: '待补充',
+ },
+ {
+ key: '1',
+ config_item: 'webserver_port',
+ default_value: '8040',
+ desc: '待补充',
+ },
+ {
+ key: '2',
+ config_item: 'heartbeat_service_port',
+ default_value: '9050',
+ desc: '待补充',
+ },
+ {
+ key: '3',
+ config_item: 'brpc_port',
+ default_value: '8060',
+ desc: '待补充',
+ },
+ {
+ key: '4',
+ config_item: 'priority_networks',
+ default_value: '10.10.10.0/24',
+ desc: '待补充',
+ },
+ {
+ key: '5',
+ config_item: 'storage_root_path',
+ default_value: '/data01',
+ desc: '待补充',
+ }
+]
+export const BASE_BROKER_CONFIG = [
+ {
+ key: '0',
+ config_item: 'broker_ipc_port',
+ default_value: '8000',
+ desc: '待补充',
+ }
+]
\ No newline at end of file
diff --git a/frontend/src/routes/space/new-cluster/steps/node-config/node-config.tsx b/frontend/src/routes/space/new-cluster/steps/node-config/node-config.tsx
new file mode 100644
index 0000000..87f9f49
--- /dev/null
+++ b/frontend/src/routes/space/new-cluster/steps/node-config/node-config.tsx
@@ -0,0 +1,293 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { useState, useEffect, useReducer, useContext, useCallback } from 'react';
+import { Table, Input, Button, Popconfirm, Form, Row, Space, Switch, Tabs, message } from 'antd';
+import { FormInstance } from 'antd/lib/form';
+import { PageContainer } from '@ant-design/pro-layout';
+import { CustomConfig } from './components/custom-config';
+import ProCard from '@ant-design/pro-card';
+import type { ProColumns } from '@ant-design/pro-table';
+import { EditableProTable } from '@ant-design/pro-table';
+import { useHistory } from 'react-router';
+import { BASE_BE_CONFIG, BASE_FE_CONFIG, BASE_BROKER_CONFIG } from './data';
+import * as types from './../../types/index.type';
+import API from './../../new-cluster.api';
+import { processId, roleListQuery, stepState } from './../../recoils/index';
+import { useRecoilState, useRecoilValue } from 'recoil';
+import { isSuccess } from '@src/utils/http';
+import { DorisNodeTypeEnum } from './../../types/index.type';
+import { NewSpaceInfoContext } from '@src/common/common.context';
+import { useAsync } from '@src/hooks/use-async';
+import { SpaceAPI } from '@src/routes/space/space.api';
+const { TabPane } = Tabs;
+
+interface DataType {
+ key: string;
+ value: any;
+ desc: string | null;
+}
+
+const initData = {
+ feCustom: '',
+ beCustom: '',
+ brokerCustom: '',
+};
+const reducer = (state: any, action: { type: any; feCustom?: any; beCustom?: any; brokerCustom?: any }) => {
+ let newState = { ...state };
+ switch (action.type) {
+ case 'SET_FE_CONFIG':
+ newState.feCustom = action.feCustom;
+ break;
+ case 'SET_BE_CONFIG':
+ newState.beCustom = action.beCustom;
+ break;
+ case 'SET_BROKER_CONFIG':
+ newState.brokerCustom = action.brokerCustom;
+ break;
+ default:
+ newState = state;
+ }
+ return newState;
+};
+
+function getDeployConfigs(name: string, modules: any[]) {
+ return modules.find(module => module.name === name)?.config || [];
+}
+
+function tranverseModulesToObject(modules: DataType[]) {
+ return modules.reduce((memo, current) => {
+ memo[current.key] = {
+ value: current.value,
+ desc: current.desc,
+ };
+ return memo;
+ }, {} as Record>);
+}
+
+function tranverseStringToObject(customString: string) {
+ if(customString === '') return {}
+ const customModules = customString.split('\n');
+ return customModules.reduce((memo, current) => {
+ const [key, value] = current.split('=');
+ memo[key] = {
+ value: value,
+ desc: null,
+ };
+ return memo;
+ }, {} as Record>);
+}
+
+function tranverseObjectToModules(moduleObject: Record>) {
+ return Object.keys(moduleObject).map(moduleKey => ({
+ key: moduleKey,
+ value: moduleObject[moduleKey].value == null ? null : moduleObject[moduleKey].value,
+ desc: moduleObject[moduleKey].desc == null ? null : moduleObject[moduleKey].desc,
+ }));
+}
+
+function getNewConfigsWithCustom(modules: DataType[], customString: string) {
+ const moduleObject = tranverseModulesToObject(modules);
+ const customModuleObject = tranverseStringToObject(customString);
+ return tranverseObjectToModules({
+ ...moduleObject,
+ ...customModuleObject,
+ });
+}
+
+export function NodeConfig() {
+ const { form } = useContext(NewSpaceInfoContext);
+ return (
+
+
+
+
+ );
+}
+
+export function NodeConfigContent(props: any) {
+ const { reqInfo } = useContext(NewSpaceInfoContext);
+ const history = useHistory();
+ const [step, setStep] = useRecoilState(stepState);
+ const [customState, dispatch] = useReducer(reducer, initData);
+ const [activeKey, setActiveKey] = useState(DorisNodeTypeEnum.FE);
+ const { data: clusterModules, run: runGetClusterModules, loading } = useAsync({ data: [], loading: true });
+ const columns: ProColumns[] = [
+ {
+ title: '配置项',
+ dataIndex: 'key',
+ editable: false,
+ },
+ {
+ title: '默认值',
+ dataIndex: 'value',
+ formItemProps: {
+ rules: [
+ {
+ required: true,
+ message: '此项为必填项',
+ },
+ ],
+ },
+ width: '30%',
+ },
+ {
+ title: '说明',
+ dataIndex: 'desc',
+ editable: false,
+ render: (desc: any, record: DataType) => {
+ return {record.desc == null ? '待补充' : record.desc} ;
+ },
+ },
+ ];
+ const [feDataSource, setFeDataSource] = useState([]);
+ const [beDataSource, setBeDataSource] = useState([]);
+ const [brokerDataSource, setBrokerDataSource] = useState([]);
+ // const {fe, be} = useRecoilValue(roleListQuery);
+ const processID = useRecoilValue(processId);
+
+ useEffect(() => {
+ if (!reqInfo.cluster_id) return;
+ runGetClusterModules(
+ SpaceAPI.getClusterModule({ clusterId: reqInfo.cluster_id }).then(res => {
+ if (isSuccess(res)) return res.data;
+ return Promise.reject(res);
+ }),
+ )
+ .then(res => {
+ setFeDataSource(getDeployConfigs('fe', res));
+ setBeDataSource(getDeployConfigs('be', res));
+ setBrokerDataSource(getDeployConfigs('broker', res));
+ })
+ .catch(res => {
+ message.error(res.msg);
+ });
+ }, [runGetClusterModules, reqInfo.cluster_id]);
+
+ const getNewConfigs = useCallback(
+ (moduleName: string) => {
+ const { feCustom, beCustom, brokerCustom } = customState;
+ switch (moduleName) {
+ case 'fe':
+ return getNewConfigsWithCustom(feDataSource, feCustom);
+ case 'be':
+ return getNewConfigsWithCustom(beDataSource, beCustom);
+ case 'broker':
+ return getNewConfigsWithCustom(brokerDataSource, brokerCustom);
+ }
+ },
+ [feDataSource, beDataSource, brokerDataSource, customState],
+ );
+
+ useEffect(() => {
+ const newClusterModules = clusterModules!.map(module => ({
+ moduleName: module.name,
+ configs: getNewConfigs(module.name),
+ }));
+ props.onChange?.(newClusterModules)
+ }, [getNewConfigs]);
+
+ return (
+ <>
+ 配置参数,
+ }}
+ >
+ setActiveKey(key)} type="card">
+
+
+
+
+ {activeKey === DorisNodeTypeEnum.FE && (
+ <>
+
+ rowKey="key"
+ maxLength={5}
+ bordered
+ columns={columns}
+ value={feDataSource}
+ loading={loading}
+ editable={{
+ type: 'multiple',
+ editableKeys: feDataSource.map(item => item.key),
+ onValuesChange: (record, recordList) => {
+ setFeDataSource(recordList);
+ },
+ }}
+ />
+ {
+ dispatch({ type: 'SET_FE_CONFIG', feCustom: val.value });
+ }}
+ />
+ >
+ )}
+ {activeKey === DorisNodeTypeEnum.BE && (
+ <>
+
+ rowKey="key"
+ maxLength={5}
+ bordered
+ columns={columns}
+ value={beDataSource}
+ loading={loading}
+ editable={{
+ type: 'multiple',
+ editableKeys: beDataSource.map(item => item.key),
+ onValuesChange: (record, recordList) => {
+ setBeDataSource(recordList);
+ },
+ }}
+ />
+ {
+ dispatch({ type: 'SET_BE_CONFIG', beCustom: val.value });
+ }}
+ />
+ >
+ )}
+ {activeKey === DorisNodeTypeEnum.BROKER && (
+ <>
+
+ rowKey="key"
+ maxLength={5}
+ bordered
+ columns={columns}
+ value={brokerDataSource}
+ recordCreatorProps={false}
+ loading={loading}
+ editable={{
+ type: 'multiple',
+ editableKeys: brokerDataSource.map(item => item.key),
+ onValuesChange: (record, recordList) => {
+ setBrokerDataSource(recordList);
+ },
+ }}
+ />
+ {
+ dispatch({ type: 'SET_BROKER_CONFIG', brokerCustom: val.value });
+ }}
+ />
+ >
+ )}
+
+ >
+ );
+}
diff --git a/frontend/src/routes/space/new-cluster/steps/node-register/node-register.tsx b/frontend/src/routes/space/new-cluster/steps/node-register/node-register.tsx
new file mode 100644
index 0000000..d800fed
--- /dev/null
+++ b/frontend/src/routes/space/new-cluster/steps/node-register/node-register.tsx
@@ -0,0 +1,62 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React from 'react';
+import { PageContainer } from '@ant-design/pro-layout';
+import ProCard from '@ant-design/pro-card';
+import { Button, Row, Space, Table } from 'antd';
+import { useHistory } from 'react-router';
+
+export function NodeRegister(props: any) {
+ const history = useHistory();
+ const columns = [
+ {
+ title: '序号',
+ dataIndex: 'order',
+ key: 'order',
+ },
+ {
+ title: '节点IP',
+ dataIndex: 'ip',
+ key: 'ip',
+ },
+ {
+ title: '注册状态',
+ dataIndex: 'register_status',
+ key: 'register_status',
+ },
+ {
+ title: '操作',
+ key: 'option',
+ render: () => (
+
+ 重试
+ 跳过
+
+ ),
+ },
+ ];
+ return (
+
+
+
+ );
+}
diff --git a/frontend/src/routes/space/new-cluster/steps/run-cluster/run-cluster.tsx b/frontend/src/routes/space/new-cluster/steps/run-cluster/run-cluster.tsx
new file mode 100644
index 0000000..953b6a2
--- /dev/null
+++ b/frontend/src/routes/space/new-cluster/steps/run-cluster/run-cluster.tsx
@@ -0,0 +1,158 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import ProCard from '@ant-design/pro-card';
+import { Button, message, Row, Space, Steps, Table } from 'antd';
+import React, {useEffect, useState } from 'react';
+import { useHistory } from 'react-router-dom';
+import { processId, roleListQuery, stepState } from './../../recoils/index';
+import { useRecoilState, useRecoilValue } from 'recoil';
+import API from '../../new-cluster.api';
+import * as types from '../../types/index.type'
+import { isSuccess } from '@src/utils/http';
+import { NewClusterStepsEnum } from '../../new-cluster.data';
+import { DorisNodeTypeEnum } from '../../types/index.type';
+
+export function RunCluster(props: any) {
+ const history = useHistory();
+ const processID = useRecoilValue(processId)
+ const {fe, be} = useRecoilValue(roleListQuery);
+ const [taskInfo, setTaskInfo] = useState([])
+ const [step, setStep] = useRecoilState(stepState);
+ const [completed, setCompleted] = useState(false);
+
+ useEffect(() => {
+ const feArr = fe.map(item => ({ host: item, role: DorisNodeTypeEnum.FE}))
+ const beArr = be.map(item => ({ host: item, role: DorisNodeTypeEnum.BE}))
+ API.startService({
+ processId: processID,
+ dorisStarts: [...feArr, ...beArr]
+ }).then(res => {
+ if(isSuccess(res)){
+ API.getTaskStatus(processID).then(cur => {
+ setTaskInfo(cur.data)
+ })
+ }
+ })
+ }, [fe,be])
+
+ const columns = [
+ {
+ title: '序号',
+ dataIndex: 'id',
+ key: 'id',
+ },
+ {
+ title: '节点IP',
+ dataIndex: 'host',
+ key: 'host',
+ },
+ {
+ title: '状态信息',
+ dataIndex: 'status',
+ key: 'status',
+ render: (_, record:any) => (
+
+ {record.status} {record.result}
+
+ ),
+ },
+ {
+ title: '操作',
+ key: 'option',
+ render: (_, record:any) => (
+
+ reTry(record)}>重试
+ doSkip(record)}>跳过
+ viewLog(record)}>查看日志
+
+ ),
+ },
+ ];
+
+ const reTry = (record: any) => {
+ API.reTryTask(record.id).then(res => {
+ if(isSuccess(res)){
+ message.success(res.msg)
+ }else{
+ message.warn(res.msg)
+ }
+ })
+ }
+ const doSkip = (record: any) => {
+ API.skipTask(record.id).then(res => {
+ if(isSuccess(res)){
+ message.success(res.msg)
+ }else{
+ message.warn(res.msg)
+ }
+ })
+ }
+ const viewLog = (record: any) => {
+ history.push({
+ pathname: `/cluster/logs/${record.id}`
+ })
+ }
+
+ const startCluster = () => {
+ API.getTaskStatus(processID).then(cur => {
+ if(!isSuccess(cur)){
+ message.error(cur.msg)
+ return;
+ }
+ let JOIN_BE = cur.data.filter((item: any) => item.taskType === "JOIN_BE")
+ if(JOIN_BE?.length){
+ message.info('已经组建过了哦')
+ return;
+ }
+ setTaskInfo(cur.data)
+ let len = cur.data.filter((item: any) => item.status !== "SUCCESS")
+ if(len?.length > 0){
+ message.info('部分节点还未安装成功,请稍后重试!')
+ }else{
+ API.startCluster({
+ processId: processID,
+ feHosts: fe,
+ beHosts: be
+ }).then(res => {
+ message.success(res.msg)
+ if(res.code === 0){
+ setCompleted(true)
+ }
+ })
+ }
+ })
+
+ }
+
+ return (
+ <>
+
+ (item.taskRole === DorisNodeTypeEnum.FE))} rowKey="id" pagination={false}/>
+
+
+ (item.taskRole === DorisNodeTypeEnum.BE))} rowKey="id" pagination={false}/>
+
+
+ (item.taskRole === DorisNodeTypeEnum.BROKER))} rowKey="id" pagination={false}/>
+
+ 开始组建}>
+
+ >
+ );
+}
diff --git a/frontend/src/routes/space/new-cluster/types/index.type.ts b/frontend/src/routes/space/new-cluster/types/index.type.ts
new file mode 100644
index 0000000..2675b02
--- /dev/null
+++ b/frontend/src/routes/space/new-cluster/types/index.type.ts
@@ -0,0 +1,25 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+export * from './params.type'
+export * from './result.types'
+export enum NewClusterStepsEnum {
+ 'create-cluster',
+ 'cluster-plan',
+ 'node-config',
+ 'run-cluster',
+}
\ No newline at end of file
diff --git a/frontend/src/routes/space/new-cluster/types/params.type.ts b/frontend/src/routes/space/new-cluster/types/params.type.ts
new file mode 100644
index 0000000..167bea1
--- /dev/null
+++ b/frontend/src/routes/space/new-cluster/types/params.type.ts
@@ -0,0 +1,65 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+export interface CreateClusterRequestParams {
+ clusterId: string;
+ packageUrl: string;
+ installDir: string;
+ hosts: string[];
+ user: string;
+ sshPort: number;
+ sshKey: string;
+}
+export enum DorisNodeTypeEnum {
+ FE = "FE",
+ BE = "BE",
+ BROKER = "BROKER"
+}
+export enum feNodeType {
+ FOLLOWER = "FOLLOWER",
+ OBSERVER = "OBSERVER"
+}
+export interface InstallServiceRequestParams {
+ processId: number;
+ installInfos: {
+ host: string;
+ role: DorisNodeTypeEnum;
+ feNodeType?: feNodeType;
+ }[];
+}
+
+export interface DeployConfigRequestParams {
+ processId: number;
+ deployConfigs: {
+ hosts: string[];
+ role: DorisNodeTypeEnum;
+ conf: string;
+ }[];
+}
+
+export interface StartServiceRequestParams {
+ processId: number;
+ dorisStarts: {
+ host: string;
+ role: DorisNodeTypeEnum;
+ }[];
+}
+
+export interface StartClusterRequestParams {
+ processId: number;
+ feHosts: string[];
+ beHosts: string[];
+}
\ No newline at end of file
diff --git a/frontend/src/routes/space/new-cluster/types/result.types.ts b/frontend/src/routes/space/new-cluster/types/result.types.ts
new file mode 100644
index 0000000..3440ee8
--- /dev/null
+++ b/frontend/src/routes/space/new-cluster/types/result.types.ts
@@ -0,0 +1,79 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import * as types from './params.type';
+
+export enum taskStatusEnum {
+ "SUBMITTED" = "SUBMITTED", //已提交
+ "RUNNING"= "RUNNING",
+ "SUCCESS" = "SUCCESS",
+ "FAILURE" = "FAILURE"
+}
+export enum taskTypeEnum {
+ "INSTALL_AGENT" = "INSTALL_AGENT", //安装Agent
+ "INSTALL_FE" = "INSTALL_FE",
+ "INSTALL_BE" = "INSTALL_BE",
+ "DEPLOY_FE_CONFIG" = "DEPLOY_FE_CONFIG",
+ "DEPLOY_BE_CONFIG" = "DEPLOY_BE_CONFIG",
+ "START_BE" = "START_BE",
+ "JOIN_BE" = "JOIN_BE" //加入集群
+}
+export enum processTypeEnum {
+ "INSTALL_AGENT" = "INSTALL_AGENT" , //安装Agent
+ "INSTALL_SERVICE" = "INSTALL_SERVICE",
+ "DEPLOY_CONFIG" = "DEPLOY_CONFIG", //分发配置
+ "START_SERVICE" = "START_SERVICE", //启动服务
+ "BUILD_CLUSTER" = "BUILD_CLUSTER", //组件集群
+}
+export interface ItaskResult{
+ endTime: string;
+ executorId: null;
+ finish: "NO"| "YES";
+ host: string;
+ id: number;
+ processId: number;
+ processType: processTypeEnum;
+ result: string;
+ skip: "NO"| "YES";
+ startTime: string;
+ status: taskStatusEnum;
+ taskJson: string;
+ taskRole: types.dorisRoleEnum;
+ taskType: taskStatusEnum;
+}
+
+export interface IroleListResult{
+ id: number;
+ host: string;
+ clusterId: number;
+ role: types.dorisRoleEnum,
+ feNodeType: types.feNodeType,
+ installDir: string;
+ register: "NO"| "YES"
+}
+
+
+export interface IroleListResult{
+ cpu: string;
+ totalMemory: string;
+}
+
+export interface IcurrentProcess{
+ clusterId: number;
+ id: number;
+ processStep: number;
+}
\ No newline at end of file
diff --git a/frontend/src/routes/space/space.api.ts b/frontend/src/routes/space/space.api.ts
new file mode 100644
index 0000000..5e156ad
--- /dev/null
+++ b/frontend/src/routes/space/space.api.ts
@@ -0,0 +1,99 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+/** @format */
+
+import { http } from '@src/utils/http';
+import { ISpaceParam, ISpaceUser, ClusterAccessParams } from './space.interface';
+import { IResult } from 'src/interfaces/http.interface';
+function spaceCreate(params: any): Promise> {
+ return http.post(`/api/space/create`, params);
+}
+function spaceList(): Promise> {
+ return http.get(`/api/space/all`);
+}
+function spaceCheck(name: string): Promise> {
+ return http.post(`/api/space/name/check`, { name: name && name.trim() });
+}
+function spaceValidate(data: any): Promise> {
+ return http.post(`/api/space/validate`, data);
+}
+function spaceDelete(spaceId: string): Promise> {
+ return http.delete(`/api/space/${spaceId}`);
+}
+function spaceGet(spaceId: string): Promise> {
+ return http.get(`/api/space/${spaceId}`);
+}
+function spaceUpdate(data: ISpaceParam): Promise