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} + + ) + } */} + + + + 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 ( +
+
+ {children} +
+ ); +} 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`} + + + + + + {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)} + + +
+ {visible && ( + setVisible(false)} + confirmLoading={confirmLoading} + onOk={() => { + form.validateFields() + .then(values => { + handleCreate(values); + }) + .catch(info => { + console.log('Validate Failed:', info); + }); + }} + > +
+ + + + +
+ )} + + ); +} 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 ( + +
+ + + + +
+ ); +} 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(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' && ( + + + + )} */} + +
+ ); +} 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 ( +
+ + + + + + + +
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`}: + + + + + + + + +
+ + + + +

{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`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 ( +
+
+ + + + + + + + +
+ ); +} 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认证 */} + + +

注意,初始化选择好认证方式后不可再改变。

+ + + +
+
+
+ ); +} 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`}: + + + + + + + + +
+ + + + {t`nodeSelection`}: + + + + +

{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 ( + + ); + }); + 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 ( + + ); + }); + 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`}: + + + + { t`Node`}: + + + + + + + { 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`}: + + + + { t`Node`}: + + + + {' '} + + { 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 ( + + ); + }); + 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 ( + + ); + }); + 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`}: + + + + { t`Node`}: + + + + + + + { 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`login`}

+ + + + + + + + 记住我 + + + + + {/* + 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: ( +
+
{currentText}
+
+ ), + 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 && ( + + + + )} + + + + + + ); +} 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 => ( + + ))} + + } + 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 ( + + + + + + ); +} 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_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 ( + <> + + + + + {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) ? ( + <> + ) : ( + + )} + {step === AccessClusterStepsEnum['finish'] ? ( + + ) : ( + + )} + + +
+
+
+ + ); +} 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 ( + +
+ + + + + + + + + + + + + + + + + + + + +   + { + (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信任? + + +
+ + + + + + + +