docs: Add Chinese documents

1. Add Chinese development documents
2. Add Chinese test document

Change-Id: I29cd91f47069c9e64928800094282d4cbc977920
This commit is contained in:
Jingwei.Zhang 2021-06-25 09:51:34 +08:00
parent f771795307
commit 781d751722
164 changed files with 6819 additions and 1 deletions

1
.gitignore vendored
View File

@ -3,7 +3,6 @@
.DS_Store .DS_Store
yarn-error.log yarn-error.log
package-lock.json package-lock.json
docs/
.vscode .vscode
test/e2e/videos test/e2e/videos
test/e2e/screenshots test/e2e/screenshots

View File

@ -1,3 +1,4 @@
dist dist
node_modules node_modules
coverage coverage
docs

View File

@ -0,0 +1,94 @@
简体中文 | [English](/docs/en/develop/1-ready-to-work.md)
# 开发前准备
- node 环境
- package.json 中要求:`"node": ">=10.22.0"`
- 验证 nodejs 版本
```shell
node -v
```
- yarn
- 安装 yarn
```shell
npm install -g yarn
```
- 安装依赖包
- 在项目根目录下执行,即`package.json`同级,需要耐心等待安装完成
```shell
yarn install
```
- 准备好可用的后端
- 准备好可访问的后端举个例子https://172.20.154.250
- 修改`config/webpack.dev.js`中的相应配置:
```javascript
if (API === 'mock' || API === 'dev') {
devServer.proxy = {
'/api': {
target: 'https://172.20.154.250',
changeOrigin: true,
secure: false,
},
};
}
```
- 配置访问的 host 与 port
- 修改`devServer.host`与`devServer.port`
- 修改`config/webpack.dev.js`中的相应配置
```javascript
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
};
```
- 搭建完成
- 在项目根目录下执行,即`package.json`同级
```shell
yarn run dev
```
- 使用`config/webpack.dev.js`中配置的`host`与`port`访问即可,如`http://localhost:8088`
- 开发使用的前端实时更新环境搞定。
# 生产环境使用的前端包
- 具备符合要求的`nodejs`与`yarn`
- 在项目根目录下执行,即`package.json`同级
```shell
yarn run build
```
- 打包后的文件在`dist`目录,交给部署相关人员即可。
# 测试使用的前端包
- 具备符合要求的`nodejs`与`yarn`
- 在项目根目录下执行,即`package.json`同级
```shell
yarn run build:test
```
- 打包后的文件在`dist`目录
- 注意!!!这个测试包为了测出代码覆盖率的
- 建议使用 nginx以完成带有代码覆盖率的 E2E 测试。

View File

@ -0,0 +1,380 @@
简体中文 | [English](/docs/en/develop/2-catalog-introduction.md)
# 一级目录简介
- `Gruntfile.js`:用于收集 i18n
- `LICENSE`: 该项目使用 Apache License
- `Makefile`:
- `README.md`: 前端启动的简单说明,详细信息请参考 docs 文档
- `config`目录: webpack 配置,其内包含公用、开发环境、测试环境、生成环境下的 webpack 配置
- `cypress.json`: e2e 测试的配置文件
- `docker`: 内含开发环境、生成环境、测试环境使用的 docker 配置
- `docs`目录: 文档介绍,包含中文、英文、开发说明文档、测试说明文档,其中 en 文档暂缺失
- `jest.config.js`: 单元测试的配置文件
- `jsconfig.json`: js 代码的配置文件
- `package.json`: 安装包、命令等配置文件
- `yarn.lock`: 包的版本锁定文件
- `.babelrc`: bebel 配置文件
- `.dockerignore`: docker 忽略的文件配置
- `.eslintignore`: eslint 忽略的文件配置
- `.eslint`: eslint 配置
- `.gitignore`: git 忽悠的文件配置
- `.gitreview`: gitreview 配置
- `.prettierignore`: prettier 忽略的文件配置
- `.prettierrc`: prettier 的配置
- `src`目录: 开发代码所在文件夹!!!
- `test`目录: 测试代码所在文件夹!!!包含 e2e 测试代码及单元测试的基础代码
- `tools`目录: 其他工具文件夹,内含 git 工具
# src 目录介绍
- `src/components`目录:公共组件
- `src/api`目录API暂未使用
- `src/asset`目录images, template 等静态文件
- `src/containers`目录:
- 带状态的组件
- 基础类
- [BaseList](3-1-BaseList-introduction.md)
- [BaseDetail](3-3-BaseDetail-introduction.md)
- [BaseForm](3-6-FormAction-introduction.md)
- [BaseModalAction](3-7-ModalAction-introduction.md)
- [BaseConfirmAction](3-8-ConfirmAction-introduction.md)
- [BaseStepAction](3-9-StepAction-introduction.md)
- `src/core`目录:
- `index.js`: 入口文件
- `routes.js`: 按模块的路由配置
- `i18n.js`
- `App.jsx`
- `src/layouts`目录:
- 定义所有整体页面布局的组件
- 空白布局 BlankLayout
- 登录页使用的布局 UserLayout
- 内容页使用的布局 BaseLayout(列表、详情、表单等使用)
- `menu.jsx`: 控制台使用的菜单配置
- `admin-menu.jsx`: 管理平台使用的菜单配置
- `src/locales`目录: i18n
- `src/resources`目录:
- 定义各资源被公用的状态 / 搜索项
- 定义各资源被公用的表格列
- 定义各资源的复用函数
- `src/stores`目录:
- 对资源的数据获取、操作等
- 按照资源名小写字母加连字符命名
- 目录分为两级:例如 `nova/instances.js`, `cinder/volume.js`
- `src/utils`目录:
- 公共函数(时间处理、正则、cookie、localStorage、......)
- 对应的单元测试,以 test.js 或 spec.js 结尾
- `src/styles`目录: 基础样式、公用样式、样式变量等
- `src/pages`目录:
- 按照页面层级结构递进(按照:菜单项--二级菜单)
- 所有目录命名均为小写加连字符命名, 目录包含两个文件夹 `containers``routers`, 一个文件 `App.js`
- `containers`下存放二级目录对应的页面
- `routes`用于配置路由
# src/pages 目录介绍
- 以一级、二级菜单划分目录,一级菜单列在`src/pages`下,其对应的二级菜单页面位于`src/pages/xxx/containers`下,以“计算-云主机”为例,“计算”对应于`src/pages/compute`目录,“云主机”对应于`src/pages/compute/containers/Instance`目录
- `src/pages/compute/containers/Instance/index.jsx`: 云主机列表页,继承于[BaseList 组件](3-1-BaseList-introduction.md)(带有 Tab
的页面,继承 TabBaseList 组件即可)
- `src/pages/compute/containers/Instance/Detail`目录
- 云主机详情页
- `index.jsx`继承于[BaseDetail 组件](3-3-BaseDetail-introduction.md)
- `src/pages/compute/containers/Instance/actions`目录
- 云主机的操作
- `Lock.jsx` 锁定云主机,继承于[BaseConfirmAction](3-8-ConfirmAction-introduction.md)
- `AttachInterface.jsx` 继承于[BaseModalAction](3-7-ModalAction-introduction.md)
- `StepCreate/index.jsx`,继承于[BaseStepAction](3-9-StepAction-introduction.md)
- `src/pages/compute/routes`目录:
- `index.js`,配置路由
- 约定以路由中是否含有“-admin”来判定是管理平台还是控制台
# test 目录介绍
[简体中文](/docs/zh/test/2-catalog-introduction.md) | [English](/docs/en/test/2-catalog-introduction.md)
# 目录简介-图像版
```
.
├── Gruntfile.js (用于收集i18n)
├── LICENSE
├── Makefile
├── README.md
├── config
│   ├── theme.js
│   ├── webpack.common.js
│   ├── webpack.dev.js (开发时使用的webpack配置)
│   ├── webpack.e2e.js (e2e测试时使用的webpack配置能生成用于检测覆盖率的包)
│   └── webpack.prod.js (生成环境使用的webpack打包配置)
├── cypress.json (e2e的配置)
├── docker
│   ├── dev.dockerfile
│   ├── nginx.conf
│   ├── prod.dockerfile
│   └── test.dockerfile
├── docs (文档)
├── jest.config.js (单元测试配置)
├── jsconfig.json
├── package.json
├── src
│   ├── api (api汇总暂未使用)
│   ├── asset
│   │   ├── image (图片放置位置)
│   │   └── template
│   │   └── index.html
│   ├── components (公用组件)
│   ├── containers
│   │   ├── Action
│   │   │   ├── ConfirmAction (确认型的action基类)
│   │   │   ├── FormAction (单页的action基类)
│   │   │   ├── ModalAction (弹窗型的action基类)
│   │   │   ├── StepAction (分多步的单页action例如创建云主机)
│   │   │   └── index.jsx
│   │   ├── BaseDetail (带有详情信息的详情页基类)
│   │   ├── List (列表页的基类,例如:云主机)
│   │   ├── TabDetail (带有tab切换的详情页的基类例如云主机详情)
│   │   └── TabList (带有tab切换的列表页)
│   ├── core
│   │   ├── App.jsx
│   │   ├── i18n.js
│   │   ├── index.jsx (入口)
│   │   └── routes.js (按模块的路由配置)
│   ├── layouts
│   │   ├── Base (登录后使用的布局)
│   │   ├── Blank (空白布局)
│   │   ├── User (登录使用的布局)
│   │   ├── admin-menu.jsx (管理平台使用的菜单配置)
│   │   └── menu.jsx (控制台使用的菜单配置)
│   ├── locales (翻译)
│   │   ├── en.json
│   │   ├── index.js
│   │   └── zh.json
│   ├── pages (页面-目录结构按照:菜单项--二级菜单 分配其中二级菜单的页面放在containers文件夹下)
│   │   ├── base
│   │   │   ├── App.jsx
│   │   │   ├── containers
│   │   │   │   ├── 404 (404页面)
│   │   │   │   │   └── index.jsx
│   │   │   │   ├── AdminOverview (管理平台首页)
│   │   │   │   │   ├── components
│   │   │   │   │   │   ├── ComputeService.jsx
│   │   │   │   │   │   ├── NetworkService.jsx
│   │   │   │   │   │   ├── PlatformInfo.jsx
│   │   │   │   │   │   ├── ResourceOverview.jsx
│   │   │   │   │   │   └── VirtualResource.jsx
│   │   │   │   │   ├── index.jsx
│   │   │   │   │   └── style.less
│   │   │   │   └── Overview (控制台首页)
│   │   │   │   ├── components
│   │   │   │   │   ├── ProjectInfo.jsx
│   │   │   │   │   ├── QuotaOverview.jsx
│   │   │   │   │   └── ResourceStatistic.jsx
│   │   │   │   ├── index.jsx
│   │   │   │   └── style.less
│   │   │   └── routes (路由配置)
│   │   │   └── index.js
│   │   ├── compute
│   │   │   ├── App.jsx
│   │   │   ├── containers
│   │   │   │   ├── BareMetalNode (裸机配置)
│   │   │   │   ├── Flavor (云主机类型)
│   │   │   │   ├── HostAggregate (主机集合)
│   │   │   │   │   ├── Aggregate (主机集合)
│   │   │   │   │   ├── AvailabilityZone (可用域)
│   │   │   │   │   └── index.jsx
│   │   │   │   ├── Hypervisors (虚拟机管理器)
│   │   │   │   │   ├── ComputeHost (计算节点)
│   │   │   │   │   ├── Hypervisor (虚拟机管理器)
│   │   │   │   │   └── index.jsx
│   │   │   │   ├── Image (镜像)
│   │   │   │   ├── Instance (云主机)
│   │   │   │   │   ├── Detail (详情页)
│   │   │   │   │   │   ├── BaseDetail (基础信息)
│   │   │   │   │   │   ├── SecurityGroup (安全组)
│   │   │   │   │   │   └── index.jsx
│   │   │   │   │   ├── actions (操作)
│   │   │   │   │   │   ├── AssociateFip.jsx (绑定浮动IP)
│   │   │   │   │   │   ├── AttachInterface.jsx (挂载网卡)
│   │   │   │   │   │   ├── AttachIsoVolume.jsx (挂载ISO光盘)
│   │   │   │   │   │   ├── AttachVolume.jsx (挂载云硬盘)
│   │   │   │   │   │   ├── ChangePassword.jsx (修改密码)
│   │   │   │   │   │   ├── Console.jsx (控制台)
│   │   │   │   │   │   ├── CreateImage.jsx (创建镜像)
│   │   │   │   │   │   ├── CreateIronic (创建裸机-分步型Form)
│   │   │   │   │   │   │   ├── BaseStep
│   │   │   │   │   │   │   │   └── index.jsx
│   │   │   │   │   │   │   ├── ConfirmStep
│   │   │   │   │   │   │   │   └── index.jsx
│   │   │   │   │   │   │   ├── NetworkStep
│   │   │   │   │   │   │   │   └── index.jsx
│   │   │   │   │   │   │   ├── SystemStep
│   │   │   │   │   │   │   │   └── index.jsx
│   │   │   │   │   │   │   ├── index.jsx
│   │   │   │   │   │   │   └── index.less
│   │   │   │   │   │   ├── CreateSnapshot.jsx (创建快照)
│   │   │   │   │   │   ├── Delete.jsx (删除云主机)
│   │   │   │   │   │   ├── DeleteIronic.jsx (删除裸机实例)
│   │   │   │   │   │   ├── DetachInterface.jsx (卸载网卡)
│   │   │   │   │   │   ├── DetachIsoVolume.jsx (卸载ISO镜像)
│   │   │   │   │   │   ├── DetachVolume.jsx (卸载云硬盘)
│   │   │   │   │   │   ├── DisassociateFip.jsx (解绑浮动IP)
│   │   │   │   │   │   ├── Edit.jsx (编辑云主机)
│   │   │   │   │   │   ├── ExtendRootVolume.jsx (扩容根磁盘)
│   │   │   │   │   │   ├── LiveMigrate.jsx (热迁移)
│   │   │   │   │   │   ├── Lock.jsx (锁定云主机)
│   │   │   │   │   │   ├── ManageSecurityGroup.jsx (管理安全组)
│   │   │   │   │   │   ├── Migrate.jsx (迁移)
│   │   │   │   │   │   ├── Pause.jsx (暂停云主机)
│   │   │   │   │   │   ├── Reboot.jsx (重启云主机)
│   │   │   │   │   │   ├── Rebuild.jsx (重建云主机)
│   │   │   │   │   │   ├── RebuildSelect.jsx (选镜像重建云主机)
│   │   │   │   │   │   ├── Resize.jsx (修改配置)
│   │   │   │   │   │   ├── ResizeOnline.jsx (在线修改配置)
│   │   │   │   │   │   ├── Resume.jsx (恢复云主机)
│   │   │   │   │   │   ├── Shelve.jsx (归档云主机)
│   │   │   │   │   │   ├── SoftDelete.jsx (软删除云主机)
│   │   │   │   │   │   ├── SoftReboot.jsx (软重启云主机)
│   │   │   │   │   │   ├── Start.jsx (启动云主机)
│   │   │   │   │   │   ├── StepCreate (创建云主机-分步创建)
│   │   │   │   │   │   │   ├── BaseStep
│   │   │   │   │   │   │   │   └── index.jsx
│   │   │   │   │   │   │   ├── ConfirmStep
│   │   │   │   │   │   │   │   └── index.jsx
│   │   │   │   │   │   │   ├── NetworkStep
│   │   │   │   │   │   │   │   └── index.jsx
│   │   │   │   │   │   │   ├── SystemStep
│   │   │   │   │   │   │   │   └── index.jsx
│   │   │   │   │   │   │   ├── index.jsx
│   │   │   │   │   │   │   └── index.less
│   │   │   │   │   │   ├── Stop.jsx (关闭云主机)
│   │   │   │   │   │   ├── Suspend.jsx (挂起云主机)
│   │   │   │   │   │   ├── Unlock.jsx (解锁云主机)
│   │   │   │   │   │   ├── Unpause.jsx (恢复暂停的云主机)
│   │   │   │   │   │   ├── Unshelve.jsx (恢复归档的云主机)
│   │   │   │   │   │   ├── index.jsx
│   │   │   │   │   │   └── index.less
│   │   │   │   │   ├── components (组件)
│   │   │   │   │   │   ├── FlavorSelectTable.jsx
│   │   │   │   │   │   └── index.less
│   │   │   │   │   ├── index.jsx
│   │   │   │   │   └── index.less
│   │   │   │   ├── Keypair (密钥)
│   │   │   │   └── ServerGroup (云主机组)
│   │   │   └── routes (计算菜单下的路由配置)
│   │   │   └── index.js
│   │   ├── configuration (平台配置)
│   │   │   ├── App.jsx
│   │   │   ├── containers
│   │   │   │   ├── Metadata (元数据定义)
│   │   │   │   ├── Setting (系统配置)
│   │   │   │   └── SystemInfo (系统信息)
│   │   │   └── routes (平台配置菜单下的路由配置)
│   │   │   └── index.js
│   │   ├── heat (资源编排)
│   │   │   ├── App.jsx
│   │   │   ├── containers
│   │   │   │   └── Stack (堆栈)
│   │   │   └── routes (资源编排菜单下的路由配置)
│   │   │   └── index.js
│   │   ├── identity (身份管理)
│   │   │   ├── App.jsx
│   │   │   ├── containers
│   │   │   │   ├── Domain (域)
│   │   │   │   ├── Project (项目)
│   │   │   │   ├── Role (角色)
│   │   │   │   ├── User (用户)
│   │   │   │   └── UserGroup (用户组)
│   │   │   └── routes (路由配置)
│   │   │   └── index.js
│   │   ├── management (运维管理)
│   │   │   ├── App.jsx
│   │   │   ├── containers
│   │   │   │   └── RecycleBin (回收站)
│   │   │   └── routes (路由配置)
│   │   │   └── index.js
│   │   ├── network (网络)
│   │   │   ├── App.jsx
│   │   │   ├── containers
│   │   │   │   ├── FloatingIp (浮动IP)
│   │   │   │   ├── LoadBalancers (负载均衡)
│   │   │   │   ├── Network (网络)
│   │   │   │   ├── QoSPolicy (Qos策略)
│   │   │   │   ├── Router (路由器)
│   │   │   │   ├── SecurityGroup (安全组)
│   │   │   │   ├── Topology (网络拓扑)
│   │   │   │   ├── VPN (VPN)
│   │   │   │   └── VirtualAdapter (虚拟网卡)
│   │   │   └── routes (路由配置)
│   │   │   └── index.js
│   │   ├── storage (存储)
│   │   │   ├── App.jsx
│   │   │   ├── containers
│   │   │   │   ├── Backup (备份)
│   │   │   │   ├── Snapshot (云硬盘快照)
│   │   │   │   ├── Storage (存储后端)
│   │   │   │   ├── Volume (云硬盘)
│   │   │   │   └── VolumeType (云硬盘类型)
│   │   │   │   ├── QosSpec (QoS)
│   │   │   │   ├── VolumeType (云硬盘类型)
│   │   │   │   └── index.jsx
│   │   │   └── routes ()
│   │   │   └── index.js
│   │   └── user (登录页面)
│   │   ├── App.jsx
│   │   ├── containers
│   │   │   ├── ChangePassword (修改密码--根据系统配置)
│   │   │   │   ├── index.jsx
│   │   │   │   └── index.less
│   │   │   └── Login (登录)
│   │   │   ├── index.jsx
│   │   │   └── index.less
│   │   └── routes (路由配置)
│   │   └── index.js
│   ├── resources (存放各资源的自身使用的公用函数,状态等)
│   ├── stores (数据处理,按资源类型划分文件夹)
│   │   ├── base-list.js (列表数据的基类)
│   │   ├── base.js (数据操作的基类)
│   │   ├── cinder
│   │   ├── glance
│   │   ├── heat
│   │   ├── ironic
│   │   ├── keystone
│   │   ├── neutron
│   │   ├── nova
│   │   ├── octavia
│   │   ├── overview-admin.js
│   │   ├── project.js
│   │   ├── root.js
│   │   └── skyline
│   ├── styles (公用样式)
│   │   ├── base.less
│   │   ├── main.less
│   │   ├── reset.less
│   │   └── variables.less
│   └── utils (基础函数)
│   ├── RouterConfig.jsx
│   ├── constants.js
│   ├── cookie.js
│   ├── file.js
│   ├── file.spec.js
│   ├── index.js
│   ├── index.test.js (单元测试)
│   ├── local-storage.js
│   ├── local-storage.spec.js (单元测试)
│   ├── request.js
│   ├── table.jsx
│   ├── time.js
│   ├── time.spec.js
│   ├── translate.js
│   ├── translate.spec.js
│   ├── validate.js
│   ├── yaml.js
│   └── yaml.spec.js
├── test
│   ├── e2e (E2E测试)
│   └── unit (单元测试)
├── tools
│   └── git_config
│   └── commit_message.txt
└── yarn.lock
```

View File

