diff --git a/.babelrc b/.babelrc new file mode 100644 index 00000000..aa746c01 --- /dev/null +++ b/.babelrc @@ -0,0 +1,66 @@ +{ + "presets": [ + [ + "@babel/preset-env", + { + "targets": { + "esmodules": true + } + } + ], + "@babel/preset-react" + ], + "plugins": [ + [ + "@babel/plugin-transform-runtime", + { + "corejs": { + "version": 3, + "proposals": true + } + } + ], + "@babel/plugin-transform-modules-commonjs", + [ + "@babel/plugin-proposal-decorators", + { + "legacy": true + } + ], + "@babel/plugin-proposal-class-properties", + "@babel/plugin-proposal-throw-expressions", + "@babel/plugin-syntax-dynamic-import", + "@babel/plugin-syntax-import-meta", + "react-hot-loader/babel", + [ + "import", + { + "libraryName": "antd", + "libraryDirectory": "lib", + "style": true + }, + "antd" + ], + [ + "import", + { + "libraryName": "@ant-design/icons", + "libraryDirectory": "lib/icons", + "camel2DashComponentName": false + }, + "@ant-design/icons" + ] + ], + "env": { + "test": { + "plugins": [ + [ + "istanbul", + { + "useInlineSourceMaps": false + } + ] + ] + } + } +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..79187b2d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +npm-debug.log +yarn-error.log +node_modules +package-lock.json + +.git/ +.idea/ +.vscode/ +run/ + +.DS_Store +*.sw* +*.un~ \ No newline at end of file diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..8e5bb916 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,4 @@ +dist +node_modules +coverage +test/e2e/report \ No newline at end of file diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 00000000..77090a30 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,77 @@ +{ + "extends": ["airbnb", "plugin:prettier/recommended"], + "parser": "babel-eslint", + "plugins": ["cypress"], + "parserOptions": { + "sourceType": "module", + "ecmaFeatures": { + "jsx": true, + "modules": true, + "legacyDecorators": true + } + }, + "env": { + "es6": true, + "commonjs": true, + "browser": true, + "jest": true, + "cypress/globals": true + }, + "settings": { + "import/resolver": { + "alias": { + "map": [ + ["@", "./src"], + ["src", "./src"], + ["image", "./src/asset/image"], + ["components", "./src/components"], + ["utils", "./src/utils"], + ["stores", "./src/stores"], + ["pages", "./src/pages"], + ["containers", "./src/containers"], + ["layouts", "./src/layouts"], + ["client", "./src/client"], + ["resources", "./src/resources"], + ["core", "./src/core"] + ], + "extensions": [".js", ".jsx"] + } + } + }, + "rules": { + "camelcase": "warn", + "react/prop-types": "warn", + "class-methods-use-this": "off", + "react/prefer-stateless-function": "warn", + "no-plusplus": "warn", + "no-param-reassign": "warn", + "react/jsx-props-no-spreading": "warn", + "react/static-property-placement": "warn", + "prefer-destructuring": "warn", + "no-use-before-define": "warn", + "react/forbid-prop-types": "warn", + "react/no-array-index-key": "warn", + "react/require-default-props": "warn", + "consistent-return": "warn", + "no-underscore-dangle": "warn", + "no-unused-expressions": "warn", + "import/no-cycle": "warn", + "no-empty": [ + 2, + { + "allowEmptyCatch": true + } + ], + "react/destructuring-assignment": "warn", + "jsx-a11y/click-events-have-key-events": "warn", + "jsx-a11y/no-static-element-interactions": "warn", + "import/no-extraneous-dependencies": "warn", + "import/prefer-default-export": "warn", + "no-nested-ternary": "warn", + }, + "globals": { + "t": true, + "globals": true, + "request": true + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..b56c835d --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +/node_modules/ +/dist +.DS_Store +yarn-error.log +package-lock.json +docs/ +.vscode +test/e2e/videos +test/e2e/screenshots +test/e2e/downloads +.nyc_output +coverage +test/e2e/results +test/e2e/report +*.qcow2 + +# config +test/e2e/config/local_config.yaml + +# Python +__pycache__/ +*.py[cod] +*$py.class +.env +.venv +env/ +venv/ +ENV/ +/skyline_console/static diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..007ea8a7 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +dist +node_modules +coverage diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..c3e03287 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "semi": true, + "singleQuote": true, + "trailingComma": "es5" +} diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 00000000..2aed5cbd --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,57 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +module.exports = function (grunt) { + grunt.initConfig({ + i18next: { + dev: { + src: ['src/**/*.{jsx,js}'], + dest: 'src', + options: { + lngs: ['en', 'zh'], + removeUnusedKeys: true, + sort: true, + keySeparator: false, + nsSeparator: false, + interpolation: { + prefix: '{{', + suffix: '}}', + }, + resource: { + // loadPath: 'src/locales/{{lng}}/{{ns}}.json', + loadPath: 'src/locales/{{lng}}.json', + // savePath: 'locales/{{lng}}/{{ns}}.json' + savePath: 'locales/{{lng}}.json', + }, + func: { + list: ['t', 't.html'], + extensions: ['.js', '.jsx'], + }, + defaultValue: (lng, ns, key) => { + if (lng === 'zh') { + return ''; + } + return key; + }, + }, + }, + }, + }); + + // Load the plugin that provides the "i18next" task. + grunt.loadNpmTasks('i18next-scanner'); + + // Default task(s). + grunt.registerTask('default', ['i18next']); +}; diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..f433b1a5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..754c9e71 --- /dev/null +++ b/Makefile @@ -0,0 +1,83 @@ +SOURCES := src +ROOT_DIR ?= $(shell git rev-parse --show-toplevel) + +# Color +no_color = \033[0m +black = \033[0;30m +red = \033[0;31m +green = \033[0;32m +yellow = \033[0;33m +blue = \033[0;34m +purple = \033[0;35m +cyan = \033[0;36m +white = \033[0;37m + +# Params +MODE ?= prod +BUILD_ENGINE ?= docker + +# Version +RELEASE_VERSION ?= $(shell git rev-parse --short HEAD)_$(shell date -u +%Y-%m-%dT%H:%M:%S%z) +GIT_BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD) +GIT_COMMIT ?= $(shell git rev-parse --verify HEAD) + + +.PHONY: help +help: + @echo "Skyline console development makefile" + @echo + @echo "Usage: make " + @echo + @echo "Target:" + @echo " git_config Initialize git configuration." + @echo " install Installs the project dependencies." + @echo " build Build source and wheel packages." + @echo " lint Check JavaScript code." + @echo " test Run unit tests." + @echo + + +.PHONY: git_config +user_name = $(shell git config --get user.name) +user_email = $(shell git config --get user.email) +commit_template = $(shell git config --get commit.template) +git_config: +ifeq ($(user_name),) + @printf "$(cyan)\n" + @read -p "Set your git user name: " user_name; \ + git config --local user.name $$user_name; \ + printf "$(green)User name was set.\n$(cyan)" +endif +ifeq ($(user_email),) + @printf "$(cyan)\n" + @read -p "Set your git email address: " user_email; \ + git config --local user.email $$user_email; \ + printf "$(green)User email address was set.\n$(no_color)" +endif +ifeq ($(commit_template),) + @git config --local commit.template $(ROOT_DIR)/tools/git_config/commit_message.txt +endif + @printf "$(green)Project git config was successfully set.\n$(no_color)" + @printf "${yellow}You may need to run 'pip install git-review' install git review tools.\n\n${no_color}" + + +.PHONY: install +install: + yarn install + + +.PHONY: build +build: + rm -rf $(ROOT_DIR)/skyline_console/static + yarn run build + poetry build + + +.PHONY: lint +lint: + yarn run lint + + +.PHONY: test +test: + yarn run test:unit diff --git a/README.md b/README.md new file mode 100644 index 00000000..b2fc48e3 --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# 使用说明 + +简体中文 | [English](./en.md) + +## 环境依赖 + +- `node`: lts/erbium (v12.\*) +- `yarn`: 1.22.4 + + +## 本地环境搭建 + +以 CentOS 为例 + +- 安装 nvm (nodejs 版本管理工具) + + ```shell + wget -P /root/ --tries=10 --retry-connrefused --waitretry=60 --no-dns-cache --no-cache https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh + bash /root/install.sh + . /root/.nvm/nvm.sh + ``` + +- 安装 nodejs + + ```shell + NODE_VERSION=erbium + nvm install --lts=$NODE_VERSION + nvm alias default lts/$NODE_VERSION + nvm use default + ``` + +- 验证 nodejs 和 npm 版本 + + ```shell + node -v + # v12.*.* + npm -v + # 6.*.* + ``` + +- 安装 yarn + + ```shell + npm install -g yarn + ``` + +- 安装项目依赖 + + 在项目根目录下,`package.json`同级。 + + ```shell + yarn install + ``` + + 等待安装完成即可。 + +## 开发使用方法 + +在项目根目录下,`package.json`同级。 + +- `yarn run mock`: 使用[rap2](http://rap2.taobao.org/)工具 mock 接口 +- `yarn run dev`: 使用实际接口,需要将`webpack.dev.js`文件第 27 行的 "http://pre.xxx.com" + 修改为实际地址 +- `yarn run build`: 构建打包,可将生成的 dist 目录的内容交给后端 diff --git a/config/theme.js b/config/theme.js new file mode 100644 index 00000000..3360e387 --- /dev/null +++ b/config/theme.js @@ -0,0 +1,27 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +module.exports = { + 'primary-color': '#0068FF', + 'link-color': '#0068FF', + // 'link-hover-color': '#005ADE', + // 'link-active-color': '005ADE', + 'success-color': '#57E39B', + 'warning-color': '#979797', + 'error-color': '#EB354D', + 'btn-default-color': '#0068FF', + 'btn-default-border': '#0068FF', + 'border-radius-base': '4px', + 'font-size-base': '12px', +}; diff --git a/config/webpack.common.js b/config/webpack.common.js new file mode 100644 index 00000000..ce1e68ab --- /dev/null +++ b/config/webpack.common.js @@ -0,0 +1,142 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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 webpack = require('webpack'); +const { normalize, resolve } = require('path'); +// const path = require("path"); +// const CleanWebpackPlugin = require('clean-webpack-plugin'); +const HappyPack = require('happypack'); +const os = require('os'); +const moment = require('moment'); + +const root = (path) => resolve(__dirname, `../${path}`); +const version = moment().unix(); + +module.exports = { + module: { + rules: [ + { + test: /\.jsx?$/, + include: [root('src'), root('common')], + use: 'happypack/loader?id=jsx', + }, + { + test: /\.jsx?$/, + include: root('node_modules'), + use: 'cache-loader', + }, + { + test: /\.js$/, + exclude: /node_modules/, + use: { + loader: 'babel-loader', + }, + }, + { + test: /\.(png|gif|jpg)$/, + use: [ + { + loader: 'url-loader', + options: { + limit: 10240, + name: normalize(`asset/image/[name].${version}.[ext]`), + }, + }, + ], + exclude: [ + root('src/asset/image/logo.png'), + root('src/asset/image/loginRightLogo.png'), + ], + }, + { + test: /\.(png|gif|jpg)$/, + use: [ + { + loader: 'url-loader', + options: { + limit: false, + name: normalize('asset/image/[name].[ext]'), + }, + }, + ], + include: [ + root('src/asset/image/logo.png'), + root('src/asset/image/loginRightLogo.png'), + ], + }, + { + test: /\.svg$/, + use: [ + { + loader: 'url-loader', + options: { + limit: false, + name: normalize('asset/image/[name].[ext]'), + }, + }, + ], + include: [ + root('src/asset/image/logo-small.svg'), + root('src/asset/image/logo-extend.svg'), + ], + }, + { + test: /\.(woff|woff2|ttf|eot|svg)$/, + use: [ + { + loader: 'url-loader', + options: { + limit: 10240, + name: normalize(`asset/image/[name].${version}.[ext]`), + }, + }, + ], + exclude: [ + root('src/asset/image/logo-small.svg'), + root('src/asset/image/logo-extend.svg'), + ], + }, + ], + }, + resolve: { + extensions: ['.js', '.jsx'], + modules: [root('src'), root('src/pages'), 'node_modules'], + alias: { + '@': root('src'), + src: root('src'), + asset: root('src/asset'), + image: root('src/asset/image'), + core: root('src/core'), + containers: root('src/containers'), + layouts: root('src/layouts'), + components: root('src/components'), + pages: root('src/pages'), + utils: root('src/utils'), + stores: root('src/stores'), + locales: root('src/locales'), + styles: root('src/styles'), + resources: root('src/resources'), + }, + }, + plugins: [ + new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), + new HappyPack({ + threads: os.cpus().length - 1, + id: 'jsx', + loaders: ['babel-loader?cacheDirectory'], + }), + ], +}; + +module.exports.version = version; diff --git a/config/webpack.dev.js b/config/webpack.dev.js new file mode 100644 index 00000000..01e0e2b3 --- /dev/null +++ b/config/webpack.dev.js @@ -0,0 +1,150 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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 { resolve } = require('path'); +const webpack = require('webpack'); +const merge = require('webpack-merge'); +const HtmlWebPackPlugin = require('html-webpack-plugin'); +const autoprefixer = require('autoprefixer'); +const common = require('./webpack.common'); +const theme = require('./theme'); +// const OpenBrowserPlugin = require('open-browser-webpack-plugin'); + +const root = (path) => resolve(__dirname, `../${path}`); + +module.exports = (env) => { + const API = (env || {}).API || 'mock'; + + console.log('API %s', API); + + const devServer = { + host: '0.0.0.0', + // host: 'localhost', + port: 8088, + contentBase: root('dist'), + historyApiFallback: true, + compress: true, + hot: true, + inline: true, + disableHostCheck: true, + // progress: true + }; + + if (API === 'mock' || API === 'dev') { + devServer.proxy = { + '/api': { + target: 'http://localhost', + changeOrigin: true, + secure: false, + }, + }; + } + + const { version, ...restConfig } = common; + + return merge(restConfig, { + entry: { + main: root('src/core/index.jsx'), + }, + output: { + filename: '[name].js', + path: root('dist'), + publicPath: '/', + }, + mode: 'development', + devtool: 'inline-source-map', + devServer, + module: { + rules: [ + { + test: /\.css$/, + use: [ + { + loader: 'style-loader', + }, + { + loader: 'css-loader', + }, + ], + }, + { + test: /\.(css|less)$/, + exclude: /node_modules/, + use: [ + { + loader: 'style-loader', // creates style nodes from JS strings + }, + { + loader: 'css-loader', // translates CSS into CommonJS + options: { + modules: { + mode: 'global', + }, + localIdentName: '[name]__[local]--[hash:base64:5]', + }, + }, + { + loader: 'postcss-loader', + options: { + plugins: [autoprefixer('last 2 version')], + sourceMap: true, + }, + }, + { + loader: 'less-loader', // compiles Less to CSS + options: { + importLoaders: true, + javascriptEnabled: true, + }, + }, + ], + }, + { + test: /\.(less)$/, + include: /node_modules/, + use: [ + { + loader: 'style-loader', // creates style nodes from JS strings + }, + { + loader: 'css-loader', // translates CSS into CommonJS + }, + { + loader: 'less-loader', // compiles Less to CSS + options: { + javascriptEnabled: true, + modifyVars: theme, + }, + }, + ], + }, + ], + }, + plugins: [ + new webpack.HotModuleReplacementPlugin(), + // new OpenBrowserPlugin({ + // url: 'http://localhost:8080', + // browser: "Google Chrome", + // }), + new webpack.DefinePlugin({ + // 为项目注入环境变量 + 'process.env.API': JSON.stringify(API), + }), + new HtmlWebPackPlugin({ + template: root('src/asset/template/index.html'), + favicon: root('src/asset/image/favicon.ico'), + }), + ], + }); +}; diff --git a/config/webpack.e2e.js b/config/webpack.e2e.js new file mode 100644 index 00000000..0dfeed4e --- /dev/null +++ b/config/webpack.e2e.js @@ -0,0 +1,175 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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 { resolve } = require('path'); +const webpack = require('webpack'); +const merge = require('webpack-merge'); +const HtmlWebPackPlugin = require('html-webpack-plugin'); +// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; +const autoprefixer = require('autoprefixer'); +const CleanWebpackPlugin = require('clean-webpack-plugin'); +const TerserPlugin = require('terser-webpack-plugin'); +const common = require('./webpack.common'); +const theme = require('./theme'); + +const root = (path) => resolve(__dirname, `../${path}`); + +const { version, ...restConfig } = common; + +module.exports = (env) => { + const API = (env || {}).API || 'mock'; + + // const devServer = { + // // host: '0.0.0.0', + // host: 'localhost', + // port: 8088, + // contentBase: root('dist'), + // historyApiFallback: true, + // compress: true, + // hot: false, + // inline: false, + // disableHostCheck: true, + // // progress: true + // }; + + return merge(restConfig, { + entry: { + main: root('src/core/index.jsx'), + }, + output: { + filename: '[name].js', + path: root('dist'), + publicPath: '/', + chunkFilename: `[name].bundle.${version}.js`, + }, + mode: 'production', + // devtool: 'inline-source-map', + // devServer: devServer, + module: { + rules: [ + { + test: /\.css$/, + use: [ + { + loader: 'style-loader', + }, + { + loader: 'css-loader', + }, + ], + }, + { + test: /\.(css|less)$/, + exclude: /node_modules/, + use: [ + { + loader: 'style-loader', // creates style nodes from JS strings + }, + { + loader: 'css-loader', // translates CSS into CommonJS + options: { + modules: { + mode: 'global', + }, + localIdentName: '[name]__[local]--[hash:base64:5]', + }, + }, + { + loader: 'postcss-loader', + options: { + plugins: [autoprefixer('last 2 version')], + sourceMap: false, + }, + }, + { + loader: 'less-loader', // compiles Less to CSS + options: { + importLoaders: true, + javascriptEnabled: true, + }, + }, + ], + }, + { + test: /\.(less)$/, + include: /node_modules/, + use: [ + { + loader: 'style-loader', // creates style nodes from JS strings + }, + { + loader: 'css-loader', // translates CSS into CommonJS + }, + { + loader: 'less-loader', // compiles Less to CSS + options: { + javascriptEnabled: true, + modifyVars: theme, + }, + }, + ], + }, + ], + }, + plugins: [ + new webpack.DefinePlugin({ + // 为项目注入环境变量 + 'process.env.API': JSON.stringify(API), + }), + new HtmlWebPackPlugin({ + template: root('src/asset/template/index.html'), + favicon: root('src/asset/image/favicon.ico'), + }), + new CleanWebpackPlugin(['dist'], { + root: resolve(__dirname, `../`), + }), + // new BundleAnalyzerPlugin(), + ], + optimization: { + splitChunks: { + maxInitialRequests: 10, + cacheGroups: { + commons: { + chunks: 'all', + name: 'common', + minChunks: 1, + minSize: 0, + }, + vendor: { + test: /node_modules/, + chunks: 'all', + name: 'vendor', + minChunks: 1, + priority: 10, + enforce: true, + }, + }, + }, + runtimeChunk: { + name: () => `runtime.${version}`, + }, + minimize: true, // default true for production + minimizer: [ + new TerserPlugin({ + sourceMap: false, + terserOptions: { + compress: { + drop_console: true, + }, + }, + }), + ], + }, + }); +}; diff --git a/config/webpack.prod.js b/config/webpack.prod.js new file mode 100644 index 00000000..669df036 --- /dev/null +++ b/config/webpack.prod.js @@ -0,0 +1,174 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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 { resolve } = require('path'); +const webpack = require('webpack'); +const merge = require('webpack-merge'); +const HtmlWebPackPlugin = require('html-webpack-plugin'); +// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; +const autoprefixer = require('autoprefixer'); +const CleanWebpackPlugin = require('clean-webpack-plugin'); +const TerserPlugin = require('terser-webpack-plugin'); +const CompressionWebpackPlugin = require('compression-webpack-plugin'); +const common = require('./webpack.common'); +const theme = require('./theme'); + +const root = (path) => resolve(__dirname, `../${path}`); + +const { version, ...restConfig } = common; + +module.exports = (env) => { + const API = (env || {}).API || 'mock'; + + return merge(restConfig, { + entry: { + main: root('src/core/index.jsx'), + }, + output: { + filename: '[name].js', + path: root('skyline_console/static'), + publicPath: '/', + chunkFilename: `[name].bundle.${version}.js`, + }, + mode: 'production', + // devtool: 'inline-source-map', + module: { + rules: [ + { + test: /\.css$/, + use: [ + { + loader: 'style-loader', + }, + { + loader: 'css-loader', + }, + ], + }, + { + test: /\.(css|less)$/, + exclude: /node_modules/, + use: [ + { + loader: 'style-loader', // creates style nodes from JS strings + }, + { + loader: 'css-loader', // translates CSS into CommonJS + options: { + modules: { + mode: 'global', + }, + localIdentName: '[name]__[local]--[hash:base64:5]', + }, + }, + { + loader: 'postcss-loader', + options: { + plugins: [autoprefixer('last 2 version')], + sourceMap: false, + }, + }, + { + loader: 'less-loader', // compiles Less to CSS + options: { + importLoaders: true, + javascriptEnabled: true, + }, + }, + ], + }, + { + test: /\.(less)$/, + include: /node_modules/, + use: [ + { + loader: 'style-loader', // creates style nodes from JS strings + }, + { + loader: 'css-loader', // translates CSS into CommonJS + }, + { + loader: 'less-loader', // compiles Less to CSS + options: { + javascriptEnabled: true, + modifyVars: theme, + }, + }, + ], + }, + ], + }, + plugins: [ + // 热更新没必要。 + // new webpack.HotModuleReplacementPlugin(), + // new OpenBrowserPlugin({ + // url: 'http://localhost:8080', + // browser: "Google Chrome", + // }), + new webpack.DefinePlugin({ + // 为项目注入环境变量 + 'process.env.API': JSON.stringify(API), + }), + new HtmlWebPackPlugin({ + template: root('src/asset/template/index.html'), + favicon: root('src/asset/image/favicon.ico'), + }), + new CleanWebpackPlugin(['dist'], { + root: resolve(__dirname, `../`), + }), + new CompressionWebpackPlugin({ + algorithm: 'gzip', + test: /\.js$/, + threshold: 10240, + minRatio: 0.8, + }), + // new BundleAnalyzerPlugin(), + ], + optimization: { + splitChunks: { + maxInitialRequests: 10, + cacheGroups: { + commons: { + chunks: 'async', + name: 'common', + minChunks: 2, + minSize: 0, + }, + vendor: { + test: /node_modules/, + chunks: 'async', + name: 'vendor', + priority: 10, + enforce: true, + }, + }, + }, + runtimeChunk: { + name: () => `runtime.${version}`, + }, + minimize: true, // default true for production + minimizer: [ + new TerserPlugin({ + extractComments: false, + sourceMap: false, + terserOptions: { + compress: { + drop_console: true, + }, + }, + }), + ], + }, + }); +}; diff --git a/cypress.json b/cypress.json new file mode 100644 index 00000000..65de70e5 --- /dev/null +++ b/cypress.json @@ -0,0 +1,28 @@ +{ + "baseUrl": "http://localhost:8081", + "viewportWidth": 1600, + "viewportHeight": 900, + "video": false, + "env": { + "username": "administrator", + "password": "passw0rd", + "region": "RegionOne", + "domain": "Default", + "sessionKey": "X-Auth-Token", + "language": "en" + }, + "reporter": "mochawesome", + "reporterOptions": { + "reportDir": "test/e2e/results", + "overwrite": false, + "html": false, + "json": true + }, + "fixturesFolder": "test/e2e/fixtures", + "integrationFolder": "test/e2e/integration", + "pluginsFile": "test/e2e/plugins/index.js", + "screenshotsFolder": "test/e2e/screenshots", + "videosFolder": "test/e2e/videos", + "supportFile": "test/e2e/support/index.js", + "downloadsFolder": "test/e2e/downloads" +} diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 00000000..1ad15170 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,36 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +module.exports = { + bail: true, + transformIgnorePatterns: ['/node_modules/'], + transform: { + '^.+\\.(js|jsx)$': 'babel-jest', + }, + moduleNameMapper: { + '.+\\.(css|styl|less|sass|scss)$': 'identity-obj-proxy', + '\\.svg': '/test/unit/svg-mock.js', + '^@/(.*)$': '/src/$1', + '^src(.*)$': '/src$1', + '^components(.*)$': '/src/components$1', + '^layouts(.*)$': '/src/layouts$1', + '^stores(.*)$': '/src/stores$1', + '^utils(.*)$': '/src/utils$1', + '^pages(.*)$': '/src/pages$1', + }, + moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx'], + moduleDirectories: ['node_modules', 'src'], + testPathIgnorePatterns: ['node_modules', '.cache', 'test/e2e', 'config'], + setupFiles: ['/test/unit/setup-tests.js'], +}; diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 00000000..827a2714 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "jsx": "react", + "baseUrl": ".", + "experimentalDecorators": true, + "paths": { + "@/*": ["./src/*"], + "src/*": ["./src/*"], + "asset/*": ["./src/asset/*"], + "image/*": ["./src/asset/image/*"], + "core/*": ["./src/core/*"], + "containers/*": ["./src/containers/*"], + "layouts/*": ["./src/layouts/*"], + "components/*": ["./src/components/*"], + "pages/*": ["./src/pages/*"], + "utils/*": ["./src/utils/*"], + "stores/*": ["./src/stores/*"], + "locales/*": ["./src/locales/*"], + "styles/*": ["./src/styles/*"], + "resources/*": ["./src/resources/*"] + } + }, + "include": ["src/**/*"] +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..919f5901 --- /dev/null +++ b/package.json @@ -0,0 +1,156 @@ +{ + "name": "skyline-console", + "version": "0.1.0", + "description": "", + "author": "OpenStack ", + "license": "Apache 2.0", + "scripts": { + "mock": "webpack-dev-server --open --config config/webpack.dev.js", + "dev": "cross-env NODE_ENV=development webpack-dev-server --open --config config/webpack.dev.js --env.API=dev", + "build": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=2048 webpack --progress --config config/webpack.prod.js", + "build:win": "set NODE_OPTIONS=--max-old-space-size=2048 && webpack --progress --config config/webpack.prod.js", + "build:test": "cross-env NODE_ENV=test NODE_OPTIONS=--max-old-space-size=2048 webpack --progress --config config/webpack.e2e.js", + "i18n": "grunt", + "lint": "eslint src --fix --quiet --ext .js,.jsx", + "lint:test": "eslint test --fix --quiet --ext .js", + "report:delete-json": "rm -rf test/e2e/results/* || true", + "report:delete-html": "rm -rf test/e2e/report || true", + "report:pre": "npm run report:delete-json && npm run report:delete-html && mkdir test/e2e/report", + "report:merge": "npx mochawesome-merge test/e2e/results/*.json > test/e2e/report/merge-report.json", + "report:generate": "npm run report:merge && npx mochawesome-report-generator test/e2e/report/merge-report.json -o test/e2e/report", + "test:e2e:run": "npm run report:pre && cypress run || true", + "test:e2e": "npm run test:e2e:run && npm run report:generate", + "test:e2e:open": "cypress open", + "test:e2e:server": "cross-env NODE_ENV=test webpack-dev-server --open --progress --config config/webpack.e2e.js", + "test:unit": "cross-env NODE_ENV=development jest", + "test:unit:coverage": "cross-env NODE_ENV=development jest --coverage" + }, + "husky": { + "hooks": { + "pre-commit": "lint-staged" + } + }, + "lint-staged": { + "*.{js,jsx}": [ + "eslint --fix", + "git add" + ], + "*.{html,css,scss,md,json}": [ + "prettier --write", + "git add" + ] + }, + "dependencies": { + "@ant-design/icons": "^4.0.6", + "@antv/data-set": "^0.11.4", + "@antv/g6": "^3.5.10", + "@babel/runtime-corejs3": "^7.14.0", + "ace-builds": "^1.4.12", + "antd": "^4.1.3", + "array-move": "3.0.1", + "axios": "^0.21.1", + "bizcharts": "^4.0.6", + "cache-loader": "^4.1.0", + "cidr-regex": "^3.1.1", + "classnames": "^2.2.6", + "cookie": "^0.4.1", + "escape-html": "^1.0.3", + "eslint-plugin-babel": "^5.3.1", + "file-saver": "^2.0.2", + "history": "4.7.2", + "intersection-observer": "^0.11.0", + "intl-messageformat": "7.8.4", + "invariant": "^2.2.4", + "ip-address": "^7.1.0", + "js-yaml": "^4.0.0", + "json2csv": "^5.0.1", + "lodash": "^4.17.19", + "mobx": "^5.1.0", + "mobx-react": "^5.2.8", + "mobx-react-router": "^4.1.0", + "moment": "^2.24.0", + "nanoid": "^3.0.2", + "promise-polyfill": "^8.1.3", + "prop-types": "^15.7.2", + "qs": "^6.9.4", + "react": "^16.2.0", + "react-ace": "^9.2.0", + "react-document-title": "^2.0.3", + "react-dom": "^16.2.0", + "react-fast-compare": "^3.0.1", + "react-highcharts": "^16.0.2", + "react-router": "^4.3.1", + "react-router-dom": "^4.3.1", + "react-sortable-hoc": "1.11.0" + }, + "devDependencies": { + "@babel/core": "^7.14.3", + "@babel/plugin-proposal-class-properties": "^7.13.0", + "@babel/plugin-proposal-decorators": "^7.14.2", + "@babel/plugin-proposal-throw-expressions": "^7.12.13", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-transform-modules-commonjs": "^7.14.0", + "@babel/plugin-transform-runtime": "^7.14.3", + "@babel/preset-env": "^7.14.2", + "@babel/preset-react": "^7.13.13", + "@cypress/code-coverage": "^3.9.5", + "autoprefixer": "^9.3.1", + "babel-eslint": "^9.0.0", + "babel-jest": "^26.6.3", + "babel-loader": "^8.1.0", + "babel-plugin-import": "^1.8.0", + "babel-plugin-istanbul": "^6.0.0", + "babel-plugin-react-css-modules": "^3.4.2", + "clean-webpack-plugin": "^1.0.0", + "compression-webpack-plugin": "5.0.1", + "cross-env": "^7.0.3", + "css-loader": "^0.28.11", + "cypress": "6.8.0", + "cypress-file-upload": "^5.0.6", + "enzyme": "^3.11.0", + "enzyme-adapter-react-16": "^1.15.6", + "eslint": "^7.2.0", + "eslint-config-airbnb": "18.2.1", + "eslint-config-prettier": "^8.3.0", + "eslint-import-resolver-alias": "^1.1.2", + "eslint-plugin-cypress": "^2.11.2", + "eslint-plugin-import": "^2.22.1", + "eslint-plugin-jsx-a11y": "^6.4.1", + "eslint-plugin-prettier": "^3.4.0", + "eslint-plugin-react": "^7.23.2", + "eslint-plugin-react-hooks": "^1.7.0", + "file-loader": "^6.0.0", + "grunt": "^1.2.1", + "happypack": "^5.0.1", + "html-webpack-plugin": "^3.1.0", + "husky": "^1.0.0-rc.14", + "i18next-scanner": "2.9.2", + "identity-obj-proxy": "^3.0.0", + "istanbul-lib-coverage": "^3.0.0", + "jest": "^26.6.3", + "jest-enzyme": "^7.1.2", + "less": "^3.8.1", + "less-loader": "^4.1.0", + "lint-staged": "^11.0.0", + "mochawesome": "^6.2.2", + "mochawesome-merge": "^4.2.0", + "mochawesome-report-generator": "^5.2.0", + "postcss-less": "^2.0.0", + "postcss-loader": "^3.0.0", + "prettier": "^2.3.0", + "react-css-modules": "^4.7.7", + "react-hot-loader": "^4.12.20", + "style-loader": "^0.20.3", + "terser-webpack-plugin": "4.2.3", + "url-loader": "^4.1.1", + "webpack": "^4.42.1", + "webpack-cli": "3.3.0", + "webpack-dev-server": "^3.1.10", + "webpack-merge": "^4.1.4" + }, + "engines": { + "node": ">=10.22.0", + "yarn": ">=1.22.4" + } +} diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 00000000..d9af7385 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,8 @@ +package = [] + +[metadata] +lock-version = "1.1" +python-versions = "*" +content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" + +[metadata.files] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..abf912c2 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,10 @@ +[tool.poetry] +name = "skyline-console" +version = "0.1.0" +description = "" +authors = ["OpenStack "] +include = ["skyline_console/static/**/*"] + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/skyline_console/__init__.py b/skyline_console/__init__.py new file mode 100644 index 00000000..3dc1f76b --- /dev/null +++ b/skyline_console/__init__.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/skyline_console/__main__.py b/skyline_console/__main__.py new file mode 100644 index 00000000..9bbb058c --- /dev/null +++ b/skyline_console/__main__.py @@ -0,0 +1,12 @@ +import sys +from pathlib import Path + +import skyline_console + +static_path = Path(skyline_console.__file__).parent.joinpath("static") + +if static_path.joinpath("index.html").exists(): + print(f'Static resource directory of "skyline-console" is:\n{str(static_path)}') +else: + print('Error, "skyline-console" doesn\'t contain any static resources') + sys.exit(1) diff --git a/src/api/cinder/backup.js b/src/api/cinder/backup.js new file mode 100644 index 00000000..48acbce0 --- /dev/null +++ b/src/api/cinder/backup.js @@ -0,0 +1,47 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import cinderBase from './base'; + +/** + * Create a restore on backup + * @param {String} projectId The UUID of the project in a multi-tenancy cloud. + * @param {String} backupId The UUID of the backupchains. + * @param {Object} data request body + * @param {Object} data.restore The restore object. + * @returns {Promise} + */ +export const createBackupRestoreOnCinder = (projectId, backupId, data) => + axios.request({ + method: 'post', + url: cinderBase(`${projectId}/backups/${backupId}/restore`), + data, + }); + +/** + * Create a restore on backup chain + * @param {String} projectId The UUID of the project in a multi-tenancy cloud. + * @param {String} backupId The UUID of the backupchains. + * @param {Object} data request body + * @param {Object} data.restore The restore object. + * @returns {Promise} + */ +export const createBackupChainRestoreOnCinder = (projectId, backupId, data) => + axios.request({ + method: 'post', + url: cinderBase(`${projectId}/backup_chains/${backupId}/restore`), + data, + }); diff --git a/src/api/cinder/base.js b/src/api/cinder/base.js new file mode 100644 index 00000000..a232d23b --- /dev/null +++ b/src/api/cinder/base.js @@ -0,0 +1,23 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @param {String} key api url + * @returns {String} + */ +import { cinderBase } from 'utils/constants'; + +const getCinderBaseUrl = (key) => `${cinderBase()}/${key}`; + +export default getCinderBaseUrl; diff --git a/src/api/cinder/os-availability-zone.js b/src/api/cinder/os-availability-zone.js new file mode 100644 index 00000000..58bc4e89 --- /dev/null +++ b/src/api/cinder/os-availability-zone.js @@ -0,0 +1,31 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import cinderBase from './base'; + +/** + * Show quota usage for a project + * @param {String} adminProjectId The UUID of the administrative project. + * @param {Object} params request query + * @param {Boolean} params.usage Default : false + * @returns {Promise} + */ +export const fetchAvailabilityZoneOnProject = (adminProjectId, params) => + axios.request({ + method: 'get', + url: cinderBase(`${adminProjectId}/os-availability-zone`), + params, + }); diff --git a/src/api/cinder/os-quota-sets.js b/src/api/cinder/os-quota-sets.js new file mode 100644 index 00000000..4aabd324 --- /dev/null +++ b/src/api/cinder/os-quota-sets.js @@ -0,0 +1,52 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import cinderBase from './base'; + +/** + * Show quota usage for a project + * @param {String} adminProjectId The UUID of the administrative project. + * @param {String} projectId The UUID of the project in a multi-tenancy cloud. + * @param {Object} params request query + * @param {Boolean} params.usage Default : false + * @returns {Promise} + */ +export const fetchQuotaUsageOnProject = (adminProjectId, projectId, params) => + axios.request({ + method: 'get', + url: cinderBase(`${adminProjectId}/os-quota-sets/${projectId}`), + params, + }); + +/** + * Update quotas for a project + * @param {String} adminProjectId The UUID of the tenant in a multi-tenancy cloud. + * @param {String} projectId The UUID of the project in a multi-tenancy cloud. + * @param {Object} data request body + * @param {Object} data.quota_set A quota object. + * @param {String} data.quota_set.volumes The number of volumes that are allowed for each project. + * @param {Number} data.quota_set.gigabytes The size (GB) of volumes and snapshots that are allowed for each project. + * @param {Number} data.quota_set.backup_gigabytes The size (GB) of backups that are allowed for each project. + * @param {Number} data.quota_set.snapshots The number of snapshots that are allowed for each project. + * @param {Number} data.quota_set.backups The number of backups that are allowed for each project. + * @returns {Promise} + */ +export const updateCinderQuotaSets = (adminProjectId, projectId, data) => + axios.request({ + method: 'put', + url: cinderBase(`${adminProjectId}/os-quota-sets/${projectId}`), + data, + }); diff --git a/src/api/cinder/os-services.js b/src/api/cinder/os-services.js new file mode 100644 index 00000000..bf1f3a16 --- /dev/null +++ b/src/api/cinder/os-services.js @@ -0,0 +1,54 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import cinderBase from './base'; + +/** + * Cinder Service change + * @param {String} projectId The UUID of the project in a multi-tenancy cloud. + * @param {String} actionName Actions name Avaliable Values : disable,disable-log-reason,enable,get-log,set-log,freeze,thaw,failover_host + * @param {Object} data request body + * @param {String} data.host The name of the host, when actionName is disable + * @param {String} data.binary The binary name of the service, when actionName is disable + * @param {String} data.disabled_reason The reason for disabling a service. + * @param {String} data.server The name of the host. + * @param {String} data.prefix The prefix for the log path we are querying, for example cinder. or sqlalchemy.engine + * @param {String} data.levels The log level to set, case insensitive, accepted values are INFO, WARNING, ERROR and DEBUG. + * @param {String} data.backend_id ID of backend to failover to. Default is None. + * @param {String} data.cluster The cluster name. Only in cinder-volume service.New in version 3.7 + * @returns {Promise} + */ +export const toggleChangeCinderOsService = (projectId, actionName, data) => + axios.request({ + method: 'put', + url: cinderBase(`${projectId}/os-services/${actionName}`), + data, + }); + +/** + * List All Cinder Services + * @param {String} projectId The UUID of the project in a multi-tenancy cloud. + * @param {Object} params request query + * @param {String} params.binary Filter the service list result by binary name of the service. + * @param {String} params.host Filter the service list result by host name of the service. + * @returns {Promise} + */ +export const fetchListCinderServices = (projectId, params) => + axios.request({ + method: 'get', + url: cinderBase(`${projectId}/os-services`), + params, + }); diff --git a/src/api/cinder/qos-specs.js b/src/api/cinder/qos-specs.js new file mode 100644 index 00000000..78aff00c --- /dev/null +++ b/src/api/cinder/qos-specs.js @@ -0,0 +1,106 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import cinderBase from './base'; + +/** + * Create a QoS specification + * @param {String} projectId The UUID of the project in a multi-tenancy cloud. + * @param {Object} data request body + * @param {Object} data.qos_specs A qos_specs object. + * @param {String} data.qos_specs.name The name of the QoS specification. + * @returns {Promise} + */ +export const createQosSpecOnCinder = (projectId, data) => + axios.request({ + method: 'post', + url: cinderBase(`${projectId}/qos-specs`), + data, + }); + +/** + * Set keys in a QoS specification + * @param {String} projectId The UUID of the project in a multi-tenancy cloud. + * @param {String} qosId The ID of the QoS specification. + * @param {Object} data request body + * @param {Object} data.qos_specs A qos_specs object. + * @returns {Promise} + */ +export const updateQosSpecOnCinder = (projectId, qosId, data) => + axios.request({ + method: 'put', + url: cinderBase(`${projectId}/qos-specs/${qosId}`), + data, + }); + +/** + * Unset keys in a QoS specification + * @param {String} projectId The UUID of the project in a multi-tenancy cloud. + * @param {String} qosId The ID of the QoS specification. + * @param {Object} data request body + * @param {Array} data.keys List of Keys. + * @returns {Promise} + */ +export const deleteKeysInQosSpecOnCinder = (projectId, qosId, data) => + axios.request({ + method: 'put', + url: cinderBase(`${projectId}/qos-specs/${qosId}`), + data, + }); + +/** + * Associate QoS specification with a volume type + * @param {String} projectId The UUID of the project in a multi-tenancy cloud. + * @param {String} qosId The ID of the QoS specification. + * @param {Object} params request query + * @param {Object} params.vol_type_id A volume type ID. + * @returns {Promise} + */ +export const fetchAssociateQosSpecOnCinder = (projectId, qosId, params) => + axios.request({ + method: 'get', + url: cinderBase(`${projectId}/qos-specs/${qosId}/associate`), + params, + }); + +/** + * Disassociate QoS specification from a volume type + * @param {String} projectId The UUID of the project in a multi-tenancy cloud. + * @param {String} qosId The ID of the QoS specification. + * @param {Object} params request query + * @param {Object} params.vol_type_id A volume type ID. + * @returns {Promise} + */ +export const fetchDisassociateQosSpecOnCinder = (projectId, qosId, params) => + axios.request({ + method: 'get', + url: cinderBase(`${projectId}/qos-specs/${qosId}/associate`), + params, + }); + +/** + * Show a QoS specification details + * @param {String} projectId The UUID of the project in a multi-tenancy cloud. + * @param {String} qosId The ID of the QoS specification. + * @param {Object} params request query + * @returns {Promise} + */ +export const fetchQosSpecDetailsOnCinder = (projectId, qosId, params) => + axios.request({ + method: 'get', + url: cinderBase(`${projectId}/qos-specs/${qosId}`), + params, + }); diff --git a/src/api/cinder/snapshots.js b/src/api/cinder/snapshots.js new file mode 100644 index 00000000..b0e14128 --- /dev/null +++ b/src/api/cinder/snapshots.js @@ -0,0 +1,62 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import cinderBase from './base'; + +/** + * List accessible snapshots + * @param {String} projectId The UUID of the project in a multi-tenancy cloud. + * @param {Object} params request body + * @param {String} params.all_tenants Shows details for all project. Admin only. + * @param {String} params.sort A valid direction is asc (ascending) or desc (descending). + * @param {String} params.limit Default value : 10 + * @param {String} params.offset Used in conjunction with limit to return a slice of items. + * @param {String} params.marker The ID of the last-seen item. + * @param {String} params.with_count Whether to show count in API response or not, default is False. + * @returns {Promise} + */ +export const fetchListAccessibleSnapshots = (projectId, params) => + axios.request({ + method: 'get', + url: cinderBase(`${projectId}/snapshots`), + params, + }); + +/** + * Show a snapshot’s details + * @param {String} projectId The UUID of the project in a multi-tenancy cloud. + * @param {String} snapshotId The UUID of the snapshot. + * @returns {Promise} + */ +export const fetchListAccessibleSnapshotDetails = (projectId, snapshotId) => + axios.request({ + method: 'get', + url: cinderBase(`${projectId}/snapshots/${snapshotId}`), + }); + +/** + * Update a snapshot + * @param {String} projectId The UUID of the project in a multi-tenancy cloud. + * @param {String} snapshotId The UUID of the snapshot. + * @param {Object} data request body + * @returns {Promise} + */ +export const updateSnapshotOnCinder = (projectId, snapshotId, data) => + axios.request({ + method: 'put', + url: cinderBase(`${projectId}/snapshots/${snapshotId}`), + data, + }); diff --git a/src/api/cinder/types.js b/src/api/cinder/types.js new file mode 100644 index 00000000..13ae0c08 --- /dev/null +++ b/src/api/cinder/types.js @@ -0,0 +1,180 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import cinderBase from './base'; + +/** + * Show all extra specifications for volume type + * @param {String} projectId The UUID of the project in a multi-tenancy cloud. + * @param {String} volumeTypeId The UUID for an existing volume type. + * @param {Object} params request query + * @returns {Promise} + */ +export const fetchExtraSpecsForTypes = (projectId, volumeTypeId, params) => + axios.request({ + method: 'get', + url: cinderBase(`${projectId}/types/${volumeTypeId}/extra_specs`), + params, + }); + +/** + * Create or update extra specs for volume type + * @param {String} projectId The UUID of the project in a multi-tenancy cloud. + * @param {String} volumeTypeId The UUID for an existing volume type. + * @param {Object} body request body + * @param {Object} body.extra_specs A set of key and value pairs that contains the specifications for a volume type. + * @returns {Promise} + */ +export const createExtraSpecsForTypes = (projectId, volumeTypeId, data) => + axios.request({ + method: 'post', + url: cinderBase(`${projectId}/types/${volumeTypeId}/extra_specs`), + data, + }); + +/** + * Delete extra specification for volume type + * @param {String} projectId The UUID of the project in a multi-tenancy cloud. + * @param {String} volumeTypeId The UUID for an existing volume type. + * @param {String} keyName The key name of the extra spec for a volume type. + * @returns {Promise} + */ +export const deleteExtraSpecsForTypes = (projectId, volumeTypeId, keyName) => + axios.request({ + method: 'delete', + url: cinderBase( + `${projectId}/types/${volumeTypeId}/extra_specs/${keyName}` + ), + }); + +/** + * Create volume type for v2 + * @param {String} projectId The UUID of the project in a multi-tenancy cloud. + * @param {Object} data request body + * @param {Object} data.volume_type A volume_type object. + * @param {String} data.volume_type.name The name of the Volume Transfer. + * @param {String} data.volume_type.description The backup description or null. + * @param {Boolean} data.volume_type.is_public Volume type which is accessible to the public. + * @param {Object} data.volume_type.extra_specs A set of key and value pairs that contains the specifications for a volume type. + * @param {String} data.volume_type.extra_specs.capabilities example : "gpu" + * @returns {Promise} + */ +export const createVolumeType = (projectId, data) => + axios.request({ + method: 'post', + url: cinderBase(`${projectId}/types`), + data, + }); + +/** + * Update volume type + * @param {String} projectId The UUID of the project in a multi-tenancy cloud. + * @param {String} volumeTypeId The UUID for an existing volume type. + * @param {Object} data request body + * @param {Object} data.volume_type A volume_type object. + * @param {String} data.volume_type.name The name of the Volume Transfer. + * @param {String} data.volume_type.description The backup description or null. + * @param {Boolean} data.volume_type.is_public Volume type which is accessible to the public. + * @param {Object} data.volume_type.extra_specs A set of key and value pairs that contains the specifications for a volume type. + * @param {String} data.volume_type.extra_specs.capabilities example : "gpu" + * @returns {Promise} + */ +export const updateVolumeType = (projectId, volumeTypeId, data) => + axios.request({ + method: 'put', + url: cinderBase(`${projectId}/types/${volumeTypeId}`), + data, + }); + +/** + * Show an encryption type for v2 + * @param {String} projectId The UUID of the project in a multi-tenancy cloud. + * @param {String} volumeTypeId The UUID for an existing volume type. + * @returns {Promise} + */ +export const fetchVolumeTypesEncryption = (projectId, volumeTypeId) => + axios.request({ + method: 'get', + url: cinderBase(`${projectId}/types/${volumeTypeId}/encryption`), + }); + +/** + * Create an encryption type for v2 + * @param {String} projectId The UUID of the project in a multi-tenancy cloud. + * @param {String} volumeTypeId The UUID for an existing volume type. + * @param {Object} data request body + * @param {Object} data.encryption The encryption information. + * @param {String} data.encryption.key_size Size of encryption key, in bits. For example, 128 or 256. The default value is None. + * @param {String} data.encryption.provider The class that provides encryption support. + * @param {Boolean} data.encryption.control_location The default value is “front-end”. + * @param {Object} data.encryption.cipher The encryption algorithm or mode. For example, aes-xts-plain64. The default value is None. + * @returns {Promise} + */ +export const createVolumeTypesEncryption = (projectId, volumeTypeId, data) => + axios.request({ + method: 'post', + url: cinderBase(`${projectId}/types/${volumeTypeId}/encryption`), + data, + }); + +/** + * Delete an encryption type for v2 + * @param {String} projectId The UUID of the project in a multi-tenancy cloud. + * @param {String} volumeTypeId The UUID for an existing volume type. + * @param {String} encryptionId The ID of the encryption type. + * @returns {Promise} + */ +export const deleteVolumeTypesEncryption = ( + projectId, + volumeTypeId, + encryptionId +) => + axios.request({ + method: 'delete', + url: cinderBase( + `${projectId}/types/${volumeTypeId}/encryption/${encryptionId}` + ), + }); + +/** + * Add private volume type access + * @param {String} projectId The UUID of the project in a multi-tenancy cloud. + * @param {String} volumeTypeId The ID of Volume Type to be accessed by project. + * @param {Object} data request body + * @param {Object} data.addProjectAccess A addProjectAccess object. When add request + * @param {String} data.addProjectAccess.project The ID of the project. When add request + * @param {Object} data.removeProjectAccess A removeProjectAccess project. When delete request + * @param {String} data.removeProjectAccess.project The ID of the project. When delete request + * @returns {Promise} + */ +export const addOrDeleteVolumeTypeAccess = (projectId, volumeTypeId, data) => + axios.request({ + method: 'post', + url: cinderBase(`${projectId}/types/${volumeTypeId}/action`), + data, + }); + +/** + * List private volume type access details + * @param {String} projectId The UUID of the project in a multi-tenancy cloud. + * @param {String} volumeTypeId The ID of Volume Type to be accessed by project. + * @returns {Promise} + */ +export const fetchVolumeTypesAccessDetails = (projectId, volumeTypeId) => + axios.request({ + method: 'get', + url: cinderBase(`${projectId}/types/${volumeTypeId}/os-volume-type-access`), + }); diff --git a/src/api/cinder/volume-transfers.js b/src/api/cinder/volume-transfers.js new file mode 100644 index 00000000..ebf75940 --- /dev/null +++ b/src/api/cinder/volume-transfers.js @@ -0,0 +1,80 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import cinderBase from './base'; + +/** + * Create a volume transfer + * @param {String} projectId The UUID of the project in a multi-tenancy cloud. + * @param {Object} data request body + * @param {Object} data.transfer The volume transfer object. + * @param {String} data.transfer.name The name of the object. + * @param {String} data.transfer.volume_id The UUID of the volume. + * @param {Boolean} data.transfer.no_snapshots Transfer volume without snapshots. Defaults to False if not specified. + * @returns {Promise} + */ +export const createVolumenTransfer = (projectId, data) => + axios.request({ + method: 'post', + url: cinderBase(`${projectId}/volume-transfers`), + data, + }); + +/** + * List volume transfers for a project + * @param {String} projectId The UUID of the project in a multi-tenancy cloud. + * @param {Object} params request query + * @param {Object} params.all_tenants Shows details for all project. Admin only. + * @param {String} params.limit Requests a page size of items. Returns a number of items up to a limit value. + * @param {String} params.offset Used in conjunction with limit to return a slice of items. offset is where to start in the list. + * @param {Boolean} params.marker The ID of the last-seen item. + * @param {Boolean} params.sort_key Sorts by an attribute. Default is created_at. + * @param {Boolean} params.sort_dir Sorts by one or more sets of attribute and sort direction combinations. + * @returns {Promise} + */ +export const fetchVolumenTransfersForProject = (projectId, params) => + axios.request({ + method: 'get', + url: cinderBase(`${projectId}/volume-transfers`), + params, + }); + +/** + * Delete a volume transfer + * @param {String} projectId The UUID of the project in a multi-tenancy cloud. + * @param {Object} transferId The unique identifier for a volume transfer. + * @returns {Promise} + */ +export const deleteVolumenTransfer = (projectId, transferId) => + axios.request({ + method: 'delete', + url: cinderBase(`${projectId}/volume-transfers/${transferId}`), + }); + +/** + * Accept a volume transfer + * @param {String} projectId The UUID of the project in a multi-tenancy cloud. + * @param {Object} data request body + * @param {Object} data.accept The accept object. + * @param {String} data.accept.auth_key The name of the object. + * @returns {Promise} + */ +export const acceptVolumenTransfer = (projectId, transferId, data) => + axios.request({ + method: 'post', + url: cinderBase(`${projectId}/volume-transfers/${transferId}/accept`), + data, + }); diff --git a/src/api/cinder/volume.js b/src/api/cinder/volume.js new file mode 100644 index 00000000..a91e6d3b --- /dev/null +++ b/src/api/cinder/volume.js @@ -0,0 +1,62 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import cinderBase from './base'; + +/** + * Show a volume’s details + * @param {String} projectId The UUID of the project in a multi-tenancy cloud. + * @param {Object} volumeId The UUID of the volume. + * @returns {Promise} + */ +export const fetchAccessibleVolumeDetails = (projectId, volumeId) => + axios.request({ + method: 'get', + url: cinderBase(`${projectId}/volumes/${volumeId}`), + }); + +/** + * Volume actions + * @param {String} projectId The UUID of the project in a multi-tenancy cloud. + * @param {Object} volumeId The UUID of the volume. + * @param {Object} data request body + * @see https://docs.openstack.org/api-ref/block-storage/v3/index.html?expanded=id356-detail#volume-transfers-volume-transfers-3-55-or-later + * @returns {Promise} + */ +export const volumeActionsOnCinder = (projectId, volumeId, data) => + axios.request({ + method: 'post', + url: cinderBase(`${projectId}/volumes/${volumeId}/action`), + data, + }); + +/** + * Update a volume + * @param {String} projectId The UUID of the project in a multi-tenancy cloud. + * @param {String} volumeId The UUID of the volume. + * @param {Object} data request body + * @param {Object} data.volume A volume object. + * @param {String} data.volume.description The volume description. + * @param {String} data.volume.name The volume name. + * @param {Object} data.volume.metadata One or more metadata key and value pairs that are associated with the volume. + * @returns {Promise} + */ +export const updateVolumeOnCinder = (projectId, volumeId, data) => + axios.request({ + method: 'put', + url: cinderBase(`${projectId}/volumes/${volumeId}/action`), + data, + }); diff --git a/src/api/glance/base.js b/src/api/glance/base.js new file mode 100644 index 00000000..485ed93a --- /dev/null +++ b/src/api/glance/base.js @@ -0,0 +1,23 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @param {String} key api url + * @returns {String} + */ +import { glanceBase } from 'utils/constants'; + +const getGlanceBaseUrl = (key) => `${glanceBase()}/${key}`; + +export default getGlanceBaseUrl; diff --git a/src/api/glance/images.js b/src/api/glance/images.js new file mode 100644 index 00000000..8d6228e6 --- /dev/null +++ b/src/api/glance/images.js @@ -0,0 +1,174 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import cinderBase from './base'; + +/** + * Create image + * @param {Object} data request body + * @param {String} data.container_format Format of the image container. + * @param {String} data.disk_format The format of the disk. + * @param {String} data.id A unique, user-defined image UUID, in the format: nnnnnnnn-nnnn-nnnn-nnnn-nnnnnnnnnnnn + * @param {Number} data.min_disk Amount of disk space in GB that is required to boot the image. + * @param {Number} data.min_ram Amount of RAM in MB that is required to boot the image. + * @param {String} data.name The name of the image. + * @param {Boolean} data.protected Image protection for deletion. + * @param {Array} data.tags List of tags for this image. + * @param {String} data.visibility Visibility for this image. Valid value is one of: public, private, shared, or community. + * @returns {Promise} + */ +export const createImage = (data) => + axios.request({ + method: 'post', + url: cinderBase('images'), + data, + }); + +/** + * Update image + * @param {String} imageId The UUID of the image. + * @param {Object} data request body + * @param {String} data.container_format Format of the image container. + * @param {String} data.disk_format The format of the disk. + * @param {String} data.id A unique, user-defined image UUID, in the format: nnnnnnnn-nnnn-nnnn-nnnn-nnnnnnnnnnnn + * @param {Number} data.min_disk Amount of disk space in GB that is required to boot the image. + * @param {Number} data.min_ram Amount of RAM in MB that is required to boot the image. + * @param {String} data.name The name of the image. + * @param {Boolean} data.protected Image protection for deletion. + * @param {Array} data.tags List of tags for this image. + * @param {String} data.visibility Visibility for this image. Valid value is one of: public, private, shared, or community. + * @returns {Promise} + */ +export const updateImage = (imageId, data) => + axios.request({ + method: 'patch', + url: cinderBase(`images/${imageId}`), + headers: { + 'content-type': 'application/openstack-images-v2.1-json-patch', + }, + data, + }); + +/** + * Upload binary image data + * Set the Content-Type request header to application/octet-stream. + * @param {String} imageId The UUID of the image. + * @param {File} data Image file + * @returns {Promise} + */ +export const uploadBinaryImageData = (imageId, data) => + axios.request({ + method: 'put', + url: cinderBase(`images/${imageId}/file`), + headers: { + 'content-type': 'application/octet-stream', + }, + data, + }); + +/** + * List images + * @param {Object} params request query + * @param {Number} params.limit Requests a page size of items + * @param {String} params.disk_format Example : "iso" + * @param {String} params.marker The ID of the last-seen item. + * @param {String} params.name Filters the response by a name. + * @param {String} params.owner Filters the response by a project (also called a “tenant”) ID. + * @param {Boolean} params.protected Filters the response by the ‘protected’ image property. + * @param {Number} params.status Filters the response by an image status. + * @param {Number} params.tag Filters the response by the specified tag value. + * @param {String} params.visibility Filters the response by an image visibility value. + * @param {Boolean} params.os_hidden When true, filters the response to display only “hidden” images. + * @param {String} params.member_status Filters the response by a member status. + * @param {String} params.size_max Filters the response by a maximum image size, in bytes. + * @param {String} params.size_min Filters the response by a minimum image size, in bytes. + * @param {String} params.created_at Specify a comparison filter based on the date and time when the resource was created. + * @param {String} params.updated_at Specify a comparison filter based on the date and time when the resource was most recently modified. + * @param {String} params.sort_dir Sorts the response by a set of one or more sort direction and attribute (sort_key) combinations. + * @param {String} params.sort_key Sorts the response by an attribute, such as name, id, or updated_at. + * @param {String} params.sort Sorts the response by one or more attribute and sort direction combinations. You can also set multiple sort keys and directions. Default direction is desc. + * @returns {Promise} + */ +export const fetchImages = (params) => + axios.request({ + method: 'get', + url: cinderBase('images'), + params, + }); + +/** + * List image members + * @param {String} imageId The UUID of the image. + * @returns {Promise} + */ +export const fetchListImageMembers = (imageId) => + axios.request({ + method: 'get', + url: cinderBase(`images/${imageId}/members`), + }); + +/** + * Create image member + * @param {String} imageId The UUID of the image. + * @param {Object} data request body + * @param {String} data.member The ID of the image member. + * @returns {Promise} + */ +export const createImageMember = (imageId, data) => + axios.request({ + method: 'get', + url: cinderBase(`images/${imageId}/members`), + data, + }); + +/** + * Update image member + * @param {String} imageId The UUID of the image. + * @param {String} memberId The ID of the image member. + * @param {Object} data request body + * @param {String} data.status The status of this image member. Value is one of pending, accepted, rejected. + * @returns {Promise} + */ +export const updateImageMember = (imageId, memberId, data) => + axios.request({ + method: 'put', + url: cinderBase(`images/${imageId}/members/${memberId}`), + data, + }); + +/** + * Delete image member + * @param {String} imageId The UUID of the image. + * @param {String} memberId The ID of the image member. + * @returns {Promise} + */ +export const deleteImageMember = (imageId, memberId) => + axios.request({ + method: 'delete', + url: cinderBase(`images/${imageId}/members/${memberId}`), + }); + +/** + * List images count + * @param {Object} params request query + * @returns {Promise} + */ +export const fetchImagesCountOnGlance = (params) => + axios.request({ + method: 'get', + url: cinderBase('images/count'), + params, + }); diff --git a/src/api/glance/metadefs.js b/src/api/glance/metadefs.js new file mode 100644 index 00000000..6f806ce0 --- /dev/null +++ b/src/api/glance/metadefs.js @@ -0,0 +1,106 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import cinderBase from './base'; + +/** + * Get namespace details + * @param {String} namespaceName The name of the namespace whose details you want to see. + * @param {Object} params request query + * @param {String} params.resource_type Apply the prefix for the specified resource type to the names of the properties listed in the response. + * @returns {Promise} + */ +export const fetchNamespaceDetailsOnGlance = (namespaceName, params) => + axios.request({ + method: 'get', + url: cinderBase(`metadefs/namespaces/${namespaceName}`), + params, + }); + +/** + * Update namespace + * @param {String} namespaceName The name of the namespace whose details you want to see. + * @param {Object} data request body + * @param {String} data.description The description of the namespace. + * @param {String} data.display_name User-friendly name to use in a UI to display the namespace name. + * @param {String} data.namespace An identifier (a name) for the namespace. + * @param {Boolean} data.protected Namespace protection for deletion. A valid value is true or false. Default is false. + * @param {String} data.visibility The namespace visibility. A valid value is public or private. Default is private. + * @returns {Promise} + */ +export const updateNamespaceOnGlance = (namespaceName, data) => + axios.request({ + method: 'put', + url: cinderBase(`metadefs/namespaces/${namespaceName}`), + data, + }); + +/** + * Create namespace + * @param {Object} data request body + * @param {String} data.description The description of the namespace. + * @param {String} data.display_name User-friendly name to use in a UI to display the namespace name. + * @param {String} data.namespace An identifier (a name) for the namespace. + * @param {Boolean} data.protected Namespace protection for deletion. A valid value is true or false. Default is false. + * @param {String} data.visibility The namespace visibility. A valid value is public or private. Default is private. + * @returns {Promise} + */ +export const createNamespaceOnGlance = (data) => + axios.request({ + method: 'post', + url: cinderBase('metadefs/namespaces'), + data, + }); + +/** + * List resource types + * @returns {Promise} + */ +export const fetchListResourceTypesOnGlance = () => + axios.request({ + method: 'get', + url: cinderBase('metadefs/resource_types'), + }); + +/** + * Remove resource type association + * @param {String} namespaceName The name of the namespace whose details you want to see. + * @param {String} resourceTypeName The name of the resource type. + * @returns {Promise} + */ +export const deleteResourceTypeOnGlance = (namespaceName, resourceTypeName) => + axios.request({ + method: 'delete', + url: cinderBase( + `metadefs/namespaces/${namespaceName}/resource_types/${resourceTypeName}` + ), + }); + +/** + * Create resource type association + * @param {String} namespaceName The name of the namespace whose details you want to see. + * @param {Object} data request body + * @param {String} data.name Name of the resource type. A Name is limited to 80 chars in length. + * @param {String} data.prefix Prefix for any properties in the namespace that you want to apply to the resource type. + * @param {String} data.properties_target Some resource types allow more than one key and value pair for each instance. + * @returns {Promise} + */ +export const createResourceTypeOnGlance = (namespaceName, data) => + axios.request({ + method: 'post', + url: cinderBase(`metadefs/namespaces/${namespaceName}/resource_types`), + data, + }); diff --git a/src/api/gocron/base.js b/src/api/gocron/base.js new file mode 100644 index 00000000..47e29752 --- /dev/null +++ b/src/api/gocron/base.js @@ -0,0 +1,23 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @param {String} key api url + * @returns {String} + */ +import { gocronBase } from 'utils/constants'; + +const getGocronBaseUrl = (key) => `${gocronBase()}/${key}`; + +export default getGocronBaseUrl; diff --git a/src/api/heat/base.js b/src/api/heat/base.js new file mode 100644 index 00000000..aee33519 --- /dev/null +++ b/src/api/heat/base.js @@ -0,0 +1,23 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @param {String} key api url + * @returns {String} + */ +import { heatBase } from 'utils/constants'; + +const getHeatBaseUrl = (key) => `${heatBase()}/${key}`; + +export default getHeatBaseUrl; diff --git a/src/api/heat/stacks.js b/src/api/heat/stacks.js new file mode 100644 index 00000000..3684e3b5 --- /dev/null +++ b/src/api/heat/stacks.js @@ -0,0 +1,86 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import getHeatBaseUrl from './base'; + +/** + * Create stack + * @param {Object} tenantId The UUID of the tenant. A tenant is also known as a project. + * @param {Object} data request body + * @see https://docs.openstack.org/api-ref/orchestration/v1/index.html?expanded=create-stack-detail#stacks + * @returns {Promise} + */ +export const createStackOnHeat = (tenantId, data) => + axios.request({ + method: 'post', + url: getHeatBaseUrl(`${tenantId}/stacks`), + data, + }); + +/** + * Update stack + * @param {Object} tenantId The UUID of the tenant. A tenant is also known as a project. + * @param {Object} stackName The name of a stack. + * @param {Object} stackId The UUID of the stack. + * @param {Object} data request body + * @see https://docs.openstack.org/api-ref/orchestration/v1/index.html?expanded=update-stack-detail#stacks + * @returns {Promise} + */ +export const updateStackOnHeat = (tenantId, stackName, stackId, data) => + axios.request({ + method: 'post', + url: getHeatBaseUrl(`${tenantId}/stacks/${stackName}/${stackId}`), + data, + }); + +/** + * Delete stack + * @param {Object} tenantId The UUID of the tenant. A tenant is also known as a project. + * @param {Object} stackName The name of a stack. + * @param {Object} stackId The UUID of the stack. + * @returns {Promise} + */ +export const deleteStackOnHeat = (tenantId, stackName, stackId) => + axios.request({ + method: 'delete', + url: getHeatBaseUrl(`${tenantId}/stacks/${stackName}/${stackId}`), + }); + +/** + * Abandon stack + * @param {Object} tenantId The UUID of the tenant. A tenant is also known as a project. + * @param {Object} stackName The name of a stack. + * @param {Object} stackId The UUID of the stack. + * @returns {Promise} + */ +export const abandonStackOnHeat = (tenantId, stackName, stackId) => + axios.request({ + method: 'delete', + url: getHeatBaseUrl(`${tenantId}/stacks/${stackName}/${stackId}/abandon`), + }); + +/** + * Get stack template + * @param {Object} tenantId The UUID of the tenant. A tenant is also known as a project. + * @param {Object} stackName The name of a stack. + * @param {Object} stackId The UUID of the stack. + * @returns {Promise} + */ +export const fetchStackTemplateOnHeat = (tenantId, stackName, stackId) => + axios.request({ + method: 'get', + url: getHeatBaseUrl(`${tenantId}/stacks/${stackName}/${stackId}/template`), + }); diff --git a/src/api/ironic-inspector/base.js b/src/api/ironic-inspector/base.js new file mode 100644 index 00000000..61d557ba --- /dev/null +++ b/src/api/ironic-inspector/base.js @@ -0,0 +1,23 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @param {String} key api url + * @returns {String} + */ +import { ironicInspectorBase } from 'utils/constants'; + +const getIronicInspectorBaseUrl = (key) => `${ironicInspectorBase()}/${key}`; + +export default getIronicInspectorBaseUrl; diff --git a/src/api/ironic/base.js b/src/api/ironic/base.js new file mode 100644 index 00000000..bc909b08 --- /dev/null +++ b/src/api/ironic/base.js @@ -0,0 +1,23 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @param {String} key api url + * @returns {String} + */ +import { ironicBase } from 'utils/constants'; + +const getIronicBaseUrl = (key) => `${ironicBase()}/${key}`; + +export default getIronicBaseUrl; diff --git a/src/api/ironic/nodes.js b/src/api/ironic/nodes.js new file mode 100644 index 00000000..553a32c8 --- /dev/null +++ b/src/api/ironic/nodes.js @@ -0,0 +1,205 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import getIronicBaseUrl from './base'; + +/** + * Create Node + * @param {Object} data request body + * @param {String} data.console_interface The boot interface for a Node, e.g. “pxe”. + * @param {String} data.conductor_group The conductor group for a node. + * @param {String} data.console_interface The console interface for a node, e.g. “no-console”. + * @param {String} data.deploy_interface The deploy interface for a node, e.g. “iscsi”. + * @param {String} data.driver_info All the metadata required by the driver to manage this Node. + * @param {String} data.driver The name of the driver used to manage this Node. + * @param {String} data.extra A set of one or more arbitrary metadata key and value pairs. + * @param {String} data.inspect_interface The interface used for node inspection, e.g. “no-inspect”. + * @param {String} data.management_interface Interface for out-of-band node management, e.g. “ipmitool”. + * @param {String} data.name Human-readable identifier for the Node resource. May be undefined. Certain words are reserved. + * @param {String} data.network_interface Which Network Interface provider to use when plumbing the network connections for this Node. + * @param {String} data.power_interface Interface used for performing power actions on the node, e.g. “ipmitool”. + * @param {String} data.properties Physical characteristics of this Node. + * @param {String} data.raid_interface Interface used for configuring RAID on this node, e.g. “no-raid”. + * @param {String} data.rescue_interface The interface used for node rescue, e.g. “no-rescue”. + * @param {String} data.resource_class A string which can be used by external schedulers to identify this Node as a unit of a specific type of resource. + * @param {String} data.storage_interface Interface used for attaching and detaching volumes on this node, e.g. “cinder”. + * @see https://docs.openstack.org/api-ref/baremetal/?expanded=create-node-detail + * @returns {Promise} + */ +export const createNodeOnIronic = (data) => + axios.request({ + method: 'post', + url: getIronicBaseUrl('nodes'), + data, + }); + +/** + * Update Node + * @param {String} nodeIdent The UUID or Name of the node. + * @see https://docs.openstack.org/api-ref/baremetal/?expanded=update-node-detail + * @returns {Promise} + */ +export const updateNodeOnIronic = (nodeIdent, data) => + axios.request({ + method: 'patch', + url: getIronicBaseUrl(`nodes/${nodeIdent}`), + data, + }); + +/** + * Node State Summary + * @param {String} nodeIdent The UUID or Name of the node. + * @returns {Promise} + */ +export const fetchNodeStateSummaryOnIronic = (nodeIdent) => + axios.request({ + method: 'get', + url: getIronicBaseUrl(`nodes/${nodeIdent}/states`), + }); + +/** + * Validate Node + * @param {String} nodeIdent The UUID or Name of the node. + * @returns {Promise} + */ +export const fetchNodeValidateOnIronic = (nodeIdent) => + axios.request({ + method: 'get', + url: getIronicBaseUrl(`nodes/${nodeIdent}/validate`), + }); + +/** + * List Ports by Node + * @param {String} nodeIdent The UUID or Name of the node. + * @returns {Promise} + */ +export const fetchNodePortsOnIronic = (nodeIdent) => + axios.request({ + method: 'get', + url: getIronicBaseUrl(`nodes/${nodeIdent}/ports`), + }); + +/** + * Change Node Provision State + * @param {String} nodeIdent The UUID or Name of the node. + * @param {Object} data request body + * @param {String} data.target The requested provisioning state of this Node. + * @param {String | Object} data.configdrive A config drive to be written to a partition on the Node’s boot disk. + * @param {Array} data.clean_steps An ordered list of cleaning steps that will be performed on the node. + * @param {Array} data.deploy_steps A list of deploy steps that will be performed on the node. + * @param {String} data.rescue_password Non-empty password used to configure rescue ramdisk during node rescue operation. + * @param {Boolean} data.disable_ramdisk If set to true, the ironic-python-agent ramdisk will not be booted for cleaning. + * @returns {Promise} + */ +export const changeNodeProvisionStateOnIronic = (nodeIdent, data) => + axios.request({ + method: 'put', + url: getIronicBaseUrl(`nodes/${nodeIdent}/states/provision`), + data, + }); + +/** + * Change Node Power State + * @param {String} nodeIdent The UUID or Name of the node. + * @param {Object} data request body + * @param {String} data.target Avaliable value : “power on”, “power off”, “rebooting”, “soft power off” or “soft rebooting”. + * @param {Number} data.timeout Timeout (in seconds) for a power state transition. + * @returns {Promise} + */ +export const changeNodePowerStateOnIronic = (nodeIdent, data) => + axios.request({ + method: 'put', + url: getIronicBaseUrl(`nodes/${nodeIdent}/states/power`), + data, + }); + +/** + * Set Maintenance Flag + * @param {String} nodeIdent The UUID or Name of the node. + * @param {Object} data request body + * @param {String} data.reason Specify the reason for setting the Node into maintenance mode. + * @returns {Promise} + */ +export const setMaintenanceFlagOnIronic = (nodeIdent, data) => + axios.request({ + method: 'put', + url: getIronicBaseUrl(`nodes/${nodeIdent}/maintenance`), + data, + }); + +/** + * Clear Maintenance Flag + * @param {String} nodeIdent The UUID or Name of the node. + * @returns {Promise} + */ +export const deleteMaintenanceFlagOnIronic = (nodeIdent) => + axios.request({ + method: 'delete', + url: getIronicBaseUrl(`nodes/${nodeIdent}/maintenance`), + }); + +/** + * Get Boot Device + * @param {String} nodeIdent The UUID or Name of the node. + * @returns {Promise} + */ +export const fetchBootDeviceOnIronic = (nodeIdent) => + axios.request({ + method: 'get', + url: getIronicBaseUrl(`nodes/${nodeIdent}/management/boot_device`), + }); + +/** + * Set Boot Device + * @param {String} nodeIdent The UUID or Name of the node. + * @param {Object} data request body + * @param {String} data.boot_device The boot device for a Node, eg. “pxe” or “disk”. + * @param {String} data.persistent Whether the boot device should be set only for the next reboot, or persistently. + * @returns {Promise} + */ +export const setBootDeviceOnIronic = (nodeIdent, data) => + axios.request({ + method: 'put', + url: getIronicBaseUrl(`nodes/${nodeIdent}/management/boot_device`), + data, + }); + +/** + * Get Supported Boot Devices + * @param {String} nodeIdent The UUID or Name of the node. + * @returns {Promise} + */ +export const fetchBootDeviceSupportedOnIronic = (nodeIdent) => + axios.request({ + method: 'get', + url: getIronicBaseUrl( + `nodes/${nodeIdent}/management/boot_device/supported` + ), + }); + +/** + * Set all traits of a node + * @param {String} nodeIdent The UUID or Name of the node. + * @param {Object} data request body + * @param {Object} data.traits List of traits for this node. + * @returns {Promise} + */ +export const setAllTraitsOnIronic = (nodeIdent, data) => + axios.request({ + method: 'put', + url: getIronicBaseUrl(`nodes/${nodeIdent}/traits`), + data, + }); diff --git a/src/api/ironic/port-groups.js b/src/api/ironic/port-groups.js new file mode 100644 index 00000000..f50be64e --- /dev/null +++ b/src/api/ironic/port-groups.js @@ -0,0 +1,51 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import getIronicBaseUrl from './base'; + +/** + * Create Portgroup + * @param {Object} data request body + * @param {String} data.node_uuid UUID of the Node this resource belongs to. + * @param {String} data.address Physical hardware address of this network Port. + * @param {String} data.name Human-readable identifier for the Portgroup resource. May be undefined. + * @param {Object} data.mode Mode of the port group. + * @param {Boolean} data.standalone_ports_supported Indicates whether ports that are members of this portgroup can be used as stand-alone ports. + * @param {String} data.properties Key/value properties related to the port group’s configuration. + * @param {String} data.extra A set of one or more arbitrary metadata key and value pairs. + * @param {String} data.uuid The UUID for the resource. + * @see https://docs.openstack.org/api-ref/baremetal/?expanded=set-all-traits-of-a-node-detail,create-portgroup-detail + */ +export const createPortGroupOnIronic = (data) => + axios.request({ + method: 'post', + url: getIronicBaseUrl('portgroups'), + data, + }); + +/** + * Update a Port + * @param {String} portId The UUID of the port. + * @param {Object} data request body + * @see https://docs.openstack.org/api-ref/baremetal/?expanded=set-all-traits-of-a-node-detail,update-a-portgroup-detail + * @returns {Promise} + */ +export const updatePortGroupOnIronic = (portgroupIdent, data) => + axios.request({ + method: 'patch', + url: getIronicBaseUrl(`portgroups/${portgroupIdent}`), + data, + }); diff --git a/src/api/ironic/ports.js b/src/api/ironic/ports.js new file mode 100644 index 00000000..7c6ddd74 --- /dev/null +++ b/src/api/ironic/ports.js @@ -0,0 +1,63 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import getIronicBaseUrl from './base'; + +/** + * List Detailed Ports + * @param {Object} params request query + * @param {String} params.node Filter the list of returned Ports. + */ +export const fetchDetailedPortsOnIronic = (params) => + axios.request({ + method: 'get', + url: getIronicBaseUrl('ports'), + params, + }); + +/** + * Create Port + * @param {Object} data request body + * @param {String} data.node_uuid UUID of the Node this resource belongs to. + * @param {String} data.address Physical hardware address of this network Port. + * @param {String} data.portgroup_uuid UUID of the Portgroup this resource belongs to. + * @param {Object} data.local_link_connection The Port binding profile. + * @param {Boolean} data.pxe_enabled Indicates whether PXE is enabled or disabled on the Port. + * @param {String} data.physical_network The name of the physical network to which a port is connected. May be empty. + * @param {String} data.extra A set of one or more arbitrary metadata key and value pairs. + * @param {Boolean} data.is_smartnicIndicates whether the Port is a Smart NIC port. + * @param {String} data.uuid The UUID for the resource. + */ +export const createPortsOnIronic = (data) => + axios.request({ + method: 'post', + url: getIronicBaseUrl('ports'), + data, + }); + +/** + * Update a Port + * @param {String} portId The UUID of the port. + * @param {Object} data request body + * @see https://docs.openstack.org/api-ref/baremetal/?expanded=set-all-traits-of-a-node-detail,update-a-port-detail + * @returns {Promise} + */ +export const updatePortsOnIronic = (portId, data) => + axios.request({ + method: 'patch', + url: getIronicBaseUrl(`ports/${portId}`), + data, + }); diff --git a/src/api/keystone/base.js b/src/api/keystone/base.js new file mode 100644 index 00000000..cb7cad0e --- /dev/null +++ b/src/api/keystone/base.js @@ -0,0 +1,23 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @param {String} key api url + * @returns {String} + */ +import { keystoneBase } from 'utils/constants'; + +const getKeystoneBaseUrl = (key) => `${keystoneBase()}/${key}`; + +export default getKeystoneBaseUrl; diff --git a/src/api/keystone/domain.js b/src/api/keystone/domain.js new file mode 100644 index 00000000..270790d3 --- /dev/null +++ b/src/api/keystone/domain.js @@ -0,0 +1,134 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import getKeystoneBaseUrl from './base'; + +/** + * List domains + * @returns {Promise} + */ +export const fetchDomains = () => + axios.request({ + method: 'get', + url: getKeystoneBaseUrl('domains'), + }); + +/** + * Show domain details + * @param {String} domainId The domain ID. + * @returns {Promise} + */ +export const fetchDomainDetails = (domainId) => + axios.request({ + method: 'get', + url: getKeystoneBaseUrl(`domains/${domainId}`), + }); + +/** + * Update domain + * @param {String} domainId The domain ID. + * @param {Object} data request body + * @param {Object} data request body + * @returns {Promise} + */ +export const updateDomain = (domainId, data) => + axios.request({ + method: 'patch', + url: getKeystoneBaseUrl(`domains/${domainId}`), + data, + }); + +/** + * List role assignments for user on domain + * @param {String} domainId The domain ID. + * @param {String} userId The user ID. + * @returns {Promise} + */ +export const fetchRolesOnDomain = (domainId, userId) => + axios.request({ + method: 'get', + url: getKeystoneBaseUrl(`domains/${domainId}/users/${userId}/roles`), + }); + +/** + * Assign role to user on domain + * @param {String} domainId The domain ID. + * @param {String} userId The user ID. + * @param {String} roleId The role ID. + * @returns {Promise} + */ +export const updateRoleOnDomain = (domainId, userId, roleId) => + axios.request({ + method: 'put', + url: getKeystoneBaseUrl( + `domains/${domainId}/users/${userId}/roles/${roleId}` + ), + }); + +/** + * Unassigns role from user on domain + * @param {String} domainId The domain ID. + * @param {String} userId The user ID. + * @param {String} roleId The role ID. + * @returns {Promise} + */ +export const deleteRoleOnDomain = (domainId, userId, roleId) => + axios.request({ + method: 'delete', + url: getKeystoneBaseUrl( + `domains/${domainId}/users/${userId}/roles/${roleId}` + ), + }); + +/** + * List role assignments for group on domain + * @param {String} domainId The domain ID. + * @param {String} groupId The group ID. + * @returns {Promise} + */ +export const fetchRolesForGroupOnDomain = (domainId, groupId) => + axios.request({ + method: 'get', + url: getKeystoneBaseUrl(`domains/${domainId}/groups/${groupId}/roles`), + }); + +/** + * Assign role to group on domain + * @param {String} domainId The domain ID. + * @param {String} groupId The group ID. + * @returns {Promise} + */ +export const assignRoleForGroupOnDomain = (domainId, groupId, roleId) => + axios.request({ + method: 'put', + url: getKeystoneBaseUrl( + `domains/${domainId}/groups/${groupId}/roles/${roleId}` + ), + }); + +/** + * Assign role to group on domain + * @param {String} domainId The domain ID. + * @param {String} groupId The group ID. + * @returns {Promise} + */ +export const unassignRoleForGroupOnDomain = (domainId, groupId, roleId) => + axios.request({ + method: 'delete', + url: getKeystoneBaseUrl( + `domains/${domainId}/groups/${groupId}/roles/${roleId}` + ), + }); diff --git a/src/api/keystone/group.js b/src/api/keystone/group.js new file mode 100644 index 00000000..c4edbbc4 --- /dev/null +++ b/src/api/keystone/group.js @@ -0,0 +1,113 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import getKeystoneBaseUrl from './base'; + +/** + * List groups + * @param {String} groupId path + * @param {Object} parmas request query + * @param {String} parmas.name Filters the response by a group name. + * @param {String} parmas.domain_id Filters the response by a domain ID. + * @returns {Promise} + */ +export const fetchGroups = (parmas) => + axios.request({ + method: 'get', + url: getKeystoneBaseUrl('groups'), + parmas, + }); + +/** + * Show group details + * @param {String} groupId path + * @returns {Promise} + */ +export const fetchGroupDetails = (groupId) => + axios.request({ + method: 'get', + url: getKeystoneBaseUrl(`groups/${groupId}`), + }); + +/** + * List users in group + * @param {String} groupId path + * @param {Object} parmas request query + * @param {String} parmas.password_expires_at Filter results based on which user passwords have expired. + * @returns {Promise} + */ +export const fetchGroupUsers = (groupId, parmas) => + axios.request({ + method: 'get', + url: getKeystoneBaseUrl(`groups/${groupId}/users`), + parmas, + }); + +/** + * Remove user from group + * @param {String} groupId The group ID. + * @param {String} userId The user ID. + * @returns {Promise} + */ +export const deleteGroupUsers = (groupId, userId) => + axios.request({ + method: 'delete', + url: getKeystoneBaseUrl(`groups/${groupId}/users/${userId}`), + }); + +/** + * Add user to group + * @param {String} groupId The group ID. + * @param {String} userId The user ID. + * @returns {Promise} + */ +export const addGroupUsers = (groupId, userId) => + axios.request({ + method: 'put', + url: getKeystoneBaseUrl(`groups/${groupId}/users/${userId}`), + }); + +/** + * Create group + * @param {Object} data request body + * @param {Object} data.group request body + * @param {Object} data.group.description The description of the group. + * @param {Object} data.group.domain_id The ID of the domain of the group. + * @param {Object} data.group.name The name of the group. + * @returns {Promise} + */ +export const createGroup = (groupId, data) => + axios.request({ + method: 'post', + url: getKeystoneBaseUrl('groups'), + data, + }); + +/** + * Update group + * @param {Object} data request body + * @param {Object} data.group request body + * @param {Object} data.group.description The description of the group. + * @param {Object} data.group.domain_id The ID of the domain of the group. + * @param {Object} data.group.name The name of the group. + * @returns {Promise} + */ +export const updateGroup = (groupId, data) => + axios.request({ + method: 'patch', + url: getKeystoneBaseUrl(`groups/${groupId}`), + data, + }); diff --git a/src/api/keystone/project.js b/src/api/keystone/project.js new file mode 100644 index 00000000..d591aa28 --- /dev/null +++ b/src/api/keystone/project.js @@ -0,0 +1,170 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import getKeystoneBaseUrl from './base'; + +/** + * List projects + * @returns {Promise} + */ +export const fetchProjects = () => + axios.request({ + method: 'get', + url: getKeystoneBaseUrl('projects'), + }); + +/** + * Show project details + * @param {String} projectId path + * @returns {Promise} + */ +export const fetchProject = (projectId) => + axios.request({ + method: 'get', + url: getKeystoneBaseUrl(`projects/${projectId}`), + }); + +/** + * Create project + * @param {Object} data request body + * @param {Object} data.project A project object + * @param {String} data.project.name The name of the project, which must be unique within the owning domain + * @param {Boolean} data.project.domain_id The ID of the domain for the project. + * @param {String} data.project.description The description of the project. + * @param {Boolean} data.project.enabled If set to true, project is enabled. If set to false, project is disabled. + * @returns {Promise} + */ +export const createProject = (data) => + axios.request({ + method: 'post', + url: getKeystoneBaseUrl('projects}'), + data, + }); + +/** + * Update project + * @param {String} projectId The project ID. + * @param {Object} data request body + * @param {Object} data.project A project object + * @param {String} data.project.name The name of the project + * @param {String} data.project.description The description of the project. + * @param {Boolean} data.project.enabled If set to true, project is enabled. If set to false, project is disabled. + * @returns {Promise} + */ +export const updateProject = (projectId, data) => + axios.request({ + method: 'patch', + url: getKeystoneBaseUrl(`projects/${projectId}`), + data, + }); + +/** + * List role assignments for user on project + * @param {String} projectId projects id + * @param {String} userId users id + * @returns {Promise} + */ +export const fetchRolesOnProject = (projectId, userId) => + axios.request({ + method: 'get', + url: getKeystoneBaseUrl(`projects/${projectId}/users/${userId}/roles`), + }); + +/** + * List role assignments for group on project + * @param {String} projectId projects id + * @param {String} groupId groups id + * @returns {Promise} + */ +export const fetchRolesForGroupOnProject = (projectId, groupId) => + axios.request({ + method: 'get', + url: getKeystoneBaseUrl(`projects/${projectId}/groups/${groupId}/roles`), + }); + +/** + * Modify tag list for a project + * @param {String} projectId The project ID. + * @param {Object} data request body + * @param {Array[String]} data.tags example : ["foo", "bar"] + * @returns {Promise} + */ +export const updateTagsOnProject = (projectId, data) => + axios.request({ + method: 'get', + url: getKeystoneBaseUrl(`projects/${projectId}/tags`), + data, + }); + +/** + * Assign role to group on project + * @param {String} projectId The project ID. + * @param {String} groupId The group ID. + * @param {String} roleId The role ID. + * @returns {Promise} + */ +export const assignRoleToGroupOnProject = (projectId, groupId, roleId) => + axios.request({ + method: 'put', + url: getKeystoneBaseUrl( + `projects/${projectId}/groups/${groupId}/roles/${roleId}` + ), + }); + +/** + * Assign role to group on project + * @param {String} projectId The project ID. + * @param {String} groupId The group ID. + * @param {String} roleId The role ID. + * @returns {Promise} + */ +export const unassignRoleToGroupOnProject = (projectId, groupId, roleId) => + axios.request({ + method: 'delete', + url: getKeystoneBaseUrl( + `projects/${projectId}/groups/${groupId}/roles/${roleId}` + ), + }); + +/** + * Assign role to user on project + * @param {String} projectId The project ID. + * @param {String} userId The user ID. + * @param {String} roleId The role ID. + * @returns {Promise} + */ +export const assignRoleToUserOnProject = (projectId, userId, roleId) => + axios.request({ + method: 'put', + url: getKeystoneBaseUrl( + `projects/${projectId}/users/${userId}/roles/${roleId}` + ), + }); + +/** + * Unassign role from user on project + * @param {String} projectId The project ID. + * @param {String} userId The user ID. + * @param {String} roleId The role ID. + * @returns {Promise} + */ +export const unassignRoleToUserOnProject = (projectId, userId, roleId) => + axios.request({ + method: 'delete', + url: getKeystoneBaseUrl( + `projects/${projectId}/users/${userId}/roles/${roleId}` + ), + }); diff --git a/src/api/keystone/role-assignment.js b/src/api/keystone/role-assignment.js new file mode 100644 index 00000000..5afa4ebf --- /dev/null +++ b/src/api/keystone/role-assignment.js @@ -0,0 +1,27 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import getKeystoneBaseUrl from './base'; + +/** + * List role assignments + * @returns {Promise} + */ +export const fetchRoleAssignments = () => + axios.request({ + method: 'get', + url: getKeystoneBaseUrl('role_assignments'), + }); diff --git a/src/api/keystone/role.js b/src/api/keystone/role.js new file mode 100644 index 00000000..a14c08e3 --- /dev/null +++ b/src/api/keystone/role.js @@ -0,0 +1,38 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import getKeystoneBaseUrl from './base'; + +/** + * List roles + * @returns {Promise} + */ +export const fetchRoles = () => + axios.request({ + method: 'get', + url: getKeystoneBaseUrl('roles'), + }); + +/** + * List implied (inference) roles for role + * @param {String} priorRoleId Role ID for a prior role. + * @returns {Promise} + */ +export const fetchImpliesForRole = (priorRoleId) => + axios.request({ + method: 'get', + url: getKeystoneBaseUrl(`roles/${priorRoleId}/implies`), + }); diff --git a/src/api/keystone/system.js b/src/api/keystone/system.js new file mode 100644 index 00000000..04562310 --- /dev/null +++ b/src/api/keystone/system.js @@ -0,0 +1,76 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import getKeystoneBaseUrl from './base'; + +/** + * Assign a system role to a user + * @param {String} userId The user ID. + * @param {String} roleId The role ID. + * @returns {Promise} + */ +export const updateSystemRole = (userId, roleId) => + axios.request({ + method: 'put', + url: getKeystoneBaseUrl(`system/users/${userId}/roles/${roleId}`), + }); + +/** + * Delete a system role assignment from a user + * @param {String} userId The user ID. + * @param {String} roleId The role ID. + * @returns {Promise} + */ +export const deleteSystemRole = (userId, roleId) => + axios.request({ + method: 'delete', + url: getKeystoneBaseUrl(`system/users/${userId}/roles/${roleId}`), + }); + +/** + * List system role assignments for a group + * @param {String} groupId The group ID. + * @returns {Promise} + */ +export const fetchSystemRolesForGroup = (groupId) => + axios.request({ + method: 'get', + url: getKeystoneBaseUrl(`system/groups/${groupId}/roles`), + }); + +/** + * Assign a system role to a group + * @param {String} groupId The group ID. + * @param {String} roleId The role ID. + * @returns {Promise} + */ +export const assignSystemRoleForGroup = (groupId, roleId) => + axios.request({ + method: 'put', + url: getKeystoneBaseUrl(`system/groups/${groupId}/roles/${roleId}`), + }); + +/** + * Assign a system role to a group + * @param {String} groupId The group ID. + * @param {String} roleId The role ID. + * @returns {Promise} + */ +export const unassignSystemRoleForGroup = (groupId, roleId) => + axios.request({ + method: 'delete', + url: getKeystoneBaseUrl(`system/groups/${groupId}/roles/${roleId}`), + }); diff --git a/src/api/keystone/user.js b/src/api/keystone/user.js new file mode 100644 index 00000000..58fa92c7 --- /dev/null +++ b/src/api/keystone/user.js @@ -0,0 +1,126 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import getKeystoneBaseUrl from './base'; + +/** + * List users + * @param {Object} params request query + * @param {String} params.domain_id Filters the response by a domain ID. + * @param {String} params.enabled Filters the response by either enabled (true) or disabled (false) users. + * @param {String} params.idp_id Filters the response by an identity provider ID. + * @param {String} params.name Filters the response by a user name. + * @param {String} params.password_expires_at Filter results based on which user passwords have expired. + * @param {String} params.protocol_id Filters the response by a protocol ID. + * @param {String} params.unique_id Filters the response by a unique ID. + * @returns {Promise} + */ +export const fetchUsers = () => + axios.request({ + method: 'get', + url: getKeystoneBaseUrl('users'), + }); + +/** + * Show user details + * @param {String} userId The user ID. + * @returns {Promise} + */ +export const fetchUserDetails = (userId) => + axios.request({ + method: 'get', + url: getKeystoneBaseUrl(`users/${userId}`), + }); + +/** + * Create user + * @param {Object} data request body + * @param {Object} data.user A user object + * @param {String} data.user.id id + * @param {String} data.user.domain_id The ID of the domain of the user, Default value : "default" + * @param {String} data.user.name The name for the user. + * @param {String} data.user.email The email for the user. + * @param {String} data.user.password The password for the user. + * @param {String} data.user.phone The phone for the user. + * @param {String} data.user.full_name The true name for the user. + * @param {Boolean} data.user.enabled Default value : true + * @param {String} data.user.description The description for the user. + * @returns {Promise} + */ +export const createUser = (data) => + axios.request({ + method: 'post', + url: getKeystoneBaseUrl('users'), + data, + }); + +/** + * Update user + * @param {String} userId The user ID. + * @param {Object} data request body + * @param {Object} data.user A user object + * @param {String} data.user.name The name for the user. + * @param {String} data.user.email The email for the user. + * @param {String} data.user.phone The phone for the user. + * @param {String} data.user.full_name The true name for the user. + * @param {String} data.user.description The description for the user. + * @returns {Promise} + */ +export const updateUser = (userId, data) => + axios.request({ + method: 'patch', + url: getKeystoneBaseUrl(`users/${userId}`), + data, + }); + +/** + * Change password for user + * @param {String} userId The user ID. + * @param {Object} data request body + * @param {Object} data.user A user object + * @param {String} data.user.original_password The original password for the user. + * @param {String} data.user.password The new password for the user. + * @returns {Promise} + */ +export const changeUserPassword = (userId, data) => + axios.request({ + method: 'post', + url: getKeystoneBaseUrl(`users/${userId}/password`), + data, + }); + +/** + * List projects for user + * @param {String} userId The user ID. + * @param {Object} params request query + * @returns {Promise} + */ +export const fetchUserProjects = (userId) => + axios.request({ + method: 'get', + url: getKeystoneBaseUrl(`users/${userId}/projects`), + }); + +/** + * List groups to which a user belongs + * @param {String} userId The user ID. + * @returns {Promise} + */ +export const fetchUserGroups = (userId) => + axios.request({ + method: 'get', + url: getKeystoneBaseUrl(`users/${userId}/groups`), + }); diff --git a/src/api/neutron/agent.js b/src/api/neutron/agent.js new file mode 100644 index 00000000..20a812f3 --- /dev/null +++ b/src/api/neutron/agent.js @@ -0,0 +1,79 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import getNeutronBaseUrl from './base'; + +/** + * List agents + * @returns {Promise} + */ +export const fetchAgents = () => + axios.request({ + method: 'get', + url: getNeutronBaseUrl('agents'), + }); + +/** + * Schedule a network to a DHCP agent + * @param {String} agentId The ID of the agent. + * @param {Object} data request body + * @param {String} data.network_id The ID of the network. + * @returns {Promise} + */ +export const addNetworkToDhcpAgent = (agentId, data) => + axios.request({ + method: 'post', + url: getNeutronBaseUrl(`agents/${agentId}/dhcp-networks`), + data, + }); + +/** + * Remove network from a DHCP agent + * @param {String} agentId The ID of the agent. + * @param {String} networkId The ID of the network. + * @returns {Promise} + */ +export const deleteNetworkToDhcpAgent = (agentId, networkId) => + axios.request({ + method: 'delete', + url: getNeutronBaseUrl(`agents/${agentId}/dhcp-networks/${networkId}`), + }); + +/** + * Schedule router to an l3 agent + * @param {String} agentId The ID of the agent. + * @param {Object} data request body + * @param {String} data.router_id The ID of the router. + * @returns {Promise} + */ +export const addRoterToL3Agent = (agentId, data) => + axios.request({ + method: 'post', + url: getNeutronBaseUrl(`agents/${agentId}/l3-routers`), + data, + }); + +/** + * Remove l3 router from an l3 agent + * @param {String} agentId The ID of the agent. + * @param {String} routerId The ID of the router. + * @returns {Promise} + */ +export const deleteL3RouterFromL3Agent = (agentId, routerId) => + axios.request({ + method: 'delete', + url: getNeutronBaseUrl(`agents/${agentId}/l3-routers/${routerId}`), + }); diff --git a/src/api/neutron/availability-zones.js b/src/api/neutron/availability-zones.js new file mode 100644 index 00000000..7fb37f40 --- /dev/null +++ b/src/api/neutron/availability-zones.js @@ -0,0 +1,32 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import getNeutronBaseUrl from './base'; + +/** + * List all availability zones + * @param {Object} params request query + * @param {String} params.state Filter the list result by the state of the availability zone, which is either available or unavailable. + * @param {String} params.resource Filter the list result by the resource type of the availability zone. + * @param {String} params.name Filter the list result by the human-readable name of the resource. + * @returns {Promise} + */ +export const fetchListAvailabilityZonesOnNeutron = (params) => + axios.request({ + method: 'get', + url: getNeutronBaseUrl('availability_zones'), + params, + }); diff --git a/src/api/neutron/base.js b/src/api/neutron/base.js new file mode 100644 index 00000000..d30feb56 --- /dev/null +++ b/src/api/neutron/base.js @@ -0,0 +1,23 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @param {String} key api url + * @returns {String} + */ +import { neutronBase } from 'utils/constants'; + +const getNeutronBaseUrl = (key) => `${neutronBase()}/${key}`; + +export default getNeutronBaseUrl; diff --git a/src/api/neutron/extensions.js b/src/api/neutron/extensions.js new file mode 100644 index 00000000..5f01de43 --- /dev/null +++ b/src/api/neutron/extensions.js @@ -0,0 +1,27 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import getNeutronBaseUrl from './base'; + +/** + * List extensions + * @returns {Promise} + */ +export const fetchListExtensionsOnNeutron = () => + axios.request({ + method: 'get', + url: getNeutronBaseUrl('extensions'), + }); diff --git a/src/api/neutron/floating-ips.js b/src/api/neutron/floating-ips.js new file mode 100644 index 00000000..8ae06f99 --- /dev/null +++ b/src/api/neutron/floating-ips.js @@ -0,0 +1,89 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import getNeutronBaseUrl from './base'; + +/** + * Update floating IP + * @param {String} floatingipId The ID of the floating IP address. + * @param {Object} data request body + * @param {Object} data.floatingip A floatingip object. + * @param {String} data.floatingip.port_id The ID of a port associated with the floating IP. + * @param {String} data.floatingip.fixed_ip_address The fixed IP address that is associated with the floating IP. + * @param {String} data.floatingip.description A human-readable description for the resource. Default is an empty string. + * @returns {Promise} + */ +export const updateFloatingIp = (floatingipId, data) => + axios.request({ + method: 'put', + url: getNeutronBaseUrl(`floatingips/${floatingipId}`), + data, + }); + +/** + * List floating IPs + * @param {String} floatingipId The ID of the floating IP address. + * @param {Object} query request query + * @see https://docs.openstack.org/api-ref/network/v2/index.html?expanded=list-floating-ips-detail#floating-ips-floatingips + * @returns {Promise} + */ +export const fetchListFloatingIps = (floatingipId, params) => + axios.request({ + method: 'get', + url: getNeutronBaseUrl(`floatingips/${floatingipId}`), + params, + }); + +/** + * List floating IP port forwardings + * @param {String} floatingipId The ID of the floating IP address. + * @param {Object} query request query + * @see https://docs.openstack.org/api-ref/network/v2/index.html?expanded=list-floating-ips-detail,list-floating-ip-port-forwardings-detail#floating-ips-floatingips + * @returns {Promise} + */ +export const fetchListPortForwardings = (floatingipId, params) => + axios.request({ + method: 'get', + url: getNeutronBaseUrl(`floatingips/${floatingipId}`), + params, + }); + +/** + * Create port forwarding + * @param {String} floatingipId The ID of the floating IP address. + * @param {Object} data request body + * @param {Object} data.port_forwarding A floating IP port forwarding object. + * @returns {Promise} + */ +export const createPortForwarding = (floatingipId, data) => + axios.request({ + method: 'post', + url: getNeutronBaseUrl(`floatingips/${floatingipId}/port_forwardings`), + data, + }); + +/** + * Delete a floating IP port forwarding + * @param {String} floatingipId The ID of the floating IP address. + * @param {String} portForwardingId The ID of the floating IP port forwarding. + */ +export const deletePortForwarding = (floatingipId, portForwardingId) => + axios.request({ + method: 'delete', + url: getNeutronBaseUrl( + `floatingips/${floatingipId}/port_forwardings/${{ portForwardingId }}` + ), + }); diff --git a/src/api/neutron/networks.js b/src/api/neutron/networks.js new file mode 100644 index 00000000..b2513a97 --- /dev/null +++ b/src/api/neutron/networks.js @@ -0,0 +1,72 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import getNeutronBaseUrl from './base'; + +/** + * List networks + * @param {Object} params request query + * @param {Boolean} params.admin_state_up Filter the list result by the administrative state of the resource, which is up (true) or down (false). + * @param {Number} params.mtu Filter the network list result by the maximum transmission unit (MTU) value to address fragmentation. Minimum value is 68 for IPv4, and 1280 for IPv6. + * @param {String} params.name Filter the list result by the human-readable name of the resource. + * @param {String} params.project_id Filter the list result by the ID of the project that owns the resource. + * @param {String} params['provider:network_type'] Filter the list result by the type of physical network that this network/segment is mapped to. + * @param {String} params['provider:physical_network'] Filter the list result by the physical network where this network/segment is implemented. + * @param {String} params['provider:segmentation_id'] Filter the list result by the ID of the isolated segment on the physical network. + * @param {String} params.revision_number Filter the list result by the revision number of the resource. + * @param {Boolean} params.shared Filter the network list result based on if the network is shared across all tenants. + * @param {String} params.status Filter the network list result by network status. Values are ACTIVE, DOWN, BUILD or ERROR. + * @returns {Promise} + */ +export const fetchNetworksOnNeutron = (params) => + axios.request({ + method: 'get', + url: getNeutronBaseUrl('networks'), + params, + }); + +/** + * List DHCP agents hosting a network + * @param {String} networkId The ID of the attached network. + * @returns {Promise} + */ +export const fetchListDhcpAgentsOnNeutron = (networkId) => + axios.request({ + method: 'get', + url: getNeutronBaseUrl(`networks/${networkId}/dhcp-agents`), + }); + +/** + * Show network details + * @param {String} networkId The ID of the attached network. + * @returns {Promise} + */ +export const fetchNetworkDetailsOnNeutron = (networkId) => + axios.request({ + method: 'get', + url: getNeutronBaseUrl(`networks/${networkId}`), + }); + +/** + * Show Network IP Availability + * @param {String} networkId The ID of the attached network. + * @returns {Promise} + */ +export const fetchNetworkIpAvailabilityDetailsOnNeutron = (networkId) => + axios.request({ + method: 'get', + url: getNeutronBaseUrl(`network-ip-availabilities/${networkId}`), + }); diff --git a/src/api/neutron/ports.js b/src/api/neutron/ports.js new file mode 100644 index 00000000..dcb84866 --- /dev/null +++ b/src/api/neutron/ports.js @@ -0,0 +1,58 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import getNeutronBaseUrl from './base'; + +/** + * List ports + * @param {Object} params request query + * @param {String} params.device_id Filter the port list result by the ID of the device that uses this port. + * @param {String} params.device_owner Filter the port result list by the entity type that uses this port. + * @see https://docs.openstack.org/api-ref/network/v2/index.html?expanded=list-ports-detail#ports + */ +export const fetchPortsOnNeutron = (params) => + axios.request({ + method: 'get', + url: getNeutronBaseUrl('ports'), + params, + }); + +/** + * Update port + * @param {String} portId The ID of the port. + * @param {Object} data request body + * @param {String} data.port A port object. + * @see https://docs.openstack.org/api-ref/network/v2/index.html?expanded=update-port-detail#ports + */ +export const updatePortOnNeutron = (portId, data) => + axios.request({ + method: 'put', + url: getNeutronBaseUrl(`ports/${portId}`), + data, + }); + +/** + * Create port + * @param {Object} data request body + * @param {String} data.port A port object. + * @see https://docs.openstack.org/api-ref/network/v2/index.html?expanded=create-port-detail#ports + */ +export const createPortOnNeutron = (data) => + axios.request({ + method: 'post', + url: getNeutronBaseUrl('ports'), + data, + }); diff --git a/src/api/neutron/qos-policies.js b/src/api/neutron/qos-policies.js new file mode 100644 index 00000000..1d4ce27f --- /dev/null +++ b/src/api/neutron/qos-policies.js @@ -0,0 +1,144 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import getNeutronBaseUrl from './base'; + +/** + * Update QoS policy + * @param {String} policyId The ID of the QoS policy. + * @param {Object} data request body + * @param {Object} data.policy A QoS policy object. + * @param {String} data.policy.description A human-readable description for the resource. Default is an empty string. + * @param {Boolean} data.policy.is_default If true, the QoS policy is the default policy. + * @param {Boolean} data.policy.shared Set to true to share this policy with other projects. Default is false. + * @param {String} data.policy.name Human-readable name of the resource. + * @returns {Promise} + */ +export const updateQosPolicy = (policyId, data) => + axios.request({ + method: 'put', + url: getNeutronBaseUrl(`qos/policies/${policyId}`), + data, + }); + +/** + * Create bandwidth limit rule + * @param {String} policyId The ID of the QoS policy. + * @param {Object} data request body + * @param {Object} data.bandwidth_limit_rule A bandwidth_limit_rule object. + * @param {Number} data.bandwidth_limit_rule.max_kbps The maximum KBPS (kilobits per second) value. + * @param {Number} data.bandwidth_limit_rule.max_burst_kbps The maximum burst size (in kilobits). Default is 0. + * @param {Boolean} data.bandwidth_limit_rule.direction Valid values are egress and ingress. Default value is egress. + * @returns {Promise} + */ +export const createBandwidthLimitRulesQosPolicy = (policyId, data) => + axios.request({ + method: 'post', + url: getNeutronBaseUrl(`qos/policies/${policyId}/bandwidth_limit_rules`), + data, + }); + +/** + * Update bandwidth limit rule + * @param {String} policyId The ID of the QoS policy. + * @param {Object} ruleId The ID of the QoS rule. + * @param {Object} data request body + * @param {Object} data.bandwidth_limit_rule A bandwidth_limit_rule object. + * @param {Number} data.bandwidth_limit_rule.max_kbps The maximum KBPS (kilobits per second) value. + * @param {Number} data.bandwidth_limit_rule.max_burst_kbps The maximum burst size (in kilobits). Default is 0. + * @param {Boolean} data.bandwidth_limit_rule.direction Valid values are egress and ingress. Default value is egress. + * @returns {Promise} + */ +export const updateBandwidthLimitRulesQosPolicy = (policyId, ruleId, data) => + axios.request({ + method: 'put', + url: getNeutronBaseUrl( + `qos/policies/${policyId}/bandwidth_limit_rules/${ruleId}` + ), + data, + }); + +/** + * Delete bandwidth limit rule + * @param {String} policyId The ID of the QoS policy. + * @param {Object} ruleId The ID of the QoS rule. + * @returns {Promise} + */ +export const deleteBandwidthLimitRulesQosPolicy = (policyId, ruleId) => + axios.request({ + method: 'delete', + url: getNeutronBaseUrl( + `qos/policies/${policyId}/bandwidth_limit_rules/${ruleId}` + ), + }); + +/** + * Create DSCP marking rule + * @param {String} policyId The ID of the QoS policy. + * @param {Object} data request body + * @param {Object} data.dscp_marking_rule A dscp_marking_rule object. + * @param {Number} data.dscp_marking_rule.dscp_mark The DSCP mark value. + * @returns {Promise} + */ +export const createDscpMarkingRuleQosPolicy = (policyId, data) => + axios.request({ + method: 'post', + url: getNeutronBaseUrl(`qos/policies/${policyId}/dscp_marking_rules`), + data, + }); + +/** + * Update DSCP marking rule + * @param {String} policyId The ID of the QoS policy. + * @param {String} dscpRuleId The ID of the DSCP rule. + * @param {Object} data request body + * @param {Object} data.dscp_marking_rule A dscp_marking_rule object. + * @param {Number} data.dscp_marking_rule.dscp_mark The DSCP mark value. + * @returns {Promise} + */ +export const updateDscpMarkingRuleQosPolicy = (policyId, dscpRuleId, data) => + axios.request({ + method: 'put', + url: getNeutronBaseUrl( + `qos/policies/${policyId}/dscp_marking_rules/${dscpRuleId}` + ), + data, + }); + +/** + * Delete DSCP marking rule + * @param {String} policyId The ID of the QoS policy. + * @param {String} dscpRuleId The ID of the DSCP rule. + * @returns {Promise} + */ +export const deleteDscpMarkingRuleQosPolicy = (policyId, dscpRuleId) => + axios.request({ + method: 'delete', + url: getNeutronBaseUrl( + `qos/policies/${policyId}/dscp_marking_rules/${dscpRuleId}` + ), + }); + +/** + * Show QoS policy details + * @param {String} policyId The ID of the QoS policy. + * @returns {Promise} + */ +export const fetchQosPolicieDetailsOnNeutron = (policyId) => + axios.request({ + method: 'get', + url: getNeutronBaseUrl(`qos/policies/${policyId}`), + }); diff --git a/src/api/neutron/quotas.js b/src/api/neutron/quotas.js new file mode 100644 index 00000000..4ce79cf9 --- /dev/null +++ b/src/api/neutron/quotas.js @@ -0,0 +1,49 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import getNeutronBaseUrl from './base'; + +/** + * Show quota details for a tenant + * @param {String} projectId The ID of the project. + * @returns {Promise} + */ +export const fetchQuotaDetails = (projectId) => + axios.request({ + method: 'get', + url: getNeutronBaseUrl(`quotas/${projectId}/details`), + }); + +/** + * Update quota for a project + * @param {String} projectId The ID of the project. + * @param {Object} data request body + * @param {Object} data.quota A quota object. + * @param {String} data.quota.floatingip The number of floating IP addresses allowed for each project. A value of -1 means no limit. + * @param {String} data.quota.network The number of networks allowed for each project. A value of -1 means no limit. + * @param {String} data.quota.router The number of routers allowed for each project. A value of -1 means no limit. + * @param {String} data.quota.subnet The number of subnets allowed for each project. A value of -1 means no limit. + * @param {String} data.quota.security_group The number of security groups allowed for each project. A value of -1 means no limit. + * @param {String} data.quota.security_group_rule The number of security group rules allowed for each project. A value of -1 means no limit. + * @param {String} data.quota.firewall_group A firewall group can have a firewall policy for ingress traffic and/or a firewall policy for egress traffic. + * @returns {Promise} + */ +export const updateQuotaDetails = (projectId, data) => + axios.request({ + method: 'put', + url: getNeutronBaseUrl(`quotas/${projectId}`), + data, + }); diff --git a/src/api/neutron/routers.js b/src/api/neutron/routers.js new file mode 100644 index 00000000..478a534c --- /dev/null +++ b/src/api/neutron/routers.js @@ -0,0 +1,102 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import getNeutronBaseUrl from './base'; + +/** + * List routers + * @param {Object} params request query + * @param {String} params.project_id Filter the list result by the ID of the project that owns the resource. + * @see https://docs.openstack.org/api-ref/network/v2/index.html?expanded=list-dhcp-agents-hosting-a-network-detail,show-subnet-details-detail,list-routers-detail#id5 + * @returns {Promise} + */ +export const fetchListRoutersOnNeutron = (params) => + axios.request({ + method: 'get', + url: getNeutronBaseUrl('routers'), + params, + }); + +/** + * Show router details + * @param {String} routerId The ID of the router. + * @returns {Promise} + */ +export const fetchRouterDetailsOnNeutron = (routerId) => + axios.request({ + method: 'get', + url: getNeutronBaseUrl(`routers/${routerId}`), + }); + +/** + * Add extra routes to router + * @param {String} routerId The ID of the router. + * @param {Object} data request body + * @param {Object} data.router The router object. + * @param {Array} data.router.routes The extra routes configuration for L3 router. + * @returns {Promise} + */ +export const addExtraRoutesToRouterOnNeutron = (routerId, data) => + axios.request({ + method: 'put', + url: getNeutronBaseUrl(`routers/${routerId}/add_extraroutes`), + data, + }); + +/** + * Remove extra routes from router + * @param {String} routerId The ID of the router. + * @param {Object} data request body + * @param {Object} data.router The router object. + * @param {Array} data.router.routes The extra routes configuration for L3 router. + * @returns {Promise} + */ +export const removeExtraRoutesFromRouterOnNeutron = (routerId, data) => + axios.request({ + method: 'put', + url: getNeutronBaseUrl(`routers/${routerId}/remove_extraroutes`), + data, + }); + +/** + * Add interface to router + * @param {String} routerId The ID of the router. + * @param {Object} data request body + * @param {Object} data.subnet_id The ID of the subnet. One of subnet_id or port_id must be specified. + * @param {Array} data.port_id The ID of the port. One of subnet_id or port_id must be specified. + * @returns {Promise} + */ +export const addInterfaceToRouterOnNeutron = (routerId, data) => + axios.request({ + method: 'put', + url: getNeutronBaseUrl(`routers/${routerId}/add_router_interface`), + data, + }); + +/** + * Remove interface from router + * @param {String} routerId The ID of the router. + * @param {Object} data request body + * @param {Object} data.subnet_id The ID of the subnet. One of subnet_id or port_id must be specified. + * @param {Array} data.port_id The ID of the port. One of subnet_id or port_id must be specified. + * @returns {Promise} + */ +export const removeInterfaceFromRouterOnNeutron = (routerId, data) => + axios.request({ + method: 'put', + url: getNeutronBaseUrl(`routers/${routerId}/remove_router_interface`), + data, + }); diff --git a/src/api/neutron/security-groups.js b/src/api/neutron/security-groups.js new file mode 100644 index 00000000..afc262e9 --- /dev/null +++ b/src/api/neutron/security-groups.js @@ -0,0 +1,28 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import getNeutronBaseUrl from './base'; + +/** + * Show security group + * @param {securityGroupId} params The security group id + * @returns {Promise} + */ +export const fetchSecurityGroupsDetails = (securityGroupId) => + axios.request({ + method: 'get', + url: getNeutronBaseUrl(`security-groups/${securityGroupId}`), + }); diff --git a/src/api/neutron/subnets.js b/src/api/neutron/subnets.js new file mode 100644 index 00000000..13b86442 --- /dev/null +++ b/src/api/neutron/subnets.js @@ -0,0 +1,69 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import getNeutronBaseUrl from './base'; + +/** + * List subnets + * @param {Object} params request query + * @returns {Promise} + */ +export const fetchListSubnetsOnNeutron = (params) => + axios.request({ + method: 'get', + url: getNeutronBaseUrl('subnets'), + params, + }); + +/** + * Show subnet details + * @param {String} subnetId The ID of the subnet. + * @returns {Promise} + */ +export const fetchSubnetDetailsOnNeutron = (subnetId) => + axios.request({ + method: 'get', + url: getNeutronBaseUrl(`subnets/${subnetId}`), + }); + +/** + * Create subnet + * @param {Object} data request body + * @param {Object} data.subnet A subnet object. + * @see https://docs.openstack.org/api-ref/network/v2/index.html?expanded=list-dhcp-agents-hosting-a-network-detail,show-subnet-details-detail,list-routers-detail,create-subnet-detail#id5 + * @returns {Promise} + */ +export const createSubnetOnNeutron = (data) => + axios.request({ + method: 'post', + url: getNeutronBaseUrl('subnets'), + data, + }); + +/** + * Update subnet + * @param {String} subnetId The ID of the subnet. + * @param {Object} data request body + * @param {Object} data.subnet A subnet object. + * @see https://docs.openstack.org/api-ref/network/v2/index.html?expanded=list-ports-detail,update-subnet-detail#ports + * @returns {Promise} + */ +export const updateSubnetOnNeutron = (subnetId, data) => + axios.request({ + method: 'put', + url: getNeutronBaseUrl(`subnets/${subnetId}`), + data, + }); diff --git a/src/api/neutron/vpn.js b/src/api/neutron/vpn.js new file mode 100644 index 00000000..678183d2 --- /dev/null +++ b/src/api/neutron/vpn.js @@ -0,0 +1,108 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import getNeutronBaseUrl from './base'; + +/** + * Update IKE policy + * @param {String} ikepolicyId The ID of the IKE policy. + * @param {Object} data request body + * @param {Object} data.ikepolicy An ikepolicy object. + * @see https://docs.openstack.org/api-ref/network/v2/index.html?expanded=update-ike-policy-detail#vpnaas-2-0-vpn-vpnservices-ikepolicies-ipsecpolicies-endpoint-groups-ipsec-site-connections + * @returns {Promise} + */ +export const updateIkePolicyOnNeutron = (ikepolicyId, data) => + axios.request({ + method: 'put', + url: getNeutronBaseUrl(`vpn/ikepolicies/${ikepolicyId}`), + data, + }); + +/** + * Update IPsec connection + * @param {String} connectionId The ID of the IPsec site-to-site connection. + * @param {Object} data request body + * @param {Object} data.ipsec_site_connection An ipsec_site_connection object. + * @see https://docs.openstack.org/api-ref/network/v2/index.html?expanded=update-ipsec-connection-detail#vpnaas-2-0-vpn-vpnservices-ikepolicies-ipsecpolicies-endpoint-groups-ipsec-site-connections + * @returns {Promise} + */ +export const updateIpConnectionOnNeutron = (connectionId, data) => + axios.request({ + method: 'put', + url: getNeutronBaseUrl(`vpn/ipsec-site-connections/${connectionId}`), + data, + }); + +/** + * Show IPsec connection + * @param {String} connectionId The ID of the IPsec site-to-site connection. + * @param {Object} params request query + * @returns {Promise} + */ +export const fetchIpConnectionDetailsOnNeutron = (connectionId, params) => + axios.request({ + method: 'get', + url: getNeutronBaseUrl(`vpn/ipsec-site-connections/${connectionId}`), + params, + }); + +/** + * Update VPN endpoint group + * @param {String} endpointGroupId The ID of the VPN endpoint group. + * @param {Object} data request body + * @param {Object} data.endpoint_group An ipsec_site_connection object. + * @param {Object} data.endpoint_group.name Human-readable name of the resource. Default is an empty string. + * @param {Object} data.endpoint_group.description A human-readable description for the resource. Default is an empty string. + * @returns {Promise} + */ +export const updateEndpointGroupOnNeutron = (endpointGroupId, data) => + axios.request({ + method: 'put', + url: getNeutronBaseUrl(`vpn/endpoint-groups/${endpointGroupId}`), + data, + }); + +/** + * Update VPN service + * @param {String} serviceId The ID of the VPN service. + * @param {Object} data request body + * @param {Object} data.vpnservice A vpnservice object. + * @param {Object} data.vpnservice.name Human-readable name of the resource. Default is an empty string. + * @param {Object} data.vpnservice.description A human-readable description for the resource. Default is an empty string. + * @param {Boolean} data.vpnservice.admin_state_up The administrative state of the resource, which is up (true) or down (false). + * @returns {Promise} + */ +export const updateVpnServiceNeutron = (serviceId, data) => + axios.request({ + method: 'put', + url: getNeutronBaseUrl(`vpn/vpnservices/${serviceId}`), + data, + }); + +/** + * Update IPsec policy + * @param {String} ipsecpolicyId The ID of the IPsec policy. + * @param {Object} data request body + * @param {Object} data.ipsecpolicy An ipsecpolicy object. + * @see https://docs.openstack.org/api-ref/network/v2/index.html?expanded=update-ipsec-policy-detail#vpnaas-2-0-vpn-vpnservices-ikepolicies-ipsecpolicies-endpoint-groups-ipsec-site-connections + * @returns {Promise} + */ +export const updateIpsecPolicyOnNeutron = (ipsecpolicyId, data) => + axios.request({ + method: 'put', + url: getNeutronBaseUrl(`vpn/ipsecpolicies/${ipsecpolicyId}`), + data, + }); diff --git a/src/api/nova/base.js b/src/api/nova/base.js new file mode 100644 index 00000000..0d4a3366 --- /dev/null +++ b/src/api/nova/base.js @@ -0,0 +1,23 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @param {String} key api url + * @returns {String} + */ +import { novaBase } from 'utils/constants'; + +const getNovaBaseUrl = (key) => `${novaBase()}/${key}`; + +export default getNovaBaseUrl; diff --git a/src/api/nova/flavor.js b/src/api/nova/flavor.js new file mode 100644 index 00000000..43347fa6 --- /dev/null +++ b/src/api/nova/flavor.js @@ -0,0 +1,116 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import getNovaBaseUrl from './base'; + +/** + * List Flavor Access Information For Given Flavor + * @param {String} flavorId The ID of the flavor. + * @returns {Promise} + */ +export const fetchFlavorAccessinfomation = (flavorId) => + axios.request({ + method: 'get', + url: getNovaBaseUrl(`flavors/${flavorId}/os-flavor-access`), + }); + +/** + * Create Flavor + * @param {Object} data request body + * @param {Object} data.flavor A flavor is a combination of memory, disk size, and CPUs. + * @param {String} data.flavor.name The display name of a flavor. + * @param {String} data.flavor.description A free form description of the flavor. + * @param {String} data.flavor.id The ID of the flavor. + * @param {Number} data.flavor.ram The amount of RAM a flavor has, in MiB. + * @param {Number} data.flavor.disk The size of the root disk that will be created in GiB. + * @param {Number} data.flavor.vcpus The number of virtual CPUs that will be allocated to the server. + * @param {Number} data.flavor.swap The size of a dedicated swap disk that will be allocated, in MiB. + * @param {Number} data.flavor['OS-FLV-EXT-DATA:ephemeral'] The size of the ephemeral disk that will be created, in GiB. + * @param {Number} data.flavor.rxtx_factor The receive / transmit factor (as a float) that will be set on ports if the network backend supports the QOS extension. + * @param {Boolean} data.flavor.['os-flavor-access:is_public'] Whether the flavor is public + * @returns {Promise} + */ +export const createFlavor = (data) => + axios.request({ + method: 'post', + url: getNovaBaseUrl('flavors'), + data, + }); + +/** + * Add Or Remove Flavor Access To Tenant + * @param {String} flavorId The ID of the flavor. + * @param {Object} data request body + * @param {Object} data.addTenantAccess The action. + * @param {String} data.addTenantAccess.tenant The UUID of the tenant in a multi-tenancy cloud. + * @param {String} data.removeTenantAccess The action. + * @param {String} data.removeTenantAccess.tenant The UUID of the tenant in a multi-tenancy cloud. + * @returns {Promise} + */ +export const addOrDeleteFlavorAccessToTenant = (flavorId, data) => + axios.request({ + method: 'post', + url: getNovaBaseUrl(`flavors/${flavorId}/action`), + data, + }); + +/** + * Create Extra Specs For A Flavor + * @param {String} flavorId The ID of the flavor. + * @param {Object} data request body + * @param {Object} data.extra_specs A dictionary of the flavor’s extra-specs key-and-value pairs. + * @param {String} data.extra_specs.key The extra spec key of a flavor. + * @param {String} data.extra_specs.value The extra spec value of a flavor. + * @returns {Promise} + */ +export const createExtraSpecsForFlavor = (flavorId, data) => + axios.request({ + method: 'post', + url: getNovaBaseUrl(`flavors/${flavorId}/os-extra_specs`), + data, + }); + +/** + * Update An Extra Spec For A Flavor + * @param {String} flavorId The ID of the flavor. + * @param {Object} flavorExtraSpecKey The extra spec key for the flavor. + * @param {Object} data request body + * @param {String} data.key The extra spec key of a flavor. + * @param {String} data.value The extra spec value of a flavor. + * @returns {Promise} + */ +export const updateExtraSpecsForFlavor = (flavorId, flavorExtraSpecKey, data) => + axios.request({ + method: 'put', + url: getNovaBaseUrl( + `flavors/${flavorId}/os-extra_specs/${flavorExtraSpecKey}` + ), + data, + }); + +/** + * Delete An Extra Spec For A Flavor + * @param {String} flavorId The ID of the flavor. + * @param {Object} flavorExtraSpecKey The extra spec key for the flavor. + * @returns {Promise} + */ +export const deleteExtraSpecsForFlavor = (flavorId, flavorExtraSpecKey) => + axios.request({ + method: 'delete', + url: getNovaBaseUrl( + `flavors/${flavorId}/os-extra_specs/${flavorExtraSpecKey}` + ), + }); diff --git a/src/api/nova/os-aggregates.js b/src/api/nova/os-aggregates.js new file mode 100644 index 00000000..778e4ca6 --- /dev/null +++ b/src/api/nova/os-aggregates.js @@ -0,0 +1,32 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import getNovaBaseUrl from './base'; + +/** + * Add Host, Remove Host, Create Or Update Aggregate Metadata + * @param {Object} data request body + * @param {Object} data.add_host when add host + * @param {Object} data.remove_host when remove host + * @param {Object} data.set_metadata when set metadata + * @returns {Promise} + */ +export const toggleChangeAggregate = (aggregateId, data) => + axios.request({ + method: 'get', + url: getNovaBaseUrl(`os-aggregates/${aggregateId}/action`), + data, + }); diff --git a/src/api/nova/os-hypervisors.js b/src/api/nova/os-hypervisors.js new file mode 100644 index 00000000..417fed09 --- /dev/null +++ b/src/api/nova/os-hypervisors.js @@ -0,0 +1,38 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import getNovaBaseUrl from './base'; + +/** + * List Hypervisors Details + * @returns {Promise} + */ +export const fetchOsHypervisorsDetails = () => + axios.request({ + method: 'get', + url: getNovaBaseUrl('os-hypervisors/detail'), + }); + +/** + * Show Hypervisor Details + * @param {String} hypervisorId path + * @returns {Promise} + */ +export const fetchOsHypervisorDetails = (hypervisorId) => + axios.request({ + method: 'get', + url: getNovaBaseUrl(`os-hypervisors/${hypervisorId}`), + }); diff --git a/src/api/nova/os-quota-sets.js b/src/api/nova/os-quota-sets.js new file mode 100644 index 00000000..a982e5ff --- /dev/null +++ b/src/api/nova/os-quota-sets.js @@ -0,0 +1,45 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import getNovaBaseUrl from './base'; + +/** + * Show The Detail of Quota + * @returns {Promise} + */ +export const fetchOsQuotaSetsDetails = (tenantId) => + axios.request({ + method: 'get', + url: getNovaBaseUrl(`os-quota-sets/${tenantId}/detail`), + }); + +/** + * Update Quotas + * @param {String} tenantId The UUID of the tenant in a multi-tenancy cloud. + * @param {Object} data request body + * @param {Object} data.quota_set A quota object. + * @param {String} data.quota_set.instances The number of allowed servers for each tenant. + * @param {String} data.quota_set.cores The number of allowed server cores for each tenant. + * @param {String} data.quota_set.ram The amount of allowed server RAM, in MiB, for each tenant. + * @param {String} data.quota_set.server_groups The number of allowed server groups for each tenant. + * @returns {Promise} + */ +export const updateQuotaSets = (tenantId, data) => + axios.request({ + method: 'put', + url: getNovaBaseUrl(`os-quota-sets/${tenantId}`), + data, + }); diff --git a/src/api/nova/os-service.js b/src/api/nova/os-service.js new file mode 100644 index 00000000..526c07fe --- /dev/null +++ b/src/api/nova/os-service.js @@ -0,0 +1,47 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import getNovaBaseUrl from './base'; + +/** + * List Compute Services + * @param {Object} params request query + * @param {String} params.binary Filter the service list result by binary name of the service. example : nova-compute + * @param {String} params.host Filter the service list result by the host name. + * @returns {Promise} + */ +export const fetchOsServices = (params) => + axios.request({ + method: 'get', + url: getNovaBaseUrl('os-services'), + params, + }); + +/** + * Update Compute Service + * @param {String} serviceId The id of the service as a uuid. + * @param {Object} data request body + * @param {String} data.status The status of the service. One of enabled or disabled. + * @param {String} data.disabled_reason The reason for disabling a service. + * @param {Boolean} data.forced_down forced_down is a manual override to tell nova that the service in question has been fenced manually by the operations team. + * @returns {Promise} + */ +export const updateComputeService = (serviceId, data) => + axios.request({ + method: 'put', + url: getNovaBaseUrl(`os-services/${serviceId}`), + data, + }); diff --git a/src/api/nova/server.js b/src/api/nova/server.js new file mode 100644 index 00000000..ddbdea13 --- /dev/null +++ b/src/api/nova/server.js @@ -0,0 +1,202 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import getNovaBaseUrl from './base'; + +/** + * Create server + * @param {Object} data request body + * @param {Object} data.server A server object + * @param {String} data.server.availability_zone availability_zone + * @param {Array[Object]} data.server.block_device_mapping_v2 example : [{boot_index:0,delete_on_termination:true,destination_type:"volume",source_type:"image",uuid:"66e129c5-7386-4620-b02a-8e578405e735",volume_size:10,volume_type:"9bcdbe9a-2e06-430f-a6a6-ba77c507cf51"}] + * @param {String} data.server.flavorRef The flavor reference, as an ID (including a UUID) or full URL, for the flavor for your server instance. + * @param {String} data.server.imageRef imageRef + * @param {String} data.server.key_name key_name + * @param {String} data.server.adminPass admins password + * @param {Number} data.server.min_count when count > 1 + * @param {Number} data.server.max_count when count > 1 + * @param {String} data.server.return_reservation_id when count > 1 + * @param {String} data.server.name The server name + * @param {String} data.server.hypervisor_hostname when physicalNodeType.value !== "smart" + * @param {String} data.server.user_data Configuration information or scripts to use upon launch. Must be Base64 encoded. Restricted to 65535 bytes. + * @param {Object} data.server["OS-SCH-HNT:scheduler_hints"] example : {group: "xxxxx"} + * @param {Array[Object]} data.server.networks example : [{uuid: "xxxx"}] + * @param {Array[Object]} data.server.security_groups example : [{name: "xxxx"}] + * @returns {Promise} + */ +export const createServer = (data) => + axios.request({ + method: 'post', + url: getNovaBaseUrl('servers'), + data, + }); + +/** + * Delete server + * @param {String} id The UUID of the server. + * @returns {Promise} + */ +export const deleteServer = (id) => + axios.request({ + method: 'delete', + url: getNovaBaseUrl(`servers/${id}`), + }); + +/** + * List Servers + * @param {Object} params request query + * @param {String} params.reservation_id A reservation id as returned by a servers multiple create call. + * @returns {Promise} + */ +export const fetchListServersOnNova = (params) => + axios.request({ + method: 'get', + url: getNovaBaseUrl('servers'), + params, + }); + +/** + * Show Server Details + * @param {String} serverId The UUID of the server. + * @param {Object} params request query + * @returns {Promise} + */ +export const fetchServerDetails = (serverId, params) => + axios.request({ + method: 'get', + url: getNovaBaseUrl(`servers/${serverId}`), + params, + }); + +/** + * Create Console + * @param {String} serverId The UUID of the server. + * @param {Object} data request body + * @param {Object} data.remote_console The remote console object. + * @param {String} data.remote_console.protocol The protocol of remote console. + * @param {String} data.remote_console.type The type of remote console. + * @returns {Promise} + */ +export const createConsoleOnServer = (serverId, data) => + axios.request({ + method: 'post', + url: getNovaBaseUrl(`servers/${serverId}/remote-consoles`), + data, + }); + +/** + * Servers - run an administrative action + * @param {String} serverId The UUID of the server. + * @param {Object} data request body + * @param {String} data.injectNetworkInfo Inject Network Information (injectNetworkInfo Action + * @param {String} data.migrate The action to cold migrate a server. + * @returns {Promise} + */ +export const serverActionOnNova = (serverId, data) => + axios.request({ + method: 'post', + url: getNovaBaseUrl(`servers/${serverId}/action`), + data, + }); + +/** + * List Port Interfaces + * @param {String} serverId The UUID of the server. + * @returns {Promise} + */ +export const fetchListPortInterfaces = (serverId) => + axios.request({ + method: 'get', + url: getNovaBaseUrl(`servers/${serverId}/os-interface`), + }); + +/** + * Create Interface + * @param {String} serverId The UUID of the server. + * @param {Object} data request body + * @param {Object} data.interfaceAttachment Specify the interfaceAttachment action in the request body. + * @param {Object} data.ip_address The IP address. It is required when fixed_ips is specified. + * @param {Object} data.port_id The ID of the port for which you want to create an interface. + * @param {Object} data.net_id The ID of the network for which you want to create a port interface. + * @param {Object} data.fixed_ips Fixed IP addresses. + * @param {Object} data.tag A device role tag that can be applied to a network interface when attaching it to the VM. + * @returns {Promise} + */ +export const createOsInterfaces = (serverId, data) => + axios.request({ + method: 'post', + url: getNovaBaseUrl(`servers/${serverId}/os-interface`), + data, + }); + +/** + * Detach Interface + * @param {String} serverId The UUID of the server. + * @param {String} portId The UUID of the port. + * @returns {Promise} + */ +export const deletePortInterfaces = (serverId, portId) => + axios.request({ + method: 'delete', + url: getNovaBaseUrl(`servers/${serverId}/os-interface/${portId}`), + }); + +/** + * List volume attachments for an instance + * @param {String} serverId The UUID of the server. + * @param {Object} params request query + * @param {Number} params.limit max_limit + * @param {Number} params.offset offset is where to start in the list + * @returns {Promise} + */ +export const fetchVolumeAttachments = (serverId, params) => + axios.request({ + method: 'get', + url: getNovaBaseUrl(`servers/${serverId}/os-volume_attachments`), + params, + }); + +/** + * Attach a volume to an instance + * @param {String} serverId The UUID of the server. + * @param {Object} data The UUID of the port. + * @param {Object} data.volumeAttachment A dictionary representation of a volume attachment containing the fields device and volumeId. + * @param {Object} data.volumeAttachment.volumeId The UUID of the volume to attach. + * @param {Object} data.volumeAttachment.device Name of the device. + * @param {Object} data.volumeAttachment.tag A device role tag that can be applied to a volume when attaching it to the VM. + * @param {Boolean} data.volumeAttachment.delete_on_termination To delete the attached volume when the server is destroyed. + * @returns {Promise} + */ +export const attachVolumeOnInstance = (serverId, data) => + axios.request({ + method: 'post', + url: getNovaBaseUrl(`servers/${serverId}/os-volume_attachments}`), + data, + }); + +/** + * Detach a volume from an instance + * @param {String} serverId The UUID of the server. + * @param {Object} volumeId The UUID of the volume to detach. + * @returns {Promise} + */ +export const deleteVolumeOnInstance = (serverId, volumeId) => + axios.request({ + method: 'delete', + url: getNovaBaseUrl( + `servers/${serverId}/os-volume_attachments/${volumeId}}` + ), + }); diff --git a/src/api/octavia/base.js b/src/api/octavia/base.js new file mode 100644 index 00000000..85f0e052 --- /dev/null +++ b/src/api/octavia/base.js @@ -0,0 +1,23 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @param {String} key api url + * @returns {String} + */ +import { octaviaBase } from 'utils/constants'; + +const getOctaviaBaseUrl = (key) => `${octaviaBase()}/${key}`; + +export default getOctaviaBaseUrl; diff --git a/src/api/octavia/lbaas.js b/src/api/octavia/lbaas.js new file mode 100644 index 00000000..cc08b198 --- /dev/null +++ b/src/api/octavia/lbaas.js @@ -0,0 +1,132 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import getOctaviaBaseUrl from './base'; + +/** + * Update a Load Balancer + * @param {String} loadbalancerId The ID of the load balancer to query. + * @param {Object} data request body + * @param {Object} data.loadbalancer A load balancer object. + * @param {Boolean} data.loadbalancer.admin_state_up The administrative state of the resource, which is up (true) or down (false). + * @param {String} data.loadbalancer.name Human-readable name of the resource. + * @param {Array} data.loadbalancer.tags A list of simple strings assigned to the resource. + * @param {String} data.loadbalancer.vip_qos_policy_id The ID of the QoS Policy which will apply to the Virtual IP (VIP). + * @returns {Promise} + */ +export const updateLoadBalancer = (loadbalancerId, data) => + axios.request({ + method: 'put', + url: getOctaviaBaseUrl(`lbaas/loadbalancers/${loadbalancerId}`), + data, + }); + +/** + * List Load Balancers + * @param {Object} params request query + * @param {String} params.project_id The ID of the project to query. + * @param {String} params.fields A load balancer object. + * @returns {Promise} + */ +export const fetchListLoadBalancers = (params) => + axios.request({ + method: 'get', + url: getOctaviaBaseUrl('lbaas/loadbalancers'), + params, + }); + +/** + * Show Load Balancer details + * @param {String} loadbalancerId The ID of the load balancer to query. + * @param {Object} params request query + * @param {String} params.fields A load balancer object. + * @returns {Promise} + */ +export const fetchLoadBalancerDetails = (loadbalancerId, params) => + axios.request({ + method: 'get', + url: getOctaviaBaseUrl(`lbaas/loadbalancers/${loadbalancerId}`), + params, + }); + +/** + * Remove a Load Balancer + * @param {String} loadbalancerId The ID of the load balancer to query. + * @param {Object} params request query + * @param {Boolean} params.cascade If true will delete all child objects of the load balancer. + * @returns {Promise} + */ +export const deleteLoadBalancer = (loadbalancerId, params) => + axios.request({ + method: 'delete', + url: getOctaviaBaseUrl(`lbaas/loadbalancers/${loadbalancerId}`), + params, + }); + +/** + * Create Member + * @param {String} poolId The ID of the pool to query. + * @param {Object} data request body + * @param {Object} data.member The member object. + * @returns {Promise} + */ +export const createMemberOnOctavia = (poolId, data) => + axios.request({ + method: 'post', + url: getOctaviaBaseUrl(`lbaas/pools/${poolId}/members`), + data, + }); + +/** + * Batch Update Members + * @param {String} poolId The ID of the pool to query. + * @param {Object} data request body + * @param {Object} data.members The members object. + * @returns {Promise} + */ +export const batchUpdateMembersOnOctavia = (poolId, data) => + axios.request({ + method: 'put', + url: getOctaviaBaseUrl(`lbaas/pools/${poolId}/members`), + data, + }); + +/** + * Update A Member + * @param {String} poolId The ID of the pool to query. + * @param {String} memberId The ID of the member to query. + * @param {Object} data request body + * @param {Object} data.member The member object. + * @returns {Promise} + */ +export const updateAMemberOnOctavia = (poolId, memberId, data) => + axios.request({ + method: 'put', + url: getOctaviaBaseUrl(`lbaas/pools/${poolId}/members/${memberId}`), + data, + }); + +/** + * Delete A Member + * @param {String} poolId The ID of the pool to query. + * @param {String} memberId The ID of the member to query. + * @returns {Promise} + */ +export const deleteAMemberOnOctavia = (poolId, memberId) => + axios.request({ + method: 'delete', + url: getOctaviaBaseUrl(`lbaas/pools/${poolId}/members/${memberId}`), + }); diff --git a/src/api/octavia/pools.js b/src/api/octavia/pools.js new file mode 100644 index 00000000..db37880a --- /dev/null +++ b/src/api/octavia/pools.js @@ -0,0 +1,28 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import getOctaviaBaseUrl from './base'; + +/** + * Show Pool Details + * @param {Object} poolId The ID of the pool to query. + * @returns {Promise} + */ +export const fetchPoolDetailsOnOctavia = (poolId) => + axios.request({ + method: 'get', + url: getOctaviaBaseUrl(`pools/${poolId}`), + }); diff --git a/src/api/panko/base.js b/src/api/panko/base.js new file mode 100644 index 00000000..e3b159d3 --- /dev/null +++ b/src/api/panko/base.js @@ -0,0 +1,23 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @param {String} key api url + * @returns {String} + */ +import { pankoBase } from 'utils/constants'; + +const getPankoBaseUrl = (key) => `${pankoBase()}/${key}`; + +export default getPankoBaseUrl; diff --git a/src/api/panko/event.js b/src/api/panko/event.js new file mode 100644 index 00000000..faa139a1 --- /dev/null +++ b/src/api/panko/event.js @@ -0,0 +1,50 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import getPankoBaseUrl from './base'; + +/** + * List events + * @param {Object} params request query + * @param {String} params['q.field'] 'filter_self' + * @param {String} params['q.op'] 'le' + * @param {Boolean} params['q.value'] true or false + * @param {String} params.sort 'generated:desc' + * @param {Number} params.limit 10 + * @param {String} params.mariker "string" + * @returns {Promise} + */ +export const fetchEvents = (params) => + axios.request({ + method: 'get', + url: getPankoBaseUrl('events'), + params, + }); + +/** + * Fetch count for event + * @param {Object} params request query + * @param {String} params['q.field'] 'filter_self' + * @param {String} params['q.op'] 'le' + * @param {Boolean} params['q.value'] true or false + * @returns {Promise} + */ +export const fetchEventCount = (params) => + axios.request({ + method: 'get', + url: getPankoBaseUrl('events/count'), + params, + }); diff --git a/src/api/placement/base.js b/src/api/placement/base.js new file mode 100644 index 00000000..30017b79 --- /dev/null +++ b/src/api/placement/base.js @@ -0,0 +1,23 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @param {String} key api url + * @returns {String} + */ +import { placementBase } from 'utils/constants'; + +const getPlacementBaseUrl = (key) => `${placementBase()}/${key}`; + +export default getPlacementBaseUrl; diff --git a/src/api/placement/resource-providers.js b/src/api/placement/resource-providers.js new file mode 100644 index 00000000..4f7aff71 --- /dev/null +++ b/src/api/placement/resource-providers.js @@ -0,0 +1,28 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import getPlacementBaseUrl from './base'; + +/** + * List resource provider inventories + * @param {String} uuid path + * @returns {Promise} + */ +export const fetchResourceProviderInventories = (uuid) => + axios.request({ + method: 'get', + url: getPlacementBaseUrl(`resource_providers/${uuid}/inventories`), + }); diff --git a/src/api/placement/traits.js b/src/api/placement/traits.js new file mode 100644 index 00000000..9db353ba --- /dev/null +++ b/src/api/placement/traits.js @@ -0,0 +1,31 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import getPlacementBaseUrl from './base'; + +/** + * List traits + * @param {Object} params request query + * @param {String} params.name A string to filter traits. + * @param {Boolean} params.associated Available values for the parameter are true and false. + * @returns {Promise} + */ +export const fetchListTraitsOnPlacement = (params) => + axios.request({ + method: 'get', + url: getPlacementBaseUrl('traits'), + params, + }); diff --git a/src/api/skyline/base.js b/src/api/skyline/base.js new file mode 100644 index 00000000..19765c92 --- /dev/null +++ b/src/api/skyline/base.js @@ -0,0 +1,23 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @param {String} key api url + * @returns {String} + */ +import { skylineBase } from 'utils/constants'; + +const getSkylineBaseUrl = (key) => `${skylineBase()}/${key}`; + +export default getSkylineBaseUrl; diff --git a/src/api/skyline/contrib.js b/src/api/skyline/contrib.js new file mode 100644 index 00000000..aafea31b --- /dev/null +++ b/src/api/skyline/contrib.js @@ -0,0 +1,47 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import getSkylineBaseUrl from './base'; + +/** + * List Keystone Endpoints + * @returns {Promise} + */ +export const fetchKeystoneEndpoints = () => + axios.request({ + method: 'get', + url: getSkylineBaseUrl('contrib/keystone_endpoints'), + }); + +/** + * List Domains + * @returns {Promise} + */ +export const fetchDomains = () => + axios.request({ + method: 'get', + url: getSkylineBaseUrl('contrib/domains'), + }); + +/** + * List Regions + * @returns {Promise} + */ +export const fetchRegions = () => + axios.request({ + method: 'get', + url: getSkylineBaseUrl('contrib/regions'), + }); diff --git a/src/api/skyline/extension.js b/src/api/skyline/extension.js new file mode 100644 index 00000000..51d9279e --- /dev/null +++ b/src/api/skyline/extension.js @@ -0,0 +1,155 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import getSkylineBaseUrl from './base'; + +/** + * List Servers. + * Notes: + * The host of sort_keys is only used for admin/system_admin role users. + * The name is to support for fuzzy queries. + * @param {Object} params request query + * @param {Number} params.limit Default value : 10 + * @param {String} params.marker marker + * @param {String} params.sort_dirs Available values : desc, asc + * @param {Array[String]} params.sort_keys Available values : uuid, display_name, vm_state, locked, created_at, host, project_id + * @param {Boolean} params.all_projects Default value : false + * @param {String} params.project_id Only works when the all_projects filter is also specified. + * @param {String} params.project_name Only works when the all_projects filter is also specified. + * @param {String} params.name name + * @param {String} params.floating_ip Floating IP of server. + * @param {String} params.fixed_ip Fixed IP of server. + * @param {String} params.status Available values : ACTIVE, BUILD, ERROR, HARD_REBOOT, MIGRATING, PAUSED, REBOOT, REBUILD, RESCUE, RESIZE, SHELVED, SHELVED_OFFLOADED, SHUTOFF, SOFT_DELETED, SUSPENDED, UNKNOWN + * @param {String} params.host It will be ignored for non-admin user. + * @param {String} params.flavor_id flavors id + * @param {Array[String]} params.uuid UUID of server. + * @returns {Promise} + */ +export const fetchListServers = (params) => + axios.request({ + method: 'get', + url: getSkylineBaseUrl('extension/servers'), + params, + }); + +/** + * List Recycle Servers. + * Notes: + * The updated_at of sort_keys is used as deleted_at. + * The name is to support for fuzzy queries. + * @param {Object} params request query + * @param {Number} params.limit Default value : 10 + * @param {String} params.marker marker + * @param {String} params.sort_dirs Available values : desc, asc + * @param {Array[String]} params.sort_keys Available values : uuid, display_name, updated_at, project_id + * @param {Boolean} params.all_projects Default value : false + * @param {String} params.project_id Only works when the all_projects filter is also specified. + * @param {String} params.project_name Only works when the all_projects filter is also specified. + * @param {String} params.name name + * @param {String} params.floating_ip Floating IP of server. + * @param {String} params.fixed_ip Fixed IP of server. + * @param {Array[String]} params.uuid UUID of server. + * @returns {Promise} + */ +export const fetchListRecycleServers = (params) => + axios.request({ + method: 'get', + url: getSkylineBaseUrl('extension/recycle_servers'), + params, + }); + +/** + * List Volumes. + * @param {Object} params request query + * @param {Number} params.limit Default value : 10 + * @param {String} params.marker marker + * @param {String} params.sort_dirs Available values : desc, asc + * @param {Array[String]} params.sort_keys Available values : id, name, size, status, bootable, created_at + * @param {Boolean} params.all_projects Default value : false + * @param {String} params.project_id Only works when the all_projects filter is also specified. + * @param {String} params.name name + * @param {Boolean} params.multiattach Default value : false + * @param {String} params.status Available values : creating, available, reserved, attaching, detaching, in-use, maintenance, deleting, awaiting-transfer, error, error_deleting, backing-up, restoring-backup, error_backing-up, error_restoring, error_extending, downloading, uploading, retyping, extending + * @param {Boolean} params.bootable Default value : false + * @param {Array[String]} params.uuid UUID of volume. + * @returns {Promise} + */ +export const fetchListVolumes = (params) => + axios.request({ + method: 'get', + url: getSkylineBaseUrl('extension/volumes'), + params, + }); + +/** + * List Volume Snapshots. + * @param {Object} params request query + * @param {Number} params.limit Default value : 10 + * @param {String} params.marker marker + * @param {String} params.sort_dirs Available values : desc, asc + * @param {Array[String]} params.sort_keys Available values : id, name, status, created_at + * @param {Boolean} params.all_projects Default value : false + * @param {String} params.project_id Only works when the all_projects filter is also specified. + * @param {String} params.name name + * @param {String} params.status Available values : CREATING, AVAILABLE, DELETING, ERROR, ERROR_DELETING + * @param {String} params.volume_id volumes id + * @returns {Promise} + */ +export const fetchListVolumeSnapshots = (params) => + axios.request({ + method: 'get', + url: getSkylineBaseUrl('extension/volume_snapshots'), + params, + }); + +/** + * List Ports. + * @param {Object} params request query + * @param {Number} params.limit Default value : 10 + * @param {String} params.marker marker + * @param {String} params.sort_dirs Available values : desc, asc + * @param {Array[String]} params.sort_keys Available values : id, name, mac_address, status, project_id + * @param {Boolean} params.all_projects Default value : false + * @param {String} params.project_id Only works when the all_projects filter is also specified. + * @param {String} params.name name + * @param {String} params.status Available values : ACTIVE, DOWN, BUILD, ERROR, N/A + * @param {String} params.network_name networks name + * @param {String} params.network_id networks id + * @param {String} params.device_id devices id + * @param {Array[String]} params.device_owner Available values : , compute:nova, network:dhcp, network:floatingip, network:router_gateway, network:router_ha_interface, network:ha_router_replicated_interface + * @param {Array[String]} params.uuid UUID of port. + * @returns {Promise} + */ +export const fetchListPorts = (params) => + axios.request({ + method: 'get', + url: getSkylineBaseUrl('extension/ports'), + params, + }); + +/** + * List compute services. + * @param {Object} params request query + * @param {String} params.binary binary + * @param {String} params.host host + * @returns {Promise} + */ +export const fetchListComputeServices = (params) => + axios.request({ + method: 'get', + url: getSkylineBaseUrl('extension/compute-services'), + params, + }); diff --git a/src/api/skyline/login.js b/src/api/skyline/login.js new file mode 100644 index 00000000..089d4d71 --- /dev/null +++ b/src/api/skyline/login.js @@ -0,0 +1,64 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import getSkylineBaseUrl from './base'; + +/** + * login + * @param {Object} data request body + * @param {String} data.region RegionOne + * @param {String} data.domain default + * @param {String} data.username admin + * @param {String} data.password password + * @returns {Promise} + */ +export const login = (data) => + axios.request({ + method: 'post', + url: getSkylineBaseUrl('login'), + data, + }); + +/** + * logout + * @returns {Promise} + */ +export const logout = () => + axios.request({ + method: 'post', + url: getSkylineBaseUrl('logout'), + }); + +/** + * fetch profile + * @returns {Promise} + */ +export const fetchProfile = () => + axios.request({ + method: 'get', + url: getSkylineBaseUrl('profile'), + }); + +/** + * switch_project + * @param {String} projectId projects id + * @returns {Promise} + */ +export const switchProject = (projectId) => + axios.request({ + method: 'post', + url: getSkylineBaseUrl(`switch_project/${projectId}`), + }); diff --git a/src/api/skyline/policy.js b/src/api/skyline/policy.js new file mode 100644 index 00000000..3e3c2070 --- /dev/null +++ b/src/api/skyline/policy.js @@ -0,0 +1,40 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import getSkylineBaseUrl from './base'; + +/** + * List policies + * @returns {Promise} + */ +export const fetchPolicies = () => + axios.request({ + method: 'get', + url: getSkylineBaseUrl('policies'), + }); + +/** + * Check policies + * @param {Object} data request body + * @param {Array[String]} data.rules ["string"] + * @returns {Promise} + */ +export const checkPolicies = (data) => + axios.request({ + method: 'post', + url: getSkylineBaseUrl('policies/check'), + data, + }); diff --git a/src/api/skyline/setting.js b/src/api/skyline/setting.js new file mode 100644 index 00000000..ce85d8ff --- /dev/null +++ b/src/api/skyline/setting.js @@ -0,0 +1,63 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +import getSkylineBaseUrl from './base'; + +/** + * Get a setting item. + * @param {String} key path + * @returns {Promise} + */ +export const fetchSetting = (key) => + axios.request({ + method: 'get', + url: getSkylineBaseUrl(`setting/${key}`), + }); + +/** + * Reset a setting item to default + * @param {String} key path + * @returns {Promise} + */ +export const resetSetting = (key) => + axios.request({ + method: 'delete', + url: getSkylineBaseUrl(`setting/${key}`), + }); + +/** + * Update a setting item. + * @param {Object} data request body + * @param {String} data.key "string" + * @param {String} data.value "string" + * @returns {Promise} + */ +export const updateSetting = (data) => + axios.request({ + method: 'put', + url: getSkylineBaseUrl('setting'), + data, + }); + +/** + * Get all settings. + * @returns {Promise} + */ +export const fetchAllSettings = () => + axios.request({ + method: 'get', + url: getSkylineBaseUrl('settings'), + }); diff --git a/src/api/storage/storage-classes.js b/src/api/storage/storage-classes.js new file mode 100644 index 00000000..3883531f --- /dev/null +++ b/src/api/storage/storage-classes.js @@ -0,0 +1,27 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import axios from '@/libs/axios'; + +/** + * Create Storageclasses + * @param {Object} data request body + * @returns {Promise} + */ +export const createStorageclasses = (data) => + axios.request({ + method: 'post', + url: 'apis/storage.k8s.io/v1/storageclasses', + data, + }); diff --git a/src/api/swift/base.js b/src/api/swift/base.js new file mode 100644 index 00000000..1b78587a --- /dev/null +++ b/src/api/swift/base.js @@ -0,0 +1,23 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @param {String} key api url + * @returns {String} + */ +import { swiftBase } from 'utils/constants'; + +const getSwiftBaseUrl = (key) => `${swiftBase()}/${key}`; + +export default getSwiftBaseUrl; diff --git a/src/asset/image/ArchLinux.svg b/src/asset/image/ArchLinux.svg new file mode 100644 index 00000000..9d62fa46 --- /dev/null +++ b/src/asset/image/ArchLinux.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/asset/image/TmpFileImage.png b/src/asset/image/TmpFileImage.png new file mode 100644 index 00000000..ae918c9e Binary files /dev/null and b/src/asset/image/TmpFileImage.png differ diff --git a/src/asset/image/adminImage.svg b/src/asset/image/adminImage.svg new file mode 100644 index 00000000..bee2f70f --- /dev/null +++ b/src/asset/image/adminImage.svg @@ -0,0 +1,13 @@ + + + SliceCopy + + + + + + + + + + \ No newline at end of file diff --git a/src/asset/image/adminInstance.svg b/src/asset/image/adminInstance.svg new file mode 100644 index 00000000..c2040b86 --- /dev/null +++ b/src/asset/image/adminInstance.svg @@ -0,0 +1,7 @@ + + + 形状 + + + + \ No newline at end of file diff --git a/src/asset/image/adminNetwork.svg b/src/asset/image/adminNetwork.svg new file mode 100644 index 00000000..a28c68f0 --- /dev/null +++ b/src/asset/image/adminNetwork.svg @@ -0,0 +1,13 @@ + + + wangluo-2 + + + + + + + + + + \ No newline at end of file diff --git a/src/asset/image/adminRouter.svg b/src/asset/image/adminRouter.svg new file mode 100644 index 00000000..8810cdc7 --- /dev/null +++ b/src/asset/image/adminRouter.svg @@ -0,0 +1,13 @@ + + + luyouqi + + + + + + + + + + \ No newline at end of file diff --git a/src/asset/image/adminSecurityGroup.svg b/src/asset/image/adminSecurityGroup.svg new file mode 100644 index 00000000..ed7e020d --- /dev/null +++ b/src/asset/image/adminSecurityGroup.svg @@ -0,0 +1,16 @@ + + + 编组 32 + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/asset/image/adminVolume.svg b/src/asset/image/adminVolume.svg new file mode 100644 index 00000000..c071285a --- /dev/null +++ b/src/asset/image/adminVolume.svg @@ -0,0 +1,13 @@ + + + 编组 38 + + + + + + + + + + \ No newline at end of file diff --git a/src/asset/image/animnbus.png b/src/asset/image/animnbus.png new file mode 100644 index 00000000..c6351d38 Binary files /dev/null and b/src/asset/image/animnbus.png differ diff --git a/src/asset/image/arch.svg b/src/asset/image/arch.svg new file mode 100644 index 00000000..f57758c5 --- /dev/null +++ b/src/asset/image/arch.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/asset/image/centos.svg b/src/asset/image/centos.svg new file mode 100644 index 00000000..91a4c3dd --- /dev/null +++ b/src/asset/image/centos.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/asset/image/cloud.png b/src/asset/image/cloud.png new file mode 100644 index 00000000..417c2e22 Binary files /dev/null and b/src/asset/image/cloud.png differ diff --git a/src/asset/image/coreos.svg b/src/asset/image/coreos.svg new file mode 100644 index 00000000..dfc81b05 --- /dev/null +++ b/src/asset/image/coreos.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/asset/image/critical-alert.svg b/src/asset/image/critical-alert.svg new file mode 100644 index 00000000..4451ad83 --- /dev/null +++ b/src/asset/image/critical-alert.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/asset/image/debian.svg b/src/asset/image/debian.svg new file mode 100644 index 00000000..d43168d1 --- /dev/null +++ b/src/asset/image/debian.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/asset/image/empty-card.svg b/src/asset/image/empty-card.svg new file mode 100644 index 00000000..28893d0f --- /dev/null +++ b/src/asset/image/empty-card.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/asset/image/favicon.ico b/src/asset/image/favicon.ico new file mode 100644 index 00000000..3005f9b0 Binary files /dev/null and b/src/asset/image/favicon.ico differ diff --git a/src/asset/image/fedora.svg b/src/asset/image/fedora.svg new file mode 100644 index 00000000..cd09cd86 --- /dev/null +++ b/src/asset/image/fedora.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/asset/image/freebsd.svg b/src/asset/image/freebsd.svg new file mode 100644 index 00000000..7ffcbea6 --- /dev/null +++ b/src/asset/image/freebsd.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/asset/image/image.svg b/src/asset/image/image.svg new file mode 100644 index 00000000..00964e2f --- /dev/null +++ b/src/asset/image/image.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/asset/image/instance.png b/src/asset/image/instance.png new file mode 100644 index 00000000..6ea6acea Binary files /dev/null and b/src/asset/image/instance.png differ diff --git a/src/asset/image/instance.svg b/src/asset/image/instance.svg new file mode 100644 index 00000000..c56d9f23 --- /dev/null +++ b/src/asset/image/instance.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/asset/image/interface.png b/src/asset/image/interface.png new file mode 100644 index 00000000..bcd7459d Binary files /dev/null and b/src/asset/image/interface.png differ diff --git a/src/asset/image/interface.svg b/src/asset/image/interface.svg new file mode 100644 index 00000000..1fefdca3 --- /dev/null +++ b/src/asset/image/interface.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/asset/image/loadBalancer.png b/src/asset/image/loadBalancer.png new file mode 100644 index 00000000..15c6a35c Binary files /dev/null and b/src/asset/image/loadBalancer.png differ diff --git a/src/asset/image/lock.svg b/src/asset/image/lock.svg new file mode 100644 index 00000000..32838f79 --- /dev/null +++ b/src/asset/image/lock.svg @@ -0,0 +1,12 @@ + + + jiesuo + + + + + + + + + \ No newline at end of file diff --git a/src/asset/image/login.png b/src/asset/image/login.png new file mode 100644 index 00000000..c149a600 Binary files /dev/null and b/src/asset/image/login.png differ diff --git a/src/asset/image/loginFull.png b/src/asset/image/loginFull.png new file mode 100644 index 00000000..b5325744 Binary files /dev/null and b/src/asset/image/loginFull.png differ diff --git a/src/asset/image/loginRightLogo.png b/src/asset/image/loginRightLogo.png new file mode 100644 index 00000000..0c5b1aff Binary files /dev/null and b/src/asset/image/loginRightLogo.png differ diff --git a/src/asset/image/logo-extend.svg b/src/asset/image/logo-extend.svg new file mode 100644 index 00000000..a94f7312 --- /dev/null +++ b/src/asset/image/logo-extend.svg @@ -0,0 +1,11 @@ + + + + + 编组 + + diff --git a/src/asset/image/logo-small.svg b/src/asset/image/logo-small.svg new file mode 100644 index 00000000..c555b8f6 --- /dev/null +++ b/src/asset/image/logo-small.svg @@ -0,0 +1,18 @@ + + + logo-浅色 + + + + + + + + \ No newline at end of file diff --git a/src/asset/image/logo.png b/src/asset/image/logo.png new file mode 100644 index 00000000..b16a04a5 Binary files /dev/null and b/src/asset/image/logo.png differ diff --git a/src/asset/image/major-alert.svg b/src/asset/image/major-alert.svg new file mode 100644 index 00000000..5d509f7d --- /dev/null +++ b/src/asset/image/major-alert.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/asset/image/minor-alert.svg b/src/asset/image/minor-alert.svg new file mode 100644 index 00000000..69dd6a7b --- /dev/null +++ b/src/asset/image/minor-alert.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/asset/image/others.svg b/src/asset/image/others.svg new file mode 100644 index 00000000..72f85008 --- /dev/null +++ b/src/asset/image/others.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/asset/image/overview-instance.svg b/src/asset/image/overview-instance.svg new file mode 100755 index 00000000..79ad6c8c --- /dev/null +++ b/src/asset/image/overview-instance.svg @@ -0,0 +1,13 @@ + + + 编组 + + + + + + + + + + \ No newline at end of file diff --git a/src/asset/image/overview-network.svg b/src/asset/image/overview-network.svg new file mode 100755 index 00000000..a1b0ce31 --- /dev/null +++ b/src/asset/image/overview-network.svg @@ -0,0 +1,12 @@ + + + wangluo + + + + + + + + + \ No newline at end of file diff --git a/src/asset/image/overview-router.svg b/src/asset/image/overview-router.svg new file mode 100755 index 00000000..2fc3576e --- /dev/null +++ b/src/asset/image/overview-router.svg @@ -0,0 +1,14 @@ + + + luyouqi + + + + + + + + + + + \ No newline at end of file diff --git a/src/asset/image/overview-volume.svg b/src/asset/image/overview-volume.svg new file mode 100755 index 00000000..da4259ce --- /dev/null +++ b/src/asset/image/overview-volume.svg @@ -0,0 +1,17 @@ + + + 编组 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/asset/image/router.png b/src/asset/image/router.png new file mode 100644 index 00000000..e9d526a1 Binary files /dev/null and b/src/asset/image/router.png differ diff --git a/src/asset/image/security-group.svg b/src/asset/image/security-group.svg new file mode 100644 index 00000000..942ed743 --- /dev/null +++ b/src/asset/image/security-group.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/asset/image/ubuntu.svg b/src/asset/image/ubuntu.svg new file mode 100644 index 00000000..3ff1f489 --- /dev/null +++ b/src/asset/image/ubuntu.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/asset/image/unlock.svg b/src/asset/image/unlock.svg new file mode 100644 index 00000000..7e8b014a --- /dev/null +++ b/src/asset/image/unlock.svg @@ -0,0 +1,12 @@ + + + jiesuo + + + + + + + + + \ No newline at end of file diff --git a/src/asset/image/volume-container.svg b/src/asset/image/volume-container.svg new file mode 100755 index 00000000..18ff8d30 --- /dev/null +++ b/src/asset/image/volume-container.svg @@ -0,0 +1,34 @@ + + + + 编组 10 + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/asset/image/volume.svg b/src/asset/image/volume.svg new file mode 100644 index 00000000..f5d53b44 --- /dev/null +++ b/src/asset/image/volume.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/asset/image/windows.svg b/src/asset/image/windows.svg new file mode 100644 index 00000000..7c15f809 --- /dev/null +++ b/src/asset/image/windows.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/asset/template/index.html b/src/asset/template/index.html new file mode 100644 index 00000000..2b38b4e7 --- /dev/null +++ b/src/asset/template/index.html @@ -0,0 +1,12 @@ + + + + + + Cloud + + + +
+ + diff --git a/src/components/Cards/EmptyTable/index.jsx b/src/components/Cards/EmptyTable/index.jsx new file mode 100644 index 00000000..6caaa1c4 --- /dev/null +++ b/src/components/Cards/EmptyTable/index.jsx @@ -0,0 +1,69 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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 PropTypes from 'prop-types'; +import classnames from 'classnames'; + +import { Row, Col, Button } from 'antd'; + +import styles from './index.less'; + +export default class EmptyList extends React.Component { + static propTypes = { + className: PropTypes.string, + name: PropTypes.string, + desc: PropTypes.string, + createText: PropTypes.string, + operations: PropTypes.oneOfType([PropTypes.node, PropTypes.element]), + onCreate: PropTypes.func, + }; + + static defaultProps = { + name: '', + }; + + render() { + const { className, name, operations, onCreate, createText } = this.props; + const desc = + this.props.desc || + t(`${name.split(' ').join('_').toUpperCase()}_CREATE_DESC`); + + const btnText = createText || `${t('Create ')}${t(name)}`; + + return ( +
+ + + + + +

+ {operations || + (onCreate && ( + + ))} + + +

+ ); + } +} diff --git a/src/components/Cards/EmptyTable/index.less b/src/components/Cards/EmptyTable/index.less new file mode 100644 index 00000000..0b30cd2b --- /dev/null +++ b/src/components/Cards/EmptyTable/index.less @@ -0,0 +1,18 @@ +@import '~styles/variables'; + +.wrapper { + padding: 38px 72px; + border-radius: @border-radius; + background-color: #ffffff; + box-shadow: @base-shadow; + + img { + margin-right: 70px; + } +} + +.desc { + max-width: 580px; + margin-bottom: 20px; + font-family: @font-family-id; +} \ No newline at end of file diff --git a/src/components/Cards/NotFound/index.jsx b/src/components/Cards/NotFound/index.jsx new file mode 100644 index 00000000..27702ba3 --- /dev/null +++ b/src/components/Cards/NotFound/index.jsx @@ -0,0 +1,69 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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 emptyCard from '@/asset/image/empty-card.svg'; +import { firstUpperCase } from 'utils/index'; +import { Link } from 'react-router-dom'; +import styles from './index.less'; + +export default class NotFound extends React.Component { + render() { + const { title, link, codeError, endpointError, goList, isAction } = + this.props; + let h = ''; + if (codeError) { + h = 'Error'; + } else if (endpointError) { + h = t('Not Open'); + } else { + h = t('Resource Not Found'); + } + let pTitle = ''; + let linkTitle = ''; + if (isAction) { + pTitle = t('Unable to {title}, please go back to ', { + title: firstUpperCase(title), + }); + } else if (goList) { + pTitle = t('Unable to get {title}, please go back to ', { + title: firstUpperCase(title), + }); + } else { + pTitle = t('Unable to get {title}, please go to ', { + title: firstUpperCase(title), + }); + } + if (goList) { + linkTitle = {t('list page')}; + } else { + linkTitle = {t('Home page')}; + } + const p = ( +

+ {pTitle} + {linkTitle} +

+ ); + return ( +
+ +
+
{h}
+ {p} +
+
+ ); + } +} diff --git a/src/components/Cards/NotFound/index.less b/src/components/Cards/NotFound/index.less new file mode 100644 index 00000000..189f1033 --- /dev/null +++ b/src/components/Cards/NotFound/index.less @@ -0,0 +1,41 @@ +@import '~styles/variables'; + +.wrapper { + margin-top: 100px; + border-radius: @border-radius; + text-align: center; +} + +.image { + height: 200px; + user-select: none; +} + +.text { + display: inline-block; + vertical-align: top; + width: 600px; + margin-left: 60px; + + :global .h1 { + opacity: 0.4; + font-size: 120px; + line-height: 1.4; + color: #abb4be; + user-select: none; + text-align: left; + } + + p { + text-shadow: 0 4px 8px rgba(36, 46, 66, 0.1); + font-size: 20px; + font-weight: @font-bold; + line-height: 1.4; + color: @second-title-color; + text-align: left; + } + + a { + color: @text-color; + } +} diff --git a/src/components/CodeEditor/AceEditor.jsx b/src/components/CodeEditor/AceEditor.jsx new file mode 100644 index 00000000..a578d0ea --- /dev/null +++ b/src/components/CodeEditor/AceEditor.jsx @@ -0,0 +1,41 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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 AceEditor from 'react-ace'; +import 'ace-builds/src-noconflict/mode-json'; +// import 'ace-builds/src-noconflict/mode-yaml'; +// import 'ace-builds/src-noconflict/mode-groovy'; +import 'ace-builds/src-noconflict/theme-github'; + +import './custom.less'; + +export default class AceEditorWrapper extends React.Component { + render() { + return ( + + ); + } +} diff --git a/src/components/CodeEditor/custom.less b/src/components/CodeEditor/custom.less new file mode 100644 index 00000000..a7e386f5 --- /dev/null +++ b/src/components/CodeEditor/custom.less @@ -0,0 +1,59 @@ +.ace_editor { + line-height: 20px !important; + font-family: Monaco, Menlo, Consolas, Courier New, monospace; + -webkit-font-smoothing: auto; +} + +.ace_editor.ace-chaos { + color: #ffffff; + background-color: #242e42; +} + +.ace_editor.ace-chaos .ace_gutter { + color: #537f7e; + background-color: #242e42; + border-right: 1px solid #4a5974; +} + +.ace_editor.ace-chaos .ace_variable, +.ace_editor.ace-chaos .ace_identifier, +.ace_editor.ace-chaos .ace_meta.ace_tag { + color: #75e0f2; +} + +.ace_editor.ace-chaos .ace_keyword { + color: #ffffff; +} + +.ace_editor.ace-chaos .ace_string { + color: #ebe087; +} + +.ace_editor.ace-chaos .ace_constant.ace_numeric { + color: #bd99ff; +} + +.ace_editor.ace-chaos .ace_marker-layer .ace_active-line { + background-color: #36435c; +} + +.ace_editor.ace-chaos .ace_indent-guide { + border-right: 1px dotted #777; + padding: 2px 0; +} + +.ace_editor.ace-chaos .ace_marker-layer .ace_selection { + background-color: #4a5974; +} + +.ace_editor.ace-chaos .ace_comment { + color: #aaa; +} + +.ace_editor.ace-chaos .ace_fold:hover { + background-color: #fff; +} + +.ace_editor.ace-chaos .ace_line .ace_fold { + height: auto; +} diff --git a/src/components/CodeEditor/index.jsx b/src/components/CodeEditor/index.jsx new file mode 100644 index 00000000..adeac88f --- /dev/null +++ b/src/components/CodeEditor/index.jsx @@ -0,0 +1,101 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import { getValue } from 'utils/yaml'; +import styles from './index.less'; +import AceEditor from './AceEditor'; + +const getCodeValue = (value, mode) => { + if (value instanceof String) { + return value; + } + Object.keys(value).forEach((key) => { + if (typeof value[key] === 'string' && value[key].indexOf('') !== -1) { + const reg = /<\/h1>[\r\n]([\s\S]*)

/; + const results = reg.exec(value[key]); + if (results) { + value[key] = results[1]; + } + } + }); + if (mode === 'json') { + return JSON.stringify(value, null, 2); + } + if (mode === 'yaml') { + return getValue(value); + } + return value; +}; + +class CodeEditor extends PureComponent { + static propTypes = { + value: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + mode: PropTypes.string, + options: PropTypes.object, + onChange: PropTypes.func, + }; + + static defaultProps = { + value: {}, + mode: 'yaml', + options: {}, + onChange() {}, + }; + + constructor(props) { + super(props); + + this.state = { + value: getCodeValue(props.value, props.mode), + originValue: props.value, + }; + } + + static getDerivedStateFromProps(props, state) { + const { value, mode } = props; + + if (value !== state.originValue) { + return { + value: getCodeValue(value, mode), + originValue: value, + }; + } + + return null; + } + + handleChange = (value) => { + const { onChange } = this.props; + onChange(value); + }; + + render() { + const { className, mode, options } = this.props; + + return ( + + ); + } +} + +export default CodeEditor; diff --git a/src/components/CodeEditor/index.less b/src/components/CodeEditor/index.less new file mode 100644 index 00000000..fab9cdc8 --- /dev/null +++ b/src/components/CodeEditor/index.less @@ -0,0 +1,4 @@ +.editor { + min-height: 60vh; + border-radius: 4px; +} diff --git a/src/components/Confirm/index.jsx b/src/components/Confirm/index.jsx new file mode 100644 index 00000000..b5f73c31 --- /dev/null +++ b/src/components/Confirm/index.jsx @@ -0,0 +1,107 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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 } from 'antd'; +import { + CheckCircleOutlined, + CloseCircleOutlined, + QuestionCircleFilled, +} from '@ant-design/icons'; +import classnames from 'classnames'; +import PropTypes from 'prop-types'; +import { unescapeHtml } from 'utils/index'; +import styles from './index.less'; + +const normalProps = { + title: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + content: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired, + onOk: PropTypes.func, + onCancel: PropTypes.func, + icon: PropTypes.node, + isSubmitting: PropTypes.bool, + cancelText: PropTypes.string, + okText: PropTypes.string, + className: PropTypes.string, +}; + +const confirm = (props) => { + const { + title = t('Confirm'), + content, + onOk, + onCancel, + icon, + okText = t('Confirm'), + cancelText = t('Cancel'), + className, + } = props; + Modal.confirm({ + title, + icon: icon || , + className: classnames(styles['confirm-modal'], className), + content: unescapeHtml(content), + okText, + cancelText, + onOk() { + return onOk && onOk(); + }, + onCancel() { + onCancel && onCancel(); + }, + }); +}; + +confirm.propTypes = normalProps; +confirm.defaultProps = { + title: t('Confirm'), + icon: , + isSubmitting: false, + okText: t('Confirm'), + cancelText: t('Cancel'), +}; + +const error = (props) => { + const newProps = { + title: t('Error'), + ...props, + icon: , + }; + confirm(newProps); +}; + +const warn = (props) => { + const newProps = { + title: t('Warn'), + ...props, + icon: , + }; + confirm(newProps); +}; + +const success = (props) => { + const newProps = { + title: t('Success'), + ...props, + icon: , + }; + confirm(newProps); +}; + +export default { + confirm, + error, + warn, + success, +}; diff --git a/src/components/Confirm/index.less b/src/components/Confirm/index.less new file mode 100644 index 00000000..6532fd3f --- /dev/null +++ b/src/components/Confirm/index.less @@ -0,0 +1,25 @@ +@import "~styles/variables"; + +.confirm-modal { + :global { + .ant-modal-confirm-body > .anticon { + font-size: 18px; + margin-right: 20px; + } + } +} +.confirm { + color: @warn-color !important; +} + +.error { + color: @error-color !important; +} + +.success { + color: @success-color !important; +} + +.warn { + color: @warn-color !important; +} \ No newline at end of file diff --git a/src/components/DetailCard/index.jsx b/src/components/DetailCard/index.jsx new file mode 100644 index 00000000..7f5f982a --- /dev/null +++ b/src/components/DetailCard/index.jsx @@ -0,0 +1,157 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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, Skeleton, Tooltip, Typography, Popover } from 'antd'; +import { InfoCircleOutlined } from '@ant-design/icons'; +import { has, get, isNumber } from 'lodash'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import { renderFilterMap } from 'utils/index'; +import Status from 'components/Status'; +import styles from './index.less'; + +const { Paragraph } = Typography; + +const getContentValue = (value, dataIndex, data, copyable) => { + const status = get(data, dataIndex); + // get status + if ( + dataIndex.toLowerCase().indexOf('status') >= 0 || + dataIndex.toLowerCase().indexOf('state') >= 0 + ) { + return ; + } + // get copyable + if (value !== '-') { + if ( + (/_?id/g.test(dataIndex.toLowerCase()) && copyable !== false) || + copyable + ) { + return {value}; + } + } + return value || '-'; +}; + +const getContent = (data, option) => { + const { content, dataIndex, render, valueRender, copyable } = option; + if (has(option, 'content')) { + return copyable ? {content} : content; + } + let value = get(data, dataIndex); + if (!render) { + if (valueRender) { + const renderFunc = renderFilterMap[valueRender]; + value = renderFunc && renderFunc(value); + } + } else { + value = render(value, data); + } + if (!isNumber(value)) { + value = value || '-'; + } + return getContentValue(value, dataIndex, data, copyable); +}; + +const renderLabel = (option) => { + const { label, tooltip = '' } = option; + if (!tooltip) { + return label; + } + return ( + + {label} + + ); +}; + +const renderOptions = (options, data, loading, labelCol, contentCol) => + options.map((option, index) => ( + + + {renderLabel(option)} + {getContent(data, option)} + + + )); + +const DetailCard = ({ + title, + titleHelp, + loading, + options, + data, + labelCol, + contentCol, + className, + button, +}) => { + let titleHelpValue; + if (titleHelp) { + titleHelpValue = ( + + + + ); + } + return ( +
+
+ + +

{title}

+ {titleHelpValue} + {button} +
+
+ {renderOptions(options, data, loading, labelCol, contentCol)} +
+
+ ); +}; + +const detailProps = PropTypes.shape({ + label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + content: PropTypes.any, + tooltip: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + dataIndex: PropTypes.string, + valueRender: PropTypes.string, +}); + +DetailCard.defaultProps = { + labelCol: 8, + contentCol: 16, + options: [], + title: '', + titleHelp: '', + loading: false, + data: {}, +}; + +DetailCard.propTypes = { + title: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + titleHelp: PropTypes.any, + options: PropTypes.arrayOf(detailProps), + loading: PropTypes.bool, + data: PropTypes.object, + labelCol: PropTypes.number, + contentCol: PropTypes.number, +}; + +export default DetailCard; diff --git a/src/components/DetailCard/index.less b/src/components/DetailCard/index.less new file mode 100644 index 00000000..475201cc --- /dev/null +++ b/src/components/DetailCard/index.less @@ -0,0 +1,54 @@ +@import '~styles/variables'; + +@min-space: 8px; +@mid-space: 16px; +@lg-space: 24px; +@box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.05); +@topo-line: 1px solid #D2D2D2; +@resource-box-bg: #fbfdff; +@resource-box-border: 1px solid #cfe1ff; + +@success: #57E39B; +@warning: #FEDF40; +@error: #EB354D; +@link: #0068FF; + +.card { + margin-bottom: @mid-space; + box-shadow: @box-shadow; + background-color: #fff; + border-radius: @border-radius; + flex: 1; + + .card-content { + padding: @mid-space; + + .card-item { + margin-bottom: @min-space; + + h3 { + margin-bottom: 0; + } + + .title-help { + line-height: 26px; + margin-left: @min-space; + } + div { + word-break: break-all; + } + } + + :last-child { + margin-bottom: 0; + } + :global { + .ant-typography { + word-break: break-all; + } + } + } +} + + + diff --git a/src/components/Empty/index.jsx b/src/components/Empty/index.jsx new file mode 100644 index 00000000..abe4ca99 --- /dev/null +++ b/src/components/Empty/index.jsx @@ -0,0 +1,43 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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 PropTypes from 'prop-types'; +import classnames from 'classnames'; + +import styles from './index.less'; + +export default class Empty extends React.PureComponent { + static propTypes = { + className: PropTypes.string, + img: PropTypes.string, + desc: PropTypes.string, + }; + + static defaultProps = { + img: '/asset/image/empty-card.svg', + desc: 'No relevant data', + }; + + render() { + const { className, img, desc } = this.props; + + return ( +
+ No data + {desc &&
{t(desc)}
} +
+ ); + } +} diff --git a/src/components/Empty/index.less b/src/components/Empty/index.less new file mode 100644 index 00000000..55066f7a --- /dev/null +++ b/src/components/Empty/index.less @@ -0,0 +1,15 @@ +@import '~styles/variables'; + +.wrapper { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + padding: 40px 0; +} + +.content { + margin-top: 30px; +} diff --git a/src/components/Form/index.jsx b/src/components/Form/index.jsx new file mode 100644 index 00000000..182cb09f --- /dev/null +++ b/src/components/Form/index.jsx @@ -0,0 +1,511 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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 { isFunction, has, isObject, isEmpty } from 'lodash'; +import Notify from 'components/Notify'; +import { Row, Col, Form, Button, Spin } from 'antd'; +import classnames from 'classnames'; +import { InfoCircleOutlined } from '@ant-design/icons'; +import { isAdminPage, firstUpperCase, unescapeHtml } from 'utils/index'; + +import { parse } from 'qs'; +import FormItem from 'components/FormItem'; +import styles from './index.less'; + +export default class BaseForm extends React.Component { + constructor(props, options = {}) { + super(props); + + this.options = options; + + this.state = { + // eslint-disable-next-line react/no-unused-state + defaultValue: {}, + // eslint-disable-next-line react/no-unused-state + formData: {}, + isSubmitting: false, + }; + + this.values = {}; + this.response = null; + this.responseError = null; + this.formRef = React.createRef(); + this.codeError = false; + this.currentFormValue = {}; + this.init(); + } + + componentDidMount() { + try { + // this.updateDefaultValue(); + this.updateState(); + } catch (e) { + // eslint-disable-next-line no-console + console.log(e); + } + } + + componentWillUnmount() { + this.unsubscribe && this.unsubscribe(); + this.disposer && this.disposer(); + this.unMountActions && this.unMountActions(); + } + + get canSubmit() { + return true; + } + + get name() { + return ''; + } + + get title() { + return ''; + } + + get className() { + return ''; + } + + get prefix() { + return this.props.match.url; + } + + get routing() { + return this.props.rootStore.routing; + } + + get params() { + return this.props.match.params || {}; + } + + get location() { + return this.props.location || {}; + } + + get locationParams() { + return parse(this.location.search.slice(1)); + } + + get listUrl() { + return '/base/tmp'; + } + + get isAdminPage() { + const { pathname = '' } = this.props.location || {}; + return isAdminPage(pathname); + } + + get hasAdminRole() { + return this.props.rootStore.hasAdminRole; + } + + get currentProjectId() { + return globals.user.project.id; + } + + get currentProjectName() { + return globals.user.project.name; + } + + getUrl(path, adminStr) { + return this.isAdminPage ? `${path}${adminStr || '-admin'}` : path; + } + + get isStep() { + return false; + } + + get isModal() { + return false; + } + + get labelCol() { + return { + xs: { span: 5 }, + sm: { span: 3 }, + }; + } + + get wrapperCol() { + return { + xs: { span: 10 }, + sm: { span: 8 }, + }; + } + + get defaultValue() { + return null; + } + + get formDefaultValue() { + const { context = {} } = this.props; + const { defaultValue } = this; + return { + ...defaultValue, + ...context, + }; + } + + get okBtnText() { + return t('Confirm'); + } + + get instanceName() { + const { name } = this.values || {}; + return name; + } + + get successText() { + return firstUpperCase( + t('{action} successfully, instance: {name}.', { + action: this.name.toLowerCase(), + name: this.instanceName, + }) + ); + } + + get errorText() { + return t('Unable to {action}, instance: {name}.', { + action: this.name.toLowerCase(), + name: this.instanceName, + }); + } + + get isSubmitting() { + const { isSubmitting = false } = this.state; + return isSubmitting; + // return (this.store && this.store.isSubmitting) || false; + } + + get formItems() { + return []; + } + + get validateMessages() { + return []; + } + + get tips() { + return ''; + } + + get showNotice() { + return true; + } + + get nameForStateUpdate() { + const typeList = ['radio', 'more']; + return this.formItems + .filter((it) => typeList.indexOf(it.type) >= 0) + .map((it) => it.name); + } + + updateContext = (allFields) => { + const { updateContext } = this.props; + updateContext && updateContext(allFields); + }; + + unescape = (message) => unescapeHtml(message); + + getFormInstance = () => this.formRef.current; + + // eslint-disable-next-line no-unused-vars + onSubmit = (values) => Promise.resolve(); + + updateSumbitting = (value) => { + this.setState({ + isSubmitting: value || false, + }); + }; + + onOk = (values, containerProps, callback) => { + // eslint-disable-next-line no-console + console.log('onOk', values); + this.values = values; + if (this.codeError) { + return; + } + this.updateSumbitting(true); + if (!this.onSubmit) { + return callback(true, false); + } + return this.onSubmit(values, containerProps).then( + (response) => { + this.updateSumbitting(false); + !this.isModal && this.routing.push(this.listUrl); + this.response = response; + this.showNotice && Notify.success(this.successText); + if (callback && isFunction(callback)) { + callback(true, false); + } + }, + (err) => { + this.updateSumbitting(false); + this.responseError = err; + this.showNotice && Notify.errorWithDetail(this.errorText, err); + // eslint-disable-next-line no-console + console.log(err); + if (callback && isFunction(callback)) { + callback(false, true); + } + } + ); + }; + + onCancel = () => {}; + + getChangedFieldsValue = (changedFields, name) => { + const value = changedFields[name]; + if (isObject(value) && value.value) { + return value.value; + } + if (isObject(value) && value.selectedRows) { + return value.selectedRows[0]; + } + return value; + }; + + // eslint-disable-next-line no-unused-vars + onValuesChange = (changedFields, allFields) => {}; + + // eslint-disable-next-line no-unused-vars + onValuesChangeForm = (changedFields, allFields) => { + // save linkage data to state + const newState = {}; + this.currentFormValue = allFields; + this.nameForStateUpdate.forEach((name) => { + if (has(changedFields, name)) { + const value = this.getChangedFieldsValue(changedFields, name); + newState[name] = value; + } + }); + if (!isEmpty(newState)) { + this.setState({ + ...newState, + }); + } + this.onValuesChange(changedFields, allFields); + }; + + checkFormInput = (callback, failCallback) => { + this.formRef.current && + this.formRef.current.validateFields().then( + (values) => { + callback && callback(values); + this.updateContext(values); + }, + ({ values, errorFields }) => { + if (errorFields && errorFields.length) { + failCallback && failCallback(values, errorFields); + } else { + // eslint-disable-next-line no-console + console.log('checkFormInput-catch', values, errorFields); + // callback && callback(values); + } + } + ); + }; + + onClickSubmit = (callback, checkCallback, containerProps) => { + if (this.codeError) { + return; + } + this.checkFormInput((values) => { + checkCallback && checkCallback(values); + this.onOk(values, containerProps, callback); + }); + }; + + onClickCancel = () => { + this.routing.push(this.listUrl); + }; + + updateDefaultValue = () => { + if (this.formRef.current && this.formRef.current.resetFields) { + this.formRef.current.resetFields(); + } + this.updateContext(this.defaultValue); + }; + + updateFormValue = (key, value) => { + this.formRef.current && + this.formRef.current.setFieldsValue({ + [key]: value, + }); + }; + + updateState() { + // save linkage data to state + const { context } = this.props; + const names = this.nameForStateUpdate; + if (names.length === 0) { + return; + } + const newState = {}; + if (this.checkContextValue()) { + names.forEach((name) => { + newState[name] = this.getChangedFieldsValue(context, name); + }); + } else { + names.forEach((name) => { + newState[name] = this.getChangedFieldsValue(this.defaultValue, name); + }); + } + this.setState({ + ...newState, + }); + } + + checkContextValue() { + const { context } = this.props; + const names = this.nameForStateUpdate; + if (isEmpty(context)) { + return false; + } + const item = names.find((name) => has(context, name)); + return !!item; + } + + init() { + this.store = {}; + } + + renderTips() { + if (this.tips) { + return ( +
+ + {this.tips} +
+ ); + } + return null; + } + + renderFooterLeft() { + return null; + } + + renderFooter() { + if (this.isStep || this.isModal) { + return null; + } + return ( +
+
{this.renderFooterLeft()}
+
+ + +
+
+ ); + } + + renderFormItems() { + try { + return this.formItems.map((it, index) => { + const { name } = it; + this.codeError = false; + return ( + + + + ); + }); + } catch (e) { + // eslint-disable-next-line no-console + console.log(e); + const name = 'error'; + const index = 0; + const it = { + type: 'label', + label: t('Error'), + // if can't submit, go this way to not submit. + // example src/pages/network/containers/VPN/IKEPolicy/actions/Edit.js L60-71 + content: + e.message === 'Can Not Submit' + ? this.errorText + : t('Unable to render form'), + }; + this.codeError = true; + return ( + + + + ); + // return null; + } + } + + renderForms() { + return ( +
+ {this.renderFormItems()} +
+ ); + } + + render() { + return ( +
+ + {this.renderTips()} +
+ {this.renderForms()} +
+ {this.renderFooter()} +
+
+ ); + } +} diff --git a/src/components/Form/index.less b/src/components/Form/index.less new file mode 100644 index 00000000..dfb623eb --- /dev/null +++ b/src/components/Form/index.less @@ -0,0 +1,104 @@ +@import "~styles/variables"; + +.wrapper { + position: relative; + height: 100%; + overflow: hidden; + background-color: #fff; + padding-top: @body-padding; + + :global { + .ant-spin-nested-loading { + height: 100%; + } + + .ant-spin-container { + height: 100%; + } + } +} + +.form { + margin-left: @body-padding * 2; + margin-right: @body-padding * 2; + // padding-top: @body-padding; + background-color: #fff; + height: calc(100% - 48px); + overflow-y: auto; + + :global { + .ant-form-item-label>label { + margin-left: 12px; + } + + .ant-form-item-label>.ant-form-item-required { + margin-left: 0; + } + + .ant-form-item-label { + white-space: break-spaces; + } + } +} + +.footer { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 48px; + background-color: #fff; + box-shadow: 0px 2px 30px 0px rgba(0, 0, 0, 0.09); +} + +.btns { + float: right; + margin-right: 32px; + margin-top: 8px; + + :global { + button { + margin-left: 8px; + } + } +} + +:global { + .form-item-text { + margin-bottom: 0; + } + + .form-item-divider { + width: 98% !important; + min-width: 90% !important; + margin-left: auto; + margin-right: auto; + } + + .ant-radio-button-wrapper { + color: @color-text-body; + } + .ant-radio-button-wrapper-disabled { + color: rgba(0, 0, 0, 0.25); + } +} + +.tips { + padding: 8px 16px; + background: rgba(89, 157, 255, 0.15); + margin-top: -16px; + margin-bottom: 16px; +} + +.tips-icon { + margin-right: 4px; + color: #0068FF; +} + +.footer-left { + height: 48px; + float: left; + margin-left: 16px; + padding-top: 8px; + color: rgb(72, 72, 72); +} \ No newline at end of file diff --git a/src/components/FormItem/AddSelect/index.jsx b/src/components/FormItem/AddSelect/index.jsx new file mode 100644 index 00000000..87bd083c --- /dev/null +++ b/src/components/FormItem/AddSelect/index.jsx @@ -0,0 +1,272 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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, { Component } from 'react'; +import { Select, Button, Input } from 'antd'; +import { PlusCircleFilled, MinusCircleFilled } from '@ant-design/icons'; +import { isArray, isEqual, isEmpty } from 'lodash'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import { generateId } from 'utils/index'; +import styles from './index.less'; + +export default class index extends Component { + static propTypes = { + minCount: PropTypes.number, + maxCount: PropTypes.number, + tips: PropTypes.node, + options: PropTypes.array, + placeholder: PropTypes.string, + defaultItemValue: PropTypes.any, + addText: PropTypes.string, + addTextTips: PropTypes.string, + width: PropTypes.number, + itemComponent: PropTypes.any, + optionsByIndex: PropTypes.bool, // special: index=0, use [options[0]]; index=1 use [options[1]]; index >= options.length, options + initValue: PropTypes.array, + readonlyKeys: PropTypes.array, + }; + + static defaultProps = { + minCount: 0, + maxCount: Infinity, + addText: t('Add'), + placeholder: t('Please select'), + width: 200, + itemComponent: null, + optionsByIndex: false, + initValue: [], + readonlyKeys: [], + }; + + constructor(props) { + super(props); + const { initValue = [] } = props; + this.state = { + items: this.getInitItems(props), + initValue, + keyId: generateId(), + }; + } + + getInitItems = (props) => { + const { value, initValue } = props; + if (!isEmpty(initValue)) { + return isArray(initValue) ? [...initValue] || [] : []; + } + return isArray(value) ? [...value] || [] : []; + }; + + static getDerivedStateFromProps(nextProps, prevState) { + if (!isEqual(nextProps.initValue, prevState.initValue)) { + return { + initValue: nextProps.initValue, + items: JSON.parse(JSON.stringify(nextProps.initValue)), + keyId: generateId(), + }; + } + return null; + } + + addItem = () => { + const { items } = this.state; + const { maxCount } = this.props; + if (items.length >= maxCount) { + return; + } + const { defaultItemValue } = this.props; + const newItem = { + value: defaultItemValue, + index: items.length, + }; + this.updateItems([...items, newItem]); + }; + + updateItems = (newIems) => { + this.setState( + { + items: newIems, + }, + () => { + const { onChange } = this.props; + if (onChange) { + onChange(newIems); + } + } + ); + }; + + // eslint-disable-next-line no-shadow + canRemove = (index) => { + const { minCount } = this.props; + return index >= minCount; + }; + + // eslint-disable-next-line no-shadow + removeItem = (index) => { + const { items } = this.state; + items.splice(index, 1); + this.updateItems(items); + }; + + // eslint-disable-next-line no-shadow + onItemChange = (newVal, index) => { + const { items } = this.state; + items[index] = { + value: newVal, + index, + }; + this.updateItems(items); + }; + + // eslint-disable-next-line no-shadow + onItemChangeInput = (newVal, index) => { + const { items } = this.state; + items[index] = { + value: newVal, + index, + }; + this.updateItems(items); + }; + + getOptions = (itemIndex) => { + // special: index=0, use [options[0]]; index=1 use [options[1]]; index >= options.length, options + const { optionsByIndex, options } = this.props; + if (!optionsByIndex) { + return options; + } + if (itemIndex < options.length) { + return [options[itemIndex]]; + } + return options; + }; + + renderTip() { + const { tips } = this.props; + if (tips) { + return
{tips}
; + } + return null; + } + + // eslint-disable-next-line no-shadow + renderItem = (item, index) => { + const { itemComponent, readonlyKeys = [], isInput = false } = this.props; + const { placeholder, width } = this.props; + if (!itemComponent) { + if (isInput) { + return ( + { + this.onItemChange(e.currentTarget.value, index); + }} + /> + ); + } + return ( + + + + {type === 'manual' && } + + + ); +}; diff --git a/src/components/FormItem/IPDistributer/Item.jsx b/src/components/FormItem/IPDistributer/Item.jsx new file mode 100644 index 00000000..88c2337e --- /dev/null +++ b/src/components/FormItem/IPDistributer/Item.jsx @@ -0,0 +1,83 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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 { Col, Row, Select } from 'antd'; +import IPAddress from 'components/FormItem/IPDistributer/IPAddress'; + +const Item = ({ subnetsAvailable, onChange, value }) => { + value = value || { + subnet: undefined, + ip_address: { type: 'dhcp', ip: undefined }, + }; + + const [subnet, setSubnet] = useState(value.subnet); + const [ip_address, setIP] = useState({ type: 'dhcp' }); + const [visible, setVisible] = useState(!!value.subnet || false); + const subnetItem = subnetsAvailable.find((i) => i.id === subnet); + + const triggerChange = (changedValue) => { + const item = { + ...value, + subnet, + ip_address, + ...changedValue, + }; + onChange && onChange(item); + }; + + const handleSelectChange = (e, option) => { + setSubnet(option.value); + setVisible(true); + triggerChange({ + subnet: option.value, + }); + }; + + const handleIPChange = (v) => { + setIP(v); + triggerChange({ + ip_address: v, + }); + }; + + return ( + + + + ); + const input = ( + + ); + const deleteValue = deleteType === 1; + const checkbox = ( + + {t('Deleted with the instance')} + + ); + + return ( + + + + {t('Type')} + {selects} + + + {t('Size')} + {input} + GB + {checkbox} + + + + ); + } +} diff --git a/src/components/FormItem/InstanceVolume/index.less b/src/components/FormItem/InstanceVolume/index.less new file mode 100644 index 00000000..c4aa3d27 --- /dev/null +++ b/src/components/FormItem/InstanceVolume/index.less @@ -0,0 +1,15 @@ +.instance-volume { + display: block; + margin-bottom: 8px; +} +.label { + margin-right: 10px; + max-width: 20%; +} +.select { + max-width: 80%; +} +.size-label { + margin-left: 10px; + margin-right: 40px; +} diff --git a/src/components/FormItem/IpInput/index.jsx b/src/components/FormItem/IpInput/index.jsx new file mode 100644 index 00000000..97f35ede --- /dev/null +++ b/src/components/FormItem/IpInput/index.jsx @@ -0,0 +1,163 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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, { Component } from 'react'; +import { Input } from 'antd'; +import PropTypes from 'prop-types'; +import { ipValidate } from 'utils/validate'; +import { sortBy } from 'lodash'; +import styles from './index.less'; + +const { isIPv4 } = ipValidate; + +export default class index extends Component { + static propTypes = { + value: PropTypes.string, + version: PropTypes.number, + onChange: PropTypes.func, + defaultValue: PropTypes.string, + }; + + static defaultProps = { + version: 4, + defaultValue: '', + onChange() {}, + }; + + constructor(props) { + super(props); + const { value, version, defaultValue } = props; + this.state = { + value: this.getIpValues(defaultValue || value), + version, + }; + } + + componentDidUpdate() { + const { disableNotice = false } = this.props; + if (disableNotice) { + return; + } + this.triggerChange(); + } + + static getDerivedStateFromProps(nextProps, prevState) { + const { version } = nextProps; + // fix for re-render when changing ip-version + if (version !== prevState.version) { + return { + version, + value: + version === 4 + ? [undefined, undefined, undefined, undefined] + : undefined, + }; + } + return null; + } + + triggerChange = () => { + const { onChange, version } = this.props; + const { value } = this.state; + let ret; + if (version === 4) { + sortBy(value, (n) => n)[3] !== undefined && (ret = value.join('.')); + } else { + ret = value; + } + onChange && onChange(ret); + }; + + getIpValues = (value) => { + const { version } = this.props; + if (!value && version === 4) { + const ip = []; + for (let i = 0; i < 4; i++) { + ip.push(undefined); + } + return ip; + } + if (isIPv4(value)) { + return value.split('.').map((it) => Number.parseInt(it, 10)); + } + return value; + }; + + // eslint-disable-next-line no-shadow + onInputChange = (newVal, index) => { + const { value } = this.state; + let ipValue = Number.parseInt(newVal, 10); + if (Number.isNaN(ipValue)) { + ipValue = undefined; + } + if (ipValue < 0) { + ipValue = 0; + } + if (ipValue > 255) { + ipValue = 255; + } + value[index] = ipValue; + this.setState( + { + value, + }, + () => { + this.triggerChange(); + } + ); + }; + + onInputChangeIPv6 = (value) => { + this.setState( + { + value, + }, + () => { + this.triggerChange(); + } + ); + }; + + render() { + const { value } = this.state; + const { version } = this.props; + if (version === 6) { + return ( +
+ { + this.onInputChangeIPv6(e.currentTarget.value); + }} + /> +
+ ); + } + // eslint-disable-next-line no-shadow + const inputs = value.map((it, index) => ( +
+ { + this.onInputChange(e.currentTarget.value, index); + }} + /> +
+ )); + return
{inputs}
; + } +} diff --git a/src/components/FormItem/IpInput/index.less b/src/components/FormItem/IpInput/index.less new file mode 100644 index 00000000..b9475225 --- /dev/null +++ b/src/components/FormItem/IpInput/index.less @@ -0,0 +1,45 @@ +.ip-input { + width: 172px; + overflow: hidden; + border-radius: 4px; +} +.item-wrapper { + width: 44px; + position: relative; + display: inline-block; +} +.item-wrapper:last-child { + width: 40px; +} +.item-wrapper:after { + content: " "; + width: 4px; + height: 4px; + border-radius: 50%; + background-color: #484848; + position: absolute; + top: 13px; +} +.item-wrapper:last-child:after { + display: none; +} +.item { + width: 40px; + padding-left: 8px; + padding-right: 8px; + border-top: none; + border-left: none; + border-right: none; + border-radius: 0; + text-align: center; +} + +.item:focus { + border-top: none; + border-left: none; + border-right: none; + box-shadow: none; +} +.ipv6 { + width: 120px; +} \ No newline at end of file diff --git a/src/components/FormItem/IpInputSimple/index.jsx b/src/components/FormItem/IpInputSimple/index.jsx new file mode 100644 index 00000000..083c2b2a --- /dev/null +++ b/src/components/FormItem/IpInputSimple/index.jsx @@ -0,0 +1,58 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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, { Component } from 'react'; +import { Input, Form } from 'antd'; +import { ipValidate } from 'utils/validate'; + +const { ipv4Validator, ipv6Validator } = ipValidate; + +export default class index extends Component { + static isFormItem = true; + + getRules(rules, version) { + let newRules = { + validator: version === 4 ? ipv4Validator : ipv6Validator, + }; + if (rules && rules.length > 0) { + newRules = { + ...newRules, + ...rules[0], + }; + } + return [newRules]; + } + + render() { + const { componentProps = {}, formItemProps = {} } = this.props; + const { version = 4, ...componentRest } = componentProps; + const placeholder = + version === 4 ? t('Please input ipv4') : t('Please input ipv6'); + const props = { + placeholder, + ...componentRest, + }; + const { rules, ...rest } = formItemProps; + const newRules = this.getRules(rules, version); + const newFormItemProps = { + ...rest, + rules: newRules, + }; + return ( + + + + ); + } +} diff --git a/src/components/FormItem/IpInputSimple/index.less b/src/components/FormItem/IpInputSimple/index.less new file mode 100644 index 00000000..b9475225 --- /dev/null +++ b/src/components/FormItem/IpInputSimple/index.less @@ -0,0 +1,45 @@ +.ip-input { + width: 172px; + overflow: hidden; + border-radius: 4px; +} +.item-wrapper { + width: 44px; + position: relative; + display: inline-block; +} +.item-wrapper:last-child { + width: 40px; +} +.item-wrapper:after { + content: " "; + width: 4px; + height: 4px; + border-radius: 50%; + background-color: #484848; + position: absolute; + top: 13px; +} +.item-wrapper:last-child:after { + display: none; +} +.item { + width: 40px; + padding-left: 8px; + padding-right: 8px; + border-top: none; + border-left: none; + border-right: none; + border-radius: 0; + text-align: center; +} + +.item:focus { + border-top: none; + border-left: none; + border-right: none; + box-shadow: none; +} +.ipv6 { + width: 120px; +} \ No newline at end of file diff --git a/src/components/FormItem/JsonInput/index.jsx b/src/components/FormItem/JsonInput/index.jsx new file mode 100644 index 00000000..7f9f1f3b --- /dev/null +++ b/src/components/FormItem/JsonInput/index.jsx @@ -0,0 +1,63 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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, { Component } from 'react'; +import { Form } from 'antd'; +import { jsonValidator } from 'utils/validate'; +import AceEditor from 'react-ace'; + +export default class JsonInput extends Component { + static isFormItem = true; + + getRules(rules) { + let newRules = { + validator: jsonValidator, + }; + if (rules && rules.length > 0) { + newRules = { + ...newRules, + ...rules[0], + }; + } + return [newRules]; + } + + render() { + const { componentProps, formItemProps } = this.props; + const { rules, ...rest } = formItemProps; + const newRules = this.getRules(rules); + const newFormItemProps = { + ...rest, + rules: newRules, + }; + const options = { + ...componentProps, + mode: 'json', + wrapEnabled: true, + tabSize: 2, + width: '100%', + height: '200px', + setOptions: { + enableBasicAutocompletion: true, + enableLiveAutocompletion: true, + enableSnippets: true, + }, + }; + return ( + + + + ); + } +} diff --git a/src/components/FormItem/KeyValueInput/index.jsx b/src/components/FormItem/KeyValueInput/index.jsx new file mode 100644 index 00000000..dde6f706 --- /dev/null +++ b/src/components/FormItem/KeyValueInput/index.jsx @@ -0,0 +1,108 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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, { Component } from 'react'; +import { Input, Row, Col } from 'antd'; +import PropTypes from 'prop-types'; +import { PauseOutlined } from '@ant-design/icons'; + +export default class index extends Component { + static propTypes = { + onChange: PropTypes.func, + // eslint-disable-next-line react/no-unused-prop-types + value: PropTypes.object, + keyReadonly: PropTypes.bool, + valueReadonly: PropTypes.bool, + }; + + static defaultProps = { + onChange: null, + value: { + key: '', + value: '', + }, + keyReadonly: false, + valueReadonly: false, + }; + + constructor(props) { + super(props); + this.state = { + key: '', + value: '', + }; + } + + static getDerivedStateFromProps(nextProps, prevState) { + const { key, value } = nextProps.value || {}; + if (key !== prevState.key || value !== prevState.value) { + return { + key, + value, + }; + } + return null; + } + + onChange = (value) => { + const { onChange } = this.props; + if (onChange) { + onChange(value); + } + }; + + onKeyChange = (e) => { + this.onChange({ + ...this.state, + key: e.target.value, + }); + }; + + onValueChange = (e) => { + this.onChange({ + ...this.state, + value: e.target.value, + }); + }; + + render() { + const { key, value } = this.state; + const { keyReadonly, valueReadonly } = this.props; + return ( + + + + + + + + + + + + ); + } +} diff --git a/src/components/FormItem/Label/index.jsx b/src/components/FormItem/Label/index.jsx new file mode 100644 index 00000000..d02aaf08 --- /dev/null +++ b/src/components/FormItem/Label/index.jsx @@ -0,0 +1,96 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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, { Component } from 'react'; +import PropTypes from 'prop-types'; +import imageSvg from 'src/asset/image/image.svg'; + +import { + DesktopOutlined, + BorderOuterOutlined, + SecurityScanOutlined, + InboxOutlined, + GlobalOutlined, + GatewayOutlined, + UserOutlined, + CameraOutlined, + SaveOutlined, + KeyOutlined, + ClusterOutlined, + TagOutlined, + HddOutlined, + CloudServerOutlined, +} from '@ant-design/icons'; +import styles from './index.less'; + +const ImageIcon = ( + image_icon +); + +const iconTypeMap = { + instance: , + router: , + externalNetwork: , + network: , + firewall: , + volume: , + gateway: , + user: , + snapshot: , + backup: , + keypair: , + image: ImageIcon, + aggregate: , + metadata: , + flavor: , + host: , +}; + +export default class index extends Component { + static propTypes = { + content: PropTypes.any, + value: PropTypes.any, + icon: PropTypes.node, + iconType: PropTypes.string, + }; + + static defaultProps = { + icon: null, + iconType: '', + content: '', + value: null, + }; + + renderIcon() { + const { icon, iconType } = this.props; + if (iconType) { + const iconComp = iconTypeMap[iconType] || null; + return {iconComp}; + } + return {icon || null}; + } + + render() { + const { content, value, iconType, ...rest } = this.props; + if (content) { + return content; + } + return ( + + {this.renderIcon()} + {value} + + ); + } +} diff --git a/src/components/FormItem/Label/index.less b/src/components/FormItem/Label/index.less new file mode 100644 index 00000000..5060c5b8 --- /dev/null +++ b/src/components/FormItem/Label/index.less @@ -0,0 +1,3 @@ +.icon { + margin-right: 8px; +} \ No newline at end of file diff --git a/src/components/FormItem/MacAddressInput/index.jsx b/src/components/FormItem/MacAddressInput/index.jsx new file mode 100644 index 00000000..5945b36d --- /dev/null +++ b/src/components/FormItem/MacAddressInput/index.jsx @@ -0,0 +1,116 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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, { Component } from 'react'; +import { Col, Form, Input, Row, Select } from 'antd'; +import { macAddressValidate } from 'utils/validate'; + +class MacAddressInput extends Component { + constructor(props) { + super(props); + this.state = { + name: { + type: '', + mac: '', + }, + }; + } + + handleSelectChange = (e) => { + const { name } = this.state; + this.setState( + { + name: { + ...name, + type: e, + }, + }, + () => { + const { onChange } = this.props; + if (onChange) { + onChange(this.state.name); + } + } + ); + }; + + handleInputChange = (e) => { + const { name } = this.state; + this.setState({ + name: { + ...name, + mac: e, + }, + }); + }; + + render() { + const { value, name, options } = this.props; + const { type } = value || { type: undefined }; + + return ( + + + + + + ) : null + // + // {t('Mac Address Numbers can be use {num}' + // , { num: 123 })} + // + } + + + ); + } +} + +export default MacAddressInput; diff --git a/src/components/FormItem/MemberAllocator/IPAddress.jsx b/src/components/FormItem/MemberAllocator/IPAddress.jsx new file mode 100644 index 00000000..aa552eab --- /dev/null +++ b/src/components/FormItem/MemberAllocator/IPAddress.jsx @@ -0,0 +1,86 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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 { InputNumber, Row, Col, Input } from 'antd'; + +export default ({ value = {}, onChange, disabled }) => { + const [ip, setIp] = useState(value.ip || undefined); + const [protocol_port, setPort] = useState(value.protocol_port || undefined); + const [weight, setWeight] = useState(value.weight); + + const triggerChange = (changedValue) => { + onChange({ + ip, + protocol_port, + weight, + ...value, + ...changedValue, + }); + }; + + const onIpChange = ({ target: { value: newIP } }) => { + setIp(newIP); + triggerChange({ + ip: newIP, + }); + }; + + const onPortChange = (val) => { + val && setPort(val); + val && + triggerChange({ + protocol_port: val, + }); + }; + + const onWeightChange = (val) => { + val && setWeight(val); + val && + triggerChange({ + weight: val, + }); + }; + + return ( + + + + + + + + + + + + ); +}; diff --git a/src/components/FormItem/MemberAllocator/Item.jsx b/src/components/FormItem/MemberAllocator/Item.jsx new file mode 100644 index 00000000..ed9f4f88 --- /dev/null +++ b/src/components/FormItem/MemberAllocator/Item.jsx @@ -0,0 +1,51 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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 IPAddress from './IPAddress'; + +const Item = ({ onChange, value }) => { + value = value || { + ip_address: { ip: undefined, protocol_port: undefined, weight: 1 }, + canEdit: true, + }; + const [ip_address, setIP] = useState(value.ip_address); + // const [visible, setVisible] = useState(!!value.subnet || false); + + const triggerChange = (changedValue) => { + const item = { + ...value, + ip_address, + ...changedValue, + }; + onChange && onChange(item); + }; + + const handleIPChange = (v) => { + setIP(v); + triggerChange({ + ip_address: v, + }); + }; + + return ( + + ); +}; + +export default Item; diff --git a/src/components/FormItem/MemberAllocator/index.jsx b/src/components/FormItem/MemberAllocator/index.jsx new file mode 100644 index 00000000..5f6026e7 --- /dev/null +++ b/src/components/FormItem/MemberAllocator/index.jsx @@ -0,0 +1,309 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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 { Form, Button, Row, Col, Select } from 'antd'; +import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; +import SelectTable from 'components/FormItem/SelectTable'; +import { ipValidate } from 'utils/validate'; +import { isAdminPage } from 'utils/index'; +import { Link } from 'react-router-dom'; +import { Address4, Address6 } from 'ip-address'; +import Item from './Item'; + +const { isIPv4, isIpv6 } = ipValidate; + +const MemberAllocator = ({ componentProps, formItemProps }) => { + const { maxNumber = 10, ports, isLoading, members = [] } = componentProps; + const { name, onChange } = formItemProps; + + const [currentFieldsLength, setLength] = useState(0); + + const triggerChange = (data) => { + onChange && onChange(data); + }; + + function getUrl(path, adminStr) { + const { pathname } = window.location; + return isAdminPage(pathname) ? `${path}${adminStr || '-admin'}` : path; + } + + let addOuter = () => {}; + + return ( +
+ + ( +
+
+ + {record.id} + +
+
{n || '-'}
+
+ ), + }, + { + title: t('Binding Instance'), + dataIndex: 'server_name', + }, + { + title: t('IP'), + dataIndex: 'fixed_ips', + render: (fixed_ips, record) => { + if (fixed_ips.length === 0) { + return '-'; + } + const options = fixed_ips.map((i) => ({ + label: i.ip_address, + value: i.ip_address, + subnet_id: i.subnet_id, + })); + record.currentOption = options[0].value; + return ( + + this.onInputChange(value, record)} + placeholder={t('Please select')} + /> + ); + } + if ( + type === 'integer' || + type === 'number' || + (type === 'string' && enums.length === 0) + ) { + const props = { + defaultValue, + onChange: (value) => this.onInputChange(value, record), + placeholder: t('Please input'), + required: true, + }; + if (minimum !== undefined) { + props.minimum = minimum; + } + if (maximum !== undefined) { + props.maximum = maximum; + } + if (type === 'string') { + return ; + } + if (type === 'integer') { + props.precision = 0; + props.formatter = (value) => `$ ${value}`.replace(/\D/g, ''); + } + return ; + } + // if (enums.length > 0 && operators.length === 1 && operators[0] === '') { + if (enums.length > 0) { + const options = enums.map((it) => ({ + value: it, + label: it, + })); + return ( + +
+ ); + } +} diff --git a/src/components/FormItem/NetworkSelect/index.jsx b/src/components/FormItem/NetworkSelect/index.jsx new file mode 100644 index 00000000..095ec368 --- /dev/null +++ b/src/components/FormItem/NetworkSelect/index.jsx @@ -0,0 +1,369 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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, Form, Tooltip, Input } from 'antd'; +import Select from 'components/FormItem/Select'; +import PropTypes from 'prop-types'; +import { ipValidate } from 'utils/validate'; +import { QuestionCircleOutlined } from '@ant-design/icons'; +import { ipTypeOptions } from 'resources/network'; +// import { getIpInitValue } from 'resources/instance'; +import styles from './index.less'; + +const { isIPv4, isIpv6, isIpInRangeAll } = ipValidate; + +export default class NetworkSelect extends React.Component { + static propTypes = { + // eslint-disable-next-line react/no-unused-prop-types + networks: PropTypes.array, + // eslint-disable-next-line react/no-unused-prop-types + subnets: PropTypes.array, + value: PropTypes.object, + ipType: PropTypes.number, + name: PropTypes.string, + optionsByIndex: PropTypes.bool, + index: PropTypes.number, + }; + + static defaultProps = { + networks: [], + subnets: [], + value: {}, + ipType: 0, + name: 'network', + optionsByIndex: false, + index: 0, + }; + + constructor(props) { + super(props); + const { value } = props; + const { network, subnet, ip, ipType } = value; + this.state = { + network: network || null, + subnet: subnet || null, + ip: ip || '0.0.0.0', + ipType: ipType || 0, + }; + } + + static getDerivedStateFromProps(nextProps, prevState) { + if ( + nextProps.networks !== prevState.networks || + nextProps.subnets !== prevState.subnets + ) { + const { networks, subnets } = nextProps; + return { + networks, + subnets, + }; + } + return null; + } + + componentDidMount() { + this.checkNetwork(); + } + + onChange = () => { + this.checkNetwork(() => { + const { onChange } = this.props; + const { network, subnet, ip, ipType, validateStatus, errorMsg } = + this.state; + const networkOptions = this.getNetworkOptions(); + const subnetOptions = this.getSubnetOptions(); + const networkOption = networkOptions.find((it) => it.value === network); + const subnetOption = subnetOptions.find((it) => it.value === subnet); + const ipTypeOption = ipTypeOptions.find((it) => it.value === ipType); + if (onChange) { + onChange({ + network, + subnet, + ip, + ipType, + networkOption, + subnetOption, + ipTypeOption, + validateStatus, + errorMsg, + }); + } + }); + }; + + onNetworkChange = (value) => { + const { subnets } = this.state; + const subs = subnets.filter((it) => it.network_id === value); + const subnet = subs.length ? subs[0].id : null; + this.setState( + { + network: value, + subnet, + ipType: 0, + // defaultIp: this.getIpInitValue(subnet), + ip: undefined, + }, + this.onChange + ); + }; + + onSubnetChange = (value) => { + this.setState( + { + subnet: value, + // defaultIp: this.getIpInitValue(value), + ip: undefined, + }, + this.onChange + ); + }; + + // getIpInitValue = (subnet) => { + // const { subnets } = this.state; + // const subnetItem = subnets.find(it => it.id === subnet); + // return getIpInitValue(subnetItem); + // } + + onTypeChange = (value) => { + this.setState( + { + ipType: value, + }, + this.onChange + ); + }; + + onIPChange = (e) => { + const { value } = e.currentTarget; + this.setState( + { + ip: value, + }, + this.onChange + ); + }; + + checkNetwork = (callback) => { + const { network, subnets, subnet, ip, ipType } = this.state; + const item = subnets.find((it) => it.id === subnet); + const { allocation_pools: pools } = item || {}; + + if (!network) { + this.setState( + { + errorMsg: t('Pleasse select a network!'), + validateStatus: 'error', + }, + callback + ); + return; + } + // if (!subnet) { + // this.setState({ + // errorMsg: t('Pleasse select a subnet!'), + // validateStatus: 'error', + // }, callback); + // return; + // } + if (ipType === 1 && !isIPv4(ip) && !isIpv6(ip)) { + this.setState( + { + errorMsg: t('Pleasse input a valid ip!'), + validateStatus: 'error', + }, + callback + ); + return; + } + if (pools && ipType === 1) { + const okPool = pools.find((pool) => + isIpInRangeAll(ip, pool.start, pool.end) + ); + if (!okPool) { + this.setState( + { + errorMsg: t('The ip is not within the allocated pool!'), + validateStatus: 'error', + }, + callback + ); + return; + } + } + this.setState( + { + errorMsg: undefined, + validateStatus: 'success', + }, + callback + ); + }; + + getNetworkOptions = () => { + const { networks } = this.state; + const { optionsByIndex, index } = this.props; + let datas = [...networks]; + if (optionsByIndex && index < networks.length) { + datas = [networks[index]]; + } + return datas.map((it) => ({ + label: it.name, + value: it.id, + })); + }; + + getSubnetOptions = () => { + const { network, subnets } = this.state; + if (!network) { + return []; + } + return subnets + .filter((it) => it.network_id === network) + .map((it) => ({ + label: it.name, + value: it.id, + })); + }; + + renderNetwork() { + const { network } = this.state; + return ( + + +
{tips}
+ + ); + } + + renderIpType() { + const { network, ipType } = this.state; + if (!network) { + return null; + } + return ( + + + + + + + ); + } + + render() { + const { validateStatus, errorMsg } = this.state; + const { name } = this.props; + return ( + + + {this.renderNetwork()} + {this.renderIpType()} + {this.renderSubnet()} + {this.renderIp()} + + + ); + } +} diff --git a/src/components/FormItem/NetworkSelect/index.less b/src/components/FormItem/NetworkSelect/index.less new file mode 100644 index 00000000..e4b67e18 --- /dev/null +++ b/src/components/FormItem/NetworkSelect/index.less @@ -0,0 +1,36 @@ +@import "~styles/variables"; +.network-select { + display: block; + height: 61.6px; + margin-bottom: 0 !important; + position: relative; + :global { + .ant-form-item-control-input-content { + height: 61.6px; + } + .ant-form-item-explain { + position: absolute; + bottom: 0; + } + } +} +.label { + margin-right: 10px; +} +.select { + margin-right: 40px; +} +.size-label { + margin-left: 10px; + margin-right: 40px; +} +.tips { + margin-top: 0; +} +.label { + color: @color-text-caption; + line-height: 30px; +} +.content { + color: @color-text-body; +} \ No newline at end of file diff --git a/src/components/FormItem/NetworkSelectTable/index.jsx b/src/components/FormItem/NetworkSelectTable/index.jsx new file mode 100644 index 00000000..9ac5d90f --- /dev/null +++ b/src/components/FormItem/NetworkSelectTable/index.jsx @@ -0,0 +1,190 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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, { Component } from 'react'; +import { inject, observer } from 'mobx-react'; +import TabSelectTable from 'components/FormItem/TabSelectTable'; +import { NetworkStore } from 'stores/neutron/network'; +import { yesNoOptions } from 'utils/constants'; +import { networkColumns, networkSortProps } from 'resources/network'; +import { isAdminPage } from 'utils/index'; + +@inject('rootStore') +@observer +export default class NetworkSelectTable extends Component { + constructor(props) { + super(props); + this.stores = { + project: new NetworkStore(), + shared: new NetworkStore(), + external: new NetworkStore(), + all: new NetworkStore(), + }; + } + + get location() { + const { location = {} } = (this.props.rootStore || {}).routing || {}; + return location; + } + + get isAdminPage() { + const { pathname } = this.location; + return isAdminPage(pathname); + } + + get currentProjectId() { + return globals.user.project.id; + } + + get hasAdminRole() { + return this.props.rootStore.hasAdminRole; + } + + get showExternal() { + const { showExternal = false } = this.props; + return showExternal; + } + + get networkTabs() { + const tabs = [ + { title: t('Current Project Network'), key: 'project' }, + { title: t('Shared Network'), key: 'shared' }, + ]; + if (this.showExternal) { + tabs.push({ + title: t('External Network'), + key: 'external', + }); + } + if (this.hasAdminRole) { + tabs.push({ + title: t('All Network'), + key: 'all', + }); + } + tabs.forEach((tab) => { + tab.props = this.getSelectTableProps(tab); + }); + return tabs; + } + + getSelectTableProps = (tab) => ({ + columns: this.getColumns(tab), + filterParams: this.getNetworkFilters(tab), + extraParams: this.getNetworkExtraParams(tab), + backendPageStore: this.getStore(tab), + disabledFunc: this.getDisabledFunc(), + isMulti: this.props.isMulti || false, + ...networkSortProps, + }); + + getUrl(path, adminStr) { + return this.isAdminPage ? `${path}${adminStr || '-admin'}` : path; + } + + getColumns = (tab) => { + const columns = networkColumns(this); + columns[0].render = null; + const { key } = tab; + if (key === 'project') { + return columns.filter((it) => it.dataIndex !== 'project_id'); + } + if (['shared', 'router:external'].indexOf(key) >= 0) { + return columns.filter((it) => it.dataIndex !== key); + } + return columns; + }; + + onChange = (value) => { + const { onChange } = this.props; + onChange && onChange(value); + }; + + get labelStyle() { + return { + marginRight: 16, + }; + } + + getStore(tab) { + const { key } = tab; + return this.stores[key]; + } + + getNetworkFilters = (tab) => { + const { key } = tab; + const filters = [ + { + label: t('Name'), + name: 'name', + }, + ]; + if (key !== 'shared') { + filters.push({ + label: t('Shared'), + name: 'shared', + options: yesNoOptions, + }); + } + if (this.showExternal && key !== 'external') { + filters.push({ + label: t('External Network'), + name: 'router:external', + options: yesNoOptions, + }); + } + if (key !== 'project') { + filters.push({ + label: t('Project Range'), + name: 'project_id', + options: [ + { label: t('Current Project'), key: this.currentProjectId }, + { label: t('All'), key: 'all' }, + ], + }); + } + return filters; + }; + + getNetworkExtraParams = (tab) => { + const { key } = tab; + if (key === 'project') { + return { project_id: this.currentProjectId }; + } + if (key === 'shared') { + return { shared: true }; + } + if (key === 'external') { + return { 'router:external': true }; + } + return {}; + }; + + getDisabledFunc() { + return this.props.disabledFunc; + } + + render() { + const { isMulti = false, header, value } = this.props; + return ( + + ); + } +} diff --git a/src/components/FormItem/PortRange/index.jsx b/src/components/FormItem/PortRange/index.jsx new file mode 100644 index 00000000..cb1cd24b --- /dev/null +++ b/src/components/FormItem/PortRange/index.jsx @@ -0,0 +1,49 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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, { Component } from 'react'; +import { Input, Form } from 'antd'; +import { portRangeValidate, portRangeMessage } from 'utils/validate'; + +export default class index extends Component { + static isFormItem = true; + + getRules(rules) { + const newRules = { + validator: portRangeValidate, + }; + return [newRules, ...rules]; + } + + render() { + const { componentProps, formItemProps } = this.props; + const placeholder = t('Please input port range'); + const props = { + placeholder, + ...componentProps, + }; + const { rules, ...rest } = formItemProps; + const newRules = this.getRules(rules); + const newFormItemProps = { + ...rest, + rules: newRules, + extra: portRangeMessage, + }; + return ( + + + + ); + } +} diff --git a/src/components/FormItem/Radio/index.jsx b/src/components/FormItem/Radio/index.jsx new file mode 100644 index 00000000..eccfac92 --- /dev/null +++ b/src/components/FormItem/Radio/index.jsx @@ -0,0 +1,90 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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, { Component } from 'react'; +import { Radio } from 'antd'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import styles from './index.less'; + +export default class index extends Component { + static propTypes = { + options: PropTypes.array, + onChange: PropTypes.func, + optionType: PropTypes.string, + buttonStyle: PropTypes.string, + onlyRadio: PropTypes.bool, + isWrappedValue: PropTypes.bool, + }; + + static defaultProps = { + options: [], + optionType: 'button', + buttonStyle: 'solid', + onlyRadio: false, + isWrappedValue: false, + }; + + onChange = (e) => { + const { value } = e.target; + const { options, onChange, isWrappedValue } = this.props; + if (!isWrappedValue) { + onChange && onChange(value); + } else { + const option = options.find((it) => it.value === value); + onChange && onChange(option); + } + }; + + getValue = (isWrappedValue, value) => { + if (value === undefined) return value; + return isWrappedValue ? value.value : value; + }; + + render() { + const { + options, + optionType, + buttonStyle, + onlyRadio, + className, + value, + isWrappedValue, + ...rest + } = this.props; + const items = options.map((it) => + optionType === 'default' ? ( + + {it.label} + + ) : ( + + {it.label} + + ) + ); + return ( + + {items} + + ); + } +} diff --git a/src/components/FormItem/Radio/index.less b/src/components/FormItem/Radio/index.less new file mode 100644 index 00000000..ab000363 --- /dev/null +++ b/src/components/FormItem/Radio/index.less @@ -0,0 +1,18 @@ +@import "~styles/variables"; + +.only-radio { + :global { + .ant-radio-button-wrapper { + margin-left: 8px; + border-radius: @border-radius; + border-left-width: 1px; + } + .ant-radio-button-wrapper:before { + display: none; + } + .ant-radio-button-wrapper:first-child { + margin-left: 0; + border-radius: @border-radius; + } + } +} \ No newline at end of file diff --git a/src/components/FormItem/Select/index.jsx b/src/components/FormItem/Select/index.jsx new file mode 100644 index 00000000..58804478 --- /dev/null +++ b/src/components/FormItem/Select/index.jsx @@ -0,0 +1,105 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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, { Component } from 'react'; +import { Select, Row, Col, Checkbox } from 'antd'; +import { isUndefined, isNull } from 'lodash'; +import styles from './index.less'; + +export default class index extends Component { + constructor(props) { + super(props); + const { checkOptions } = props; + if (checkOptions) { + this.state = { + selectAll: false, + }; + } + } + + onChange = (value, option) => { + const { onChange, isWrappedValue } = this.props; + onChange && onChange(isWrappedValue ? option : value); + }; + + getValue = () => { + const { value, isWrappedValue } = this.props; + if (value === undefined) return value; + return isWrappedValue ? value.value : value; + }; + + onCheckChange = () => { + const { selectAll } = this.state; + const { options, checkOptions } = this.props; + this.setState( + { + selectAll: 1 - selectAll, + }, + this.onChange( + selectAll === 1 + ? checkOptions[checkOptions.length - 1].value + : options[options.length - 1].value + ) + ); + }; + + render() { + const { + value, + placeholder = t('Please select'), + isWrappedValue, + checkOptions, + checkBoxInfo, + ...rest + } = this.props; + if (isUndefined(value) || isNull(value)) { + return ( + + + + {box} + + + ); + } + return ( + + + + {items} + + ); + // return currentUser && currentUser.name ? menuHeaderDropdown : null; + return ( + +
+ + + {/* style={{ display: 'inline-block', width: '115px' }} */} + {projectName} + + + {userDomainName} +
+
+ ); + } +} diff --git a/src/components/Layout/GlobalHeader/ProjectTable.jsx b/src/components/Layout/GlobalHeader/ProjectTable.jsx new file mode 100644 index 00000000..7a03b462 --- /dev/null +++ b/src/components/Layout/GlobalHeader/ProjectTable.jsx @@ -0,0 +1,147 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { inject, observer } from 'mobx-react'; +import { ModalAction } from 'containers/Action'; +import { toJS } from 'mobx'; +import { allCanReadPolicy } from 'resources/policy'; + +@inject('rootStore') +@observer +export default class ProjectSelect extends ModalAction { + static id = 'project-id'; + + static title = t('Switch Project'); + + static buttonText = ' '; + + init() {} + + get name() { + return t('Switch Project'); + } + + static get modalSize() { + return 'large'; + } + + getModalSize() { + return 'large'; + } + + get messageHasItemName() { + return false; + } + + static policy = allCanReadPolicy; + + static allowed = () => Promise.resolve(true); + + state = { + projectName: '', + }; + + get user() { + const { user } = this.props.rootStore; + return user; + } + + get project() { + const { + project: { + id: projectId = '', + name: projectName = '', + domain: { name: userDomainName } = {}, + } = {}, + } = this.user || {}; + return { + projectId, + projectName, + userDomainName, + }; + } + + get projects() { + const { projects = {} } = this.user || {}; + const { projectName } = this.state; + const items = Object.keys(toJS(projects) || {}) + .map((key) => { + const { name, domain_id } = projects[key]; + return { + id: key, + projectId: key, + name, + domain_id, + }; + }) + .filter((it) => { + if (!projectName) { + return true; + } + return ( + it.name.toLowerCase().indexOf(projectName.toLowerCase()) >= 0 || + it.projectId.toLowerCase().indexOf(projectName.toLowerCase()) >= 0 + ); + }); + return items; + } + + get defaultValue() { + const { projectId = '' } = this.project; + return { + project: { + selectedRowKeys: [projectId], + }, + }; + } + + get formItems() { + return [ + { + name: 'project', + label: t('Owned Project'), + type: 'select-table', + datas: this.projects, + filterParams: [ + { + label: t('Project Name'), + name: 'name', + }, + ], + columns: [ + { + title: t('Project Name'), + dataIndex: 'name', + }, + { + title: t('ID'), + dataIndex: 'id', + }, + ], + }, + ]; + } + + onSubmit = async (values) => { + const { + project: { selectedRowKeys }, + } = values; + const key = selectedRowKeys[0]; + const item = this.projects.find((it) => it.id === key); + const { domain_id: domainId } = item || {}; + const { rootStore } = this.props; + this.routing.push('/base/overview'); + await rootStore.switchProject(key, domainId); + }; +} diff --git a/src/components/Layout/GlobalHeader/RightContent.jsx b/src/components/Layout/GlobalHeader/RightContent.jsx new file mode 100644 index 00000000..08ad311c --- /dev/null +++ b/src/components/Layout/GlobalHeader/RightContent.jsx @@ -0,0 +1,107 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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 { Button, Col, Row } from 'antd'; +import Avatar from './AvatarDropdown'; +import styles from './index.less'; + +const gotoConsole = (type, props) => { + const { rootStore } = props; + rootStore.clearData(); + if (type === 0) { + rootStore.routing.push('/base/overview'); + } else { + rootStore.routing.push('/base/overview-admin'); + } +}; + +const GlobalHeaderRight = (props) => { + const { isAdminPage = false, rootStore: { hasAdminRole = false } = {} } = + props; + let linkRender = null; + if (isAdminPage) { + linkRender = ( + + ); + } else if (hasAdminRole) { + linkRender = ( + + ); + } + + return ( +
+ + {linkRender} + + + + +
+ ); +}; + +export default GlobalHeaderRight; + +// import React from 'react'; +// import Avatar from './AvatarDropdown'; +// import { Divider } from 'antd'; +// import styles from './index.less'; +// import Message from './Message'; +// import { Link } from 'react-router-dom'; + +// const GlobalHeaderRight = (props) => { +// const { isAdminPage = false, rootStore: { hasAdminRole = false } = {} } = props; +// let linkRender = null; +// if (hasAdminRole) { +// const consoleLink = isAdminPage ? +// { t('Console') } : +// {t('Console')}; +// const adminLink = !isAdminPage ? +// { t('Administrator') } : +// {t('Administrator')}; +// linkRender =
+// { consoleLink } +// +// { adminLink } +//
; +// } + +// return
+// {linkRender} +// +// +//
; +// }; + +// export default GlobalHeaderRight; diff --git a/src/components/Layout/GlobalHeader/Token.jsx b/src/components/Layout/GlobalHeader/Token.jsx new file mode 100644 index 00000000..6bbd2ce0 --- /dev/null +++ b/src/components/Layout/GlobalHeader/Token.jsx @@ -0,0 +1,132 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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 { inject, observer } from 'mobx-react'; +import { Typography } from 'antd'; +import { ModalAction } from 'containers/Action'; +import { allCanReadPolicy } from 'resources/policy'; +import styles from './index.less'; + +const { Paragraph } = Typography; + +@inject('rootStore') +@observer +export default class Token extends ModalAction { + static id = 'get-token'; + + static title = t('Get Token'); + + get name() { + return t('Get Token'); + } + + get token() { + const key = 'keystone_token'; + const item = localStorage.getItem(key); + try { + return JSON.parse(item) || {}; + } catch (e) { + return {}; + } + } + + get showNotice() { + return false; + } + + get tokenValue() { + return this.token.value || ''; + } + + get tokenExpiry() { + const { expires } = this.token; + return expires || 0; + } + + getLeftStr = (value) => { + const left = value - Date.now(); + const seconds = Math.floor(left / 1000); + if (seconds < 60) { + return t('{seconds} seconds', { seconds }); + } + const minutes = Math.floor(seconds / 60); + const leftSeconds = seconds % 60; + if (minutes < 60) { + return t('{minutes} minutes {leftSeconds} seconds', { + minutes, + leftSeconds, + }); + } + const hours = Math.floor(minutes / 60); + const leftMinutes = minutes % 60; + return t('{hours} hours {leftMinutes} minutes {leftSeconds} seconds', { + hours, + leftMinutes, + leftSeconds, + }); + }; + + get tips() { + return t( + 'Please save your token properly and it will be valid for {left}.', + { left: this.getLeftStr(this.tokenExpiry) } + ); + } + + get defaultValue() { + const value = { + token: this.tokenValue, + }; + return value; + } + + static policy = allCanReadPolicy; + + static allowed = () => Promise.resolve(true); + + get labelCol() { + return { + xs: { span: 0 }, + sm: { span: 0 }, + }; + } + + get wrapperCol() { + return { + xs: { span: 24 }, + sm: { span: 24 }, + }; + } + + get formItems() { + return [ + { + name: 'token', + label: '', + type: 'label', + component: ( + +
{this.tokenValue}
+
+ ), + }, + ]; + } + + onSubmit = () => Promise.resolve(); +} diff --git a/src/components/Layout/GlobalHeader/index.jsx b/src/components/Layout/GlobalHeader/index.jsx new file mode 100644 index 00000000..41941b80 --- /dev/null +++ b/src/components/Layout/GlobalHeader/index.jsx @@ -0,0 +1,28 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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 RightContent from './RightContent'; +import ProjectDropdown from './ProjectDropdown'; +import styles from './index.less'; + +export default function HeaderContent(props) { + const { isAdminPage = false } = props; + return ( +
+ {!isAdminPage && } + +
+ ); +} diff --git a/src/components/Layout/GlobalHeader/index.less b/src/components/Layout/GlobalHeader/index.less new file mode 100644 index 00000000..ce79864f --- /dev/null +++ b/src/components/Layout/GlobalHeader/index.less @@ -0,0 +1,192 @@ +@import "~styles/variables"; + +.menu { + :global(.anticon) { + margin-right: 8px; + } + + :global(.ant-dropdown-menu-item) { + min-width: 245px; + } + + .no-hover { + overflow: hidden; + + &:hover { + background-color: #fff; + } + } + + .name-item { + padding: 0 12px; + line-height: 40px; + + .user-label { + font-weight: "bold"; + margin-right: 8px; + } + + span { + line-height: 40px; + } + } + + .menu-item { + line-height: 30px; + } +} + +.no-padding-top { + padding-top: 0; +} + +.logout { + float: right; + line-height: 40px; +} + +.right { + position: absolute; + top: 0; + right: 31px; + line-height: @header-height; + + .action { + display: inline-block; + } + + .right_message { + + .message_avatar { + background-color: #fff; + color: rgba(0, 0, 0, 0.65); + margin-top: -4px; + } + + :global { + .ant-badge-dot { + transform: translate(-55%, 10%); + } + .ant-avatar-square { + border-radius: 3px !important; + } + } + } +} + +.project-menu { + :global(.ant-dropdown-menu) { + width: 170px; + } + + :global { + + .ant-dropdown-menu-item:hover, + .ant-dropdown-menu-submenu-title:hover { + cursor: pointer; + } + + .ant-dropdown-menu-item-disabled, + .ant-dropdown-menu-submenu-title-disabled { + cursor: pointer; + } + } + + .title { + cursor: auto; + + &:hover { + background-color: #fff; + } + } +} + +.project { + float: left; + cursor: pointer; + font-size: 14px; + line-height: @header-height; + + :global { + .ant-divider { + background-color: #d2d2d2; + margin-left: 24px; + margin-right: 24px; + } + .ant-btn-link { + position: absolute; + min-width: 280px; + min-height: 40px; + } + } +} + +.header { + padding-left: 36px; + overflow: hidden; + background-color: #fff; + height: 100%; + color: @title-color; + position: relative; +} + +.avatar { + box-shadow: 0px 2px 20px 0px rgba(0, 0, 0, 0.09); + border: none; + width: 30px; + height: 30px; + color: #bfbfbf; +} + +.domain { + font-size: 14px; +} + +.links { + margin-right: 20px; + display: inline-block; + + :global { + .ant-divider { + background-color: #d2d2d2; + margin-left: 24px; + margin-right: 24px; + } + } + + .link { + color: @title-color; + + &:hover { + color: @primary-color; + } + } + + .active { + color: @primary-color; + } +} + +.password-btn { + max-width: 100px; + + span { + max-width: 80px; + } +} + +.single-link { + color: @primary-color; + margin-right: 5px; +} + +.token { + pre { + padding: .4em .6em; + white-space: pre-wrap; + word-wrap: break-word; + background: hsla(0, 0%, 58.8%, .1); + border: 1px solid hsla(0, 0%, 39.2%, .2); + border-radius: 3px; + } +} \ No newline at end of file diff --git a/src/components/Layout/GlobalNav/index.jsx b/src/components/Layout/GlobalNav/index.jsx new file mode 100644 index 00000000..3fcc92b8 --- /dev/null +++ b/src/components/Layout/GlobalNav/index.jsx @@ -0,0 +1,81 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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 PropTypes from 'prop-types'; +import classnames from 'classnames'; + +import { trimEnd } from 'lodash'; + +import NavItem from './item'; + +import styles from './index.less'; + +class GlobalNav extends React.Component { + static propTypes = { + className: PropTypes.string, + navs: PropTypes.array.isRequired, + // eslint-disable-next-line react/no-unused-prop-types + prefix: PropTypes.string, + // eslint-disable-next-line react/no-unused-prop-types + checkSelect: PropTypes.func, + onItemClick: PropTypes.func, + innerRef: PropTypes.object, + }; + + static defaultProps = { + className: '', + prefix: '', + checkSelect() {}, + onItemClick() {}, + }; + + get currentPath() { + const { + location: { pathname }, + match: { url }, + } = this.props; + + const { length } = trimEnd(url, '/'); + return pathname.slice(length + 1); + } + + render() { + const { className, navs, innerRef, onItemClick } = this.props; + const classNames = classnames(styles.wrapper, className); + + return ( +
+ {navs.map((nav) => ( +
+ {nav.title &&

{t(nav.title)}

} +
    + {nav.items.map((item) => ( + + ))} +
+
+ ))} +
+ ); + } +} + +export default GlobalNav; diff --git a/src/components/Layout/GlobalNav/index.less b/src/components/Layout/GlobalNav/index.less new file mode 100644 index 00000000..99f9f919 --- /dev/null +++ b/src/components/Layout/GlobalNav/index.less @@ -0,0 +1,102 @@ +@import '~styles/variables'; +@import '~styles/mixins'; + +.wrapper { + position: fixed; + top: 60px; + left: 8px; + width: $nav-width; + height: calc(100vh - 68px); + padding: 40px 20px; + border-radius: $border-radius; + background-color: $dark; + box-shadow: 4px 8px 16px 0 rgba(0, 0, 0, 0.1); + transition: left $trans-speed ease-in-out; + overflow-y: auto; + z-index: 212; + + .subNav > ul > li { + &.select { + box-shadow: 0 4px 8px 0 rgba(25, 30, 41, 0.2); + background-color: #d8dee5; + border: solid 1px #404e68; + } + + &:hover { + background-color: #d8dee5; + } + + &:active { + background-color: #d8dee5; + border: solid 1px #404e68; + } + + & > a { + color: $light; + transition: color $trans-speed ease-in-out; + + @media (max-width: 1366px) { + padding: 7px 12px; + } + + :global .qicon { + color: #b6c2cd; + fill: #b6c2cd; + } + } + } +} + +.subNav { + & > p { + color: $light-color02; + margin-bottom: 12px; + } + + & > ul { + margin-bottom: 20px; + + & > li { + border-radius: 18px; + border: solid 1px transparent; + transition: all $trans-speed ease-in-out; + + & > a, + .title { + display: block; + padding: 7px 12px; + color: #4a5974; + font-weight: 500; + cursor: pointer; + + @media (max-width: 1366px) { + padding: 7px 0; + } + + :global { + .icon { + margin-right: 8px; + vertical-align: text-bottom; + } + } + } + + &.select, + &:hover, + &:active { + & > a { + color: $primary; + + :global .qicon { + color: #1890ff; + fill:#6fb4f5; + } + } + } + + & + li { + margin-top: 4px; + } + } + } +} diff --git a/src/components/Layout/GlobalNav/item.jsx b/src/components/Layout/GlobalNav/item.jsx new file mode 100644 index 00000000..45128bb3 --- /dev/null +++ b/src/components/Layout/GlobalNav/item.jsx @@ -0,0 +1,57 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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 PropTypes from 'prop-types'; +import classnames from 'classnames'; +import { Link } from 'react-router-dom'; +import { Icon } from 'antd'; + +import styles from './index.less'; + +export default class NavItem extends React.Component { + static propTypes = { + item: PropTypes.object, + current: PropTypes.string, + prefix: PropTypes.string, + onClick: PropTypes.func, + }; + + checkSelect = (item = {}) => { + const { current } = this.props; + + return current.startsWith(item.name); + }; + + renderIcon(icon) { + return ; + } + + render() { + const { item, prefix, onClick } = this.props; + + return ( +
  • + + {this.renderIcon(item.icon)} {t(item.title)} + +
  • + ); + } +} diff --git a/src/components/Layout/HeaderDropdown/index.jsx b/src/components/Layout/HeaderDropdown/index.jsx new file mode 100644 index 00000000..720909ba --- /dev/null +++ b/src/components/Layout/HeaderDropdown/index.jsx @@ -0,0 +1,28 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Dropdown } from 'antd'; +import React from 'react'; +import classNames from 'classnames'; +// import styles from './index.less'; + +// const HeaderDropdown = ({ overlayClassName: cls, ...restProps }) => ( +// +// ); + +const HeaderDropdown = ({ overlayClassName: cls, ...restProps }) => ( + +); + +export default HeaderDropdown; diff --git a/src/components/Layout/HeaderDropdown/index.less b/src/components/Layout/HeaderDropdown/index.less new file mode 100644 index 00000000..cb10b589 --- /dev/null +++ b/src/components/Layout/HeaderDropdown/index.less @@ -0,0 +1,16 @@ +// @import '~antd/es/style/themes/default.less'; + +// .container > * { +// background-color: #fff; +// border-radius: @border-radius; +// box-shadow: @shadow-1-down; +// } + +// @media screen and (max-width: @screen-xs) { +// .container { +// width: 100% !important; +// } +// .container > * { +// border-radius: 0 !important; +// } +// } diff --git a/src/components/Layout/Nav/index.jsx b/src/components/Layout/Nav/index.jsx new file mode 100644 index 00000000..0c769588 --- /dev/null +++ b/src/components/Layout/Nav/index.jsx @@ -0,0 +1,81 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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 PropTypes from 'prop-types'; + +import { trimEnd } from 'lodash'; + +import NavItem from './item'; + +import styles from './index.less'; + +class Nav extends React.Component { + static propTypes = { + className: PropTypes.string, + navs: PropTypes.array.isRequired, + // eslint-disable-next-line react/no-unused-prop-types + prefix: PropTypes.string, + // eslint-disable-next-line react/no-unused-prop-types + checkSelect: PropTypes.func, + onItemClick: PropTypes.func, + innerRef: PropTypes.object, + }; + + static defaultProps = { + className: '', + prefix: '', + checkSelect() {}, + onItemClick() {}, + }; + + get currentPath() { + const { + location: { pathname }, + match: { url }, + } = this.props; + + const { length } = trimEnd(url, '/'); + return pathname.slice(length + 1); + } + + render() { + const { className, navs, match, innerRef, onItemClick } = this.props; + + const prefix = trimEnd(match.url, '/'); + + return ( +
    + {navs.map((nav) => ( +
    + {nav.title &&

    {t(nav.title)}

    } +
      + {nav.items.map((item) => ( + + ))} +
    +
    + ))} +
    + ); + } +} + +export default Nav; diff --git a/src/components/Layout/Nav/index.less b/src/components/Layout/Nav/index.less new file mode 100644 index 00000000..8a651a80 --- /dev/null +++ b/src/components/Layout/Nav/index.less @@ -0,0 +1,134 @@ +@import '~styles/variables'; + +.subNav { + & > p { + color: #79879c; + margin-bottom: 12px; + } + + & > ul { + margin-bottom: 20px; + + & > li { + border-radius: 18px; + border: solid 1px transparent; + transition: all $trans-speed ease-in-out; + + & > a, + .title { + display: block; + padding: 7px 12px; + color: #4a5974; + font-weight: 500; + cursor: pointer; + + @media (max-width: 1366px) { + padding: 7px 0; + } + + :global { + .icon { + margin-right: 8px; + vertical-align: text-bottom; + } + + .qicon-chevron-down { + margin-top: 4px; + transition: all $trans-speed ease-in-out; + } + } + + .devopsIcon { + width: 16px; + height: 16px; + padding: 2px; + margin-right: 8px; + vertical-align: text-bottom; + } + } + + &.select, + &.childSelect, + &:hover, + &:active { + & > a { + color: $primary; + + :global .qicon { + color: $icon-color; + fill: #6fb4f5; + } + + .devopsIcon { + color: $icon-color; + fill: #6fb4f5; + } + } + + .title { + :global .qicon-chevron-down { + transform: rotate(-180deg); + } + } + + .innerNav > li { + height: 20px; + margin-top: 8px; + opacity: 1; + transition: height $trans-speed ease-in-out, + margin-top $trans-speed ease-in-out, + opacity $trans-speed ease-in-out 0.1s; + } + } + + & + li { + margin-top: 4px; + } + } + } +} + +.innerNav { + margin-bottom: 4px; + padding-left: 38px; + + @media (max-width: 1366px) { + padding-left: 26px; + } + + & > li { + height: 0; + opacity: 0; + overflow: auto; + transition: height $trans-speed ease-in-out 0.1s, + margin-top $trans-speed ease-in-out 0.1s, opacity $trans-speed ease-in-out; + + & > a { + color: #4a5974; + } + + &.select, + &:hover, + &:active { + & > a { + color: $primary; + } + } + } +} + +.back { + margin: 20px 0; + padding: 8px 12px; + & > a > svg { + width: 16px; + height: 16px; + margin-right: 8px; + vertical-align: text-top; + } +} + +.rightIcon { + float: right; + margin-right: 0 !important; +} diff --git a/src/components/Layout/Nav/item.jsx b/src/components/Layout/Nav/item.jsx new file mode 100644 index 00000000..49b172ac --- /dev/null +++ b/src/components/Layout/Nav/item.jsx @@ -0,0 +1,88 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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 PropTypes from 'prop-types'; +import classnames from 'classnames'; +import { Link } from 'react-router-dom'; +import { Icon } from 'antd'; + +import styles from './index.less'; + +export default class NavItem extends React.Component { + static propTypes = { + item: PropTypes.object, + current: PropTypes.string, + prefix: PropTypes.string, + onClick: PropTypes.func, + }; + + checkSelect = (item = {}) => { + const { current } = this.props; + + if (item.children) { + return item.children.some((child) => this.checkSelect(child)); + } + + if (item.tabs) { + return item.tabs.some((tab) => this.checkSelect(tab)); + } + + return current.startsWith(item.name); + }; + + render() { + const { item, prefix, onClick } = this.props; + + if (item.children) { + return ( +
  • +
    + {t(item.title)} + +
    +
      + {item.children.map((child) => ( +
    • + {t(child.title)} +
    • + ))} +
    +
  • + ); + } + + return ( +
  • + + {t(item.title)} + +
  • + ); + } +} diff --git a/src/components/Layout/Selector/index.jsx b/src/components/Layout/Selector/index.jsx new file mode 100644 index 00000000..564cd20f --- /dev/null +++ b/src/components/Layout/Selector/index.jsx @@ -0,0 +1,150 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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 PropTypes from 'prop-types'; +import classNames from 'classnames'; + +import { Icon, Dropdown, Spin, Menu } from 'antd'; + +import styles from './index.less'; + +export default class Selector extends React.Component { + static propTypes = { + icon: PropTypes.string, + defaultIcon: PropTypes.string, + value: PropTypes.string, + type: PropTypes.string, + loading: PropTypes.bool, + options: PropTypes.array, + onSelect: PropTypes.func, + onScrollBottom: PropTypes.func, + }; + + static defaultProps = { + icon: '', + defaultIcon: '', + value: '', + type: '', + loading: false, + options: [], + onSelect() {}, + onScrollBottom() {}, + }; + + constructor(props) { + super(props); + + this.contentRef = React.createRef(); + } + + componentDidMount() { + if (this.contentRef.current) { + this.$dropdownContent = + this.contentRef.current.querySelector('.dropdown-content'); + this.$dropdownContent.addEventListener('scroll', this.handleScroll); + } + } + + componentDidUpdate() { + if (this.contentRef.current) { + const $menu = this.contentRef.current.querySelector( + '.dropdown-content > .menu-wrapper' + ); + + if ($menu && this.$dropdownContent) { + this.threshold = + $menu.offsetHeight - this.$dropdownContent.offsetHeight; + } + } + } + + componentWillUnmount() { + if (this.$dropdownContent) { + this.$dropdownContent.removeEventListener('scroll', this.handleScroll); + } + } + + get isMulti() { + return this.props.options.length > 1; + } + + handleScroll = (e) => { + if (this.threshold && e.target.scrollTop >= this.threshold - 2) { + this.props.onScrollBottom(); + } + }; + + handleMenuClick = (e, key) => { + this.props.onSelect(key); + }; + + renderList() { + const { defaultIcon, options, loading } = this.props; + + if (!this.isMulti) { + return null; + } + + return ( +
    + + {options.map((option) => ( + + + {option.label} + + ))} + +
    + {loading && } +
    +
    + ); + } + + render() { + const { icon, defaultIcon, value, type, options } = this.props; + + const option = options.find((item) => item.value === value) || {}; + + return ( +
    + +
    +
    + +
    +
    +

    {type}

    +
    {option.label || value}
    +
    + {this.isMulti && ( +
    + +
    + )} +
    +
    +
    + ); + } +} diff --git a/src/components/Layout/Selector/index.less b/src/components/Layout/Selector/index.less new file mode 100644 index 00000000..ae1413aa --- /dev/null +++ b/src/components/Layout/Selector/index.less @@ -0,0 +1,98 @@ +@import '~styles/variables'; +@import '~styles/mixins'; + +.titleWrapper { + position: relative; + margin-bottom: 20px; + padding: 12px; + border-radius: $border-radius; + background-color: $background-color; + box-shadow: 0 8px 16px 0 rgba(36, 46, 66, 0.2); + + .icon { + display: inline-block; + vertical-align: middle; + width: 40px; + height: 40px; + padding: 8px; + margin-right: 12px; + border-radius: 100px 0 100px 100px; + background-color: rgba(239, 244, 249, 0.08); + + img { + width: 24px; + height: 24px; + } + } + + .text { + display: inline-block; + vertical-align: middle; + width: 124px; + + :global .h6 { + font-family: $font-family-id; + line-height: 1.43; + color: #ffffff; + @include ellipsis; + } + + p { + color: #d8dee5; + } + } + + .arrow { + position: absolute; + bottom: 12px; + right: 12px; + width: 20px; + height: 20px; + padding: 3px; + border-radius: 50%; + background-color: rgba(85, 188, 138, 0.1); + + :global .icon { + width: 14px; + height: 14px; + background-color: $primary; + border-radius: 50%; + vertical-align: inherit; + } + } +} + +.multi { + cursor: pointer; +} + +.dropdown { + background-color: $background-color; + + :global { + .dropdown-content { + max-height: 300px; + overflow-y: auto; + overflow-x: hidden; + } + + .menu { + padding: 12px; + } + + .menu-item { + padding: 6px 20px 6px 14px; + + img { + width: 16px; + height: 16px; + margin-right: 10px; + } + } + } +} + +.bottom { + position: relative; + text-align: center; +} diff --git a/src/components/Layout/index.js b/src/components/Layout/index.js new file mode 100644 index 00000000..987dbd1b --- /dev/null +++ b/src/components/Layout/index.js @@ -0,0 +1,17 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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 { default as Nav } from './Nav'; +export { default as GlobalNav } from './GlobalNav'; +export { default as Selector } from './Selector'; diff --git a/src/components/Loading/index.jsx b/src/components/Loading/index.jsx new file mode 100644 index 00000000..c126d457 --- /dev/null +++ b/src/components/Loading/index.jsx @@ -0,0 +1,47 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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 { Icon, Modal } from 'antd'; + +const Loading = ({ pastDelay, timedOut, error }) => { + if (pastDelay) { + return ( + + +

    Loading...

    +
    + ); + } + if (timedOut) { + return
    Taking a long time...
    ; + } + if (error) { + return
    Error!
    ; + } + return null; +}; + +export default Loading; diff --git a/src/components/MagicInput/index.jsx b/src/components/MagicInput/index.jsx new file mode 100644 index 00000000..16d2823a --- /dev/null +++ b/src/components/MagicInput/index.jsx @@ -0,0 +1,574 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import { Input, Tag, Menu, Divider, Button, Checkbox } from 'antd'; +import { CloseOutlined, SearchOutlined } from '@ant-design/icons'; +import classnames from 'classnames'; +import { isEmpty } from 'lodash'; +import styles from './index.less'; + +const option = PropTypes.shape({ + label: PropTypes.string.isRequired, + key: PropTypes.oneOfType([ + PropTypes.string.isRequired, + PropTypes.bool.isRequired, + ]), + component: PropTypes.node, +}); + +const filterParam = PropTypes.shape({ + label: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + isSingle: PropTypes.bool, + isServer: PropTypes.bool, + allowed: PropTypes.func, + options: PropTypes.arrayOf(option), + isTime: PropTypes.bool, +}); + +// eslint-disable-next-line no-unused-vars +const getTags = (props) => { + // eslint-disable-next-line no-shadow + const { initValue, filterParams } = props; + if (!initValue) { + return []; + } + if (isEmpty(filterParams)) { + return []; + } + const tags = []; + Object.keys(initValue).forEach((key) => { + const item = filterParams.find((it) => it.name === key); + if (item) { + const value = initValue[key]; + tags.push({ + value, + filter: item, + }); + } + }); + return tags; +}; + +class MagicInput extends PureComponent { + static propTypes = { + filterParams: PropTypes.arrayOf(filterParam), + // eslint-disable-next-line react/no-unused-prop-types + initValue: PropTypes.object, + placeholder: PropTypes.string, + onInputChange: PropTypes.func, + onInputFocus: PropTypes.func, + }; + + static defaultProps = { + filterParams: [], + initValue: {}, + placeholder: t('Click here for filters.'), + }; + + constructor(props) { + super(props); + + this.inputRef = React.createRef(); + + this.state = { + tags: [], + currentFilter: null, + isFocus: false, + optionClear: false, + checkValues: [], + }; + } + + componentDidMount() { + this.initTags(this.props); + } + + getFilterParams = () => { + // eslint-disable-next-line no-shadow + const { filterParams } = this.props; + const { tags } = this.state; + const filters = []; + filterParams.forEach((item) => { + const alreadyTag = tags.find((it) => it.filter.name === item.name); + if (!alreadyTag) { + filters.push(item); + } + }); + return filters; + }; + + onTagsChange = () => { + const { onInputChange } = this.props; + const { tags } = this.state; + onInputChange && onInputChange(tags); + }; + + onFocusChange = (value) => { + const { onInputFocus } = this.props; + onInputFocus && onInputFocus(value); + }; + + getDefaultFilter = () => { + const { filterParams } = this.props; + return filterParams.find((it) => !it.options); + }; + + handleEnter = (e) => { + e && e.preventDefault(); + e && e.stopPropagation(); + const { value } = e.currentTarget; + if (!value) { + return; + } + this.updateInput(value); + }; + + handleBlur = () => { + const { currentFilter } = this.state; + if (currentFilter) { + this.setState({ + isFocus: true, + }); + this.onFocusChange(true); + } else { + this.onFocusChange(false); + } + }; + + handleKeyUp = (e) => { + if (e.keyCode === 8 || e.keyCode === 46) { + const { currentFilter, tags } = this.state; + const { value } = this.inputRef.current.state; + if (currentFilter && isEmpty(value)) { + this.setState({ + currentFilter: null, + }); + } else if (tags.length > 0 && isEmpty(value)) { + this.handleTagClose(tags[tags.length - 1].filter.name); + } + } + }; + + handleFocus = () => { + this.setState({ + isFocus: true, + }); + this.onFocusChange(true); + }; + + handleInputChange = (e) => { + this.setState({ + inputValue: e.target.value, + }); + }; + + handleTagClose = (name) => { + const { tags, checkValues } = this.state; + const newTags = tags.filter((it) => it.filter.name !== name); + const leftCheckValues = checkValues.filter( + (it) => it.split('--')[0] !== name + ); + this.setState( + { + tags: newTags, + optionClear: false, + checkValues: leftCheckValues, + }, + () => { + this.onTagsChange(); + } + ); + }; + + handleOptionClick = ({ key }) => { + let value; + if (key === 'true') { + value = true; + } else { + value = key === 'false' ? false : key; + } + this.updateInput(value); + }; + + handleSelectFilter = ({ key }) => { + // eslint-disable-next-line no-shadow + const { filterParams } = this.props; + const filter = filterParams.find((it) => it.name === key); + this.setState( + { + currentFilter: filter, + isFocus: true, + }, + () => { + this.inputRef.current.focus(); + this.onFocusChange(true); + } + ); + }; + + initTags(props) { + // eslint-disable-next-line no-shadow + const { initValue, filterParams } = props; + if (!initValue) { + return; + } + if (isEmpty(filterParams)) { + return; + } + const tags = []; + const checkValues = []; + Object.keys(initValue).forEach((key) => { + const item = filterParams.find((it) => it.name === key); + if (item) { + const { options = [] } = item; + const value = initValue[key]; + if (options.length) { + const optionItem = options.find((op) => op.key === value); + if (optionItem && optionItem.isQuick) { + checkValues.push(`${item.name}--${value}`); + } + } + tags.push({ + value, + filter: item, + }); + } + }); + this.setState({ + tags, + checkValues, + }); + } + + renderKey() { + const { currentFilter } = this.state; + if (!currentFilter) { + return null; + } + return ( + + {`${currentFilter.label}`} + + + ); + } + + renderTags() { + const { tags } = this.state; + const tagItems = tags.map((it) => { + const { filter, value } = it; + const { options } = filter; + let label = value; + if (options) { + const current = options.find((item) => item.key === value); + label = current ? current.label : value; + } + return ( + this.handleTagClose(filter.name)} + > + {filter.label} + + {label} + + ); + }); + return
    {tagItems}
    ; + } + + renderOptions() { + const { currentFilter, tags } = this.state; + const { options, correlateOption } = currentFilter; + if (!options) { + return null; + } + + const correlateTag = tags.filter( + (it) => it.filter.name === correlateOption + ); + let suboptions = []; + if (correlateOption && correlateTag[0]) { + suboptions = options.filter( + (it) => it.correlateValue.indexOf(correlateTag[0].value) > -1 + ); + } + const menuItems = (suboptions[0] ? suboptions : options).map((it) => ( + {it.label} + )); + return ( + + {menuItems} + + ); + } + + renderMenu() { + const { currentFilter, isFocus, optionClear, inputValue } = this.state; + if (inputValue) { + return null; + } + if (!isFocus) { + return null; + } + if (currentFilter) { + return this.renderOptions(); + } + let filters = this.getFilterParams(); + if (optionClear) { + filters = []; + } + + const menuItems = filters.map((it) => ( + {it.label} + )); + return ( + + {this.renderOptionsClose(filters)} + {menuItems} + + ); + } + + // eslint-disable-next-line react/sort-comp + clearOptions = () => { + this.setState({ + optionClear: true, + }); + }; + + renderOptionsClose = (filters) => { + const { filterParams } = this.props; + const { optionClear } = this.state; + if (optionClear || !filters[0] || filterParams.length === filters.length) { + return null; + } + return ( + + {this.renderModal()} + + ); + } +} diff --git a/src/components/NotFound/index.jsx b/src/components/NotFound/index.jsx new file mode 100644 index 00000000..50a0f69a --- /dev/null +++ b/src/components/NotFound/index.jsx @@ -0,0 +1,70 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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 './index.less'; + +export default class NotFound extends React.Component { + constructor(props) { + super(props); + this.state = { + time: 10, + }; + } + + componentDidMount() { + this.interval = setInterval(() => { + this.setState(({ time }) => ({ + time: Math.max(time - 1, 0), + })); + }, 1100); + } + + UNSAFE_componentWillUpdate(nextProps, nextState) { + if (nextState.time === 0) { + if (this.interval) { + clearInterval(this.interval); + } + + window.location.href = '/'; + } + } + + componentWillUnmount() { + if (this.interval) { + clearInterval(this.interval); + } + } + + render() { + return ( +
    + +
    +
    Not Found
    +

    + {t.html('NOT_FOUND_DESC', { + time: this.state.time, + link: '/', + })} +

    +
    +
    + ); + } +} diff --git a/src/components/NotFound/index.less b/src/components/NotFound/index.less new file mode 100644 index 00000000..ec79e424 --- /dev/null +++ b/src/components/NotFound/index.less @@ -0,0 +1,39 @@ +@import '~styles/variables'; + +.wrapper { + margin-top: 132px; + text-align: center; +} + +.image { + height: 200px; + user-select: none; +} + +.text { + display: inline-block; + vertical-align: top; + width: 600px; + margin-left: 60px; + + :global .h1 { + opacity: 0.4; + font-size: 120px; + line-height: 168px; + color: #abb4be; + user-select: none; + } + + p { + text-shadow: 0 4px 8px rgba(36, 46, 66, 0.1); + font-size: 20px; + font-weight: @font-bold; + line-height: 1.4; + color: @text-color; + text-align: left; + } + + a { + color: #329dce; + } +} diff --git a/src/components/Notify/index.jsx b/src/components/Notify/index.jsx new file mode 100644 index 00000000..ed433a61 --- /dev/null +++ b/src/components/Notify/index.jsx @@ -0,0 +1,160 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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 { notification } from 'antd'; +import PropTypes from 'prop-types'; +import { + InfoCircleOutlined, + CheckCircleOutlined, + CloseCircleOutlined, + LoadingOutlined, +} from '@ant-design/icons'; +import CodeEditor from 'components/CodeEditor'; +import ModalButton from 'components/ModalButton'; +import globalRootStore from 'stores/root'; +import { unescapeHtml } from 'utils/index'; +import styles from './index.less'; + +const open = (args) => { + const { + title = t('Error'), + type = 'error', + description = '', + onClose, + top = 48, + } = args; + + let iconColor = '#F5222D'; + let icon = null; + + if (type === 'info') { + iconColor = '#0068FF'; + icon = ; + } else if (type === 'success') { + iconColor = '#57E39B'; + icon = ; + } else if (type === 'error') { + iconColor = '#EB354D'; + icon = ; + } else if (type === 'process') { + iconColor = '#0068FF'; + icon = ; + } else if (type === 'warn') { + iconColor = '#FEDF40'; + icon = ; + } + + const duration = type === 'error' || type === 'warn' ? 0 : 4.5; + + notification.open({ + message: unescapeHtml(title), + duration, + icon, + description: unescapeHtml(description), + className: styles.notify, + onClose, + top, + style: { + whiteSpace: 'pre-line', + }, + }); +}; + +open.propTypes = { + title: PropTypes.string, + type: PropTypes.string, + description: PropTypes.string, +}; + +const success = (title, description) => { + open({ + title, + description, + type: 'success', + }); +}; + +const info = (title, description) => { + open({ + title, + description, + type: 'info', + }); +}; + +const error = (title, description) => { + globalRootStore.addNoticeCount(); + open({ + title, + description, + type: 'error', + onClose: () => { + globalRootStore.removeNoticeCount(); + }, + }); +}; + +const warn = (title, description) => { + open({ + title, + description, + type: 'warn', + }); +}; + +const process = (title, description) => { + open({ + title, + description, + type: 'process', + }); +}; + +const errorWithDetail = (title, err) => { + const description = err ? ( + + } + /> + ) : ( + '' + ); + error(title, description); +}; + +const Notify = { + open, + success, + error, + warn, + info, + process, + errorWithDetail, +}; + +export default Notify; diff --git a/src/components/Notify/index.less b/src/components/Notify/index.less new file mode 100644 index 00000000..7f177d73 --- /dev/null +++ b/src/components/Notify/index.less @@ -0,0 +1,21 @@ +.notify { + :global { + .ant-notification-notice-icon { + font-size: 18px; + } + + .ant-notification-notice-message { + font-size: 12px; + word-break: break-all; + } + + .ant-notification-notice-with-icon .ant-notification-notice-message { + margin-left: 32px; + } + } +} + +.codeEditor { + height: 400px !important; + min-height: 400px !important; +} \ No newline at end of file diff --git a/src/components/PageLoading/index.jsx b/src/components/PageLoading/index.jsx new file mode 100644 index 00000000..2d6412e6 --- /dev/null +++ b/src/components/PageLoading/index.jsx @@ -0,0 +1,33 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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 { Spin } from 'antd'; + +const PageLoading = (props) => { + const { className } = props; + return ( +
    + +
    + ); +}; + +export default PageLoading; diff --git a/src/components/Pagination/index.jsx b/src/components/Pagination/index.jsx new file mode 100644 index 00000000..204b2f8e --- /dev/null +++ b/src/components/Pagination/index.jsx @@ -0,0 +1,228 @@ +// Copyright 2021 99cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { LeftOutlined, RightOutlined } from '@ant-design/icons'; +import { Button, Select } from 'antd'; +import classnames from 'classnames'; +import styles from './index.less'; + +export default class index extends Component { + static propTypes() { + return { + total: PropTypes.number, + currentDataSize: PropTypes.number.isRequired, + current: PropTypes.number.isRequired, + pageSize: PropTypes.number.isRequired, + defaultCurrent: PropTypes.number, + defaultPageSize: PropTypes.number, + pageSizeOptions: PropTypes.array, + onChange: PropTypes.func, + isLoading: PropTypes.bool, + className: PropTypes.object, + }; + } + + static defaultProps = { + isLoading: false, + total: undefined, + defaultCurrent: 1, + defaultPageSize: 10, + pageSizeOptions: [10, 20, 50, 100], + onChange: (page, pageSize) => { + // eslint-disable-next-line no-console + console.log(page, pageSize); + }, + }; + + constructor(props) { + super(props); + const { + current, + pageSize, + defaultCurrent, + defaultPageSize, + currentDataSize, + isLoading, + total, + } = props; + this.state = { + current: current || defaultCurrent, + pageSize: pageSize || defaultPageSize, + currentDataSize, + isLoading, + total, + }; + } + + static getDerivedStateFromProps(nextProps, prevState) { + if ( + nextProps.currentDataSize !== prevState.currentDataSize || + (nextProps.current && nextProps.current !== prevState.current) || + nextProps.isLoading !== prevState.isLoading || + nextProps.total !== prevState.total + ) { + const { currentDataSize, current = 1, isLoading, total } = nextProps; + return { + currentDataSize, + current, + isLoading, + total, + }; + } + return null; + } + + onChange = (current, pageSize) => { + const { onChange } = this.props; + onChange && onChange(current, pageSize); + }; + + onChangePageSize = (pageSize) => { + this.setState( + { + pageSize, + }, + () => { + this.onChange(1, pageSize); + } + ); + }; + + onClickPre = () => { + const { current, pageSize } = this.state; + if (current === 1) { + return; + } + this.setState( + { + current: current - 1, + }, + () => { + this.onChange(current - 1, pageSize); + } + ); + }; + + onClickNext = () => { + const { current, pageSize, currentDataSize } = this.state; + if (currentDataSize < pageSize) { + return; + } + this.setState({ + current: current + 1, + }); + this.onChange(current + 1, pageSize); + }; + + onFocusChange = (value) => { + const { onFocusChange } = this.props; + onFocusChange && onFocusChange(value); + }; + + onFocus = () => { + this.onFocusChange(true); + }; + + onBlur = () => { + this.onFocusChange(false); + }; + + checkNextByTotal() { + const { pageSize, total, current } = this.state; + if (total === undefined) { + return true; + } + if (!total) { + return false; + } + return current < Math.ceil(total / pageSize); + } + + renderTotal() { + const { hideTotal } = this.props; + if (hideTotal) { + return null; + } + const { current, currentDataSize, pageSize, isLoading, total } = this.state; + if (total !== undefined) { + return {t('Total {total} items', { total })}; + } + if (isLoading) { + return null; + } + if (currentDataSize < pageSize) { + const totalCompute = (current - 1) * pageSize + currentDataSize; + return {t('Total {total} items', { total: totalCompute })}; + } + return null; + } + + renderPageSelect() { + const { pageSizeOptions, defaultPageSize } = this.props; + const { pageSize } = this.state; + const options = pageSizeOptions.map((it) => ({ + label: t('{pageSize} items/page', { pageSize: it }), + value: it, + })); + return ( + +
    + ); + } + + renderActions() { + const { + primaryActions, + containerProps, + onClickAction, + onFinishAction, + onCancelAction, + primaryActionsExtra, + } = this.props; + if (primaryActions) { + return ( + + ); + } + return null; + } + + renderCustomButton() { + const { hideCustom } = this.props; + if (hideCustom) { + return null; + } + return ( + +