@ -0,0 +1,114 @@
简体中文 | [English](/docs/en/develop/3-0-how-to-develop.md)
# 开发一个新的资源列表页
- 步骤 1确认代码位置及目录结构
- 按照预想的在菜单项中的位置,放置在 Containers 下
- 以云主机为例,对应的菜单项为`计算-云主机`,那么创建文件夹`src/pages/compute/containers/Instance`,创建文件`src/pages/compute/containers/Instance/index.jsx`
- 步骤 2编写 Store 代码
- 参考[3-5-BaseStore-introduction](3-5-BaseStore-introduction.md),复写相应的函数
- 步骤 3编写列表页代码
- 参考[3-1-BaseList-introduction](3-1-BaseList-introduction.md),复写相应的函数
- 步骤 4配置路由
- 参考[3-13-Route-introduction](3-13-Route-introduction.md)
- 在步骤 1 中的父级目录的`routes/index.js`文件中,配置路由
- 如果是全新的模块,还需要在`src/pages/storage/routes/index.js`中导入
- 步骤 5配置菜单
- 参考[3-12-Menu-introduction](3-12-Menu-introduction.md)
- 配置控制台的菜单项,在`src/layouts/menu.jsx`中配置
- 配置管理平台的菜单项,在`src/layouts/admin-menu.jsx`中配置
- 步骤 6国际化
- 参考[3-14-I18n-introduction](3-14-I18n-introduction.md),完成相应翻译
- 如果,产品需求的列表页面是含有`Tab`的页面,则可参考[3-2-BaseTabList-introduction](3-2-BaseTabList-introduction.md),通常`index.jsx`内配置`Tab`,可参考镜像页面代码`src/pages/compute/containers/Image/index.jsx`
# 开发一个新的资源详情页
- 步骤 1确认代码位置及目录结构
- 按照预想的在菜单项中的位置,放置在 Containers 下
- 以云主机为例,对应的菜单项为`计算-云主机`,创建文件`src/pages/compute/containers/Instance/Detail/index.jsx``src/pages/compute/containers/Instance/Detail/BaseDetail.jsx`
- 步骤 2编写 Store 代码
- 参考[3-5-BaseStore-introduction](3-5-BaseStore-introduction.md),复写相应的函数
- 步骤 3编写详情页代码
- 参考[3-3-BaseDetail-introduction](3-3-BaseDetail-introduction.md),复写相应的函数
- 步骤 4编写详情页-详情 Tab 代码
- 参考[3-4-BaseDetailInfo-introduction](3-4-BaseDetailInfo-introduction.md),复写相应的函数
- 步骤 5配置路由
- 参考[3-13-Route-introduction](3-13-Route-introduction.md)
- 在步骤 1 中的父级目录的`routes/index.js`文件中,配置路由
- 如果是全新的模块,还需要在`src/pages/storage/routes/index.js`中导入
- 步骤 6配置菜单
- 参考[3-12-Menu-introduction](3-12-Menu-introduction.md)
- 配置控制台的菜单项,在`src/layouts/menu.jsx`中配置
- 配置管理平台的菜单项,在`src/layouts/admin-menu.jsx`中配置
- 步骤 7国际化
- 参考[3-14-I18n-introduction](3-14-I18n-introduction.md),完成相应翻译
# 开发一个新的操作
## 开发一个页面级的操作
- 步骤 1确认代码位置及目录结构
- 按照预想的在菜单项中的位置,放置在 Containers 下
- 以云硬盘为例,对应的菜单项为`存储-云硬盘-云硬盘创建`,创建文件`src/pages/storage/containers/Volume/actions/Create/index.jsx`
- 步骤 2编写 Store 代码
- 参考[3-5-BaseStore-introduction](3-5-BaseStore-introduction.md),复写或新增相应的函数
- 步骤 3编写 FormAction 代码
- 参考[3-6-FormAction-introduction](3-6-FormAction-introduction.md),复写相应的函数
- 步骤 4配置 Action
- 参考[3-11-Action-introduction](3-11-Action-introduction.md),配置到相应为位置
- 步骤 5配置路由
- 参考[3-13-Route-introduction](3-13-Route-introduction.md),配置对应的路由
- 步骤 6配置菜单
- 参考[3-12-Menu-introduction](3-12-Menu-introduction.md)
- 配置控制台的菜单项,在`src/layouts/menu.jsx`中配置
- 配置管理平台的菜单项,在`src/layouts/admin-menu.jsx`中配置
- 步骤 7国际化
- 参考[3-14-I18n-introduction](3-14-I18n-introduction.md),完成相应翻译
## 开发一个确认型的操作
- 步骤 1确认代码位置及目录结构
- 按照预想的在菜单项中的位置,放置在 Containers 下
- 以云硬盘为例,对应的菜单项为`存储-云硬盘-删除云硬盘`,创建文件`src/pages/storage/containers/Volume/actions/Delete.jsx`
- 步骤 2编写 Store 代码
- 参考[3-5-BaseStore-introduction](3-5-BaseStore-introduction.md),复写或新增相应的函数
- 步骤 3编写 ConfirmAction 代码
- 参考[3-8-ConfirmAction-introduction](3-8-ConfirmAction-introduction.md),复写相应的函数
- 步骤 4配置 Action
- 参考[3-11-Action-introduction](3-11-Action-introduction.md),配置到相应为位置
- 步骤 5国际化
- 参考[3-14-I18n-introduction](3-14-I18n-introduction.md),完成相应翻译
## 开发一个弹窗型的操作
- 步骤 1确认代码位置及目录结构
- 按照预想的在菜单项中的位置,放置在 Containers 下
- 以云硬盘为例,对应的菜单项为`存储-云硬盘-编辑`,创建文件`src/pages/storage/containers/Volume/actions/Edit.jsx`
- 步骤 2编写 Store 代码
- 参考[3-5-BaseStore-introduction](3-5-BaseStore-introduction.md),复写或新增相应的函数
- 步骤 3编写 ModalAction 代码
- 参考[3-7-ModalAction-introduction](3-7-ModalAction-introduction.md),复写相应的函数
- 步骤 4配置 Action
- 参考[3-11-Action-introduction](3-11-Action-introduction.md),配置到相应为位置
- 步骤 5国际化
- 参考[3-14-I18n-introduction](3-14-I18n-introduction.md),完成相应翻译
## 开发一个分步骤的页面级的操作
- 步骤 1确认代码位置及目录结构
- 按照预想的在菜单项中的位置,放置在 Containers 下
- 以云硬盘为例,对应的菜单项为`计算-云主机-创建`,创建文件`src/pages/compute/containers/Instance/actions/StepCreate/index.jsx`
- 步骤 2编写 Store 代码
- 参考[3-5-BaseStore-introduction](3-5-BaseStore-introduction.md),复写或新增相应的函数
- 步骤 3编写 StepAction 代码
- 参考[3-9-StepAction-introduction](3-9-StepAction-introduction.md),复写相应的函数
- 步骤 4配置 Action
- 参考[3-11-Action-introduction](3-11-Action-introduction.md),配置到相应为位置
- 步骤 5配置路由
- 参考[3-13-Route-introduction](3-13-Route-introduction.md),配置对应的路由
- 步骤 6配置菜单
- 参考[3-12-Menu-introduction](3-12-Menu-introduction.md)
- 配置控制台的菜单项,在`src/layouts/menu.jsx`中配置
- 配置管理平台的菜单项,在`src/layouts/admin-menu.jsx`中配置
- 步骤 7国际化
- 参考[3-14-I18n-introduction](3-14-I18n-introduction.md),完成相应翻译

View File

@ -0,0 +1,563 @@
简体中文 | [English](/docs/en/develop/3-1-BaseList-introduction.md)
# 用途
- 各资源列表页的基类
![列表页](/docs/zh/develop/images/list/volumes.png)
- 支持数据分页
![列表页分页](/docs/zh/develop/images/list/pagination.png)
- 支持搜索
![列表页搜索](/docs/zh/develop/images/list/search.png)
- 支持手动刷新数据
![列表页刷新](/docs/zh/develop/images/list/fresh.png)
- 支持数据下载
![列表页下载](/docs/zh/develop/images/list/download.png)
- 支持批量操作
![列表页批量](/docs/zh/develop/images/list/batch.png)
- 具有自动刷新数据的功能(每隔 60 秒自动刷新列表数据用户无操作的情况下30 分钟后不再自动刷新,可暂停自动刷新功能)
![列表页自动刷新](/docs/zh/develop/images/list/stop-auto-refresh.png)
- 可配置列表表头
![列表页自动刷新](/docs/zh/develop/images/list/hide.png)
- 各资源列表页通过复写函数即可完成
# BaseList 代码文件
- `src/containers/List/index.jsx`
# BaseList 属性与函数定义介绍
- 资源列表继承于 BaseList 组件
- 只需要复写部分函数即可完成页面的开发
- 属性与函数分为以下四种,
- 通常需要复写的属性与函数,主要包含:
- 页面的权限
- 页面的资源名称
- 表格的列的配置
- 表格的搜索项
- 表格的操作项等
- 表格对应的`store`
- 按需复写的函数与属性,主要包含:
- 资源数据分页使用前端分页还是后端分页
- 资源数据排序使用前端排序还是后端排序
- 无需复写的函数与属性,主要包含:
- 当前页是否是管理平台页面
- 当前页是否是详情页中的资源列表
- 基类中的基础函数,主要包含:
- 渲染页面
- 处理下载
- 处理自动刷新
- 隐藏/展示某些表格列
- 处理搜索
- 处理分页信息变动后的数据请求与展示
- 更详细与全面的介绍见下
## 通常需要复写的属性与函数
- `policy`:
- 必须复写该函数
- 页面对应的权限,如果权限验证不通过,则无法请求数据。
- 以云硬盘`src/pages/storage/containers/Volume/index.jsx`为例
```javascript
get policy() {
return 'volume:get_all';
}
```
- `name`
- 必须复写该函数
- 页面资源对应的名称。
- 以云硬盘`src/pages/storage/containers/Volume/index.jsx`为例
```javascript
get name() {
return t('volumes');
}
```
- `actionConfigs`
- 配置资源的各种操作
- 主按钮操作,如:创建
- 批量操作
- 每一行数据的操作
- 配置定义在资源的 actions 目录下
- 以密钥`src/pages/compute/containers/Keypair/index.jsx`为例
```javascript
import actionConfigs from './actions';
get actionConfigs() {
return actionConfigs;
}
```
- `searchFilters`
- 配置资源的搜索项
- 支持基于字符串的搜索
- 支持选择的搜索,如:基于状态的搜索
- 支持多种搜索条件均需满足的搜索
- 返回配置的数组,每个配置代表一个搜索条件
- 每个配置需要满足一下条件:
- `label`,必须项,搜索的标题
- `name`,必须项,该搜索项对应的参数`Key`
- `options`,可选项
- 如果不设置`options`属性,表示,该搜索是基于输入字符串的搜索,如:对名称的搜索
- 如设置`options`属性,则在页面内需要从`options`中选择
- `options`的格式:
- `key`: 必须项,`option`对应的值
- `label`:必须项,`option`对应的文字,即页面上看到的内容
- 以云硬盘`src/pages/storage/containers/Volume/index.jsx`为例
```javascript
get searchFilters() {
return [
{
label: t('Name'),
name: 'name',
},
{
label: t('Status'),
name: 'status',
options: ['available', 'in-use', 'error'].map((it) => ({
key: it,
label: volumeStatus[it],
})),
},
{
label: t('Shared'),
name: 'multiattach',
options: yesNoOptions,
},];
}
```
- `getColumns`
- 返回列表表格的配置信息列表
- 每个配置项的设置:
- `title`,必须项,表头的标题
- `dataIndex`,必须项,对应的后端数据的 key 值
- `hidden`,可选项,该列是否可隐藏,默认值为`false`
- `sorter`,该列是否可排序,默认可排序
- `stringify`,可选项,下载到`csv`中时,该列中数据显示的内容,因为有些列有额外的样式或是 UI 处理,会导致对该列的转字符串的结果出现问题,此时需要编写该函数
- `render`,可选项,默认是基于`dataIndex`来展示内容,使用该属性,可基于`render`的结果渲染表格内容
- `valueRender`,可选项,使用已有的函数自动处理数据
- `sinceTime`,处理时间,显示成"XX 小时前"
- `keepTime`,显示剩余时间
- `yesNo`,处理`Boolean`值,显示成“是”、“否”
- `GBValue`,处理大小,显示成"XXXGB"
- `noValue`,没有值时,显示成“-”
- `bytes`,处理大小
- `uppercase`,大写
- `formatSize`处理大小显示如“2.32 GB”“56.68 MB”
- `toLocalTime`处理时间显示如“2021-06-17 04:13:07”
- `toLocalTimeMoment`处理时间显示如“2021-06-17 04:13:07”
- `linkPrefix`,可选项,当`dataIndex=name`时,`linkPrefix`属性用于处理名称对应的链接的前缀
- 以镜像`src/pages/compute/containers/Image/Image.jsx`为例
- 表格包含的列ID/名称、项目 ID/名称(管理平台中展示)、描述、使用类型、类型、状态、可见性、硬盘格式、容量、创建于
```javascript
getColumns = () => [
{
title: t('ID/Name'),
dataIndex: 'name',
linkPrefix: `/compute/${this.getUrl('image')}/detail`,
},
{
title: t('Project ID/Name'),
dataIndex: 'project_name',
hidden: !this.isAdminPage && this.tab !== 'all',
sorter: false,
},
{
title: t('Description'),
dataIndex: 'description',
isHideable: true,
sorter: false,
},
{
title: t('Use Type'),
dataIndex: 'usage_type',
isHideable: true,
render: (value) => imageUsage[value] || '-',
sorter: false,
},
{
title: t('Type'),
dataIndex: 'os_distro',
isHideable: true,
render: (value) => <ImageType type={value} title={value} />,
width: 80,
sorter: false,
},
{
title: t('Status'),
dataIndex: 'status',
render: (value) => imageStatus[value] || '-',
},
{
title: t('Visibility'),
dataIndex: 'visibility',
render: (value) => imageVisibility[value] || '-',
sorter: false,
},
{
title: t('Disk Format'),
dataIndex: 'disk_format',
isHideable: true,
render: (value) => imageFormats[value] || '-',
},
{
title: t('Size'),
dataIndex: 'size',
isHideable: true,
valueRender: 'formatSize',
},
{
title: t('Created At'),
dataIndex: 'created_at',
isHideable: true,
valueRender: 'sinceTime',
},
];
```
- `init`
- 配置 Store 的函数,在这个函数中配置用于处理数据请求的 Store以及用于下载数据的 Store
- 通常使用的是单例的 Store但是对于某些详情页下的列表页使用`new XXXStore()`
- `init`中可配置`this.store`与`this.downloadStore`
- `this.store`用于处理列表数据
- `this.downloadStore`用于处理下载数据
- 如果使用前端页面,只配置`this.store`即可,因为是一次性获取所有的数据,下载的数据等于列表中的数据,即这时,`this.downloadStore = this.store`
- 以项目`src/pages/identity/containers/Project/index.jsx`为例
```javascript
init() {
this.store = globalProjectStore;
}
```
- 如果使用后端分页,需要分别配置`this.store`与`this.downloadStore`
- 以路由器`src/pages/network/containers/Router/index.jsx`为例
```javascript
init() {
this.store = new RouterStore();
this.downloadStore = new RouterStore();
}
```
## 按需复写的属性与函数
- `alsoRefreshDetail`
- 详情页中的列表数据刷新时,是否需要同步刷新详情数据
- 默认同步刷新,如不需要同步刷新,复写该函数
```javascript
get alsoRefreshDetail() {
return false;
}
```
- `list`
- 页面对象的 store 中的数据
- 默认值是`this.store.list`
- `rowKey`
- 列表数据的唯一标识的 Key
- 默认值是`id`
- 以密钥 Keypair `src/pages/compute/containers/Keypair/index.jsx`为例
```javascript
get rowKey() {
return 'name';
}
```
- `hasTab`
- 列表页是否是 Tab 下的列表页
- 默认值为`false`
- 会根据改值调整表格的高度
- 以`src/pages/configuration/containers/SystemInfo/Catalog.jsx`为例
```javascript
get hasTab() {
return true;
}
```
![列表页Tab](/docs/zh/develop/images/list/tab-service.png)
- `hideCustom`
- 是否显示表头配置图标
- 默认值是`true`
- 以`src/pages/configuration/containers/Setting/index.jsx`为例
```javascript
get hideCustom() {
return 'name';
}
```
- `hideSearch`
- 是否显示搜索框
- 默认显示
- 以资源编排-堆栈-详情页-日志`src/pages/heat/containers/Stack/Detail/Event.jsx`为例
```javascript
get hideSearch() {
return true;
}
```
- `hideRefresh`
- 是否显示自动刷新按钮
- 默认显示
- 如不显示,则列表不具有自动刷新数据功能
- `hideDownload`
- 是否展示下载按钮
- 默认显示
- `checkEndpoint`
- 是否需要检测 endpoint
- 默认不需要
- 某些服务可能未部署,需要二次验证,一旦检测未部署,是显示“未开放”样式页面
- 以 VPN`src/pages/network/containers/VPN/index.jsx`为例
```javascript
get checkEndpoint() {
return true;
}
```
- `endpoint`
- 当`checkEndpoint`为`true`时使用
- 以 VPN`src/pages/network/containers/VPN/index.jsx`为例
```javascript
get endpoint() {
return vpnEndpoint();
}
```
- `isFilterByBackend`
- 是否由后端分页
- 默认值是`false`,即使用前端分页
- 使用前端分页时,是一次性从后端获取全部数据,然后按页面内的页码、单页数量展示数据
- 使用后端分页时,以页面、单页数量向后端请求相应数量的数据
- 以路由器`src/pages/network/containers/Router/index.jsx`为例
```javascript
get isFilterByBackend() {
return true;
}
```
- `isSortByBackend`
- 是否由后端排序
- 默认值是`false`,即使用前端排序
- 使用前端排序时,基于列表内的数据大小排序(可自定义排序函数)
- 如果使用前端分页+前端排序,那么能基于所有数据排序
- 如果使用后端分页+前端排序,只能基于当前页的数据排序
- 使用后端分页时,按列表内设置的排序项、排序方向向后端请求数据
- 以路由器`src/pages/network/containers/Router/index.jsx`为例
```javascript
get isSortByBackend() {
return true;
}
```
- 当`isSortByBackend`设置为`true`时,通常需要重写相应`store`中的`updateParamsSortPage`函数
- 以`src/stores/neutron/router.js`为例
```javascript
updateParamsSortPage = (params, sortKey, sortOrder) => {
if (sortKey && sortOrder) {
params.sort_key = sortKey;
params.sort_dir = sortOrder === 'descend' ? 'desc' : 'asc';
}
};
```
- `adminPageHasProjectFilter`
- 管理平台的搜索项中是否包含基于项目 ID 的搜索
- 默认值为`false`
- 以云主机`src/pages/compute/containers/Instance/index.jsx`为例
```javascript
get adminPageHasProjectFilter() {
return true;
}
```
- `transitionStatusList`
- 数据处于过渡状态时对应的状态值列表
- 默认值为`[]`空列表
- 数据处于过渡状态时,页面的自动刷新会加快,变为 30 秒一次
- 默认值为`false`
- 以云硬盘`src/pages/storage/containers/Volume/index.jsx`为例
```javascript
const volumeTransitionStatuses = [
'creating',
'extending',
'downloading',
'attaching',
'detaching',
'deleting',
'backing-up',
'restoring-backup',
'awaiting-transfer',
'uploading',
'rollbacking',
'retyping',
];
get transitionStatusList() {
return volumeTransitionStatuses;
}
```
- `fetchDataByAllProjects`
- 管理平台请求数据时,是否带有`all_projects`参数
- 默认值为`true`
- 以云硬盘类型`src/pages/storage/containers/VolumeType/VolumeType/index.jsx`为例
```javascript
get fetchDataByAllProjects() {
return false;
}
```
- `fetchDataByCurrentProject`
- 控制台请求数据时,是否带有`project_id`参数
- 默认值为`false`
- 以浮动 IP`src/pages/network/containers/FloatingIp/index.jsx`为例
```javascript
get fetchDataByCurrentProject() {
return true;
}
```
- `defaultSortKey`
- 使用后端排序时,默认的排序 Key
- 以路由器`src/pages/network/containers/Router/index.jsx`为例
```javascript
get defaultSortKey() {
return 'status';
}
```
- `clearListUnmount`
- 页面切换时,是否需要情况当前 store 内的 list 数据
- 一般情况,资源列表页使用的是`GlobalXXStore`,即单例的 Store页面切换时列表数据并不会清空当回到该页面时会先展示之前的数据然后页面自动刷新获取新数据
- 默认值为`false`,页面切换时不清空数据
- `ableAutoFresh`
- 是否自动刷新
- 默认值为`true`
- `projectFilterKey`
- 请求时project 对应的 key 值
- 默认值是`project_id`
- 以镜像`src/pages/compute/containers/Image/Image.jsx`为例
```javascript
get projectFilterKey() {
return 'owner';
}
```
- `getCheckboxProps`
- 列表内的数据是否可选择,选中后可进行批量操作
- 默认都可选择
- 以云主机`src/pages/compute/containers/Instance/index.jsx`为例
- 裸机实例不可被选择
```javascript
getCheckboxProps(record) {
return {
disabled: isIronicInstance(record),
name: record.name,
};
}
```
- `getData`
- 处理数据请求的函数
- 默认使用`store.fetchList`或`store.fetchListByPage`方法从服务端获取数据
- 不建议复写该函数
- `fetchDataByPage`
- 采用后端分页时,处理数据请求的函数
- 默认使用`store.fetchListByPage`方法获取数据
- 不建议复写该函数
- `fetchData`
- 采用前端分页时,处理数据请求的函数
- 默认使用`store.fetchList`方法获取数据
- 不建议复写该函数
- `updateFetchParamsByPage`
- 采用后端分页时,在基类的基础上调整请求参数的函数
- 如果基类的默认参数无法满足请求时,建议通过复写该函数,并同步修改对应的`store`中的`listDidFetch`方法以完成数据请求
- `updateFetchParams`
- 采用前端分页时,在基类的基础上调整请求参数的函数
- 如果基类的默认参数无法满足请求时,建议通过复写该函数,并同步修改对应的`store`中的`listDidFetch`方法以完成数据请求
- `updateHints`
- 表格上放的提示语
## 不需要复写的属性与函数
- `isInDetailPage`
- 标识当前页面是否为详情页下的列表页
- `location`
- 页面的路由信息
- `isAdminPage`
- 当前页面是否是“管理平台”的页面
- `hasAdminRole`
- 登录的用户角色是否具有管理员角色
- `getUrl`
- 生成页面 Url 的函数
- 如:需要给列表页的关联资源提供跳转功能,使用该函数,可以在控制台跳转到控制台的相应地址,在管理平台跳转到管理平台的相应地址
- `params`
- 路由带有的参数信息
- 一般用于生成页面请求 API 时的参数
- `routing`
- 页面对应的路由信息
- `isLoading`
- 当前页面是否在数据更新,更新时会显示 loading 样式
- `endpointError`
- 判定 Endpoint 是否有效
- `hintHeight`
- 页面内的提示的高度
- `tableTopHeight`
- 表格上方占用的高度
- 基于提示、Tab 计算
- `tableHeight`
- 表格的高度
- `currentProjectId`
- 当前登录的用户所属的项目 ID
- `defaultSortOrder`
- 使用后端排序时,默认的排序方向为降序`descend`
- `itemInTransitionFunction`
- 判定是否有数据处于过渡状态,如果有数据处于过渡状态,则自动刷新数据的时间间隔由 60 秒变为 30 秒
- `primaryActions`
- 主按钮操作列表
- `batchActions`
- 批量操作列表
- `itemActions`
- 每一行数据对应的操作列表
## 基类中的基础函数
- 建议查看代码理解,`src/containers/List/index.jsx`

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,141 @@
简体中文 | [English](/docs/en/develop/3-11-Action-introduction.md)
# 用途
- 配置资源对应的所有操作
![操作](/docs/zh/develop/images/form/action.png)
- 按照相应的配置编写后,会在资源列表页相应的位置展示相应的操作按钮
# 代码位置
- `pages/xxxx/containers/XXXX/actions/index.jsx`
# 如何使用
- 返回一个对象,其内配置主操作按钮、批量操作按钮、行操作按钮
- 以网络`src/pages/network/containers/Network/actions/index.jsx`为例
- 配置了主按钮为创建
- 配置了批量操作为删除
- 配置了行操作为编辑、创建子网、删除
```javascript
import CreateNetwork from './CreateNetwork';
import CreateSubnet from './CreateSubnet';
import DeleteAction from './Delete';
import Edit from './Edit';
const actionConfigs = {
rowActions: {
firstAction: Edit,
moreActions: [
{
action: CreateSubnet,
},
{
action: DeleteAction,
},
],
},
batchActions: [DeleteAction],
primaryActions: [CreateNetwork],
};
export default actionConfigs;
```
- 在资源对应的列表代码中配置`actionConfigs`即可
- 以网络`src/pages/network/containers/Network/ProjectNetwork.jsx`为例
```javascript
import actionConfigs from './actions';
get actionConfigs() {
return actionConfigs;
}
```
## 主操作按钮配置`primaryActions`
- 返回组件的列表
- 如果没有主按钮,可以设置为`null`或`[]`
- 如果不可操作(比如权限不够),将自动隐藏
## 批量操作按钮配置`batchActions`
- 返回组件的列表
- 如果没有主按钮,可以设置为`null`或`[]`
- 如果不可操作(比如权限不够),将自动隐藏
## 行操作按钮配置`rowActions`
- 返回一个对象,内含`firstAction`, `moreActions`对应的组件
- 批量操作按钮如果被禁用(比如)
- 可以返回一个空对象`{}`
- `firstAction`,行操作对应的第一个按钮
- 如果不可操作,按钮灰掉
- 可以是一个组件
- 可以是`null`
- 以系统信息-网络`src/pages/configuration/containers/SystemInfo/NeutronAgent/actions/index.jsx`为例
```javascript
import Enable from './Enable';
import Disable from './Disable';
const actionConfigs = {
rowActions: {
firstAction: null,
moreActions: [
{
action: Enable,
},
{
action: Disable,
},
],
},
batchActions: [],
primaryActions: [],
};
export default actionConfigs;
```
- `moreActions``更多`按钮下对应的操作组件
- 操作的数组
- 其内的操作如果不可用,将直接隐藏该操作按钮
- 支持两种格式的配置,对应了不同的展示方案
- 每个元素是个含有`action`属性的对象
![云硬盘操作](/docs/zh/develop/images/form/volume-action.png)
- 每个元数是个含有`title`、`actions`属性的对象
![云主机操作](/docs/zh/develop/images/form/instance-action.png)
- 以云主机`src/pages/compute/containers/Instance/actions/index.jsx`为例
```javascript
const statusActions = [
StartAction,
StopAction,
LockAction,
UnlockAction,
RebootAction,
SoftRebootAction,
SuspendAction,
ResumeAction,
PauseAction,
UnpauseAction,
Shelve,
Unshelve,
];
const actionConfigs = {
rowActions: {
firstAction: Console,
moreActions: [
{
title: t('Instance Status'),
actions: statusActions,
},...}}
```

View File

@ -0,0 +1,110 @@
简体中文 | [English](/docs/en/develop/3-12-Menu-introduction.md)
# 用途
- 点击后直接跳转到相应页面
- 配置控制台的左侧菜单项
![控制台](/docs/zh/develop/images/menu/console-menu.png)
- 配置管理平台的左右菜单项
![管理平台](/docs/zh/develop/images/menu/admin-menu.png)
- 支持一级菜单带图标
- 支持二级菜单展开
- 支持路由变更后菜单选中项自动切换
- 支持右侧内容中面包屑的自动处理
# 代码位置
- 控制台的菜单配置`src/layouts/menu.jsx`
- 管理平台的菜单配置`src/layouts/admin-menu.jsx`
# 如何使用
- 控制台与管理平台的菜单配置,采用相同的配置结构
- 返回一个 renderMenu 函数,函数返回一个配置数组
## 一级菜单的配置
- `path`
- 一级菜单对应的路由
- `name`
- 一级菜单对应的名称
- 菜单项中显示的名称
- 面包屑中一级菜单对应的名称
- `key`
- 一级菜单对应的 ID 值
- 要求具有唯一性
- `icon`
- 一级菜单对应的图标
- 菜单完全展开时,显示图标与名称
- 菜单折叠时,只显示图标
- `hasBreadcrumb`
- 页面是否展示面包屑
- 默认展示面包屑
- 以首页为例,`hasBreadcrumb: false`
- `hasChildren`
- 一级菜单是否含有子菜单
- 默认值为`true`
- 一级菜单可以不包含二级菜单,以`首页`为例
```javascript
{
path: '/base/overview',
name: t('Home'),
key: '/home',
icon: <HomeOutlined />,
hasBreadcrumb: false,
hasChildren: false,
}
```
- 一级菜单默认包含二级菜单,以`计算`为例
```javascript
{
path: '/compute',
name: t('Compute'),
key: '/compute',
icon: <DesktopOutlined />,
children: [...]
}
```
## 二级菜单的配置
- 二级菜单配置在一级菜单的`children`中
- 详情页、创建页面等不需要展示在菜单项中的页面,配置在二级菜单的`children`中
- 以云主机类型为例
```javascript
{
path: '/compute/flavor',
name: t('Flavor'),
key: '/compute/flavor',
level: 1,
children: [
{
path: /^\/compute\/flavor\/detail\/.[^/]+$/,
name: t('Flavor Detail'),
key: 'flavor-detail',
level: 2,
},
],
},
```
- `path`
- 菜单对应的路由
- `name`
- 菜单对应的名称
- 菜单项中显示的名称
- 面包屑中菜单对应的名称
- `key`
- 菜单对应的 ID 值
- 要求具有唯一性
- `level`
- 二级菜单的`level=1`
- 二级菜单的`children`中的菜单配置`level=2`

View File

@ -0,0 +1,72 @@
简体中文 | [English](/docs/en/develop/3-13-Route-introduction.md)
# 用途
- 需要独立展示的页面均需要配置路由
- 按产品的需求,菜单下的二级页面,均需要以整页方式展示,如`计算-云主机`
- 资源列表页
- 如,云主机列表页
- 注意,详情页下的相关资源列表页不需要配置路由
- 资源详情页
- 如,云主机详情页
- 整页展示的 Form 表单
- 如,创建云主机
- 某些菜单只有一级页面,如`首页`,也需要配置路由
# 如何使用
## 二级菜单对应的路由
- 按[目录介绍](2-catalog-introduction.md)中的要求,每个菜单一级页面,在`pages`下有一个独立的文件夹,其内的`containers文件夹`放置二级页面代码,`routes文件夹`配置路由
- 配置写在`pages/xxxx/routes/index.js`中
- 路由的配置需要遵守固定格式,可参考`src/pages/compute/routes/index.js`
- 是个数组
- 数组中的每个元素,要求
- `path`, 一级菜单对应的名称,如`计算`用`compute`
- `component`,布局组件
- `auth`相关的页面,如登录,使用的是`src/layouts/User/index.jsx`组件
- 登录后展示的页面,如云主机、页面等,使用的是`src/layouts/Base/index.jsx`组件
- 该布局自动处理菜单项的展示、右侧内容头部的`header`展示、右侧内容的面包屑等
- `routes`,配置的主体内容,是个数组,每个元素要求
- 以计算的路由配置`src/pages/compute/routes/index.js`为例
```javascript
{ path: `${PATH}/instance`, component: Instance, exact: true },
```
- `path`, 每个整页页面对应的路径,如`compute/instance`
- `component`,页面对应的组件,即`containers`下的组件
- 对于资源型的页面,一般会配置
- 控制台访问的列表页、详情页、复杂的创建页(简单的创建一般使用弹出窗即可)
- 管理平台访问的列表页、详情页(`path`中要求包含`-admin`或`_admin`
- 对于详情页,我们推荐使用`id`参数项
- 以云主机`src/pages/compute/routes/index.js`为例
```javascript
{ path: `${PATH}/instance`, component: Instance, exact: true },
{ path: `${PATH}/instance-admin`, component: Instance, exact: true },
{
path: `${PATH}/instance/detail/:id`,
component: InstanceDetail,
exact: true,
},
{
path: `${PATH}/instance-admin/detail/:id`,
component: InstanceDetail,
exact: true,
},
{ path: `${PATH}/instance/create`, component: StepCreate, exact: true },
```
## 一级菜单对应的路由
- 一级菜单需要添加在`src/core/routes.js`
- 以计算为例
```javascript
{
path: '/compute',
component: Compute,
},
```

View File

@ -0,0 +1,41 @@
简体中文 | [English](/docs/en/develop/3-14-I18n-introduction.md)
# 用途
- 框架支持国际化,默认支持英文、中文
![i18n](/docs/zh/develop/images/i18n/i18n.png)
![english](/docs/zh/develop/images/i18n/english.png)
# 代码位置
- `src/locales/index.js`
- 英文:`src/locales/en.json`
- 中文:`src/locales/zh.json`
# 如何使用
- 代码中的需要国际化展示的字符串均使用英文,使用命令行完成字符采集后,无需更新 en.json 文件,只需要修改 zh.json 中对应的中文即可完成国际化的操作
- 对于需要国际化的字符串,使用`t`函数即可
- 以`云主机`为例,对应的国际化写法为`t('instance')`
- 注意,英文是大小写相关的
- `t`函数支持带有参数的字符串
- 参数使用`{}`标识,如
```javascript
confirmContext = () =>
t('Are you sure to { action }?', {
action: this.actionName || this.title,
});
```
- 采集
```shell
yarn run i18n
```
- 采集后,`en.json`与`zh.json`文件会自动更新
- 更新中文
- 采集后,直接在`zh.json`中更新相应的中文翻译即可

View File

@ -0,0 +1,123 @@
简体中文 | [English](/docs/en/develop/3-2-BaseTabList-introduction.md)
# 用途
- 各可切换列表页的基类
![Tab列表页](/docs/zh/develop/images/list/tab-list.png)
- 支持切换时自动处理数据展示
# BaseTabList 代码文件
- `src/containers/TabList/index.jsx`
# BaseTabList 属性与函数定义介绍
- 带有 Tab 切换的资源列表继承于 BaseTabList
- 只需要复写部分函数即可完成页面的开发
- 属性与函数分为以下四种,
- 需要复写的属性与函数,主要包含:
- 页面内的`Tab`配置
- 按需复写的函数与属性,主要包含:
- 页面内的权限配置
- 无需复写的函数与属性,主要包含:
- 当前页是否是管理平台页面
- 基类中的基础函数,主要包含:
- 渲染页面
- 处理切换 Tab 时的路由变动
- 更详细与全面的介绍见下
## 需要复写的属性与函数
- `tabs`:
- 需要复写该函数
- 用于配置页面内的 Tab
- 每个 Tab 的配置项:
- `title`Tab 标签上的标题
- `key`,每个 Tab 的唯一标识
- `component`,每个 Tab 对应的组件,基本都是继承于`BaseList`的资源列表组件
- 返回 Tab 配置的列表
- 页面默认显示 Tab 列表中的第一个`component`
- 以镜像`src/pages/compute/containers/Image/index.jsx`为例
```javascript
get tabs() {
const tabs = [
{
title: t('Current Project Image'),
key: 'project',
component: Image,
},
{
title: t('Public Image'),
key: 'public',
component: Image,
},
{
title: t('Shared Image'),
key: 'shared',
component: Image,
},
];
if (this.hasAdminRole) {
tabs.push({
title: t('All Image'),
key: 'all',
component: Image,
});
}
return tabs;
}
```
## 按需复写的属性与函数
- 以下涉及到的属性与函数,一般均不需要配置
- 目前只在 VPN 页面(`src/pages/network/containers/VPN/index.jsx`)使用,该页面使用这些配置判定权限,以及判定失败时展示使用
- `name`
- 整个 Tab 页面的全称
- 以 VPN `src/pages/network/containers/VPN/index.jsx`为例
```javascript
get name() {
return t('VPN');
}
```
- `checkEndpoint`
- 是否需要验证该页面对应服务的 endpoint
- 默认值是`false`
- 以 VPN `src/pages/network/containers/VPN/index.jsx`为例
```javascript
get checkEndpoint() {
return true;
}
```
- `endpoint`
- 该页面对应服务的 endpoint
- 仅在`checkEndpoint=true`时有用
- 以 VPN `src/pages/network/containers/VPN/index.jsx`为例
```javascript
get endpoint() {
return vpnEndpoint();
}
```
## 不需要复写的属性与函数
- `location`
- 页面的路由信息
- `isAdminPage`
- 当前页面是否是“管理平台”的页面
- `hasAdminRole`
- 登录的用户角色是否具有管理员角色
- `getUrl`
- 生成页面 Url 的函数
- 如:需要给列表页的关联资源提供跳转功能,使用该函数,可以在控制台跳转到控制台的相应地址,在管理平台跳转到管理平台的相应地址
## 基类中的基础函数
- 建议查看代码理解,`src/containers/TabList/index.jsx`

View File

@ -0,0 +1,259 @@
简体中文 | [English](/docs/en/develop/3-3-BaseDetail-introduction.md)
# 用途
![详情页](/docs/zh/develop/images/detail/volume.png)
- 各资源详情页的基类
- 支持返回列表页
- 支持与列表页一致的数据操作
- 支持详情页头部的展示与折叠
- 支持基于 Tab 形式展示的基本信息与相关资源信息
- 支持上下分层的展示方案
- 需要复写部分函数即可完成页面的开发
# BaseDetail 代码文件
- `src/containers/TabDetail/index.jsx`
# BaseDetail 属性与函数定义介绍
- 资源详情继承于 BaseDetail 组件
- 代码位置:`pages/xxxx/containers/XXXX/Detail/index.jsx`
- 只需要复写部分函数即可完成页面的开发
- 属性与函数分为以下四种,
- 通常需要复写的属性与函数,主要包含:
- 详情页的权限
- 详情页的资源名称
- 详情页对应的列表页
- 详情页的操作配置
- 详情页的上方信息配置
- 详情页的下方 Tab 页面配置
- 详情页对应的`store`
- 按需复写的函数与属性,主要包含:
- 详情页操作对应的数据
- 获取详情数据的参数
- 获取详情数据的函数
- 无需复写的函数与属性,主要包含:
- 当前页是否是管理平台页面
- 基类中的基础函数,主要包含:
- 渲染页面
- 下方列表页数据变动时详情数据的自动刷新
- 折叠/展开头部信息
- 更详细与全面的介绍见下
## 通常需要复写的属性与函数
- `policy`:
- 必须复写该函数
- 页面对应的权限,如果权限验证不通过,则无法请求数据。
- 以云硬盘`src/pages/storage/containers/Volume/Detail/index.jsx`为例
```javascript
get policy() {
return 'volume:get';
}
```
- `name`
- 必须复写该函数
- 页面资源对应的名称。
- 以云硬盘`src/pages/storage/containers/Volume/Detail/index.jsx`为例
```javascript
get name() {
return t('volume');
}
```
- `listUrl`
- 该详情页对应的资源列表页
- 以云硬盘`src/pages/storage/containers/Volume/Detail/index.jsx`为例
```javascript
get listUrl() {
return this.getUrl('/storage/volume');
}
```
- `actionConfigs`
- 配置资源的各种操作
- 对数据的操作
- 配置定义在资源的 actions 目录下
- 一般直接使用与资源列表页相一致的配置即可
- 以密钥`src/pages/compute/containers/Keypair/Detail/index.jsx`为例
```javascript
import actionConfigs from '../actions';
get actionConfigs() {
return actionConfigs;
}
```
- `detailInfos`
- 详情页上方的信息
- 是一个配置列表
- 每个配置
- `title`,必须项,标题
- `dataIndex`,必须项,对应于数据的 Key
- `render`,可选项,默认是基于`dataIndex`来展示内容,使用该属性,可基于`render`的结果渲染表格内容
- `valueRender`,可选项,基于`dataIndex`及`valueRender`生成展示数据
- `sinceTime`,处理时间,显示成"XX 小时前"
- `keepTime`,显示剩余时间
- `yesNo`,处理`Boolean`值,显示成“是”、“否”
- `GBValue`,处理大小,显示成"XXXGB"
- `noValue`,没有值时,显示成“-”
- `bytes`,处理大小
- `uppercase`,大写
- `formatSize`处理大小显示如“2.32 GB”“56.68 MB”
- `toLocalTime`处理时间显示如“2021-06-17 04:13:07”
- `toLocalTimeMoment`处理时间显示如“2021-06-17 04:13:07”
- 以云硬盘`src/pages/storage/containers/Volume/Detail/index.jsx`为例
```javascript
get detailInfos() {
return [
{
title: t('Name'),
dataIndex: 'name',
},
{
title: t('Description'),
dataIndex: 'description',
},
{
title: t('Shared'),
dataIndex: 'multiattach',
valueRender: 'yesNo',
},
{
title: t('Status'),
dataIndex: 'status',
render: (value) => volumeStatus[value] || '-',
},
{
title: t('Size'),
dataIndex: 'size',
},
{
title: t('Created At'),
dataIndex: 'created_at',
valueRender: 'toLocalTime',
},
{
title: t('Type'),
dataIndex: 'volume_type',
},
{
title: t('Encrypted'),
dataIndex: 'encrypted',
valueRender: 'yesNo',
},
];
}
```
- `tabs`
- 详情页下方的 Tab 配置
- 每个 Tab 的配置项:
- `title`Tab 标签上的标题
- `key`,每个 Tab 的唯一标识
- `component`,每个 Tab 对应的组件,基本都是继承于`BaseList`的资源列表组件
- 返回 Tab 配置的列表
- 页面默认显示 Tab 列表中的第一个`component`
- 通常,基础信息继承于`BaseDetail`类
- 通常,详情页中的资源列表页直接复用资源列表即可,只需同步处理下列表页内的参数请求即可
- 以云硬盘详情页中的备份列表`src/pages/storage/containers/Backup/index.jsx`为例
```javascript
updateFetchParamsByPage = (params) => {
if (this.isInDetailPage) {
const { id, ...rest } = params;
return {
volume_id: id,
...rest,
};
}
return params;
};
```
- 以云硬盘`src/pages/storage/containers/Volume/Detail/index.jsx`为例
```javascript
get tabs() {
const tabs = [
{
title: t('Detail'),
key: 'base',
component: BaseDetail,
},
{
title: t('Backup'),
key: 'backup',
component: Backup,
},
{
title: t('Snapshot'),
key: 'snapshot',
component: Snapshot,
},
];
return tabs;
}
```
- `init`
- 配置 Store 的函数,在这个函数中配置用于处理数据请求的 Store
- 一般使用的是`new XXXStore()`形式
- 以云硬盘`src/pages/storage/containers/Volume/Detail/index.jsx`为例
```javascript
init() {
this.store = new VolumeStore();
}
```
## 按需复写的属性与函数
- `fetchData`
- 详情页中的获取数据的函数
- 不建议重写该方法
- 默认使用`this.store.fetchDetail`获取数据
- `updateFetchParams`
- 更新数据请求的参数
- 一般配合 store 中的`detailDidFetch`使用
- 以云主机`src/pages/compute/containers/Instance/Detail/index.jsx`为例
```javascript
updateFetchParams = (params) => ({
...params,
isRecycleBinDetail: this.isRecycleBinDetail,
});
```
## 不需要复写的属性与函数
- `params`
- 路由带有的参数信息
- 一般用于生成页面请求 API 时的参数
- `id`
- 路由信息中的`id`
- `isAdminPage`
- 当前页面是否是“管理平台”的页面
- `getUrl`
- 生成页面 Url 的函数
- 如:需要给关联资源提供跳转功能,使用该函数,可以在控制台跳转到控制台的相应地址,在管理平台跳转到管理平台的相应地址
- `routing`
- 页面对应的路由信息
- `isLoading`
- 当前页面是否在数据更新,更新时会显示 loading 样式
- `tab`
- 当前展示的下方 Tab 页面信息
- `detailData`
- 页面内展示的数据信息
- 来源于`this.store.detail`
## 基类中的基础函数
- 建议查看代码理解,`src/containers/TabDetail/index.jsx`

View File

@ -0,0 +1,144 @@
简体中文 | [English](/docs/en/develop/3-4-BaseDetailInfo-introduction.md)
# 用途
![详情信息页](/docs/zh/develop/images/detail/image-detail-info.png)
- 各资源详情页中详情 Tab 中组件的基类
- 左右结构展示
- 以 Card 的形式展示
- 以配置 Card 的方式即可完成页面内容的展示
# BaseDetailInfo 代码文件
- `src/containers/BaseDetail/index.jsx`
# BaseDetailInfo 属性与函数定义介绍
- 资源详情信息继承于 BaseDetailInfo
- 代码位置:`pages/xxxx/containers/XXXX/Detail/BaseDetail.jsx`
- 只需要复写部分函数即可完成页面的开发
- 属性与函数分为以下四种,
- 通常需要复写的属性与函数,主要包含:
- 左侧的 Card 列表
- 按需复写的函数与属性,主要包含:
- 右侧的 Card 列表
- 获取数据的函数
- 展示数据的来源
- 无需复写的函数与属性,主要包含:
- 当前页是否是管理平台页面
- 基类中的基础函数,主要包含:
- 渲染页面
- 更详细与全面的介绍见下
## Card 的配置
- 页面中左侧、右侧的 Card 均采用相同的配置方式
- 每个 Card 的配置如下,
- `title`必须项Card 的标题
- `titleHelp`, 可选项Card 的标题旁显示的提示信息
- `render`,可选项,如果存在,则基于`render`渲染 Card 的内容
- `options`可选项Card 中每行的配置列表,每个 option 配置如下,
- `label`,必须项,行中的标签
- `dataIndex`,必须项,对应于`this.detailData`中的 key默认是基于`dataIndex`展示行内的数据
- `render`,可选项,可基于`render`的结果渲染行内的内容
- `valueRender`,可选项,基于`dataIndex`及`valueRender`生成行内的展示数据
- `sinceTime`,处理时间,显示成"XX 小时前"
- `keepTime`,显示剩余时间
- `yesNo`,处理`Boolean`值,显示成“是”、“否”
- `GBValue`,处理大小,显示成"XXXGB"
- `noValue`,没有值时,显示成“-”
- `bytes`,处理大小
- `uppercase`,大写
- `formatSize`处理大小显示如“2.32 GB”“56.68 MB”
- `toLocalTime`处理时间显示如“2021-06-17 04:13:07”
- `toLocalTimeMoment`处理时间显示如“2021-06-17 04:13:07”
- `copyable`,可选项,该行内的数据是否可复制,如可复制,会显示复制 icon
- 以密钥`src/pages/compute/containers/Keypair/Detail/BaseDetail.jsx`为例
```javascript
get keypairInfoCard() {
const options = [
{
label: t('Fingerprint'),
dataIndex: 'fingerprint',
},
{
label: t('Public Key'),
dataIndex: 'public_key',
copyable: true,
},
{
label: t('User ID'),
dataIndex: 'user_id',
},
];
return {
title: t('Keypair Info'),
options,
};
}
```
## 通常需要复写的属性与函数
- `leftCards`:
- 必须复写该函数
- 左侧展示的 Card 列表
- 以镜像`src/pages/compute/containers/Image/Detail/BaseDetail.jsx`为例
```javascript
get leftCards() {
const cards = [this.baseInfoCard, this.securityCard];
return this.isImageDetail ? cards : [this.InstanceCard, ...cards];
}
```
- `init`
- 配置 Store 的函数,在这个函数中配置用于处理数据请求的
Store如果配置了该函数则会在展示该页面时发起数据请求但是有时展示该页面时并不需要额外请求只需要使用`this.props.detail`即可
- 一般使用的是`new XXXStore()`形式
- 以镜像`src/pages/compute/containers/Image/Detail/BaseDetail.jsx`为例
```javascript
init() {
this.store = new ImageStore();
}
```
## 按需复写的属性与函数
- `rightCards`
- 右侧展示的 Card 列表
- `fetchData`
- 获取 Card 数据的函数
- 一般不需要复写该函数
- `detailData`
- 页面 Card 的数据来源
- 默认是`this.props.detail || toJS(this.store.detail)`
- 一般不需要复写该函数
- 以云硬盘的 Qos`src/pages/storage/containers/VolumeType/QosSpec/Detail/index.jsx`为例
```javascript
get detailData() {
return this.store.detail.qos_specs;
}
```
## 不需要复写的属性与函数
- `id`
- 路由信息中的`id`
- `isAdminPage`
- 当前页面是否是“管理平台”的页面
- `getUrl`
- 生成页面 Url 的函数
- 如:需要给关联资源提供跳转功能,使用该函数,可以在控制台跳转到控制台的相应地址,在管理平台跳转到管理平台的相应地址
- `routing`
- 页面对应的路由信息
- `isLoading`
- 当前页面是否在数据更新,更新时会显示 loading 样式
## 基类中的基础函数
- 建议查看代码理解,`src/containers/BaseDetail/index.jsx`

View File

@ -0,0 +1,554 @@
简体中文 | [English](/docs/en/develop/3-5-BaseStore-introduction.md)
# 用途
- 数据请求的处理
- 支持获取全部数据
- 支持分页获取数据
- 支持对数据的各种请求处理(PUT、POST、GET、PATCH、DELETE、HEAD 等)
# BaseStore 代码文件
- `src/stores/base.js`
# BaseStore 属性与函数定义介绍
- 资源数据的 Store 继承于 BaseStore 类
- 代码位置:`src/stores/xxx/xxx.js`,如云主机对应的 store 在`src/stores/nova/instance.js`
- 只需要复写部分函数即可完成数据的请求操作
- 属性与函数分为以下四种,
- 通常需要复写的属性与函数,主要包含:
- 与生成 url 相关的属性与函数
- 按需复写的函数与属性,主要包含:
- 列表数据的再加工
- 详情数据的再加工
- 请求参数的处理
- url 的处理
- 无需复写的函数与属性,主要包含:
- 清空数据
- 封装数据时对项目信息的处理
- 基类中的基础函数,主要包含:
- 处理分页数据的`marker`
- 更详细与全面的介绍见下
## 名词说明
- 前端分页
- 一次性从后端获取所有列表数据
- 前端基于获取到的数据总量、页面内配置的当前页数、每页数量来展示数据(`BaseList`组件处理)
- 后端分页
- 以当前页号、每页数量向后端请求数据
- 前端排序
- 使用前端分页时,按设定的排序信息对所有数据排序
- 使用后端分页时,按设定的排序信息对当前页内的数据排序
- 后端排序
- 以当前页号、每页数量、当前排序信息向后端请求数据
- 不存在前端分页+后端排序这种组合方式
## 通常需要复写的属性与函数
- `module`:
- 必须复写该函数
- 资源对应的模块
- 该函数用于生成请求的 url
- 以云主机`src/stores/nova/instance.js`为例
```javascript
get module() {
return 'servers';
}
```
- `apiVersion`
- 必须复写该函数
- 资源对应的 api 前缀
- 因所有的请求需要由服务端转发所以api 的前缀需要基于`profile`内的信息生成
- 以云主机`src/stores/nova/instance.js`为例
```javascript
get apiVersion() {
return novaBase();
}
```
- `responseKey`
- 必须复写该函数
- 用于生成数据返回的 key创建的 key 等
- 以云主机`src/stores/nova/instance.js`为例
```javascript
get responseKey() {
return 'server';
}
```
![请求](/docs/zh/develop/images/store/response-key.png)
## 按需复写的属性与函数
- `listDidFetch`
- 列表数据二次加工使用的函数
- 可请求其他 API 后,整合数据
- 可过滤数据
- 请求某个指定云硬盘的快照列表时,可以基于`filters`中的参数再次过滤数据
- 以云硬盘快照`src/stores/cinder/snapshot.js`为例
```javascript
async listDidFetch(items, allProjects, filters) {
if (items.length === 0) {
return items;
}
const { id } = filters;
const datas = id ? items.filter((it) => it.volume_id === id) : items;
return datas;
}
```
- 如果需要显示加密信息,需要发起额外请求后,整合数据
- 以云硬盘类型`src/stores/cinder/volume-type.js`为例
```javascript
async listDidFetch(items, allProjects, filters) {
const { showEncryption } = filters;
if (items.length === 0) {
return items;
}
if (!showEncryption) {
return items;
}
const promiseList = items.map((i) =>
request.get(`${this.getDetailUrl({ id: i.id })}/encryption`)
);
const encryptionList = await Promise.all(promiseList);
const result = items.map((i) => {
const { id } = i;
const encryption = encryptionList.find((e) => e.volume_type_id === id);
return {
...i,
encryption,
};
});
return result;
}
```
- `detailDidFetch`
- 详情数据二次加工使用的函数
- 可请求其他 API 后,整合数据
- 以云硬盘快照`src/stores/cinder/snapshot.js`为例
```javascript
async detailDidFetch(item) {
const { volume_id } = item;
const volumeUrl = `${cinderBase()}/${
globals.user.project.id
}/volumes/${volume_id}`;
const { volume } = await request.get(volumeUrl);
item.volume = volume;
return item;
}
```
- `listResponseKey`
- 列表数据的返回 Key
- 默认是`${this.responseKey}s`
- 以云硬盘快照`src/stores/cinder/snapshot.js`为例
```javascript
get responseKey() {
return 'snapshot';
}
get listResponseKey() {
return 'volume_snapshots';
}
```
- `getListUrl`
- 请求数据使用的 url
- 前端分页请求列表数据(即一次性获取所有数据)时,优先使用`this.getListDetailUrl()`
- 后端分页请求列表数据时,按优先级`this.getListPageUrl()` > `this.getListDetailUrl()` > `this.getListUrl()`
- 默认值为
```javascript
getListUrl = () => `${this.apiVersion}/${this.module}`;
```
- 以 Heat 的堆栈的日志`src/stores/heat/event.js`为例
```javascript
getListUrl = ({ id, name }) =>
`${this.apiVersion}/${this.module}/${name}/${id}/events`;
```
- `getListDetailUrl`
- 请求数据使用的 url
- 前端分页请求列表数据(即一次性获取所有数据)时,优先使用`this.getListDetailUrl()`
- 后端分页请求列表数据时,按优先级`this.getListPageUrl()` > `this.getListDetailUrl()` > `this.getListUrl()`
- 默认值为
```javascript
getListDetailUrl = () => '';
```
- 以云硬盘`src/stores/cinder/volume.js`为例
```javascript
getListDetailUrl = () => `${skylineBase()}/extension/volumes`;
```
- `getListPageUrl`
- 后端分页数据使用的 url
- 后端分页请求列表数据时,按优先级`this.getListPageUrl()` > `this.getListDetailUrl()` > `this.getListUrl()`
- 默认值为
```javascript
getListPageUrl = () => '';
```
- 以云硬盘`src/stores/cinder/volume.js`为例
```javascript
getListPageUrl = () => `${skylineBase()}/extension/volumes`;
```
- `getDetailUrl`
- 详情数据对应的 url
- 使用 rest 风格的 API所以该 url 也是 put, delete, patch 对应的 url
- 默认值为
```javascript
getDetailUrl = ({ id }) => `${this.getListUrl()}/${id}`;
```
- 以堆栈`src/stores/heat/stack.js`为例
```javascript
getDetailUrl = ({ id, name }) => `${this.getListUrl()}/${name}/${id}`;
```
- `needGetProject`
- 对服务端返回的数据是否需要二次加工其中的项目信息
- 一般 Openstack API 返回的数据只有`project_id`信息,按页面展示的需求,在管理平台需要展示项目名称
- 默认值是`true`
- 以元数据`src/stores/glance/metadata.js`为例
```javascript
get needGetProject() {
return false;
}
```
- `mapper`
- 对服务端返回的列表、详情数据做二次加工
- 一般是为了更便捷的在资源列表、资源详情中展示数据使用
- 默认值为
```javascript
get mapper() {
return (data) => data;
}
```
- 以云硬盘`src/stores/cinder/volume.js`为例
```javascript
get mapper() {
return (volume) => ({
...volume,
disk_tag: isOsDisk(volume) ? 'os_disk' : 'data_disk',
description: volume.description || (volume.origin_data || {}).description,
});
}
```
- `mapperBeforeFetchProject`
- 在处理项目信息前,对服务端返回的列表、详情数据做二次加工
- 一般是为了处理返回数据中的项目信息使用
- 默认值为
```javascript
get mapperBeforeFetchProject() {
return (data) => data;
}
```
- 以镜像`src/stores/glance/image.js`为例
```javascript
get mapperBeforeFetchProject() {
return (data, filters, isDetail) => {
if (isDetail) {
return {
...data,
project_id: data.owner,
};
}
return {
...data,
project_id: data.owner,
project_name: data.owner_project_name || data.project_name,
};
};
}
```
- `paramsFunc`
- 前端分页请求(即`fetchList`)时,对请求参数的更新
- 默认是对从资源列表代码(`pages/xxxx/xxx/index.jsx`)使用`fetchList`时,参数的过滤
- 默认值
```javascript
get paramsFunc() {
if (this.filterByApi) {
return (params) => params;
}
return (params) => {
const reservedKeys = [
'all_data',
'all_projects',
'device_id',
'network_id',
'floating_network_id',
'start_at_gt',
'start_at_lt',
'binary',
'fixed_ip_address',
'device_owner',
'project_id',
'type',
'sort',
'security_group_id',
'id',
'security_group_id',
'owner_id',
'status',
'fingerprint',
'resource_types',
'floating_ip_address',
'uuid',
'loadbalancer_id',
'ikepolicy_id',
'ipsecpolicy_id',
'endpoint_id',
'peer_ep_group_id',
'local_ep_group_id',
'vpnservice_id',
];
const newParams = {};
Object.keys(params).forEach((key) => {
if (reservedKeys.indexOf(key) >= 0) {
newParams[key] = params[key];
}
});
return newParams;
};
}
```
- 以云硬盘`src/stores/cinder/volume.js`为例
```javascript
get paramsFunc() {
return (params) => {
const { serverId, ...rest } = params;
return rest;
};
}
```
- `paramsFuncPage`
- 后端分页请求(即`fetchListByPage`)时,对请求参数的更新
- 默认是对从资源列表代码(`pages/xxxx/xxx/index.jsx`)使用`fetchListByPage`时,参数的过滤
- 默认值
```javascript
get paramsFuncPage() {
return (params) => {
const { current, ...rest } = params;
return rest;
};
}
```
- 以云硬盘类型`src/stores/cinder/volume-type.js`为例
```javascript
get paramsFuncPage() {
return (params) => {
const { current, showEncryption, ...rest } = params;
return rest;
};
}
```
- `fetchListByLimit`
- 前端分页请求所有数据时,是否要基于`limit`发起多个请求,最终实现所有数据的获取
- Openstack API 默认返回的是 1000 个数据,对于某些资源数据很多的情况,需要使用该配置以便获取到所有数据
- 默认值是`false`
- 以镜像`src/stores/glance/image.js`为例
```javascript
get fetchListByLimit() {
return true;
}
```
- `markerKey`
- 后端分页请求数据时marker 的来源
- 因为对 Openstack 的请求是由后端转发的,所以并没有直接使用列表数据返回的 Openstack 拼接好的下一页数据应该使用的 Url而是根据返回的数据解析出`marker`
- 默认值是`id`
- 通常不需要复写
- 以密钥`src/stores/nova/keypair.js`为例
```javascript
get markerKey() {
return 'keypair.name';
}
```
- `requestListByMarker`
- 后端分页时,使用`marker`请求分页下的数据
- 通常不需要复写
- 默认值是
```javascript
async requestListByMarker(url, params, limit, marker) {
const newParams = {
...params,
limit,
};
if (marker) {
newParams.marker = marker;
}
return request.get(url, newParams);
}
```
- 以云主机组`src/stores/nova/server-group.js`为例
```javascript
async requestListByMarker(url, params, limit, marker) {
const newParams = {
...params,
limit,
};
if (marker) {
newParams.offset = marker;
}
return request.get(url, newParams);
}
```
- `requestListAllByLimit`
- 当`this.fetchListByLimit=true`时,前端分页使用该方法获取所有数据
- 通常不需要复写
- `updateUrl`
- 更新列表数据请求的 url
- 不常用
- `updateParamsSortPage`
- 使用后端排序时,对排序参数的处理
- 使用后端排序时,会在资源列表代码`pages/xxx/XXX/index.jsx`中自动生成相应的请求参数store 对这些参数往往需要再次整理,否则会不符合 API 的参数要求
- 以云硬盘`src/stores/cinder/volume.js`为例
```javascript
updateParamsSortPage = (params, sortKey, sortOrder) => {
if (sortKey && sortOrder) {
params.sort_keys = sortKey;
params.sort_dirs = sortOrder === 'descend' ? 'desc' : 'asc';
}
};
```
- `listFilterByProject`
- 列表数据是否需要基于项目信息过滤
- `admin`权限下的部分 Openstack 资源(如`neutron`),会默认返回所有项目的数据,所以在控制台展示资源时,会根据该配置过滤数据
- 默认值是`false`
- 以 VPN`src/stores/neutron/vpn-service.js`为例
```javascript
get listFilterByProject() {
return true;
}
```
- `fetchList`
- `pages`下的列表页通常使用`this.store.fetchList`来获取前端分页数据
- 不建议复写该函数,如果需要再加工数据,建议使用`listDidFetch`
- 该函数会更新`this.list`属性中的相关数据,`pages`下的资源列表组件也是基于`this.list`进行数据展示
- `fetchListByPage`
- `pages`下的列表页通常使用`this.store.fetchList`来获取后端分页数据
- 不建议复写该函数,如果需要再加工数据,建议使用`listDidFetch`
- 该函数会更新`this.list`属性中的相关数据,`pages`下的资源列表组件也是基于`this.list`进行数据展示
- `getCountForPage`
- 获取列表数据的总量
- 通常在后端分页时可复写
- `getDetailParams`
- 更新详情数据请求时的参数
- 默认值为
```javascript
getDetailParams = () => undefined;
```
- `fetchDetail`
- `pages`下的详情页通常使用`this.store.fetchDetail`来获取详情数据
- 通常不需要复写
- 数据再加工通常是重写`mapper`或`detailDidFetch`
- `create`
- 创建资源
- 使用`POST`api
- 通常不需要复写
- 使用`this.submitting`保证在发送请求时页面处于`loading`状态
- `edit`
- 更新资源
- 使用`PUT`api
- 通常不需要复写
- 使用`this.submitting`保证在发送请求时页面处于`loading`状态
- `patch`
- 更新资源
- 使用`PATCH`api
- 通常不需要复写
- 使用`this.submitting`保证在发送请求时页面处于`loading`状态
- `delete`
- 删除资源
- 使用`DELETE`api
- 通常不需要复写
- 使用`this.submitting`保证在发送请求时页面处于`loading`状态
## 不需要复写的属性与函数
- `submitting`
- 用于数据创建、数据更新时
- 依据请求的响应变更`this.isSubmitting`,对应的 Form列表页等会展示 Loading 状态
- `currentProject`
- 当前用户登录的项目 ID
- `itemInCurrentProject`
- 数据是否属于当前用户登录的项目
- `listDidFetchProject`
- 对列表数据添加项目信息
- `requestListAll`
- 前端分页获取所有数据
- `requestListByPage`
- 后端分页所有当前页的数据
- `pureFetchList`
- 列表数据的请求函数
- 返回原始数据,不会对 API 的返回数据做加工
- `parseMarker`
- 使用后端分页时,从返回数据中解析出`marker`,用于请求上一页、下一页数据时使用
- `updateMarker`
- 更新`list`的`markers`
- `list.markers`是个数组,每个元素对应于`下标+1`页的`marker`
- `getMarker`
- 获取指定页对应的`marker`
- `getListDataFromResult`
- 从 API 的返回值中取出列表数据
- 利用`this.listResponseKey`获取
- `setSelectRowKeys`
- 对`pages`下的资源列表组件列表中数据项的选中记录
- `clearData`
- 清空`list`数据
## 基类中的基础函数
- 建议查看代码理解,`src/stores/base.js`

View File

@ -0,0 +1,391 @@
简体中文 | [English](/docs/en/develop/3-6-FormAction-introduction.md)
# 用途
![Form单页](/docs/zh/develop/images/form/page.png)
- 操作按钮点击后,单页显示 Form 表单
- 有独立的路由可供访问
- 一般用于创建资源,或是表单内容较多的 Form
- 点击`确认`按钮后,会根据请求的发送情况,展示`loading`状态,请求成功后,会自动跳转到相应的资源列表页
- 点击`取消`按钮后,会自动跳转到相应的资源列表页
- 如果请求发送成功,会在右上角展示操作成功的提示信息,该提示信息几秒后可自动消失
![Form单页](/docs/zh/develop/images/form/create-success.png)
- 如果请求发送失败,会在表单页的右上角展示错误信息,该提示信息只有点击关闭按钮后才可消失
# FormAction 代码文件
- `src/containers/Action/FormAction/index.jsx`
# FormAction 属性与函数定义介绍
- 单页表单都继承于 FormAction 组件
- 代码位置:`pages/xxxx/containers/XXXX/actions/xxx.jsx`
- 只需要复写部分函数即可完成页面的开发
- 属性与函数分为以下四种,
- 必须复写的属性与函数,主要包含:
- 操作的 ID
- 操作的标题
- 页面对应的路径
- 资源列表页面对应的路径
- 操作对应的权限
- 对是否禁用操作的判定
- 表单项的配置
- 发送请求的函数
- 按需复写的函数与属性,主要包含:
- 表单的默认值
- 无需复写的函数与属性,主要包含:
- 当前页是否是管理平台页面
- 基类中的基础函数,主要包含:
- 渲染页面
- 对请求状态的展示
- 对请求结果的展示
- 更详细与全面的介绍见下
## 必须复写的属性与函数
- `id`
- 静态属性
- 资源操作的 ID
- 需要具有唯一性,只针对资源的`actions`中的所有操作具有唯一性即可
- 必须复写该属性
- 以创建云硬盘`src/pages/storage/containers/Volume/actions/Create/index.jsx`为例
```javascript
static id = 'volume-create';
```
- `title`
- 静态属性
- 资源操作的标题
- 以创建云硬盘`src/pages/storage/containers/Volume/actions/Create/index.jsx`为例
```javascript
static title = t('Create Volume');
```
- `path`
- 资源操作的对应的路由
- 静态属性或静态函数
- 静态函数时,参数为
- 参数`item`,资源列表中的条目数据
- 参数`containerProps`,父级 container(即按钮所在资源列表页面)的`props`属性
- 以创建镜像`src/pages/compute/containers/Image/actions/Create.jsx`为例
- 管理平台访问的路径是`/compute/image-admin/create`
- 控制台访问的路径是`/compute/image/create`
```javascript
static path = (_, containerProp) => {
const { isAdminPage } = containerProp;
return isAdminPage
? '/compute/image-admin/create'
: '/compute/image/create';
};
```
- 静态属性,以创建云硬盘`src/pages/storage/containers/Volume/actions/Create/index.jsx`为例
```javascript
static path = '/storage/volume/create';
```
- `policy`
- 静态属性
- 页面对应的权限,如果权限验证不通过,则不会在资源列表页面显示该操作按钮
- 以云硬盘`src/pages/storage/containers/Volume/actions/Create/index.jsx`为例
```javascript
static policy = 'volume:create';
```
- `allowed`
- 静态函数
- 判定操作是否需要被禁用
- 返回`Promise`
- 不需用禁用的按钮,直接写作
```javascript
static allowed() {
return Promise.resolve(true);
}
```
- 参数`item`,资源列表中的条目数据,一般用在资源列表中的条目的操作判定
- 参数`containerProps`,父级 container(即按钮所在资源列表页面)的`props`属性,一般用在详情页下相关资源的操作判定
- 以创建用户`src/pages/identity/containers/User/actions/Create.jsx`为例
- 如果是域详情中的用户列表,则不展示创建用户按钮
```javascript
static allowed(item, containerProps) {
const {
match: { path },
} = containerProps;
if (path.indexOf('domain-admin/detail') >= 0) {
return Promise.resolve(false);
}
return Promise.resolve(true);
}
```
- `name`
- 该操作对应的名称
- 在请求后提示语中使用该名称
- 以云硬盘`src/pages/storage/containers/Volume/actions/Create/index.jsx`为例
```javascript
get name() {
return t('create volume');
}
```
- `listUrl`
- 该操作对应的资源列表页
- 操作请求成功后,会自动进入到资源列表页
- 以云硬盘`src/pages/storage/containers/Volume/actions/Create/index.jsx`为例
```javascript
get listUrl() {
return this.getUrl('/storage/volume');
}
```
- `formItems`
- 该操作表单对应的表单项配置列表
- 每个表单项的配置信息可参考[3-10-FormItem 介绍](3-10-FormItem-introduction.md)
- 以创建域`src/pages/identity/containers/Domain/actions/Create.jsx`为例
- 表单包含名称、描述、状态
```javascript
get formItems() {
return [
{
name: 'name',
label: t('Name'),
type: 'input',
placeholder: t('Please input name'),
required: true,
help: t('The name cannot be modified after creation'),
},
{
name: 'description',
label: t('Description'),
type: 'textarea',
},
{
name: 'enabled',
label: t('Status'),
type: 'radio',
optionType: 'default',
options: statusTypes,
required: true,
isWrappedValue: true,
help: t(
'Forbidden the domain will have a negative impact, all project and user in domain will be forbidden'
),
},
];
}
```
- `onSubmit`
- 该操作的请求函数
- 操作请求成功后,会自动进入到资源列表页
- 操作失败后,会在表单页显示错误提示
- 返回`Promise`
- 返回表单对应的`store`中的函数
- 以创建域`src/pages/identity/containers/Domain/actions/Create.jsx`为例
```javascript
onSubmit = (values) => {
values.enabled = values.enabled.value;
return this.store.create(values);
};
```
## 按需复写的属性与函数
- `init`
- 初始化操作
- 在其中定义`this.store``loading`状态的展示是基于`this.store.isSubmitting`
- 在其中调用获取表单所需其他数据的函数
- 对`this.state`中属性的更新
- 以创建云硬盘`src/pages/storage/containers/Volume/actions/Create/index.jsx`为例
- 获取配额信息、可用域数据、镜像数据、云硬盘类型
- 更新`this.state`中的初始值
```javascript
init() {
this.snapshotStore = globalSnapshotStore;
this.imageStore = globalImageStore;
this.volumeStore = globalVolumeStore;
this.volumeTypeStore = globalVolumeTypeStore;
this.backupstore = globalBackupStore;
this.getQuota();
this.getAvailZones();
this.getImages();
this.getVolumeTypes();
this.state = {
...this.state,
count: 1,
sharedDisabled: false,
};
}
```
- `defaultValue`
- 表单的初始值
- 默认值是`{}`
- 以创建云硬盘`src/pages/storage/containers/Volume/actions/Create/index.jsx`为例
- 设置表单的默认源、大小、项目、可用域、云硬盘类型
```javascript
get defaultValue() {
const size = this.quotaIsLimit && this.maxSize < 10 ? this.maxSize : 10;
const { initVolumeType } = this.state;
const values = {
source: this.sourceTypes[0],
size,
project: this.currentProjectName,
availableZone: (this.availableZones[0] || []).value,
volume_type: initVolumeType,
};
return values;
}
```
- `nameForStateUpdate`
- 表单项内容变动时,更新到`this.state`中的表单键值对
- 这些存储到`this.store`中的键值对往往会影响表单项的展示,需要配合`get formItems`中的代码使用
- 如展开、隐藏更多配置项
- 如某些表单项必填性的变动
- 默认对`radio`, `more`类型的表单项的变动保存到`this.state`中
- 以创建云硬盘`src/pages/storage/containers/Volume/actions/Create/index.jsx`为例
- 当`source=image`时,展示镜像选择列表,并基于镜像的选择,设置云硬盘容量的最小值
- 当`source=snapshot`时,展示云硬盘快照列表,并基于镜像的选择,设置云硬盘容量的最小值
```javascript
get nameForStateUpdate() {
return ['source', 'image', 'snapshot'];
}
```
- `renderFooterLeft`
- 对表单底部左侧内部的渲染
- 默认返回`null`
- 以创建云硬盘`src/pages/storage/containers/Volume/actions/Create/index.jsx`为例
- 展示批量创建的数量
- 基于输入的数量与剩余配额判定当前表单是否正确
```javascript
const { count = 1 } = this.state;
const configs = {
min: 1,
max: 100,
precision: 0,
onChange: this.onCountChange,
formatter: (value) => `$ ${value}`.replace(/\D/g, ''),
};
return (
<div>
<span>{t('Count')}</span>
<InputNumber
{...configs}
value={count}
className={classnames(styles.input, 'volume-count')}
/>
{this.renderBadge()}
</div>
);
```
- `errorText`
- 错误信息的展示
- 一般不需要复写
- 以创建云硬盘`src/pages/storage/containers/Volume/actions/Create/index.jsx`为例
- 展示了配额验证不通过的错误信息,当配额验证不通过时,并不发送请求,而是直接展示了错误提示信息
```javascript
get errorText() {
const { status } = this.state;
if (status === 'error') {
return t(
'Unable to create volume: insufficient quota to create resources.'
);
}
return super.errorText;
}
```
- `instanceName`
- 请求发送后,提示信息中的资源名称
- 默认值为`this.values.name`
- 以创建云硬盘`src/pages/storage/containers/Volume/actions/Create/index.jsx`为例
- 如果是批量创建云硬盘,则按`${name}-${index + 1}`的形式展示名称
```javascript
get instanceName() {
const { name } = this.values || {};
const { count = 1 } = this.state;
if (count === 1) {
return name;
}
return new Array(count)
.fill(count)
.map((_, index) => `${name}-${index + 1}`)
.join(', ');
}
```
- `labelCol`
- 配置表单左侧标签的布局
- 默认值为
```javascript
get labelCol() {
return {
xs: { span: 5 },
sm: { span: 3 },
};
}
```
- 以创建镜像`src/pages/compute/containers/Image/actions/Create.jsx`为例
```javascript
get labelCol() {
return {
xs: { span: 6 },
sm: { span: 5 },
};
}
```
- `wrapperCol`
- 配置表单右侧内容的布局
- 默认值为
```javascript
get wrapperCol() {
return {
xs: { span: 10 },
sm: { span: 8 },
};
}
```
## 不需要复写的属性与函数
- `values`
- 表单验证成功后,更新的表单值
- `isAdminPage`
- 当前页面是否是“管理平台”的页面
- `getUrl`
- 生成页面 Url 的函数
- 如:需要给关联资源提供跳转功能,使用该函数,可以在控制台跳转到控制台的相应地址,在管理平台跳转到管理平台的相应地址
## 基类中的基础函数
- `FormAction`继承于`BaseForm`
- 建议查看代码理解,`src/components/Form/index.jsx`

View File

@ -0,0 +1,387 @@
简体中文 | [English](/docs/en/develop/3-7-ModalAction-introduction.md)
# 用途
![弹窗型表单](/docs/zh/develop/images/form/modal.png)
- 操作按钮点击后,弹窗显示表单
- 点击`确认`按钮后,会根据请求的发送情况,展示`loading`状态
- 点击`取消`按钮后,弹窗消失
- 如果请求发送成功,会在右上角展示操作成功的提示信息,该提示信息几秒后可自动消失
- 如果请求发送失败,会在表单页的右上角展示错误信息,该提示信息只有点击关闭按钮后才可消失
- 支持批量操作,在表格中选中多个条目后,可点击表格上方的操作按钮,进行批量操作
# ModalAction 代码文件
- `src/containers/Action/ModalAction/index.jsx`
# ModalAction 属性与函数定义介绍
- 弹窗型表单都继承于 ModalAction 组件
- 代码位置:`pages/xxxx/containers/XXXX/actions/xxx.jsx`
- 对于表单内容比较少的情况,通常是使用弹窗型的表单形式
- 只需要复写部分函数即可完成页面的开发
- 属性与函数分为以下四种,
- 必须复写的属性与函数,主要包含:
- 操作的 ID
- 操作的标题
- 操作对应的权限
- 对是否禁用操作的判定
- 表单项的配置
- 发送请求的函数
- 按需复写的函数与属性,主要包含:
- 表单的默认值
- 表单的尺寸
- 表单中右侧标题与左侧表单主题的布局
- 是否是异步操作
- 资源的名称
- 请求结果提示语中是否要展示资源名称
- 操作按钮上的文字
- 无需复写的函数与属性,主要包含:
- 当前页是否是管理平台页面
- 基类中的基础函数,主要包含:
- 渲染页面
- 对请求状态的展示
- 对请求结果的展示
- 更详细与全面的介绍见下
## 必须复写的属性与函数
- `id`
- 静态属性
- 资源操作的 ID
- 需要具有唯一性,只针对资源的`actions`中的所有操作具有唯一性即可
- 必须复写该属性
- 以挂载云硬盘`src/pages/compute/containers/Instance/actions/AttachVolume.jsx`为例
```javascript
static id = 'attach-volume';
```
- `title`
- 静态属性
- 资源操作的标题
- 以挂载云硬盘`src/pages/compute/containers/Instance/actions/AttachVolume.jsx`为例
```javascript
static title = t('Attach Volume');
```
- `name`
- 该操作对应的名称
- 在请求后提示语中使用该名称
- 以挂载云硬盘`src/pages/compute/containers/Instance/actions/AttachVolume.jsx`为例
```javascript
get name() {
return t('Attach volume');
}
```
- `policy`
- 静态属性
- 操作对应的权限,如果权限验证不通过,则不会在资源列表页面显示该操作按钮
- 以挂载云硬盘`src/pages/compute/containers/Instance/actions/AttachVolume.jsx`为例
```javascript
static policy = 'os_compute_api:os-volumes-attachments:create';
```
- `allowed`
- 静态函数
- 判定操作是否需要被禁用
- 返回`Promise`
- 不需用禁用的按钮,直接写作
```javascript
static allowed() {
return Promise.resolve(true);
}
```
- 参数`item`,资源列表中的条目数据,一般用在资源列表中的条目的操作判定
- 参数`containerProps`,父级 container(即按钮所在资源列表页面)的`props`属性,一般用在详情页下相关资源的操作判定
- 以挂载云硬盘`src/pages/compute/containers/Instance/actions/AttachVolume.jsx`为例
- 管理平台不展示该操作按钮
- 当云主机满足:运行中、不处于删除中、未锁定、不是裸机 时,才会展示按操作按钮
```javascript
static allowed = (item, containerProps) => {
const { isAdminPage } = containerProps;
return Promise.resolve(
!isAdminPage &&
isActive(item) &&
isNotDeleting(item) &&
isNotLocked(item) &&
!isIronicInstance(item)
);
};
```
- `formItems`
- 该操作表单对应的表单项配置列表
- 每个表单项的配置信息可参考[3-10-FormItem 介绍](3-10-FormItem-introduction.md)
- 以挂载云硬盘`src/pages/compute/containers/Instance/actions/AttachVolume.jsx`为例
- 表单项包含:云主机的名称展示、云硬盘的选择
```javascript
get formItems() {
return [
{
name: 'instance',
label: t('Instance'),
type: 'label',
iconType: 'instance',
},
{
name: 'volume',
label: t('Volume'),
type: 'volume-select-table',
tip: multiTip,
isMulti: false,
required: true,
serverId: this.item.id,
disabledFunc: (record) => {
const diskFormat = _get(
record,
'origin_data.volume_image_metadata.disk_format'
);
return diskFormat === 'iso';
},
},
];
}
```
- `onSubmit`
- 该操作的请求函数
- 操作请求成功后,弹窗会消失,并显示成功提示,几秒后提示会消失
- 操作失败后,弹窗会消失,并显示错误提示,需要手动关闭提示,提示才会消失
- 返回`Promise`
- 返回表单对应的`store`中的函数
- 以挂载云硬盘`src/pages/compute/containers/Instance/actions/AttachVolume.jsx`为例
```javascript
onSubmit = (values) => {
const { volume } = values;
const { id } = this.item;
const volumeId = volume.selectedRowKeys[0];
const body = {
volumeAttachment: {
volumeId,
},
};
return this.store.attachVolume({ id, body });
};
```
## 按需复写的属性与函数
- `init`
- 初始化操作
- 在其中定义`this.store``loading`状态的展示是基于`this.store.isSubmitting`
- 在其中调用获取表单所需其他数据的函数
- 对`this.state`中属性的更新
- 以挂载云硬盘`src/pages/compute/containers/Instance/actions/AttachVolume.jsx`为例
- 定义了操作对应的`store`
```javascript
init() {
this.store = globalServerStore;
}
```
- `defaultValue`
- 表单的初始值
- 默认值是`{}`
- 以挂载云硬盘`src/pages/compute/containers/Instance/actions/AttachVolume.jsx`为例
- 设置了表单中,云主机名称的初始值
```javascript
get defaultValue() {
const { name } = this.item;
const value = {
instance: name,
};
return value;
}
```
- `nameForStateUpdate`
- 表单项内容变动时,更新到`this.state`中的表单键值对
- 这些存储到`this.store`中的键值对往往会影响表单项的展示,需要配合`get formItems`中的代码使用
- 如展开、隐藏更多配置项
- 如某些表单项必填性的变动
- 默认对`radio`, `more`类型的表单项的变动保存到`this.state`中
- 以云主机挂载网卡`src/pages/compute/containers/Instance/actions/AttachInterface.jsx`为例
- 表单中的网络的选中变更后,会更新子网列表的内容
- 但表单中子网的选中变更后,会更新输入 IP 的判定等
```javascript
get nameForStateUpdate() {
return ['network', 'ipType', 'subnet'];
}
```
- `instanceName`
- 请求发送后,提示信息中的资源名称
- 默认值为`this.values.name`
- 以编辑浮动 IP`src/pages/network/containers/FloatingIp/actions/Edit.jsx`为例
- 提示的名称是浮动 IP 的地址
```javascript
get instanceName() {
return this.item.floating_ip_address;
}
```
- `isAsyncAction`
- 当前操作是否是异步操作
- 默认是`false`
- 如果是异步操作,提示语为:`xxx指令已下发实例名称xxx 您可等待几秒关注列表数据的变更或是手动刷新数据,以获取最终展示结果。`
- 如果是同步操作,提示语为:`xxx成功实例名称xxx。`
- 以挂载云硬盘`src/pages/compute/containers/Instance/actions/AttachVolume.jsx`为例
```javascript
get isAsyncAction() {
return true;
}
```
- `messageHasItemName`
- 请求结果的提示语中,是否要包含实例名称
- 默认值为`true`
- 有些资源,不存在名称,则可设置该值为`false`
- 以裸机节点创建端口``为例
```javascript
get messageHasItemName() {
return false;
}
```
- `buttonText`
- 静态属性
- 当操作按钮上的文字与弹窗的标题不一致时,需要复用该属性
- 以编辑镜像`src/pages/compute/containers/Image/actions/Edit.jsx`为例
```javascript
static buttonText = t('Edit');
```
- `buttonType`
- 静态属性
- 按钮的类型,支持`primary`、`danger`
- 当按钮要强调操作危险性时,按钮或按钮上的文字一般为红色,使用`danger`
- 以禁止 Cinder 服务`src/pages/configuration/containers/SystemInfo/CinderService/actions/Disable.jsx`为例
```javascript
static buttonType = 'danger';
```
- `modalSize`
- 静态函数
- 标识弹出框的宽度:值为`small`、`middle`、`large`
- 值与宽度的对应为:
- `small`: 520
- `middle`: 720
- `large`: 1200
- 与`getModalSize`配合使用
- 默认值为`small`,即弹窗的宽度是 520px
```javascript
static get modalSize() {
return 'small';
}
```
- 以挂载云硬盘`src/pages/compute/containers/Instance/actions/AttachVolume.jsx`为例
- 表单的大小是`large`
```javascript
static get modalSize() {
return 'large';
}
getModalSize() {
return 'large';
}
```
- `getModalSize`
- 配置表单左侧标题的布局
- 值为`small`、`middle`、`large`
- `labelCol`
- 配置表单左侧标签的布局
- 默认值为
```javascript
get labelCol() {
const size = this.getModalSize();
if (size === 'large') {
return {
xs: { span: 6 },
sm: { span: 4 },
};
}
return {
xs: { span: 8 },
sm: { span: 6 },
};
}
```
- 以编辑域`src/pages/identity/containers/Domain/actions/Edit.jsx`为例
```javascript
get labelCol() {
return {
xs: { span: 6 },
sm: { span: 5 },
};
}
```
- `wrapperCol`
- 配置表单右侧内容的布局
- 默认值为
```javascript
get wrapperCol() {
return {
xs: { span: 16 },
sm: { span: 16 },
};
}
```
- 以管理云主机类型元数据`src/pages/compute/containers/Flavor/actions/ManageMetadata.jsx`为例
```javascript
get wrapperCol() {
return {
xs: { span: 18 },
sm: { span: 20 },
};
}
```
## 不需要复写的属性与函数
- `isAdminPage`
- 当前页面是否是“管理平台”的页面
- `successText`
- 请求后生成的成功提示语
- `errorText`
- 请求失败后生成的报错提示语
- `containerProps`
- 获取来源于按钮所在父级组件的`props`
- `item`
- 获取操作对应的数据
- `items`
- 获取批量操作对应的数据
## 基类中的基础函数
- `ModalAction`继承于`BaseForm`
- 建议查看代码理解,`src/components/Form/index.jsx`

View File

@ -0,0 +1,257 @@
简体中文 | [English](/docs/en/develop/3-8-ConfirmAction-introduction.md)
# 用途
![确认型](/docs/zh/develop/images/form/confirm.png)
- 操作按钮点击后,显示确认类型的表单
- 点击`确认`按钮后,会根据请求的发送情况,展示`loading`状态
- 点击`取消`按钮后,弹窗消失
- 如果请求发送成功,会在右上角展示操作成功的提示信息,该提示信息几秒后可自动消失
- 如果请求发送失败,会在表单页的右上角展示错误信息,该提示信息只有点击关闭按钮后才可消失
- 支持批量操作,在表格中选中多个条目后,可点击表格上方的操作按钮,进行批量操作
- 使用批量操作时,会对批量选中的资源中不符合操作条件的资源做出提示
# ConfirmAction 代码文件
- `src/containers/Action/ConfirmAction/index.jsx`
# ModalAction 属性与函数定义介绍
- 弹窗型表单都继承于 ModalAction 组件
- 代码位置:`pages/xxxx/containers/XXXX/actions/xxx.jsx`
- 某些操作,只需要再次确认,无需用户输入更多内容即可,此时可使用该类型的组件,如:关闭云主机
- 只需要复写部分函数即可完成页面的开发
- 属性与函数分为以下四种,
- 必须复写的属性与函数,主要包含:
- 操作的 ID
- 操作的标题
- 操作对应的权限
- 对是否禁用操作的判定
- 发送请求的函数
- 按需复写的函数与属性,主要包含:
- 资源的名称
- 请求结果提示语中是否要展示资源名称
- 是否是异步操作
- 操作按钮上的文字
- 无需复写的函数与属性,主要包含:
- 当前页是否是管理平台页面
- 基类中的基础函数,主要包含:
- 渲染页面
- 对请求状态的展示
- 对请求结果的展示
- 更详细与全面的介绍见下
## 必须复写的属性与函数
- `id`
- 资源操作的 ID
- 需要具有唯一性,只针对资源的`actions`中的所有操作具有唯一性即可
- 必须复写该属性
- 以停止云主机`src/pages/compute/containers/Instance/actions/Stop.jsx`为例
```javascript
get id() {
return 'stop';
}
```
- `title`
- 资源操作的标题
- 以停止云主机`src/pages/compute/containers/Instance/actions/Stop.jsx`为例
```javascript
get title() {
return t('Stop Instance');
}
```
- `actionName`
- 该操作对应的名称
- 在请求后提示语中使用该名称
- 以停止云主机`src/pages/compute/containers/Instance/actions/Stop.jsx`为例
```javascript
get actionName() {
return t('stop instance');
}
```
- `policy`
- 页面对应的权限,如果权限验证不通过,则不会在资源列表页面显示该操作按钮
- 以停止云主机`src/pages/compute/containers/Instance/actions/Stop.jsx`为例
```javascript
policy = 'os_compute_api:servers:stop';
```
- `allowedCheckFunc`
- 判定操作是否需要被禁用
- 返回`Boolean`
- 不需用禁用的按钮,直接写作
```javascript
allowedCheckFunc = () => true;
```
- 参数`item`,操作对应的数据
- 以停止云主机`src/pages/compute/containers/Instance/actions/Stop.jsx`为例
- 当云主机满足以下条件才会显示该操作按钮:处于运行中状态,控制台非锁定或是在管理平台
```javascript
allowedCheckFunc = (item) => {
if (!item) {
return true;
}
return isNotLockedOrAdmin(item, this.isAdminPage) && this.isRunning(item);
};
```
- `onSubmit`
- 该操作的请求函数
- 操作请求成功后,弹窗会消失,并显示成功提示,几秒后提示会消失
- 操作失败后,弹窗会消失,并显示错误提示,需要手动关闭提示,提示才会消失
- 返回`Promise`
- 返回表单对应的`store`中的函数
- 以停止云主机`src/pages/compute/containers/Instance/actions/Stop.jsx`为例
```javascript
onSubmit = (item) => {
const { id } = item || this.item;
return globalServerStore.stop({ id });
};
```
## 按需复写的属性与函数
- `buttonText`
- 当操作按钮上的文字与弹窗的标题不一致时,需要复用该属性
- 以停止云主机`src/pages/compute/containers/Instance/actions/Stop.jsx`为例
- 弹窗上的标题是“停止云主机”,按钮上的文字是“停止”
```javascript
get buttonText() {
return t('Stop');
}
```
- `buttonType`
- 按钮的类型,支持`primary`、`danger`、`default`
- 默认值为`default`
- 当按钮要强调操作危险性时,按钮或按钮上的文字一般为红色,使用`danger`
- 以停止云主机`src/pages/compute/containers/Instance/actions/Stop.jsx`为例
```javascript
get buttonType() {
return 'danger';
}
```
- `passiveAction`
- 批量操作时,如果某个资源不符合条件,会在发送请求前展示提示语,如果提示语需要以被动语态,则需要设置该属性
- 以停止云主机`src/pages/compute/containers/Instance/actions/Stop.jsx`为例
```javascript
get passiveAction() {
return t('be stopped');
}
```
- `isAsyncAction`
- 当前操作是否是异步操作
- 默认是`false`
- 如果是异步操作,提示语为:`xxx指令已下发实例名称xxx 您可等待几秒关注列表数据的变更或是手动刷新数据,以获取最终展示结果。`
- 如果是同步操作,提示语为:`xxx成功实例名称xxx。`
- 以停止云主机`src/pages/compute/containers/Instance/actions/Stop.jsx`为例
```javascript
get isAsyncAction() {
return true;
}
```
- `messageHasItemName`
- 请求结果的提示语中,是否要包含实例名称
- 默认值为`true`
- 有些资源,不存在名称,则可设置该值为`false`
- `performErrorMsg`
- 批量操作时,如果某个资源不符合条件,会在发送请求前展示提示语
- 默认值为`无法xxx 实例名称xxxx。`
- 以停止云主机`src/pages/compute/containers/Instance/actions/Stop.jsx`为例
- 如果选中的云主机不处于运行中状态,提示`云主机\"{ name }\"状态不是运行中,无法关闭。`
- 如果选中的云主机有处于锁定状态的,提示`云主机\"{ name }\"被锁定,无法关闭。`
- 其他情况,皆提示`无法关闭云主机\"{ name }\"`
```javascript
performErrorMsg = (failedItems) => {
const instance = isArray(failedItems) ? failedItems[0] : failedItems;
let errorMsg = t('You are not allowed to stop instance "{ name }".', {
name: instance.name,
});
if (!this.isRunning(instance)) {
errorMsg = t(
'Instance "{ name }" status is not in active or suspended, can not stop it.',
{ name: instance.name }
);
} else if (!isNotLockedOrAdmin(instance, this.isAdminPage)) {
errorMsg = t('Instance "{ name }" is locked, can not stop it.', {
name: instance.name,
});
}
return errorMsg;
};
```
- `getNameOne`
- 提示语中实例名称的来源
- 默认是
```javascript
getNameOne = (data) => data.name;`
```
- 参数`data`为操作对应的资源数据
- 以释放浮动 IP`src/pages/network/containers/FloatingIp/actions/Release.jsx`为例
```javascript
getNameOne = (data) => data.floating_ip_address;
```
- `getName`
- 不建议复写该函数
- 建议复写`getNameOne`
- `confirmContext`
- 确认弹窗中的提示语
- 默认为`确认{ action }(实例名称:{name}`
- 以删除云主机类型`src/pages/compute/containers/Flavor/actions/Delete.jsx`为例
- 提示`若有云主机正在使用此 flavor删除会导致云主机的 flavor 数据缺失,确定删除 {name} `
```javascript
confirmContext = (data) => {
const name = this.getName(data);
return t(
"If an instance is using this flavor, deleting it will cause the instance's flavor data to be missing. Are you sure to delete {name}?",
{ name }
);
};
```
- `submitErrorMsg`
- 操作失败后的错误提示语
- 一般不需要复写
- 默认为`无法{action},实例名称:{name}。`
## 不需要复写的属性与函数
- `isAdminPage`
- 当前页面是否是“管理平台”的页面
- `submitSuccessMsg`
- 请求后生成的成功提示语
- `submitErrorMsgBatch`
- 批量操作请求后生成的报错提示语
- `perform`
- 批量操作时,判定选中的数据是否可操作,如果不可操作,给出相应提示语
## 基类中的基础函数
- 建议查看代码理解,`src/containers/Action/ConfirmAction/index.jsx`

View File

@ -0,0 +1,293 @@
简体中文 | [English](/docs/en/develop/3-9-StepAction-introduction.md)
# 用途
![分步Form](/docs/zh/develop/images/form/step.png)
- 操作按钮点击后,单页显示分步操作的表单
- 有独立的路由可供访问
- 一般用于创建资源,或是表单内容较多的 Form
- 支持点击`下一步`、`上一步`操作按钮
- 点击`取消`按钮后,会自动跳转到相应的资源列表页
- 如果请求发送成功,会在右上角展示操作成功的提示信息,该提示信息几秒后可自动消失
![Form单页](/docs/zh/develop/images/form/create-success.png)
- 如果请求发送失败,会在表单页的右上角展示错误信息,该提示信息只有点击关闭按钮后才可消失
# StepAction 代码文件
- `src/containers/Action/StepAction/index.jsx`
# StepAction 属性与函数定义介绍
- 分步表单都继承于 StepAction 组件
- 代码位置:`pages/xxxx/containers/XXXX/actions/xxx/index.jsx`
- 只需要复写部分函数即可完成页面的开发
- 需要编写每一步的 Form
- 属性与函数分为以下四种,
- 必须复写的属性与函数,主要包含:
- 操作的 ID
- 操作的标题
- 页面对应的路径
- 资源列表页面对应的路径
- 操作对应的权限
- 对是否禁用操作的判定
- 表单项的配置
- 发送请求的函数
- 每步操作的配置
- 按需复写的函数与属性,主要包含:
- 是否具有确认信息的页面
- 请求成功后的提示语
- 请求失败的报错提示语
- 对页面底部左侧数据的渲染
- 无需复写的函数与属性,主要包含:
- 当前页是否是管理平台页面
- 基类中的基础函数,主要包含:
- 渲染页面
- 对请求状态的展示
- 对请求结果的展示
- 更详细与全面的介绍见下
## 必须复写的属性与函数
- `id`
- 静态属性
- 资源操作的 ID
- 需要具有唯一性,只针对资源的`actions`中的所有操作具有唯一性即可
- 必须复写该属性
- 以创建云主机`src/pages/compute/containers/Instance/actions/StepCreate/index.jsx`为例
```javascript
static id = 'instance-create';
```
- `title`
- 静态属性
- 资源操作的标题
- 以创建云主机`src/pages/compute/containers/Instance/actions/StepCreate/index.jsx`为例
```javascript
static title = t('Create Instance');
```
- `path`
- 资源操作的对应的路由
- 静态属性或静态函数
- 静态函数时,参数为
- 参数`item`,资源列表中的条目数据
- 参数`containerProps`,父级 container(即按钮所在资源列表页面)的`props`属性
- 以创建云主机`src/pages/compute/containers/Instance/actions/StepCreate/index.jsx`为例
- 在云主机列表页点击创建云主机按钮,页面跳转到`/compute/instance/create`
- 在云主机组详情页中点击创建云主机按钮,页面跳转到`/compute/instance/create?servergroup=${detail.id}`
```javascript
static path = (_, containerProps) => {
const { detail, match } = containerProps || {};
if (!detail || isEmpty(detail)) {
return '/compute/instance/create';
}
if (match.path.indexOf('/compute/server') >= 0) {
return `/compute/instance/create?servergroup=${detail.id}`;
}
};
```
- 静态属性,以创建云主机类型`src/pages/compute/containers/Flavor/actions/StepCreate/index.jsx`为例
```javascript
static path = '/compute/flavor-admin/create';
```
- `policy`
- 静态属性
- 页面对应的权限,如果权限验证不通过,则不会在资源列表页面显示该操作按钮
- 以创建云主机`src/pages/compute/containers/Instance/actions/StepCreate/index.jsx`为例
```javascript
static policy = [
'os_compute_api:servers:create',
'os_compute_api:os-availability-zone:list',
];
```
- `allowed`
- 静态函数
- 判定操作是否需要被禁用
- 返回`Promise`
- 不需用禁用的按钮,直接写作
```javascript
static allowed() {
return Promise.resolve(true);
}
```
- `name`
- 该操作对应的名称
- 在请求后提示语中使用该名称
- 以创建云主机`src/pages/compute/containers/Instance/actions/StepCreate/index.jsx`为例
```javascript
get name() {
return t('Create instance');
}
```
- `listUrl`
- 该操作对应的资源列表页
- 操作请求成功后,会自动进入到资源列表页
- 以创建云主机`src/pages/compute/containers/Instance/actions/StepCreate/index.jsx`为例
- 在镜像列表页的条目操作中,点击创建云主机并操作成功后,返回到镜像列表页
- 在云硬盘列表页的条目操作中,点击创建云主机并操作成功后,返回到云硬盘列表页
- 在云硬盘列表页的条目操作中,点击创建云主机并操作成功后,返回到云硬盘列表页
- 在云主机组详情页,点击创建云主机并操作成功后,返回到云主机详情页中
- 在云主机列表页中,点击创建云主机并操作成功后,返回到云主机列表页
```javascript
get listUrl() {
const { image, volume, servergroup } = this.locationParams;
if (image) {
return '/compute/image';
}
if (volume) {
return '/storage/volume';
}
if (servergroup) {
return `/compute/server-group/detail/${servergroup}`;
}
return '/compute/instance';
}
```
- `steps`
- 每一步的配置
- 每个配置项
- `title`,每一步的标题
- `component`,每一步表单对应的组件,继承于`BaseForm`(`src/components/Form`)
- 以创建云主机`src/pages/compute/containers/Instance/actions/StepCreate/index.jsx`为例
- 包含 4 步:基础配置、网络配置、系统配置、确认配置
```javascript
get steps() {
return [
{
title: t('Base Config'),
component: BaseStep,
},
{
title: t('Network Config'),
component: NetworkStep,
},
{
title: t('System Config'),
component: SystemStep,
},
{
title: t('Confirm Config'),
component: ConfirmStep,
},
];
}
```
- `onSubmit`
- 该操作的请求函数
- 操作请求成功后,会自动进入到资源列表页
- 操作失败后,会在表单页显示错误提示
- 返回`Promise`
- 返回表单对应的`store`中的函数
## 按需复写的属性与函数
- `init`
- 初始化操作
- 在其中定义`this.store``loading`状态的展示是基于`this.store.isSubmitting`
- 在其中调用获取表单所需其他数据的函数
- 对`this.state`中属性的更新
- 以创建云主机`src/pages/compute/containers/Instance/actions/StepCreate/index.jsx`为例
- 获取配额信息
```javascript
init() {
this.store = globalServerStore;
this.projectStore = globalProjectStore;
this.getQuota();
}
```
- `instanceName`
- 请求发送后,提示信息中的资源名称
- 默认值为`this.values.name`
- 以创建云主机`src/pages/compute/containers/Instance/actions/StepCreate/index.jsx`为例
- 如果是批量创建云主机,则按`${name}-${index + 1}`的形式展示名称
```javascript
get instanceName() {
const { name, count = 1 } = this.values || {};
if (count === 1) {
return this.unescape(name);
}
return this.unescape(
new Array(count)
.fill(count)
.map((_, index) => `${name}-${index + 1}`)
.join(', ')
);
}
```
- `renderFooterLeft`
- 对表单底部左侧内部的渲染
- 默认返回`null`
- src/pages/compute/containers/Instance/actions/StepCreate/index.jsx
- 展示批量创建的数量
- 基于输入的数量与剩余配额判定当前表单是否正确
```javascript
renderFooterLeft() {
const { data } = this.state;
const { count = 1, source: { value: sourceValue } = {} } = data;
const configs = {
min: 1,
max: sourceValue === 'bootableVolume' ? 1 : 100,
precision: 0,
onChange: this.onCountChange,
formatter: (value) => `$ ${value}`.replace(/\D/g, ''),
};
return (
<div>
<span>{t('Count')}</span>
<InputNumber
{...configs}
value={count}
className={classnames(styles.input, 'instance-count')}
/>
{this.renderBadge()}
</div>
);
}
```
- `successText`
- 操作成功的提示信息
- `errorText`
- 错误信息的展示
- 一般不需要复写
- `renderFooterLeft`
- 表单底部左侧的渲染函数
## 不需要复写的属性与函数
- `values`
- 表单验证成功后,更新的表单值
- `isAdminPage`
- 当前页面是否是“管理平台”的页面
- `getUrl`
- 生成页面 Url 的函数
- 如:需要给关联资源提供跳转功能,使用该函数,可以在控制台跳转到控制台的相应地址,在管理平台跳转到管理平台的相应地址
## 基类中的基础函数
- `StepAction`继承于`StepForm`
- 建议查看代码理解,`src/components/StepForm/index.jsx`

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 555 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 788 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 874 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -0,0 +1,142 @@
简体中文 | [English](/docs/en/test/1-ready-to-work.md)
# 两种测试
我们提供了两种类型的测试
- E2E 测试
- 侧重于功能点测试
- 能提供代码覆盖率数据
- 使用`Cypress`框架
- 测试结果保存到便于预览的静态页面中
- 单元测试
- 侧重于基础函数测试
- 使用`Jest`框架
# E2E 测试
## 搭建 E2E 测试环境
在 CentosWindows 的 wsl2 中均成功搭建过 E2E 测试环境
- node 环境
- package.json 中要求:`"node": ">=10.22.0"`
- 验证 nodejs 版本
```shell
node -v
```
- yarn
- 安装 yarn
```shell
npm install -g yarn
```
- 安装依赖包
- 在项目根目录下执行,即`package.json`同级,需要耐心等待安装完成
```shell
yarn install
```
- 安装系统依赖
- `Ubuntu/Debian`
```shell
sudo apt-get install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb
```
- `CentOS`
```shell
yum install -y xorg-x11-server-Xvfb gtk2-devel gtk3-devel libnotify-devel GConf2 nss libXScrnSaver alsa-lib
```
- 调整访问路径、账号等信息
- E2E 的配置文件存放于`test/e2e/config/config.yaml`,在其中配置了
- `baseUrl`,测试访问路径
- `env`,环境变量
- `switchToAdminProject`,登录后是否需要切换到`admin`项目下
- `username`,访问控制台的用户名,需要具有控制台操作权限的用户
- `password`,访问控制台的密码
- `usernameAdmin`,访问管理平台的用户名,需要具有管理平台操作权限的用户
- `passwordAdmin`,访问管理平台的密码
- `testFiles`,测试文件列表
- 可以通过直接修改`config.yaml`中的相应数值完成配置变更
- 也可以通过`local_config.yaml`完成配置变更
- 复制`test/e2e/config/config.yaml`到`test/e2e/config/local_config.yaml`中
- 修改`local_config.yaml`中的相应变量
- 对于变量的取值,优先级为:`local_config.yaml` > `config.yaml`
## 命令行运行 E2E
```shell
yarn run test:e2e
```
![控制台](/docs/zh/test/images/e2e/console.png)
## GUI 运行 E2E
```shell
yarn run test:e2e:open
```
Cypress 提供了 GUI
![gui](/docs/zh/test/images/e2e/gui-list.png)
![work](/docs/zh/test/images/e2e/gui-work.png)
## E2E 测试结果
测试运行结束后,访问`test/e2e/report/merge-report.html`即可查看
![结果](/docs/zh/test/images/e2e/result.png)
## E2E 代码覆盖率测试结果
测试运行结束后,访问`coverage/lcov-report/index.html`即可查看
> 注意:代码覆盖率,需要 E2E 访问的`baseUrl`对应的前端包,是具有可检测代码覆盖率版本的`dist`包
```shell
yarn run build:test
```
以上述方式打包的文件,就是具有可测试代码覆盖率的前端包
以下,给出前端访问带有代码覆盖率功能的前端包的 nginx 配置
```nginx
server {
listen 0.0.0.0:8088 default_server;
root /path/to/skyline-console/dist;
index index.html;
server_name _;
location / {
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://<backend_address>;
}
}
```
# 单元测试
## 命令行运行单元测试
```shell
yarn run test:unit
```
## 单元测试结果
直接在命令行控制台中即可查看运行结果
![单元测试结果](/docs/zh/test/images/unit/result.png)

View File

@ -0,0 +1,91 @@
简体中文 | [English](/docs/en/test/2-catalog-introduction.md)
```
test
├── e2e E2E代码存放位置
│ ├── config
│ │ ├── config.yaml (E2E运行时的部分配置主要配置了测试用例文件列表登录账号等信息)
│ │ └── local_config.yaml (E2E运行时的部分配置主要配置了测试用例文件列表登录账号等信息是gitignore的优先级高于config.yaml)
│ ├── fixtures (存放运行时需要的上传文件,读取文件等)
│ │ ├── keypair (测试密钥读取的文件)
│ │ ├── metadata.json (测试元数据读取的文件)
│ │ ├── stack-content.yaml (测试堆栈读取的文件)
│ │ └── stack-params.yaml (测试堆栈读取的文件)
│ ├── integration (存放测试用例)
│ │ └── pages (按网页菜单结构调整目录)
│ │ ├── compute (计算)
│ │ │ ├── aggregate.spec.js (主机集合)
│ │ │ ├── baremetal.spec.js (裸机配置)
│ │ │ ├── flavor.spec.js (云主机类型)
│ │ │ ├── hypervisor.spec.js (虚拟机管理器)
│ │ │ ├── image.spec.js (镜像)
│ │ │ ├── instance.spec.js (云主机)
│ │ │ ├── ironic.spec.js (裸机)
│ │ │ ├── keypair.spec.js (密钥)
│ │ │ └── server-group.spec.js (云主机组)
│ │ ├── configuration (平台配置)
│ │ │ ├── metadata.spec.js (元数据)
│ │ │ └── system.spec.js (系统信息)
│ │ ├── error.spec.js (错误页面)
│ │ ├── heat (资源编排)
│ │ │ └── stack.spec.js (堆栈)
│ │ ├── identity (身份管理)
│ │ │ ├── domain.spec.js (域)
│ │ │ ├── project.spec.js (项目)
│ │ │ ├── role.spec.js (角色)
│ │ │ ├── user-group.spec.js (用户组)
│ │ │ └── user.spec.js (用户)
│ │ ├── login.spec.js (登录)
│ │ ├── management (运维管理)
│ │ │ └── recycle-bin.spec.js (回收站)
│ │ ├── network (网络)
│ │ │ ├── floatingip.spec.js (浮动IP)
│ │ │ ├── lb.spec.js (负载均衡)
│ │ │ ├── network.spec.js (网络)
│ │ │ ├── qos-policy.spec.js (Qos策略)
│ │ │ ├── router.spec.js (路由器)
│ │ │ ├── security-group.spec.js (安全组)
│ │ │ ├── topology.spec.js (网络拓扑)
│ │ │ ├── virtual-adapter.spec.js (虚拟网卡)
│ │ │ └── vpn.spec.js (VPN)
│ │ └── storage (存储)
│ │ ├── backup.spec.js (备份)
│ │ ├── qos.spec.js (QoS)
│ │ ├── snapshot.spec.js (云硬盘快照)
│ │ ├── storage.spec.js (存储后端)
│ │ ├── volume-type.spec.js (云硬盘类型)
│ │ └── volume.spec.js (云硬盘)
│ ├── plugins (Cypress的扩展)
│ │ └── index.js (配置了对配置文件的读取,配置了使用代码覆盖率功能)
│ ├── report (存放E2E的测试报告)
│ │ ├── merge-report.html (最终生成的测试报告,记录了每个用例的执行情况)
│ │ └── merge-report.json (results目录下的测试结果的汇总)
│ ├── results (存放测试用的结果文件)
│ ├── screenshots (存放测试出错时的快照)
│ ├── support (编写测试用例时,二次封装的函数)
│ │ ├── commands.js (存放登录、登出等操作函数)
│ │ ├── common.js (存放基础函数)
│ │ ├── constants.js (存放每个资源的路由)
│ │ ├── detail-commands.js (存放资源详情页相关的函数,基于框架,详情页的操作具有一致性)
│ │ ├── form-commands.js (存放表单相关的函数,基于框架,对表单项的操作具有一致性)
│ │ ├── index.js
│ │ ├── resource-commands.js (存放资源操作相关的函数,如:创建云主机、创建路由、删除资源等)
│ │ └── table-commands.js (存放资源列表相关的函数,基于框架,对列表的操作具有一致性)
│ └── utils (存放对于配置文件的读取函数)
│ └── index.js
└── unit (单元测试)
├── local-storage-mock.js (本地存储的mock函数)
├── locales (测试国际化时使用的翻译文件)
│ ├── en-US.js
│ └── zh-CN.js
├── setup-tests.js (配置单元测试)
└── svg-mock.js (图片加载的mock)
```
- E2E 测试的代码,存放在`test/e2e`目录下
- E2E 的其他全局配置,存放在`cypress.json`
- 单元测试的基础代码,存放在`test/unit`目录下
- 单元测试的其他全局配置,存放在`jest.config.js`
- 单元测试的测试代码,通常是与待测试文件放在相同的目录下,并以`test.js`或`spec.js`为后缀
- 如:`src/utils/index.js`与`src/utils/index.test.js`
- 如:`src/utils/local-storage.js`与`src/utils/local-storage.spec.js`

View File

@ -0,0 +1,104 @@
简体中文 | [English](/docs/en/test/3-0-how-to-edit-e2e-case.md)
关于 Cypress 的具体介绍及使用方法,请参考[官方文档](https://docs.cypress.io/guides/overview/why-cypress)
这里主要给出编写 Skyline-console 前端页面中,资源对应的 E2E 用例,并使用`test/e2e/support`中定义的函数的说明
以下介绍,以云主机用例`test/e2e/integration/pages/compute/instance.spec.js`为例
一般,测试资源的相应功能时,是按照以下顺序
1. 准备测试使用的相关变量
- 创建资源时的必须参数,如:名称、密码
- 编辑资源时的必须参数,如:新的名称
- 创建关联资源时,关联资源的名称,如:网络名称、路由器名称、云硬盘名称
```javascript
const uuid = Cypress._.random(0, 1e6);
const name = `e2e-instance-${uuid}`;
const newname = `${name}-1`;
const password = 'passw0rd_1';
const volumeName = `e2e-instance-attach-volume-${uuid}`;
const networkName = `e2e-network-for-instance-${uuid}`;
const routerName = `e2e-router-for-instance-${uuid}`;
```
2. 操作前登录
- 如果是操作控制台资源,使用`cy.login`
- 如果是操作管理平台资源,使用`cy.loginAdmin`
- 一般会在`login`与`loginAdmin`函数中使用变量`listUrl`,即登录后直接访问资源所在页面
```javascript
beforeEach(() => {
cy.login(listUrl);
});
```
3. 创建关联资源,使用`resource-commands.js`中提供的创建资源的函数,以测试云主机为例
- 创建网络,用于测试创建云主机、挂载网卡
```javascript
cy.createNetwork({ name: networkName });
```
- 创建路由器`cy.createRouter`,用于测试关联浮动 IP 时确保浮动 IP 可达
- 以如下方式创建的路由器将开启外网网关,并绑定了`networkName`网络的子网
```javascript
cy.createRouter({ name: routerName, network: networkName });
```
- 创建浮动 IP`cy.createFip`,用于测试关联浮动 IP
```javascript
cy.createFip();
```
- 创建云硬盘`cy.createVolume`(用于测试挂载云硬盘)
```javascript
cy.createVolume(volumeName);
```
4. 编写创建资源的用例
5. 编写访问资源详情的用例
6. 分别编写资源的所有操作对应的用例
- 一般`编辑`操作的用例写在后面,其后编写`删除`操作的用例,这样能测试到编辑是否生效
7. 删除关联资源,使用`resource-commands.js`中提供的删除资源的函数,这是为了测试用例执行后,测试账号内的资源尽可能的干净
- 删除浮动 IP
```javascript
cy.deleteAll('fip');
```
- 删除路由器`routerName`
```javascript
cy.deleteRouter(routerName, networkName);
```
- 删除网络`networkName`
```javascript
cy.deleteAll('network', networkName);
```
- 删除云硬盘`volumeName`
```javascript
cy.deleteAll('volume', volumeName);
```
- 删除所有可用状态的云硬盘
```javascript
cy.deleteAllAvailableVolume();
```
上述步骤中的`4`、`5`、`6`主要使用了
- `test/e2e/support/form-commands.js`中的函数操作表单,详细介绍见[3-1-E2E-form-operation](3-1-E2E-form-operation.md)
- `test/e2e/support/table-commands.js`中的函数,操作表格中的按钮点击、搜索、进入详情,详细介绍见[3-2-E2E-table-operation](3-2-E2E-table-operation.md)
- `test/e2e/support/detail-commands.js`中的函数,操作返回列表页、检测详情内容、切换详情 Tab详细介绍见[3-3-E2E-detail-operation](3-3-E2E-detail-operation.md)
创建、删除关联资源主要使用了`test/e2e/support/resource-commands.js`中的函数,,详细介绍见[3-4-E2E-resource-operation](3-4-E2E-resource-operation.md)

View File

@ -0,0 +1,591 @@
简体中文 | [English](/docs/en/test/3-1-E2E-form-operation.md)
因为前端框架使用的一致性,我们在编写表单操作的相关用例,选取元素并进行操作时,往往会发现有很强的规律性,所以我们对大多数表单操作都编写了相应的 Cypress 函数,极大的减少了编写测试用例的难度,以下会对主要使用的表单操作函数做出详细的说明。
> 注意:编写的函数均以能完整完成对一个表单项的操作为原则
## 点击按钮的操作
- `closeNotice`
- 关闭操作后右上角的操作成功的提示信息
![notice](/docs/zh/test/images/e2e/form/notice.png)
- `waitFormLoading`
- 等待表单请求完成
- 表单填写并验证通过后,点击确认按钮,会向服务端发起相应请求,这时表单项的确认按钮会处于`Loading`的状态
- 使用该函数,而不是`cy.wait(seconds)`,能更有效的保证同步请求已经处理完全,从而保证后续用例的先决条件
![wait-form-loading](/docs/zh/test/images/e2e/form/wait-form-loading.png)
- `clickFormActionSubmitButton`
- 点击确认型表单的确认按钮,并等待请求完成
![click-form-submit](/docs/zh/test/images/e2e/form/click-form-submit.png)
- `clickModalActionSubmitButton`
- 点击弹窗型表单的确认按钮,并等待请求完成
![click-modal-submit](/docs/zh/test/images/e2e/form/click-modal-submit.png)
- `clickModalActionCancelButton`
- 点击弹窗型表单的取消按钮
- `clickConfirmActionSubmitButton`
- 点击确认型表单的确认按钮,等待请求完成,并关闭请求成功的提示信息
- 参数`waitTime`,关闭提示信息后的等待时间
![click-confirm-submit](/docs/zh/test/images/e2e/form/click-confirm-submit.png)
- `checkDisableAction`
- 某些数据不符合要求时,使用批量操作,会弹出报错,该函数验证该数据的确不合操作要求,并关闭报错提示
- 以锁定状态的云主机`test/e2e/integration/pages/compute/instance.spec.js`为例
- 锁定后不再支持启动、关闭、重启操作
```javascript
it('successfully lock', () => {
cy.tableSearchText(name)
.clickConfirmActionInMoreSub('Lock', 'Instance Status')
.wait(10000);
cy.tableSearchText(name)
.selectFirst()
.clickHeaderButtonByTitle('Start')
.checkDisableAction(2000)
.clickHeaderButtonByTitle('Stop')
.checkDisableAction(2000)
.clickHeaderButtonByTitle('Reboot')
.checkDisableAction(2000);
});
```
![disable-action](/docs/zh/test/images/e2e/form/disable-action.png)
- `clickStepActionNextButton`
- 点击分步表单的下一步/确认按钮
- 以创建云主机用例`test/e2e/integration/pages/compute/instance.spec.js`为例
- 共需要点击 3 次下一步1 次确认按钮
```javascript
it('successfully create', () => {
cy.clickHeaderButton(1)
.url()
.should('include', `${listUrl}/create`)
.wait(5000)
.formTableSelect('flavor')
.formTableSelect('image')
.formSelect('systemDisk')
.formAddSelectAdd('dataDisk')
.formSelect('dataDisk')
.wait(2000)
.clickStepActionNextButton()
.wait(5000)
.formTableSelectBySearch('networkSelect', networkName, 5000)
.formTableSelectBySearch('securityGroup', 'default', 5000)
.wait(2000)
.clickStepActionNextButton()
.formInput('name', name)
.formRadioChoose('loginType', 1)
.formInput('password', password)
.formInput('confirmPassword', password)
.wait(2000)
.clickStepActionNextButton()
.wait(2000)
.clickStepActionNextButton()
.waitFormLoading()
.url()
.should('include', listUrl)
.closeNotice()
.waitStatusActiveByRefresh();
});
```
![click-step-next](/docs/zh/test/images/e2e/form/click-step-next.png)
- `clickStepActionCancelButton`
- 点击分步表单的取消按钮
- 以镜像创建云主机用例`test/e2e/integration/pages/compute/image.spec.js`为例
- 只验证能成功进入到创建云主机页面,然后点击取消按钮完成该用例
```javascript
it('successfully create instance with cancel', () => {
cy.tableSearchText(name)
.clickActionInMore('Create Instance')
.wait(2000)
.clickStepActionCancelButton();
});
```
## 对表单项的操作
通过页面查看元素的结构、样式等,发现,所有的表单项,都具有`id`,而且对应于开发时编写的表单配置`formItem`的`name`属性,也可直接通过查看页面内元素的`id`获取`name`,如下图所示,`form-item-col-`之后的内容便是`name`
![form-name](/docs/zh/test/images/e2e/form/form-name.png)
- `formInput`
- 带有`input`输入框的表单项输入内容
- 参数`formItemName`,即开发代码中`formItem`的`name`值
- 参数`value`,输入的内容
- 以编辑云主机用例`test/e2e/integration/pages/compute/instance.spec.js`为例
```javascript
it('successfully edit', () => {
cy.tableSearchText(name)
.clickActionInMore('Edit')
.formInput('name', newname)
.clickModalActionSubmitButton()
.wait(2000);
});
```
![input](/docs/zh/test/images/e2e/form/input.png)
- `formJsonInput`
- 带有`textarea`输入框的表单项输入`json`格式内容
- 参数`formItemName`,即开发代码中`formItem`的`name`值
- 参数`content`,输入的对象
- 以创建堆栈,编写`json`型的参数`test/e2e/integration/pages/heat/stack.spec.js`为例
```javascript
it('successfully create', () => {
const volumeJson = {
name: volumeName,
};
cy.clickHeaderButton(1, 2000)
.formAttachFile('content', contentFile)
.formAttachFile('params', paramFile)
.clickStepActionNextButton()
.wait(2000)
.formInput('name', name)
.formJsonInput('volume_name_spec', volumeJson)
.clickStepActionNextButton()
.waitFormLoading()
.wait(5000)
.tableSearchSelectText('Name', name)
.waitStatusActiveByRefresh();
});
```
![textarea-json](/docs/zh/test/images/e2e/form/textarea-json.png)
- `formCheckboxClick`
- 点击表单项中的`checkbox`
- 参数`formItemName`,即开发代码中`formItem`的`name`值
- 参数`index`,默认为`0`
- 以云主机修改配置`test/e2e/integration/pages/compute/instance.spec.js`为例
```javascript
it('successfully resize', () => {
cy.tableSearchText(name)
.clickActionInMoreSub('Resize', 'Configuration Update')
.wait(5000)
.formTableSelect('newFlavor')
.formCheckboxClick('option')
.clickModalActionSubmitButton()
.waitStatusActiveByRefresh();
});
```
![checkbox](/docs/zh/test/images/e2e/form/checkbox.png)
- `formTableSelectAll`
- 对表格选择类型的表单项做全选操作
- 参数`formItemName`,即开发代码中`formItem`的`name`值
- 以云硬盘类型修改访问`test/e2e/integration/pages/storage/volume-type.spec.js`为例
```javascript
it('successfully manage access to projects', () => {
cy.tableSearchText(name)
.clickActionInMore('Manage Access')
.formCheckboxClick('isPublic')
.formTableSelectAll('access')
.clickModalActionSubmitButton();
});
```
![select-all](/docs/zh/test/images/e2e/form/select-all.png)
- `formTableNotSelectAll`
- 对表格选择类型的表单项做取消全选操作
- 参数`formItemName`,即开发代码中`formItem`的`name`值
- 以主机集合管理主机时不选择主机`test/e2e/integration/pages/compute/aggregate.spec.js`为例
```javascript
it('successfully manage host: no host', () => {
cy.tableSearchText(newname)
.clickActionInMore('Manage Host')
.formTableNotSelectAll('hosts')
.clickModalActionSubmitButton();
});
```
![unselect-all](/docs/zh/test/images/e2e/form/unselect-all.png)
- `formTableSelect`
- 对表格选择类型的表单项做选择操作
- 参数`formItemName`,即开发代码中`formItem`的`name`值
- 参数`value`,如果设置`value`,则选择表格中含有该值的条目,如果不设置`value`,则选择表格中的第一个条目
- 以云主机挂载网卡选择网络`test/e2e/integration/pages/compute/instance.spec.js`为例
```javascript
it('successfully attach interface', () => {
cy.tableSearchText(name)
.clickActionInMoreSub('Attach Interface', 'Related Resources')
.wait(5000)
.formTableSelect('network')
.clickModalActionSubmitButton();
});
```
![select-table](/docs/zh/test/images/e2e/form/select-table.png)
- `formTableSelectBySearch`
- 对表格选择类型的表单项,先做搜索操作,然后选择条目中的第一条
- 参数`formItemName`,即开发代码中`formItem`的`name`值
- 参数`value`,搜索内容,一般是对搜索项中`名称`的搜索
- 参数`waitTime`,搜索后等待时间,不设置,等待 2 秒钟
- 以云主机挂载云硬盘选择云硬盘`test/e2e/integration/pages/compute/instance.spec.js`为例
- 操作成功后,进入云硬盘列表页查看云硬盘的状态为“已使用”
```javascript
it('successfully attach volume', () => {
// prepair volume
cy.visitPage(listUrl)
.tableSearchText(name)
.clickActionInMoreSub('Attach Volume', 'Related Resources')
.wait(5000)
.formTableSelectBySearch('volume', volumeName)
.clickModalActionSubmitButton()
.wait(5000);
// check attach successful
cy.tableSearchText(name)
.goToDetail()
.clickDetailTab('Volume')
.tableSearchText(volumeName)
.checkColumnValue(2, 'In-use');
});
```
![select-table-search](/docs/zh/test/images/e2e/form/select-table-search.png)
- `formTableSelectBySearchOption`
- 对表格选择类型的表单项,先做搜索操作,然后选择条目中的第一条
- 搜索是对搜索项的选择,不同于`formTableSelectBySearch`是基于输入
- 参数`formItemName`,即开发代码中`formItem`的`name`值
- 参数`name`,搜索项的名称
- 参数`value`,搜索项对应的值
- 参数`waitTime`,搜索后的等待时间,默认为 2 秒
- 以创建全量备份`test/e2e/integration/pages/storage/backup.spec.js`为例
- 选择状态为使用中的云硬盘
```javascript
it('successfully create full bakcup', () => {
cy.clickHeaderButton(1, 5000)
.formInput('name', name)
.formTableSelectBySearch('volume', volumeName)
.clickModalActionSubmitButton()
.wait(5000)
.waitTableLoading();
cy.clickHeaderButton(1, 5000)
.formInput('name', nameIns)
.formTableSelectBySearchOption('volume', 'Status', 'In-use')
.clickModalActionSubmitButton();
cy.wait(30000);
});
```
![select-table-option](/docs/zh/test/images/e2e/form/select-table-option.png)
- `formSelect`
- 对选择器类型的表单项的操作
- 参数`formItemName`,即开发代码中`formItem`的`name`值
- 参数`label`,选中的内容,如果不设置,选中第一个选项,如果设置,选择`label`对应的选项
- 以创建云主机组选择策略`test/e2e/integration/pages/compute/server-group.spec.js`为例
```javascript
it('successfully create', () => {
cy.clickHeaderButton(1)
.formInput('name', name)
.formSelect('policy')
.clickModalActionSubmitButton();
});
```
![select](/docs/zh/test/images/e2e/form/select.png)
- 以网络 QoS 策略创建带宽限制规则时设置方向为“入方向”`test/e2e/integration/pages/network/qos-policy.spec.js`为例
```javascript
it('successfully create bandwidth ingress limit rule', () => {
cy.tableSearchText(name)
.clickActionInMore('Create Bandwidth Limit Rule')
.formSelect('direction', 'ingress')
.clickModalActionSubmitButton();
});
```
![select-value](/docs/zh/test/images/e2e/form/select-value.png)
- `formRadioChoose`
- 对单选类型的表单项的操作
- 参数`formItemName`,即开发代码中`formItem`的`name`值
- 参数`itemIndex`,选中第几项,默认为 0即选择第一项
- 以创建密钥选择“导入密钥”`test/e2e/integration/pages/compute/keypair.spec.js`为例
```javascript
it('successfully create by file', () => {
cy.clickHeaderButton(1)
.formRadioChoose('type', 1)
.formInput('name', nameByFile)
.formAttachFile('public_key', filename)
.clickModalActionSubmitButton()
.tableSearchText(nameByFile)
.checkTableFirstRow(nameByFile);
});
```
![radio](/docs/zh/test/images/e2e/form/radio.png)
- `formAttachFile`
- 对上传文件的表单项的操作
- 参数`formItemName`,即开发代码中`formItem`的`name`值
- 参数`filename`,上传文件的名称,文件需要预先保存在`test/e2e/fixtures`目录下
- 以创建密钥选择文件为例`test/e2e/integration/pages/compute/keypair.spec.js`为例
```javascript
it('successfully create by file', () => {
cy.clickHeaderButton(1)
.formRadioChoose('type', 1)
.formInput('name', nameByFile)
.formAttachFile('public_key', filename)
.clickModalActionSubmitButton()
.tableSearchText(nameByFile)
.checkTableFirstRow(nameByFile);
});
```
![attach-file](/docs/zh/test/images/e2e/form/attach-file.png)
- 以创建镜像选择文件为例`test/e2e/integration/pages/compute/image.spec.js`为例
```javascript
it('successfully create', () => {
cy.clickHeaderButton(1)
.url()
.should('include', `${listUrl}/create`)
.formInput('name', name)
.formAttachFile('file', filename)
.formSelect('disk_format', 'QCOW2 - QEMU Emulator')
.formSelect('os_distro', 'Others')
.formInput('os_version', 'cirros-0.4.0-x86_64')
.formInput('os_admin_user', 'root')
.formSelect('usage_type', 'Common Server')
.formText('description', name)
.clickFormActionSubmitButton()
.wait(2000)
.url()
.should('include', listUrl)
.tableSearchText(name)
.waitStatusActiveByRefresh();
});
```
![attach-file-image](/docs/zh/test/images/e2e/form/attach-file-image.png)
- `formAddSelectAdd`
- 对可增加条目的表单项的操作
- 参数`formItemName`,即开发代码中`formItem`的`name`值
- 以主机集合管理元数据时,添加新的自定义元数据`test/e2e/integration/pages/compute/aggregate.spec.js`为例
```javascript
it('successfully manage metadata', () => {
cy.tableSearchText(name)
.clickActionInMore('Manage Metadata')
.wait(5000)
.formAddSelectAdd('customs')
.formInputKeyValue('customs', 'key', 'value')
.formTransferLeftCheck('systems', 0)
.clickModalActionSubmitButton();
});
```
![add-select](/docs/zh/test/images/e2e/form/add-select.png)
- `formSwitch`
- 对开关型的表单项的点击操作
- 参数`formItemName`,即开发代码中`formItem`的`name`值
- 以创建具有共享属性的网络 QoS 策略`test/e2e/integration/pages/network/qos-policy.spec.js`为例
```javascript
it('successfully create', () => {
cy.clickHeaderButton(1)
.wait(2000)
.formInput('name', name)
.formText('description', name)
.formSwitch('shared')
.clickModalActionSubmitButton();
});
```
![switch](/docs/zh/test/images/e2e/form/switch.png)
- `formButtonClick`
- 对表单项中的按钮的点击操作
- 参数`formItemName`,即开发代码中`formItem`的`name`值
- 以项目更改配额时展开/关闭“高级选项”`test/e2e/integration/pages/identity/project.spec.js`为例
```javascript
it('successfully edit quota', () => {
cy.tableSearchText(name)
.clickActionInMore('Edit Quota')
.formInput('instances', 11)
.formButtonClick('more')
.wait(2000)
.formButtonClick('more')
.clickModalActionSubmitButton();
});
```
![more](/docs/zh/test/images/e2e/form/more.png)
![more-open](/docs/zh/test/images/e2e/form/more-open.png)
- `formTransfer`
- 对穿梭框类型的表单操作
1. 对左侧的穿梭框基于搜索展示指定待选条目
2. 选中待选条目的第一条
3. 点击穿梭框中间的方向按钮,使得选中内容进入到右侧穿梭框中
- 参数`formItemName`,即开发代码中`formItem`的`name`值
- 参数`value`,搜索内容
- 以项目管理用户`test/e2e/integration/pages/identity/project.spec.js`为例
```javascript
it('successfully manage user', () => {
cy.tableSearchText(name)
.clickActionInMore('Manage User')
.formTransfer('select_user', username)
.formTransferRight('select_user', username)
.formSelect('select_user', 'admin')
.clickModalActionSubmitButton();
});
```
![transfer-left](/docs/zh/test/images/e2e/form/transfer-left.png)
- `formTransferRight`
- 对右侧的穿梭框基于搜索展示指定待选条目
- 参数`formItemName`,即开发代码中`formItem`的`name`值
- 参数`value`,搜索内容
- 以用户组管理用户为例`test/e2e/integration/pages/identity/user-group.spec.js`为例
```javascript
it('successfully manage user', () => {
cy.tableSearchText(name)
.clickActionInMore('Manage User')
.formTransfer('select_user', username)
.formTransferRight('select_user', username)
.clickModalActionSubmitButton();
cy.tableSearchText(name)
.goToDetail()
.clickDetailTab('Sub User', 'userGroup');
});
```
![transfer-right](/docs/zh/test/images/e2e/form/transfer-right.png)
- `formTabClick`
- 点击带有 Tab 的表单项中的 Tab
- 参数`formItemName`,即开发代码中`formItem`的`name`值
- 参数`index`,指定的 Tab 的下标
- 以编辑浮动 IP切换到共享策略为例`test/e2e/integration/pages/network/floatingip.spec.js`为例
```javascript
it('successfully edit', () => {
cy.clickFirstActionButton()
.formText('description', 'description')
.formTabClick('qos_policy_id', 1)
.wait(5000)
.formTableSelectBySearch('qos_policy_id', policyName)
.clickModalActionSubmitButton()
.wait(2000);
});
```
![tab](/docs/zh/test/images/e2e/form/tab.png)
- `formInputKeyValue`
- 对`KeyValue`组件的表单项进行输入操作,一般是配合`formAddSelectAdd`使用,对添加的新的`KeyValue`组件的条目,输入内容
- 参数`formItemName`,即开发代码中`formItem`的`name`值
- 参数`key`,左侧 input 输入的内容
- 参数`value`,右侧 input 输入的内容
- 以主机集合管理元数据时,添加新的自定义元数据`test/e2e/integration/pages/compute/aggregate.spec.js`为例
```javascript
it('successfully manage metadata', () => {
cy.tableSearchText(name)
.clickActionInMore('Manage Metadata')
.wait(5000)
.formAddSelectAdd('customs')
.formInputKeyValue('customs', 'key', 'value')
.formTransferLeftCheck('systems', 0)
.clickModalActionSubmitButton();
});
```
![key-value](/docs/zh/test/images/e2e/form/key-value.png)
- `formTransferLeftCheck`
- 对左侧的穿梭框的操作
1. 选中左侧穿梭框中的指定节点
2. 点击穿梭框中间的方向按钮,使得选中内容进入到右侧穿梭框中
- 参数`formItemName`,即开发代码中`formItem`的`name`值
- 参数`index`,节点的下标
- 以主机集合管理元数据时,添加新的自定义元数据`test/e2e/integration/pages/compute/aggregate.spec.js`为例
```javascript
it('successfully manage metadata', () => {
cy.tableSearchText(name)
.clickActionInMore('Manage Metadata')
.wait(5000)
.formAddSelectAdd('customs')
.formInputKeyValue('customs', 'key', 'value')
.formTransferLeftCheck('systems', 0)
.clickModalActionSubmitButton();
});
```
![transfer-left-click](/docs/zh/test/images/e2e/form/transfer-left-click.png)
- `formTransferRightCheck`
- 对右侧的穿梭框的操作
1. 选中右侧穿梭框中的节点
2. 点击穿梭框中间的方向按钮,使得选中内容进入到左侧穿梭框中
- 参数`formItemName`,即开发代码中`formItem`的`name`值
- 参数`index`,节点的下标
- 以云主机类型,修改元数据`test/e2e/integration/pages/compute/flavor.spec.js`为例
```javascript
it('successfully manage metadata', () => {
cy.clickTab('Custom')
.tableSearchText(customName)
.clickActionButtonByTitle('Manage Metadata')
.wait(5000)
.formTransferLeftCheck('systems', 0)
.clickModalActionSubmitButton();
// todo: remove key-value metadata
cy.clickTab('Custom')
.tableSearchText(customName)
.clickActionButtonByTitle('Manage Metadata')
.wait(5000)
.formTransferRightCheck('systems', 0)
.clickModalActionSubmitButton();
});
```
![transfer-right-check](/docs/zh/test/images/e2e/form/transfer-right-check.png)
对资源操作的各种操作,主要用到了上方介绍的函数,函数的具体编写,请查看`test/e2e/support/form-commands.js`

View File

@ -0,0 +1,548 @@
简体中文 | [English](/docs/en/test/3-2-E2E-table-operation.md)
因为前端框架使用的一致性,我们在编写表单操作的相关用例,选取元素并进行操作时,往往会发现有很强的规律性,所以我们对大多数表格操作都编写了相应的 Cypress 函数,极大的减少了编写测试用例的难度,以下会对主要使用的表格操作函数做出详细的说明。
## 对表格整体的操作
主要包含:等待列表加载完成
- `waitTableLoading`
- 等待列表加载完成
- 列表在加载过程中,会有`loading`状态展示,等待该状态结束
![wait-table-loading](/docs/zh/test/images/e2e/table/wait-table-loading.png)
- `checkTableFirstRow`
- 验证表格第一行是否包含指定内容,一般用于创建后验证创建资源是否存在
- 参数`name`,第一行需要包含的内容,一般用于验证名称是否存在
- 以查看密钥详情`test/e2e/integration/pages/compute/keypair.spec.js`为例
- 创建后,检查密钥是否存在,验证成功后进入详情页
```javascript
it('successfully detail', () => {
cy.tableSearchText(name)
.checkTableFirstRow(name)
.goToDetail()
.checkDetailName(name);
cy.goBackToList(listUrl);
});
```
![check-first-row](/docs/zh/test/images/e2e/table/check-first-row.png)
- `tableSearchText`
- 在表格上方的搜索栏中输入内容,并等待搜索完成
- 参数`str`,搜索的内容,一般是搜索名称
- 通过搜索,使得待操作的资源位于表格中的第一行,以便进行后续的操作
- 以查看密钥详情`test/e2e/integration/pages/compute/keypair.spec.js`为例
1. 创建后,使用名称搜索密钥,并等待搜索完成
2. 检查表格中第一行是否包含指定名称的资源
3. 进入详情页,检查名称是否与预期一致
4. 返回列表页
```javascript
it('successfully detail', () => {
cy.tableSearchText(name)
.checkTableFirstRow(name)
.goToDetail()
.checkDetailName(name);
cy.goBackToList(listUrl);
});
```
![search](/docs/zh/test/images/e2e/table/search.png)
- `tableSimpleSearchText`
- 在表格上方的搜索栏中输入内容,并等待搜索完成
- 有些表格使用的是简单搜索,搜索项只支持输入文字,此时搜索框使用的组件与`tableSearchText`中的搜索框组件不一样
- 参数`str`,搜索的内容,一般是搜索名称
- 通过搜索,使得待操作的资源位于表格中的第一行,以便进行后续的操作
- 以搜索系统信息中的服务`test/e2e/integration/pages/configuration/system.spec.js`为例
```javascript
it('successfully services', () => {
cy.tableSimpleSearchText('nova');
});
```
![simple-search](/docs/zh/test/images/e2e/table/simple-search.png)
- `tableSearchSelect`
- 使用表格上方的搜索栏中的选择项进行搜索,并等待搜索完成
1. 点击输入框,在待选搜索项中选择搜索项
2. 点击选中搜索类别下的选择项
3. 等待搜索完成
- 参数`name`,搜索项的名称
- 参数`value`,搜索项对应的选择项的标签
- 通过搜索,使得待操作的资源位于表格中的第一行,以便进行后续的操作
- 以浮动IP绑定云主机`test/e2e/integration/pages/network/floatingip.spec.js`为例
1. 在浮动IP表格中搜索`状态`为`停止`的浮动IP
2. 对表格中的第一个资源点击`关联`操作
3. 完成绑定云主机的操作
```javascript
it('successfully associate instance', () => {
cy.tableSearchSelect('Status', 'Down')
.clickActionInMore('Associate')
.wait(5000)
.formTableSelectBySearch('instance', instanceName)
.wait(5000)
.formTableSelect('port')
.clickModalActionSubmitButton();
});
```
![search-select-1](/docs/zh/test/images/e2e/table/search-select-1.png)
![search-select-2](/docs/zh/test/images/e2e/table/search-select-2.png)
![search-select-3](/docs/zh/test/images/e2e/table/search-select-3.png)
- `tableSearchSelectText`
- 使用表格上方的搜索栏进行搜索,并等待搜索完成
1. 点击输入框,在待选搜索项中选择搜索项
2. 输入搜索内容,回车
3. 等待搜索完成
- 不选择搜索项时直接输入,是对第一个支持输入的搜索项进行搜索
- 参数`name`,搜索项的名称
- 参数`value`,输入的内容
- 通过搜索,使得待操作的资源位于表格中的第一行,以便进行后续的操作
- 以创建堆栈`test/e2e/integration/pages/heat/stack.spec.js`为例
1. 创建后,进入资源列表页
2. 在列表页使用名称搜索
3. 等待资源的状态可用
```javascript
it('successfully create', () => {
const volumeJson = {
name: volumeName,
};
cy.clickHeaderButton(1, 2000)
.formAttachFile('content', contentFile)
.formAttachFile('params', paramFile)
.clickStepActionNextButton()
.wait(2000)
.formInput('name', name)
.formJsonInput('volume_name_spec', volumeJson)
.clickStepActionNextButton()
.waitFormLoading()
.wait(5000)
.tableSearchSelectText('Name', name)
.waitStatusActiveByRefresh();
});
```
![search-text-1](/docs/zh/test/images/e2e/table/search-text-1.png)
![search-text-2](/docs/zh/test/images/e2e/table/search-text-2.png)
![search-text-3](/docs/zh/test/images/e2e/table/search-text-3.png)
- `checkEmptyTable`
- 验证表格为空表格
- 一般用于删除资源后验证
- 以删除路由器`test/e2e/integration/pages/network/router.spec.js`为例
1. 关闭外网网关
2. 删除
3. 搜索
4. 验证表格为空表格,即删除成功
```javascript
it('successfully close external gateway and delete', () => {
cy.tableSearchText(newname)
.clickConfirmActionInMore('Close External Gateway')
.clickConfirmActionInMore('Delete')
.tableSearchText(newname)
.checkEmptyTable();
});
```
- `goToDetail`
- 访问第一行资源的详情页,并等待详情页加载完成
- 参数`index`,链接所在列的下标,默认为`1`
- 参数`waitTime`,详情页加载完后等待的时间
- 以镜像`test/e2e/integration/pages/compute/image.spec.js`为例
1. 搜索
2. 进入详情页
3. 验证详情名称
4. 返回列表页
```javascript
it('successfully detail', () => {
cy.tableSearchText(name).goToDetail();
cy.checkDetailName(name);
cy.goBackToList(listUrl);
});
```
![detail-1](/docs/zh/test/images/e2e/table/detail-1.png)
![detail-2](/docs/zh/test/images/e2e/table/detail-2.png)
- `checkColumnValue`
- 验证第一行指定列的内容是否符合预期
- 参数`columnIndex`,指定列的下标
- 参数`value`,预期的值
- 以云主机`test/e2e/integration/pages/compute/image.spec.js`为例
1. 搜索
2. 关闭云主机
3. 验证云主机的状态为`关闭`
4. 验证批量操作中的`关闭`操作不可用
```javascript
it('successfully stop', () => {
cy.tableSearchText(name)
.clickConfirmActionInMoreSub('Stop', 'Instance Status')
.wait(10000)
.tableSearchText(name)
.checkColumnValue(6, 'Shutoff')
.selectFirst()
.clickHeaderButtonByTitle('Stop')
.checkDisableAction(2000);
});
```
![check-value](/docs/zh/test/images/e2e/table/check-value.png)
- `selectFirst`
- 选中表格中第一行,以便做后续的批量操作
- 以云主机`test/e2e/integration/pages/compute/image.spec.js`为例
1. 搜索
2. 关闭云主机
3. 验证云主机的状态为`关闭`
4. 选中第一行
5. 点击批量操作中的`关闭`按钮
6. 弹出错误提示
```javascript
it('successfully stop', () => {
cy.tableSearchText(name)
.clickConfirmActionInMoreSub('Stop', 'Instance Status')
.wait(10000)
.tableSearchText(name)
.checkColumnValue(6, 'Shutoff')
.selectFirst()
.clickHeaderButtonByTitle('Stop')
.checkDisableAction(2000);
});
```
![select-first](/docs/zh/test/images/e2e/table/select-first.png)
- `selectAll`
- 选中表格中所有条目,以便做后续的批量操作
- 通常是为了清空数据使用
- `waitStatusActiveByRefresh`
- 每`5`秒点击表格上方的刷新按钮,直到资源状态变为可用状态
- 资源在创建或变更后,往往需要一定的时间才能变为可用状态,之后才能进行后续操作
- 以创建堆栈`test/e2e/integration/pages/heat/stack.spec.js`为例
```javascript
it('successfully create', () => {
const volumeJson = {
name: volumeName,
};
cy.clickHeaderButton(1, 2000)
.formAttachFile('content', contentFile)
.formAttachFile('params', paramFile)
.clickStepActionNextButton()
.wait(2000)
.formInput('name', name)
.formJsonInput('volume_name_spec', volumeJson)
.clickStepActionNextButton()
.waitFormLoading()
.wait(5000)
.tableSearchSelectText('Name', name)
.waitStatusActiveByRefresh();
});
```
![wait-1](/docs/zh/test/images/e2e/table/wait-1.png)
![wait-2](/docs/zh/test/images/e2e/table/wait-2.png)
## 对按钮的操作
主要包含
- 位于表单上放的主按钮操作(一般创建操作)、批量操作
- 位于表单每一行的行操作
### 表单上方按钮的操作
表格上方的按钮一般包含:刷新、创建、批量操作按钮、配置表格列表项、下载
- `clickHeaderButton`
- 点击表格上方的按钮,
- 参数`buttonIndex`,表格上方按钮的下标
- 参数`waitTime`,点击后的等待时间,默认为 2 秒
- 一般,创建按钮的下标是 1
- 以创建密钥用例`test/e2e/integration/pages/compute/keypair.spec.js`为例
```javascript
it('successfully create', () => {
cy.clickHeaderButton(1)
.formInput('name', name)
.clickModalActionSubmitButton()
.wait(5000);
});
```
![header-btn-index](/docs/zh/test/images/e2e/table/header-btn-index.png)
- `clickHeaderButtonByTitle`
- 通过名称点击表格上方的按钮,一般用于批量操作按钮的点击
- 参数`title`,表格上方按钮上的文字
- 参数`waitTime`,点击后的等待时间,默认为 2 秒
- 以关闭状态下的云主机无法进行关闭操作`test/e2e/integration/pages/compute/instance.spec.js`为例
- 点击表单顶部的关闭按钮
```javascript
it('successfully stop', () => {
cy.tableSearchText(name)
.clickConfirmActionInMoreSub('Stop', 'Instance Status')
.wait(10000)
.tableSearchText(name)
.checkColumnValue(6, 'Shutoff')
.selectFirst()
.clickHeaderButtonByTitle('Stop')
.checkDisableAction(2000);
});
```
![header-btn-title](/docs/zh/test/images/e2e/table/header-btn-title.png)
- `clickHeaderConfirmButtonByTitle`
- 该函数会完成
1. 通过名称点击表格上方的按钮,页面弹出确认操作提示
2. 点击`确认`按钮
- 参数`title`,表格上方按钮上的文字
- 参数`waitTime`,点击后的等待时间,默认为 2 秒
- 以释放浮动IP`test/e2e/integration/pages/network/floatingip.spec.js`为例
- 全选停止状态的浮动IP并批量释放
```javascript
it('successfully delete', () => {
cy.tableSearchSelect('Status', 'Down')
.selectAll()
.clickHeaderConfirmButtonByTitle('Release');
});
```
![header-confirm-title](/docs/zh/test/images/e2e/table/header-confirm-title.png)
### 表单第一行的行操作
- `clickFirstActionButton`
- 点击表单第一行的操作列中的第一个按钮,一般用于对弹窗型操作按钮
单页型操作按钮的点击
- 以编辑用户`test/e2e/integration/pages/identity/user.spec.js`为例
```javascript
it('successfully edit', () => {
cy.tableSearchText(name)
.clickFirstActionButton()
.formInput('name', newname)
.clickModalActionSubmitButton();
});
```
![click-first](/docs/zh/test/images/e2e/table/click-first.png)
- `clickActionButtonByTitle`
- 根据标题点击第一行中的操作
- 以编辑、启动服务`test/e2e/integration/pages/configuration/system.spec.js`为例
- 当服务启动时,点击`禁用`按钮
- 当服务停止时,点击`启用`按钮
```javascript
it('successfully disable compute services', () => {
cy.clickTab(computeServicesTab)
.tableSearchText('nova-compute')
.clickActionButtonByTitle('Disable')
.formText('disabled_reason', reason)
.clickModalActionSubmitButton();
});
it('successfully enable compute services', () => {
cy.clickTab(computeServicesTab)
.tableSearchSelect('Service Status', 'Disabled')
.clickActionButtonByTitle('Enable')
.clickConfirmActionSubmitButton();
});
```
![action-by-title](/docs/zh/test/images/e2e/table/action-by-title.png)
![action-by-title-2](/docs/zh/test/images/e2e/table/action-by-title-2.png)
- `clickActionInMore`
- 根据标题点击第一行中`更多`中的操作
- 以点击镜像的创建云主机按钮`test/e2e/integration/pages/compute/image.spec.js`为例
```javascript
it('successfully create instance with cancel', () => {
cy.tableSearchText(name)
.clickActionInMore('Create Instance')
.wait(2000)
.clickStepActionCancelButton();
});
```
![action-in-more](/docs/zh/test/images/e2e/table/action-in-more.png)
- `clickActionInMoreSub`
- 根据标题点击第一行操作的子菜单下的操作
- 参数`title`,按钮的标题
- 参数`subMenu`,子菜单的标题
- 以云主机点击`关联资源`下的`挂载网卡` `test/e2e/integration/pages/compute/image.spec.js`为例
```javascript
it('successfully attach interface', () => {
cy.tableSearchText(name)
.clickActionInMoreSub('Attach Interface', 'Related Resources')
.wait(5000)
.formTableSelect('network')
.clickModalActionSubmitButton();
});
```
![action-in-sub](/docs/zh/test/images/e2e/table/action-in-sub.png)
- `checkActionDisabledInFirstRow`
- 验证指定名称的资源的指定操作不可用
1. 基于指定名称搜索资源
2. 验证搜索结果的第一行中的操作列`更多`中不存在指定操作
- 参数`title`,操作的名称
- 参数`name`,资源的名称
- 资源处于某些状态后,某些操作是需要被禁用的,行操作列表中的第一个操作,如果不可操作,则处于`禁用`状态,而`更多`中的操作,如果不可用,则不展示
- 以路由器`test/e2e/integration/pages/network/router.spec.js`为例
1. 创建路由器时开启公网网关
2. 验证路由器不可删除,即不存在`删除`按钮
```javascript
it('successfully disable delete', () => {
cy.checkActionDisabledInFirstRow('Delete', name);
});
```
![disable-more-action](/docs/zh/test/images/e2e/table/disable-more-action.png)
- `clickFirstActionDisabled`
- 验证表格中第一行的操作中的第一个操作不可用
- 资源处于某些状态后,某些操作是需要被禁用的,行操作列表中的第一个操作,如果不可操作,则处于`禁用`状态,而`更多`中的操作,如果不可用,则不展示
- 以云主机组`test/e2e/integration/pages/compute/server-group.spec.js`为例
1. 在云主机组下创建云主机
2. 验证含有云主机的云主机不可删除
3. 删除云主机后,云主机组删除成功
```javascript
it('successfully delete', () => {
cy.clickFirstActionDisabled();
cy.forceDeleteInstance(instanceName);
cy.wait(5000);
cy.visitPage(listUrl)
.tableSearchText(name)
.clickConfirmActionInFirst()
.checkEmptyTable();
});
```
![disable-first](/docs/zh/test/images/e2e/table/disable-first.png)
- `clickConfirmActionInFirst`
- 完成表格中第一行的第一个操作按钮对应的操作
1. 点击表格中第一行的第一个操作按钮,该操作是一个确认型操作
2. 点击`确认`按钮,并等待请求完成,关闭请求成功的提示信息
- 参数`waitTime`,关闭操作成功提示后的等待时间
- 以云主机组`test/e2e/integration/pages/compute/server-group.spec.js`为例
1. 在云主机组下创建云主机
2. 验证含有云主机的云主机不可删除
3. 删除云主机后,云主机组删除成功
```javascript
it('successfully delete', () => {
cy.clickFirstActionDisabled();
cy.forceDeleteInstance(instanceName);
cy.wait(5000);
cy.visitPage(listUrl)
.tableSearchText(name)
.clickConfirmActionInFirst()
.checkEmptyTable();
});
```
![first-confirm](/docs/zh/test/images/e2e/table/first-confirm.png)
![first-confirm-2](/docs/zh/test/images/e2e/table/first-confirm-2.png)
- `clickConfirmActionButton`
- 完成表格中第一行的列出的操作按钮中对应的操作
1. 点击表格中第一行的指定操作,该操作是一个确认型操作
2. 点击`确认`按钮,并等待请求完成,关闭请求成功的提示信息
- 参数`title`,指定操作的名称
- 参数`waitTime`,关闭操作提示成功后的等待时间
- 以删除VPN IPsec策略`test/e2e/integration/pages/compute/server-group.spec.js`为例
```javascript
it('successfully delete ipsec policy', () => {
cy.clickTab('IPsec Policy')
.tableSearchText(ipsecPolicy)
.clickConfirmActionButton('Delete');
});
```
![confirm-action](/docs/zh/test/images/e2e/table/confirm-action.png)
- `clickConfirmActionInMore`
- 完成表格中第一行的`更多`中对应的操作
1. 点击表格中第一行的`更多`中指定操作,该操作是一个确认型操作
2. 点击`确认`按钮,并等待请求完成,关闭请求成功的提示信息
- 参数`title`,指定操作的名称
- 参数`waitTime`,关闭操作提示成功后的等待时间
- 以删除路由器`test/e2e/integration/pages/network/router.spec.js`为例
1. 搜索
2. 完成`更多`中的`关闭公网网关`操作
2. 完成`更多`中的`删除`操作
```javascript
it('successfully close external gateway and delete', () => {
cy.tableSearchText(newname)
.clickConfirmActionInMore('Close External Gateway')
.clickConfirmActionInMore('Delete')
.tableSearchText(newname)
.checkEmptyTable();
});
```
![confirm-more-1](/docs/zh/test/images/e2e/table/confirm-more-1.png)
![confirm-more-2](/docs/zh/test/images/e2e/table/confirm-more-2.png)
- `clickConfirmActionInMoreSub`
- 完成表格中第一行的`更多`中指定子菜单下对应的操作
1. 点击表格中第一行的`更多`中指定子菜单下的指定操作,该操作是一个确认型操作
2. 点击`确认`按钮,并等待请求完成,关闭请求成功的提示信息
- 参数`title`,指定操作的名称
- 参数`subMenu`,指定子菜单的名称
- 参数`waitTime`,关闭操作提示成功后的等待时间
- 以锁定云主机`test/e2e/integration/pages/compute/instance.spec.js`为例
```javascript
it('successfully lock', () => {
cy.tableSearchText(name)
.clickConfirmActionInMoreSub('Lock', 'Instance Status')
.wait(10000);
cy.tableSearchText(name)
.selectFirst()
.clickHeaderButtonByTitle('Start')
.checkDisableAction(2000)
.clickHeaderButtonByTitle('Stop')
.checkDisableAction(2000)
.clickHeaderButtonByTitle('Reboot')
.checkDisableAction(2000);
});
```
![confirm-in-sub](/docs/zh/test/images/e2e/table/confirm-in-sub.png)
对表格操作的各种操作,主要用到了上方介绍的函数,函数的具体编写,请查看`test/e2e/support/table-commands.js`

View File

@ -0,0 +1,96 @@
简体中文 | [English](/docs/en/test/3-3-E2E-detail-operation.md)
因为前端框架使用的一致性,我们在编写详情操作的相关用例,选取元素并进行操作时,往往会发现有很强的规律性,所以我们对大多数详情操作都编写了相应的 Cypress 函数,极大的减少了编写测试用例的难度,以下会对主要使用的表格操作函数做出详细的说明。
- `checkDetailName`
- 验证详情页头部包含指定资源名称
- 参数`name`,资源名称
- 以查看密钥详情`test/e2e/integration/pages/compute/keypair.spec.js`为例
```javascript
it('successfully detail', () => {
cy.tableSearchText(name)
.checkTableFirstRow(name)
.goToDetail()
.checkDetailName(name);
cy.goBackToList(listUrl);
});
```
![name](/docs/zh/test/images/e2e/detail/name.png)
- `goBackToList`
- 点击详情页的`返回`按钮,进入列表页,并等待列表加载完成
- 参数`url`列表url
- 如果设置,会验证返回的列表路由是否符合预期
- 以查看密钥详情`test/e2e/integration/pages/compute/keypair.spec.js`为例
1. 搜索
2. 验证表格第一行是否包含指定名称
3. 进入详情页
4. 验证详情页的名称
5. 返回列表页
```javascript
it('successfully detail', () => {
cy.tableSearchText(name)
.checkTableFirstRow(name)
.goToDetail()
.checkDetailName(name);
cy.goBackToList(listUrl);
});
```
![list](/docs/zh/test/images/e2e/detail/list.png)
- `goBackToList`
- 点击详情页的`返回`按钮,进入列表页,并等待列表加载完成
- 参数`url`列表url
- 如果设置,会验证返回的列表路由是否符合预期
- 以查看密钥详情`test/e2e/integration/pages/compute/keypair.spec.js`为例
1. 搜索
2. 验证表格第一行是否包含指定名称
3. 进入详情页
4. 验证详情页的名称
5. 返回列表页
```javascript
it('successfully detail', () => {
cy.tableSearchText(name)
.checkTableFirstRow(name)
.goToDetail()
.checkDetailName(name);
cy.goBackToList(listUrl);
});
```
![list](/docs/zh/test/images/e2e/detail/list.png)
- `clickDetailTab`
- 点击详情页下方的指定Tab标签并等待相关资源列表加载完成
- 参数`label`指定的Tab标签
- 参数`urlTab`路由中的tab属性
- 如果设置会验证切换标签后路由中的tab属性是否符合预期
- 参数`waitTime`,切换标签后的等待时间
- 以查看网络详情`test/e2e/integration/pages/network/network.spec.js`为例
1. 搜索
2. 验证表格第一行是否包含指定名称
3. 进入详情页
4. 验证详情页的名称
5. 点击子网Tab并等待列表加载完成
6. 点击端口Tab并等待列表加载完成
5. 返回列表页
```javascript
it('successfully detail', () => {
cy.tableSearchText(name)
.checkTableFirstRow(name)
.goToDetail()
.checkDetailName(name);
cy.clickDetailTab('Subnets', 'subnets').clickDetailTab('Ports', 'ports');
cy.goBackToList(listUrl);
});
```
![tab](/docs/zh/test/images/e2e/detail/tab.png)
对详情页主要用到了上方介绍的函数,函数的具体编写,请查看`test/e2e/support/detail-commands.js`

View File

@ -0,0 +1,276 @@
简体中文 | [English](/docs/en/test/3-4-E2E-resource-operation.md)
在E2E的过程中创建资源的时候往往需要先创建关联资源而删除资源后也需要删除掉相关资源所以以完整创建/删除为原则,封装了对相关资源的操作。
- `createInstance`
- 创建云主机,并等待云主机变为`运行中`状态
- 参数`name`,云主机的名称
- 参数`networkName`,云主机创建时选择的网络名称
- 以浮动IP关联云主机`test/e2e/integration/pages/network/floatingip.spec.js`为例
- 为了能成功关联云主机,需要满足云主机网卡所在的子网所连接的路由器开启了公网网关
1. 创建带有子网的网络`networkName`
2. 创建开启了公网网关并连接网络`networkName`子网的路由器`routerName`
3. 创建挂载了网络`networkName`上的网卡的云主机`instanceName`
```javascript
it('successfully prepair resource', () => {
cy.createNetwork({ name: networkName });
cy.createRouter({ name: routerName, network: networkName });
cy.createInstance({ name: instanceName, networkName });
});
```
- `createNetwork`
- 创建网络,该网络带有一个子网
- 参数`name`,网络的名称
- 参数`networkName`,云主机创建时选择的网络名称
- 以路由器连接子网为例`test/e2e/integration/pages/network/router.spec.js`为例
- 创建了名称为`networkName`的网络,为连接子网做准备
```javascript
it('successfully prepair resource', () => {
cy.createNetwork({ name: networkName });
});
```
- `createNetworkPolicy`
- 创建网络QoS策略
- 参数`name`,策略的名称
- 以虚拟网卡修改QoS为例`test/e2e/integration/pages/network/virtual-adapter.spec.js`为例
- 创建了名称为`policyName`的策略为修改QoS做准备
```javascript
it('successfully prepair resource by admin', () => {
cy.loginAdmin().wait(5000).createNetworkPolicy({ name: policyName });
});
```
- `createRouter`
- 创建开启了公网网关的路由器
- 参数`name`,路由器的名称
- 参数`network`
- 若设置,则路由器会连接`network`网络的子网
- 以浮动IP关联云主机`test/e2e/integration/pages/network/floatingip.spec.js`为例
- 为了能成功关联云主机,需要满足云主机网卡所在的子网所连接的路由器开启了公网网关
1. 创建带有子网的网络`networkName`
2. 创建开启了公网网关并连接网络`networkName`子网的路由器`routerName`
3. 创建挂载了网络`networkName`上的网卡的云主机`instanceName`
```javascript
it('successfully prepair resource', () => {
cy.createNetwork({ name: networkName });
cy.createRouter({ name: routerName, network: networkName });
cy.createInstance({ name: instanceName, networkName });
});
```
- `deleteRouter`
- 删除路由器,会断开路由器的子网,关闭路由器的公网网关,最终成功删除路由器
- 参数`network`
- 若设置,则需要先断开路由器的子网
- 参数`name`,路由器的名称
- 以浮动IP删除关联资源`test/e2e/integration/pages/network/floatingip.spec.js`为例
- 为了能成功关联云主机,需要满足云主机网卡所在的子网所连接的路由器开启了公网网关
```javascript
it('successfully delete related resources', () => {
cy.forceDeleteInstance(instanceName);
cy.deleteRouter(routerName, networkName);
cy.deleteAll('network', networkName);
cy.loginAdmin().wait(5000);
cy.deleteAll('networkQosPolicy', policyName);
});
```
- `forceDeleteInstance`
- 强制删除云主机,而不是使用软删除
- 参数`name`,云主机的名称
- 以删除云主机组`test/e2e/integration/pages/compute/server-group.spec.js`为例
1. 先删除云主机组下的云主机
2. 再成功删除云主机组
```javascript
it('successfully delete', () => {
cy.clickFirstActionDisabled();
cy.forceDeleteInstance(instanceName);
cy.wait(5000);
cy.visitPage(listUrl)
.tableSearchText(name)
.clickConfirmActionInFirst()
.checkEmptyTable();
});
```
- `createVolume`
- 创建云硬盘
- 参数`name`,云硬盘的名称
- 以云硬盘备份`test/e2e/integration/pages/storage/backup.spec.js`为例
- 创建云硬盘的备份,需要先准备好云硬盘
```javascript
it('successfully prepair resource', () => {
cy.createVolume(volumeName);
cy.createNetwork({ name: networkName });
cy.createInstance({ name: instanceName, networkName });
});
```
- `createSecurityGrouop`
- 创建安全组
- 参数`name`,安全组的名称
- 以虚拟网卡`test/e2e/integration/pages/network/virtual-adapter.spec.js`为例
- 测试管理安全组,需要先准备好安全组
```javascript
it('successfully prepair resource', () => {
cy.createFip();
cy.createSecurityGrouop({ name: securityGroupName });
cy.createNetwork({ name: networkName });
cy.createRouter({ name: routerName, network: networkName });
cy.createInstance({ name: instanceName, networkName });
});
```
- `createFip`
- 创建浮动IP
- 以云主机`test/e2e/integration/pages/compute/instance.spec.js`为例
- 测试绑定浮动IP需要准备好可达的浮动IP
```javascript
it('successfully prepair resource', () => {
cy.createNetwork({ name: networkName });
cy.createRouter({ name: routerName, network: networkName });
cy.createFip();
cy.createVolume(volumeName);
});
```
- `createUserGroup`
- 创建用户组
- 参数`name`,用户组的名称
- 以项目`test/e2e/integration/pages/identity/project.spec.js`为例
- 测试管理用户组操作,需要准备好用户组
```javascript
it('successfully prepair resource', () => {
cy.createUser({ name: username });
cy.createUserGroup({ name: userGroupName });
});
```
- `createUser`
- 创建用户
- 参数`name`,用户的名称
- 以项目`test/e2e/integration/pages/identity/project.spec.js`为例
- 测试管理用户操作,需要准备好用户
```javascript
it('successfully prepair resource', () => {
cy.createUser({ name: username });
cy.createUserGroup({ name: userGroupName });
});
```
- `createProject`
- 创建项目
- 参数`name`,项目的名称
- 以用户`test/e2e/integration/pages/identity/user.spec.js`为例
- 测试创建用户,需要准备项目
- 测试管理项目权限,需要准备项目
```javascript
it('successfully prepair resource', () => {
cy.createProject({ name: projectName });
cy.createProject({ name: projectName2 });
cy.createUserGroup({ name: userGroupName });
});
```
- `createIronicImage`
- 创建裸机使用的镜像
- 参数`name`,镜像的名称
- 以裸机`test/e2e/integration/pages/compute/ironic.spec.js`为例
- 创建裸机,需要能创建裸机的镜像
```javascript
it('successfully prepair resource', () => {
cy.createNetwork({ name: networkName });
cy.createRouter({ name: routerName, network: networkName });
cy.createFip();
cy.createIronicImage({ name: imageName });
});
```
- `deleteInstance`
- 删除云主机
- 参数`name`,云主机的名称
- 参数`deleteRecycleBin`,默认为`true`,表示需要进入回收站二次删除
- 以云主机删除`test/e2e/integration/pages/compute/instance.spec.js`为例
```javascript
it('successfully delete', () => {
cy.deleteInstance(newname);
});
```
- `deleteAllAvailableVolume`
- 删除所有可用的云硬盘
- 以云主机`test/e2e/integration/pages/compute/instance.spec.js`为例
```javascript
it('successfully delete related resources', () => {
cy.deleteAll('fip');
cy.deleteRouter(routerName, networkName);
cy.deleteAll('network', networkName);
cy.deleteAll('volume', volumeName);
cy.deleteAllAvailableVolume();
});
```
- `deleteAll`
- 删除符合条件的资源
- 参数`resourceName`,资源名称,支持
```javacript
export default {
// compute
instance: instanceListUrl,
image: imageListUrl,
// storage
volume: volumeListUrl,
volumeSnapshot: volumeSnapshotListUrl,
backup: backupListUrl,
volumeType: volumeTypeListUrl,
// network
network: networkListUrl,
router: routerListUrl,
networkQosPolicy: policyListUrl,
fip: fipListUrl,
virtualAdapter: virtualAdapterListUrl,
// security
securityGroup: securityGroupListUrl,
// identity
project: projectListUrl,
user: userListUrl,
userGroup: userGroupListUrl,
};
```
- 参数`name`
- 如设置,则删除指定名称的资源
- 如不设置,则删除资源列表下的所有资源
- 参数`tab`
- 如设置,表示资源位于`tab`标签下,需要先切换到指定标签下
- 以云硬盘类型`test/e2e/integration/pages/storage/volume-type.spec.js`为例
- 删除管理QoS时准备的QoS
```javascript
it('successfully delete related resources', () => {
cy.deleteAll('volumeType', qosName, 'QoS');
});
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Some files were not shown because too many files have changed in this diff Show More