├── .dockerignore
├── .gitignore
├── LICENSE
├── README.md
├── deploy
├── client-web
│ ├── Dockerfile
│ └── nginx.conf
├── server-api
│ ├── Dockerfile
│ └── config.default.json
└── server-client
│ ├── Dockerfile
│ └── config.default.json
├── packages
├── client-web
│ ├── .gitignore
│ ├── index.html
│ ├── package.json
│ ├── postcss.config.js
│ ├── src
│ │ ├── api
│ │ │ ├── axios-ins.ts
│ │ │ ├── detail
│ │ │ │ ├── account.ts
│ │ │ │ ├── domain.ts
│ │ │ │ ├── group.ts
│ │ │ │ ├── member.ts
│ │ │ │ └── project.ts
│ │ │ └── index.ts
│ │ ├── component
│ │ │ ├── category-detail
│ │ │ │ └── category-detail.tsx
│ │ │ ├── layout
│ │ │ │ ├── page-layout
│ │ │ │ │ ├── component
│ │ │ │ │ │ ├── footer
│ │ │ │ │ │ │ ├── footer.module.less
│ │ │ │ │ │ │ └── footer.tsx
│ │ │ │ │ │ └── header
│ │ │ │ │ │ │ └── header.tsx
│ │ │ │ │ └── index.tsx
│ │ │ │ └── wide-layout
│ │ │ │ │ ├── wide-layout.module.less
│ │ │ │ │ └── wide-layout.tsx
│ │ │ └── qrcode
│ │ │ │ └── qrcode.tsx
│ │ ├── index.tsx
│ │ ├── interface
│ │ │ ├── client-api
│ │ │ │ ├── domain.interface.ts
│ │ │ │ ├── group.interface.ts
│ │ │ │ ├── member.interface.ts
│ │ │ │ └── project.interface.ts
│ │ │ └── constants.ts
│ │ ├── page
│ │ │ ├── component
│ │ │ │ ├── create-project-modal
│ │ │ │ │ └── create-project-modal.tsx
│ │ │ │ └── project-list-card
│ │ │ │ │ ├── project-item
│ │ │ │ │ ├── project-item.module.less
│ │ │ │ │ └── project-item.tsx
│ │ │ │ │ └── project-list-card.tsx
│ │ │ ├── group
│ │ │ │ ├── group-detail
│ │ │ │ │ ├── group-holder
│ │ │ │ │ │ └── group-holder.tsx
│ │ │ │ │ ├── group-layout-store.ts
│ │ │ │ │ ├── group-layout.tsx
│ │ │ │ │ ├── group-project
│ │ │ │ │ │ ├── group-project-store.ts
│ │ │ │ │ │ └── group-project.tsx
│ │ │ │ │ └── group-settiing
│ │ │ │ │ │ ├── group-setting-advance
│ │ │ │ │ │ ├── group-setting-advance-store.ts
│ │ │ │ │ │ └── group-setting-advance.tsx
│ │ │ │ │ │ ├── group-setting-common
│ │ │ │ │ │ ├── group-setting-common-store.ts
│ │ │ │ │ │ └── group-setting-common.tsx
│ │ │ │ │ │ ├── group-setting-member
│ │ │ │ │ │ ├── group-role-select
│ │ │ │ │ │ │ └── group-role-select.tsx
│ │ │ │ │ │ ├── group-setting-member-store.ts
│ │ │ │ │ │ └── group-setting-member.tsx
│ │ │ │ │ │ ├── group-setting-store.ts
│ │ │ │ │ │ ├── group-setting.module.less
│ │ │ │ │ │ └── group-setting.tsx
│ │ │ │ └── group-list
│ │ │ │ │ ├── components
│ │ │ │ │ └── create-group-modal
│ │ │ │ │ │ └── create-group-modal.tsx
│ │ │ │ │ ├── group-list.store.ts
│ │ │ │ │ └── group-list.tsx
│ │ │ └── project
│ │ │ │ ├── project-detail
│ │ │ │ ├── project-holder
│ │ │ │ │ └── project-holder.tsx
│ │ │ │ ├── project-layout-store.ts
│ │ │ │ ├── project-layout.tsx
│ │ │ │ ├── project-setting
│ │ │ │ │ ├── project-setting-advance
│ │ │ │ │ │ ├── project-setting-advance-store.ts
│ │ │ │ │ │ └── project-setting-advance.tsx
│ │ │ │ │ ├── project-setting-common
│ │ │ │ │ │ ├── project-setting-common-store.ts
│ │ │ │ │ │ └── project-setting-common.tsx
│ │ │ │ │ ├── project-setting-domain
│ │ │ │ │ │ ├── create-domain-modal
│ │ │ │ │ │ │ └── create-domain-modal.tsx
│ │ │ │ │ │ ├── project-setting-domain-store.ts
│ │ │ │ │ │ └── project-setting-domain.tsx
│ │ │ │ │ ├── project-setting-member
│ │ │ │ │ │ ├── project-setting-member-store.ts
│ │ │ │ │ │ ├── project-setting-member.tsx
│ │ │ │ │ │ └── role-select
│ │ │ │ │ │ │ ├── role-select.tsx
│ │ │ │ │ │ │ └── role.ts
│ │ │ │ │ ├── project-setting-store.ts
│ │ │ │ │ ├── project-setting.module.less
│ │ │ │ │ └── project-setting.tsx
│ │ │ │ └── project-workspace
│ │ │ │ │ ├── create-workspace-modal
│ │ │ │ │ └── create-workspace-modal.tsx
│ │ │ │ │ ├── project-workspace-store.ts
│ │ │ │ │ ├── project-workspace.module.less
│ │ │ │ │ ├── project-workspace.tsx
│ │ │ │ │ └── workspace-single
│ │ │ │ │ ├── create-deploy-file
│ │ │ │ │ └── create-deploy-file.tsx
│ │ │ │ │ ├── create-deploy-url
│ │ │ │ │ └── create-deploy-url.tsx
│ │ │ │ │ ├── deploy-info-descriptions
│ │ │ │ │ └── deploy-info-descriptions.tsx
│ │ │ │ │ ├── deploy-info-modal
│ │ │ │ │ └── deploy-info-modal.tsx
│ │ │ │ │ ├── tabs-deploy-info
│ │ │ │ │ └── tabs-deploy-info.tsx
│ │ │ │ │ ├── tabs-deploys-table
│ │ │ │ │ └── tabs-deploys-table.tsx
│ │ │ │ │ ├── workspace-single-store.ts
│ │ │ │ │ └── workspace-single.tsx
│ │ │ │ └── project-list
│ │ │ │ ├── project-list-store.ts
│ │ │ │ └── project-list.tsx
│ │ ├── routes.tsx
│ │ ├── store
│ │ │ ├── router-store.ts
│ │ │ └── user-store.ts
│ │ ├── style
│ │ │ ├── style.less
│ │ │ └── variable.less
│ │ └── util
│ │ │ ├── basic-store.ts
│ │ │ ├── define-property.ts
│ │ │ ├── get-normal-url.ts
│ │ │ ├── get-preview-url.ts
│ │ │ ├── get-query.ts
│ │ │ └── time-format.ts
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ ├── typings
│ │ └── declaration.d.ts
│ └── vite.config.ts
├── server-api
│ ├── .eslintrc.json
│ ├── .gitignore
│ ├── .prettierrc.js
│ ├── bootstrap.js
│ ├── jest.config.js
│ ├── package.json
│ ├── script
│ │ └── sql
│ │ │ └── init.sql
│ ├── src
│ │ ├── configuration.ts
│ │ ├── controller
│ │ │ ├── account-ctr.ts
│ │ │ ├── common
│ │ │ │ ├── api-response.ts
│ │ │ │ └── web-util.ts
│ │ │ ├── group-ctr.ts
│ │ │ ├── project-ctr.ts
│ │ │ ├── project-domain-ctr.ts
│ │ │ ├── project-env-ctr.ts
│ │ │ ├── project-env-deploy-ctr.ts
│ │ │ └── project-member-ctr.ts
│ │ ├── entity
│ │ │ ├── base
│ │ │ │ ├── base-column.ts
│ │ │ │ └── bool-transform.ts
│ │ │ ├── group-member.ts
│ │ │ ├── group.ts
│ │ │ ├── project-domain.ts
│ │ │ ├── project-env-deploy.ts
│ │ │ ├── project-env.ts
│ │ │ ├── project-member.ts
│ │ │ ├── project.ts
│ │ │ └── user.ts
│ │ ├── error
│ │ │ └── custom-error.ts
│ │ ├── interface
│ │ │ ├── config.interface.ts
│ │ │ ├── group.interface.ts
│ │ │ ├── project-domain.interface.ts
│ │ │ ├── project-env-deploy.interface.ts
│ │ │ ├── project-env.interface.ts
│ │ │ ├── project-member.interface.ts
│ │ │ └── project.interface.ts
│ │ ├── middleware
│ │ │ ├── auth.ts
│ │ │ ├── request.ts
│ │ │ └── validate.ts
│ │ ├── plugin
│ │ │ └── noop.ts
│ │ ├── service
│ │ │ ├── account-srv.ts
│ │ │ ├── group-member-srv.ts
│ │ │ ├── group-srv.ts
│ │ │ ├── project-domain-srv.ts
│ │ │ ├── project-env-deploy-srv.ts
│ │ │ ├── project-env-srv.ts
│ │ │ ├── project-member-srv.ts
│ │ │ └── project-srv.ts
│ │ └── util
│ │ │ ├── parse-str-to-num.ts
│ │ │ └── query-runner-repo.ts
│ ├── tsconfig.json
│ └── typings
│ │ └── index.d.ts
└── server-client
│ ├── .gitignore
│ ├── .prettierrc.js
│ ├── package.json
│ ├── resource
│ ├── 404.html
│ └── favicon.ico
│ ├── src
│ ├── cache
│ │ ├── host-cache.ts
│ │ └── memory-cache.ts
│ ├── config
│ │ ├── index.ts
│ │ └── typeorm
│ │ │ └── typeorm-configuration.ts
│ ├── controller
│ │ └── dispatch-controller.ts
│ ├── entity
│ │ ├── base
│ │ │ ├── base-column.ts
│ │ │ └── bool-transform.ts
│ │ ├── project-domain.ts
│ │ ├── project-env-deploy.ts
│ │ ├── project-env.ts
│ │ └── project.ts
│ ├── error
│ │ ├── InvalidPreviewError.ts
│ │ ├── ResourceFetchFailedError.ts
│ │ ├── UnMatchedHostError.ts
│ │ ├── UnMatchedProjectEnvDeployError.ts
│ │ ├── UnMatchedProjectEnvError.ts
│ │ └── UnMatchedProjectError.ts
│ ├── index.ts
│ ├── interface
│ │ ├── application.ts
│ │ └── context.ts
│ ├── schedule
│ │ ├── base
│ │ │ ├── BaseLoop.ts
│ │ │ └── BaseSchedule.ts
│ │ ├── index.ts
│ │ └── resource-update-loop.ts
│ ├── service
│ │ ├── project-service.ts
│ │ └── resource-service.ts
│ └── util
│ │ ├── app-config.ts
│ │ ├── ctx-print.ts
│ │ ├── logger.ts
│ │ ├── query-runner-repo.ts
│ │ └── req-hostname.ts
│ └── tsconfig.json
├── pnpm-lock.yaml
└── pnpm-workspace.yaml
/.dockerignore:
--------------------------------------------------------------------------------
1 | .git
2 | node_modules
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | node_modules/
3 |
4 | # IDE
5 | /.idea
6 | /.awcache
7 | /.vscode
8 | /.devcontainer
9 | *.code-workspace
10 |
11 | # misc
12 | .DS_Store
13 | npm-debug.log
14 | yarn-error.log
15 |
16 | # output
17 | logs/
18 | dist/
19 | coverage/
20 | images/
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 JD.com, Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Pubfree 前端发布平台
2 |
3 | ## 描述
4 | ### 主要技术栈
5 | - 前端 React + Antd
6 | - 后端 NodeJS + MySQL
7 | - 工具 pnpm、vite
8 |
9 | ## 目录
10 | - [Pubfree 前端发布平台](#pubfree-前端发布平台)
11 | - [描述](#描述)
12 | - [主要技术栈](#主要技术栈)
13 | - [目录](#目录)
14 | - [安装](#安装)
15 | - [安装 pnpm](#安装-pnpm)
16 | - [安装依赖](#安装依赖)
17 | - [数据库建表](#数据库建表)
18 | - [运行 server-api](#运行-server-api)
19 | - [运行 server-client](#运行-server-client)
20 | - [运行 client-web](#运行-client-web)
21 | - [使用](#使用)
22 | - [快速发布流程](#快速发布流程)
23 | - [1. 创建项目](#1-创建项目)
24 | - [2. 资源发布](#2-资源发布)
25 | - [3. 资源生效](#3-资源生效)
26 | - [4. 查看页面](#4-查看页面)
27 | - [License](#license)
28 |
29 | ## 安装
30 | 安装前请确保已安装 MySQL 和 NodeJS,为了正常使用 vite, 请确保 NodeJS 版本为 ^14.18.0 || >=16.0.0。
31 |
32 | ### 安装 pnpm
33 | ````bash
34 | npm install -g pnpm
35 | ````
36 | 查看 pnpm 版本,确认 pnpm 已经安装成功
37 | ````bash
38 | pnpm -v
39 | ````
40 |
41 | ### 安装依赖
42 | 项目根目录下安装项目依赖
43 | ````bash
44 | pnpm install
45 | ````
46 |
47 | ### 数据库建表
48 | 将 open-source/packages/server-api/script/sql/init.sql 文件中的 sql 语句复制到 MySQL 中运行,建立服务所需的基本数据库表。
49 |
50 | ### 运行 server-api
51 | 在 open-source/packages/server-api/resource 下新建 config.default.json,并配置数据库等信息
52 | ````js
53 | {
54 | "orm": {
55 | "host": "127.0.0.1",
56 | "port": 3306,
57 | "database": "pubfree_open",
58 | "username": "root",
59 | "password": "root"
60 | }
61 | }
62 |
63 | ````
64 | 本地运行
65 | ````bash
66 | cd packages/server-api
67 | pnpm dev
68 | ````
69 | 在本地 http://127.0.0.1:7001 下便可访问到 cms 页面用到的接口。
70 |
71 | ### 运行 server-client
72 | 在 open-source/packages/server-client/resource 下新建 config.default.json,并配置数据库等信息
73 | ````js
74 | {
75 | "mysql": {
76 | "enable": true,
77 | "options": {
78 | "host": "127.0.0.1",
79 | "port": 3306,
80 | "database": "pubfree_open",
81 | "username": "root",
82 | "password": "root"
83 | }
84 | },
85 | "schedule": {
86 | "enable": true
87 | }
88 | }
89 | ````
90 | 本地运行
91 | ````bash
92 | cd packages/server-client
93 | pnpm dev
94 | ````
95 | 通过 http://127.0.0.1:3000 可访问到发布在平台上的页面。
96 |
97 | ### 运行 client-web
98 | 本地运行
99 | ````bash
100 | cd packages/client-web
101 | pnpm dev
102 | ````
103 | 通过 http://localhost:5173 可访问发布平台的 cms 页面。
104 |
105 | ## 使用
106 | ### 快速发布流程
107 | #### 1. 创建项目
108 | 
109 |
110 | #### 2. 资源发布
111 | 使用 zip 上传(本地开发环境不支持)或者输入 html 资源地址发布
112 | 
113 | 
114 |
115 | #### 3. 资源生效
116 | 
117 |
118 | #### 4. 查看页面
119 | 
120 |
121 | ## License
122 | [MIT © JD.com, Inc.](./LICENSE)
--------------------------------------------------------------------------------
/deploy/client-web/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM nginx:1.23.2-alpine
2 | COPY packages/client-web/dist/ /usr/share/nginx/html
3 | COPY deploy/client-web/nginx.conf /etc/nginx/conf.d/default.conf
4 |
--------------------------------------------------------------------------------
/deploy/client-web/nginx.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 80;
3 | listen [::]:80;
4 | server_name localhost;
5 |
6 | #access_log /var/log/nginx/host.access.log main;
7 |
8 | location / {
9 | try_files $uri /$uri /index.html;
10 | root /usr/share/nginx/html;
11 | index index.html index.htm;
12 | }
13 |
14 | #error_page 404 /404.html;
15 |
16 | # redirect server error pages to the static page /50x.html
17 | #
18 | error_page 500 502 503 504 /50x.html;
19 | location = /50x.html {
20 | root /usr/share/nginx/html;
21 | }
22 |
23 | # proxy the PHP scripts to Apache listening on 127.0.0.1:80
24 | #
25 | #location ~ \.php$ {
26 | # proxy_pass http://127.0.0.1;
27 | #}
28 |
29 | # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
30 | #
31 | #location ~ \.php$ {
32 | # root html;
33 | # fastcgi_pass 127.0.0.1:9000;
34 | # fastcgi_index index.php;
35 | # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
36 | # include fastcgi_params;
37 | #}
38 |
39 | # deny access to .htaccess files, if Apache's document root
40 | # concurs with nginx's one
41 | #
42 | #location ~ /\.ht {
43 | # deny all;
44 | #}
45 | }
--------------------------------------------------------------------------------
/deploy/server-api/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:12.22.12-alpine3.15
2 | WORKDIR /root
3 | COPY . .
4 | RUN yarn global add pnpm@6 && \
5 | pnpm config set registry https://registry.npm.taobao.org && \
6 | pnpm config set recursive-install false && \
7 | cd packages/server-api && \
8 | pnpm install && \
9 | pnpm run build
10 | CMD cd packages/server-api && pnpm run start
11 |
--------------------------------------------------------------------------------
/deploy/server-api/config.default.json:
--------------------------------------------------------------------------------
1 | {
2 | "orm": {
3 | "host": "host.docker.internal",
4 | "port": 3306,
5 | "database": "pubfree_open",
6 | "username": "root",
7 | "password": "root"
8 | }
9 | }
--------------------------------------------------------------------------------
/deploy/server-client/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:12.22.12-alpine3.15
2 | WORKDIR /root
3 | COPY . .
4 | RUN yarn global add pnpm@6 && \
5 | pnpm config set registry https://registry.npm.taobao.org && \
6 | pnpm config set recursive-install false && \
7 | cd packages/server-client && \
8 | pnpm install && \
9 | pnpm run build
10 | CMD cd packages/server-client && pnpm run start
11 |
--------------------------------------------------------------------------------
/deploy/server-client/config.default.json:
--------------------------------------------------------------------------------
1 | {
2 | "mysql": {
3 | "enable": true,
4 | "options": {
5 | "host": "host.docker.internal",
6 | "port": 3306,
7 | "database": "pubfree_open",
8 | "username": "root",
9 | "password": "root"
10 | }
11 | },
12 | "schedule": {
13 | "enable": true
14 | }
15 | }
--------------------------------------------------------------------------------
/packages/client-web/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .idea
3 | .vscode
4 |
5 | dist
6 | build
7 |
8 | config/spark-builder-config.json
9 | config/webpack.dev.js
10 |
11 | script/publish/oss-token.json
12 |
13 | node_modules
14 | package-lock.json
--------------------------------------------------------------------------------
/packages/client-web/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 | Pubfree 前端静态发布平台
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/packages/client-web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pubfree-web",
3 | "version": "1.0.0",
4 | "scripts": {
5 | "prettier": "prettier -l --write 'src/**/*.{ts,tsx,js,jsx,less,css}'",
6 | "prettier-check": "prettier -l 'src/**/*.{ts,tsx,less,css}'",
7 | "tslint": "tsc -p tsconfig.json --noEmit",
8 | "lint": "yarn prettier-check && yarn tslint",
9 | "dev": "vite",
10 | "build": "tsc && vite build",
11 | "preview": "vite preview"
12 | },
13 | "dependencies": {
14 | "@ant-design/icons": "^4.7.0",
15 | "antd": "4.18.4",
16 | "axios": "0.21.4",
17 | "classnames": "^2.3.1",
18 | "history": "^4.9.0",
19 | "lodash-es": "^4.17.21",
20 | "mobx": "4.15.7",
21 | "mobx-react": "6.3.1",
22 | "qrcode": "1.5.0",
23 | "react": "^17.0.1",
24 | "react-dom": "^17.0.1",
25 | "react-router-dom": "^5.2.0"
26 | },
27 | "devDependencies": {
28 | "@types/lodash-es": "4.17.4",
29 | "@types/qrcode": "^1.4.2",
30 | "@types/react": "17.0.3",
31 | "@types/react-dom": "17.0.3",
32 | "@types/react-router-dom": "^5.2.0",
33 | "@vitejs/plugin-react": "^2.1.0",
34 | "autoprefixer": "^10.4.12",
35 | "less": "^4.1.3",
36 | "postcss-px-to-viewport": "^1.1.1",
37 | "prettier": "^2.7.1",
38 | "typescript": "^4.6.4",
39 | "vite": "^3.1.3"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/packages/client-web/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | require('autoprefixer'),
4 | require('postcss-px-to-viewport')({
5 | unitToConvert: 'px',
6 | viewportWidth: 1920,
7 | unitPrecision: 5,
8 | propList: ['*'],
9 | viewportUnit: 'vw',
10 | fontViewportUnit: 'vw',
11 | selectorBlackList: [],
12 | minPixelValue: 1,
13 | mediaQuery: false,
14 | replace: true,
15 | exclude: /.+/,
16 | include: undefined,
17 | }),
18 | ],
19 | remove: false,
20 | }
21 |
--------------------------------------------------------------------------------
/packages/client-web/src/api/axios-ins.ts:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import { DefineProperty } from "../util/define-property";
3 |
4 | const axiosIns = axios.create({
5 | baseURL: DefineProperty.ServerUrl + "/api",
6 | timeout: 5000,
7 | withCredentials: true,
8 | });
9 |
10 | axiosIns.interceptors.response.use((response) => {
11 | return response.data;
12 | });
13 |
14 | export default axiosIns;
15 |
--------------------------------------------------------------------------------
/packages/client-web/src/api/detail/account.ts:
--------------------------------------------------------------------------------
1 | import axiosIns from "../axios-ins";
2 | import { IApiRes } from "../index";
3 |
4 | export const accountInfo = async (): Promise> => {
5 | return axiosIns.get("/account/info");
6 | };
7 |
8 | export const accountLogin = async (): Promise> => {
9 | return axiosIns.post("/account/login");
10 | };
11 |
12 | export const accountLogout = async (): Promise> => {
13 | return axiosIns.post("/account/logout");
14 | };
15 |
16 | export const accountRegister = async (): Promise> => {
17 | return axiosIns.post("/account/register");
18 | };
19 |
20 | export interface IAccountInfo {
21 | id: number;
22 | name: string;
23 | }
24 |
--------------------------------------------------------------------------------
/packages/client-web/src/api/detail/domain.ts:
--------------------------------------------------------------------------------
1 | import { IDomainDTO } from "../../interface/client-api/domain.interface";
2 | import axiosIns from "../axios-ins";
3 | import { IApiRes } from "../index";
4 |
5 | export const getProjectDomains = (
6 | projectId: number
7 | ): Promise> => {
8 | return axiosIns.get(`/projects/${projectId}/domains`);
9 | };
10 |
11 | export const createProjectDomain = (
12 | projectId: number,
13 | params: { projectEnvId: number; host: string }
14 | ): Promise> => {
15 | return axiosIns.post(`/projects/${projectId}/domains`, params);
16 | };
17 |
18 | export const deleteProjectDomain = (
19 | projectId: number,
20 | domainId: number
21 | ): Promise> => {
22 | return axiosIns.delete(`/projects/${projectId}/domains/${domainId}`);
23 | };
24 |
--------------------------------------------------------------------------------
/packages/client-web/src/api/detail/group.ts:
--------------------------------------------------------------------------------
1 | import { IGroupDTO } from "../../interface/client-api/group.interface";
2 | import { IProjectDTO } from "../../interface/client-api/project.interface";
3 | import axiosIns from "../axios-ins";
4 | import { IApiRes } from "../index";
5 |
6 | export const getGroups = async (): Promise> => {
7 | return axiosIns.get(`/groups`);
8 | };
9 |
10 | export const getGroupDetail = async (
11 | groupId: number
12 | ): Promise> => {
13 | return axiosIns.get(`/groups/${groupId}`);
14 | };
15 |
16 | export const createGroup = async (params: {
17 | name: string;
18 | description: string;
19 | }): Promise> => {
20 | return axiosIns.post(`/groups`, params);
21 | };
22 |
23 | export const updateGroup = async (groupId: number): Promise> => {
24 | return axiosIns.post(`/groups/${groupId}`);
25 | };
26 |
27 | export const deleteGroup = async (groupId: number): Promise> => {
28 | return axiosIns.delete(`/groups/${groupId}`);
29 | };
30 |
31 | export const getGroupProjects = async (
32 | groupId: number
33 | ): Promise> => {
34 | return axiosIns.get(`/groups/${groupId}/projects`);
35 | };
36 |
--------------------------------------------------------------------------------
/packages/client-web/src/api/detail/member.ts:
--------------------------------------------------------------------------------
1 | import {
2 | EGroupMemberRole,
3 | EProjectMemberRole,
4 | IGroupMemberDTO,
5 | IProjectMemberDTO,
6 | } from "../../interface/client-api/member.interface";
7 | import axiosIns from "../axios-ins";
8 | import { IApiRes } from "../index";
9 |
10 | export const getGroupMembers = (
11 | groupId: number
12 | ): Promise> => {
13 | return axiosIns.get(`/groups/${groupId}/members`);
14 | };
15 |
16 | export const addGroupMember = (
17 | groupId: number,
18 | params: { name: string; role: EGroupMemberRole }
19 | ): Promise> => {
20 | return axiosIns.post(`/groups/${groupId}/members`, params);
21 | };
22 |
23 | export const deleteGroupMember = (
24 | groupId: number,
25 | memberId: number
26 | ): Promise> => {
27 | return axiosIns.delete(`/groups/${groupId}/members/${memberId}`);
28 | };
29 |
30 | export const getProjectMembers = (
31 | projectId: number
32 | ): Promise> => {
33 | return axiosIns.get(`/projects/${projectId}/members`);
34 | };
35 |
36 | export const addProjectMember = (
37 | projectId: number,
38 | params: { name: string; role: EProjectMemberRole }
39 | ): Promise> => {
40 | return axiosIns.post(`/projects/${projectId}/members`, params);
41 | };
42 |
43 | export const deleteProjectMember = (
44 | projectId: number,
45 | memberId: number
46 | ): Promise> => {
47 | return axiosIns.delete(`/projects/${projectId}/members/${memberId}`);
48 | };
49 |
--------------------------------------------------------------------------------
/packages/client-web/src/api/detail/project.ts:
--------------------------------------------------------------------------------
1 | import {
2 | EProjectEnvType,
3 | ICreateProjectDTO,
4 | IGetProjectsQuery,
5 | IProjectDeployDTO,
6 | IProjectDTO,
7 | IProjectEnvDTO,
8 | } from "../../interface/client-api/project.interface";
9 | import axiosIns from "../axios-ins";
10 | import { IApiRes } from "../index";
11 |
12 | export const getProjects = (
13 | query: IGetProjectsQuery
14 | ): Promise<
15 | IApiRes<{
16 | total: number;
17 | projects: IProjectDTO[];
18 | }>
19 | > => {
20 | return axiosIns.get(`/projects`, {
21 | params: query,
22 | });
23 | };
24 |
25 | export const getProjectInfo = (
26 | projectId: number
27 | ): Promise> => {
28 | return axiosIns.get(`/projects/${projectId}`);
29 | };
30 |
31 | export const createProject = (
32 | params: ICreateProjectDTO
33 | ): Promise> => {
34 | return axiosIns.post(`/projects`, params);
35 | };
36 |
37 | export const updateProject = (projectId: number): Promise> => {
38 | return axiosIns.post(`/projects/${projectId}`);
39 | };
40 |
41 | export const deleteProject = (projectId: number): Promise> => {
42 | return axiosIns.delete(`/projects/${projectId}`);
43 | };
44 |
45 | export const getProjectEnvs = (
46 | projectId: number
47 | ): Promise> => {
48 | return axiosIns.get(`/projects/${projectId}/envs`);
49 | };
50 |
51 | export const createProjectEnv = (
52 | projectId: number,
53 | params: { name: string; type: EProjectEnvType }
54 | ): Promise> => {
55 | return axiosIns.post(`/projects/${projectId}/envs`, params);
56 | };
57 |
58 | export const updateProjectEnv = (
59 | projectId: number,
60 | envId: number
61 | ): Promise> => {
62 | return axiosIns.post(`/projects/${projectId}/envs/${envId}`);
63 | };
64 |
65 | export const deleteProjectEnv = (
66 | projectId: number,
67 | envId: number
68 | ): Promise> => {
69 | return axiosIns.delete(`/projects/${projectId}/envs/${envId}`);
70 | };
71 |
72 | export const getProjectDeploys = (
73 | projectId: number,
74 | envId: number
75 | ): Promise> => {
76 | return axiosIns.get(`/projects/${projectId}/envs/${envId}/deploys`);
77 | };
78 |
79 | export const createProjectDeploy = (
80 | projectId: number,
81 | envId: number,
82 | params: { type: "url" | "zip"; options: { target: string; remark: string } }
83 | ): Promise> => {
84 | return axiosIns.post(`/projects/${projectId}/envs/${envId}/deploys`, params);
85 | };
86 |
87 | export const activateDeploy = (
88 | projectId: number,
89 | envId: number,
90 | deployId: number
91 | ): Promise> => {
92 | return axiosIns.post(
93 | `/projects/${projectId}/envs/${envId}/deploys/${deployId}/activate`
94 | );
95 | };
96 |
97 | export const deactivateDeploy = (
98 | projectId: number,
99 | envId: number,
100 | deployId: number
101 | ): Promise> => {
102 | return axiosIns.post(
103 | `/projects/${projectId}/envs/${envId}/deploys/${deployId}/deactivate`
104 | );
105 | };
106 |
--------------------------------------------------------------------------------
/packages/client-web/src/api/index.ts:
--------------------------------------------------------------------------------
1 | import * as account from "./detail/account";
2 | import * as domain from "./detail/domain";
3 | import * as group from "./detail/group";
4 | import * as member from "./detail/member";
5 | import * as project from "./detail/project";
6 |
7 | export interface IApiRes {
8 | code: EApiCode;
9 | data: T;
10 | message?: string;
11 | }
12 |
13 | export enum EApiCode {
14 | Success = 200,
15 | Unauthorized = 401,
16 | }
17 |
18 | const Api = {
19 | account: account,
20 | domain: domain,
21 | group: group,
22 | member: member,
23 | project: project,
24 | };
25 |
26 | export default Api;
27 |
--------------------------------------------------------------------------------
/packages/client-web/src/component/category-detail/category-detail.tsx:
--------------------------------------------------------------------------------
1 | import { Card, Space } from "antd";
2 | import React from "react";
3 |
4 | interface IProps {
5 | title: string;
6 | direction?: "horizontal" | "vertical";
7 | }
8 |
9 | const CategoryDetail: React.FC = (props) => {
10 | const { title, direction = "horizontal" } = props;
11 | return (
12 |
20 |
21 | {props.children}
22 |
23 |
24 | );
25 | };
26 |
27 | export default CategoryDetail;
28 |
--------------------------------------------------------------------------------
/packages/client-web/src/component/layout/page-layout/component/footer/footer.module.less:
--------------------------------------------------------------------------------
1 | .common-layout-footer {
2 | background-color: #303846 !important;
3 | color: #fff !important;
4 |
5 | .footer-col {
6 | display: flex;
7 | justify-content: space-around;
8 |
9 | .footer-link {
10 | font-size: 15px;
11 | margin-bottom: 10px;
12 | display: flex;
13 | flex-direction: column;
14 | align-items: flex-start;
15 |
16 | .footer-name {
17 | color: #fff;
18 | text-decoration: underline;
19 | }
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/packages/client-web/src/component/layout/page-layout/component/footer/footer.tsx:
--------------------------------------------------------------------------------
1 | import { Space } from "antd";
2 | import { Footer } from "antd/lib/layout/layout";
3 | import React from "react";
4 | import styles from "./footer.module.less";
5 |
6 | const LayoutFooter: React.FC = () => {
7 | return (
8 |
16 | );
17 | };
18 |
19 | export default LayoutFooter;
20 |
--------------------------------------------------------------------------------
/packages/client-web/src/component/layout/page-layout/index.tsx:
--------------------------------------------------------------------------------
1 | import { Layout } from "antd";
2 | import React from "react";
3 | import Footer from "./component/footer/footer";
4 | import Header from "./component/header/header";
5 |
6 | const PageLayout: React.FC = (props) => {
7 | const { Content } = Layout;
8 |
9 | return (
10 |
17 |
18 |
23 | {props.children}
24 |
25 |
26 |
27 | );
28 | };
29 | export default PageLayout;
30 |
--------------------------------------------------------------------------------
/packages/client-web/src/component/layout/wide-layout/wide-layout.module.less:
--------------------------------------------------------------------------------
1 | .wide-layout {
2 | display: flex;
3 | flex-direction: column;
4 |
5 | .layout-top-container {
6 | width: 100%;
7 | background: #f5f5f5;
8 |
9 | .layout-top-inner {
10 | width: 100%;
11 | margin: 0 auto;
12 | padding-top: 16px;
13 |
14 | :global {
15 | .ant-tabs-nav {
16 | margin: 0;
17 | }
18 | }
19 | }
20 | }
21 |
22 | .layout-center-container {
23 | width: 100%;
24 | background: #ffffff;
25 | flex: 1;
26 |
27 | .layout-center-inner {
28 | width: 100%;
29 | height: 100%;
30 | margin: 0 auto;
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/packages/client-web/src/component/layout/wide-layout/wide-layout.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from "react";
2 | import styles from "./wide-layout.module.less";
3 |
4 | interface IProps {
5 | width?: number | string;
6 | sideMargin?: number | string;
7 | }
8 |
9 | export const WideLayout: React.FC = (props) => {
10 | const { width = "auto", sideMargin } = props;
11 |
12 | const targetStyle = useMemo(() => {
13 | if (width === "auto") {
14 | return {
15 | width: "auto",
16 | margin: `20px ${sideMargin}px`,
17 | };
18 | } else {
19 | return {
20 | width: width,
21 | margin: `20px auto`,
22 | };
23 | }
24 | }, [width, sideMargin]);
25 |
26 | return (
27 |
28 |
29 |
{props.children}
30 |
31 |
32 | );
33 | };
34 |
--------------------------------------------------------------------------------
/packages/client-web/src/component/qrcode/qrcode.tsx:
--------------------------------------------------------------------------------
1 | import { isNil } from "lodash-es";
2 | import qrCode from "qrcode";
3 | import React, { useEffect, useState } from "react";
4 |
5 | interface IProps {
6 | url: string;
7 | }
8 |
9 | const QrCode: React.FC = (props) => {
10 | const [qrCodeImg, setQrCodeImg] = useState(null);
11 |
12 | useEffect(() => {
13 | (async () => {
14 | const baseImg = await qrCode.toDataURL(props.url, {
15 | errorCorrectionLevel: "L",
16 | margin: 1,
17 | width: 150,
18 | scale: 177,
19 | type: "image/jpeg",
20 | rendererOpts: {
21 | quality: 0.9,
22 | },
23 | color: {
24 | // 二维码背景颜色
25 | dark: "#40a9ff",
26 | // 二维码前景颜色
27 | light: "#fff",
28 | },
29 | });
30 |
31 | setQrCodeImg(baseImg);
32 | })();
33 | }, [props.url]);
34 |
35 | if (isNil(qrCodeImg)) {
36 | return null;
37 | }
38 |
39 | return
;
40 | };
41 |
42 | export default QrCode;
43 |
--------------------------------------------------------------------------------
/packages/client-web/src/index.tsx:
--------------------------------------------------------------------------------
1 | import { ConfigProvider } from "antd";
2 | import zhCN from "antd/lib/locale/zh_CN";
3 |
4 | import React from "react";
5 | import ReactDOM from "react-dom";
6 | import { Router, Switch } from "react-router-dom";
7 | import PageLayout from "./component/layout/page-layout";
8 | import Routes from "./routes";
9 | import { routerStore } from "./store/router-store";
10 | import "./style/style.less";
11 |
12 | ReactDOM.render(
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | ,
22 | document.getElementById("root")
23 | );
24 |
--------------------------------------------------------------------------------
/packages/client-web/src/interface/client-api/domain.interface.ts:
--------------------------------------------------------------------------------
1 | export interface IDomainDTO {
2 | id: number;
3 |
4 | projectId: number;
5 |
6 | projectEnvId: number;
7 |
8 | host: string;
9 |
10 | isDel: boolean;
11 |
12 | createdAt: number;
13 |
14 | updatedAt: number;
15 | }
16 |
--------------------------------------------------------------------------------
/packages/client-web/src/interface/client-api/group.interface.ts:
--------------------------------------------------------------------------------
1 | export interface IGroupDTO {
2 | id: number;
3 |
4 | name: string;
5 |
6 | description: string;
7 |
8 | ownerId: number;
9 |
10 | createUserId: number;
11 |
12 | isDel: boolean;
13 |
14 | createdAt: number;
15 |
16 | updatedAt: number;
17 | }
18 |
--------------------------------------------------------------------------------
/packages/client-web/src/interface/client-api/member.interface.ts:
--------------------------------------------------------------------------------
1 | export enum EProjectMemberRole {
2 | Guest = 0,
3 | Developer = 1,
4 | Master = 2,
5 | }
6 |
7 | export enum EGroupMemberRole {
8 | Guest = 0,
9 | Developer = 1,
10 | Master = 2,
11 | }
12 |
13 | export interface IProjectMemberDTO {
14 | id: number;
15 |
16 | projectId: number;
17 |
18 | userId: number;
19 |
20 | role: EProjectMemberRole;
21 |
22 | isDel: boolean;
23 |
24 | createdAt: number;
25 |
26 | updatedAt: number;
27 |
28 | user: {
29 | id: number;
30 | name: string;
31 | };
32 | }
33 |
34 | export interface IGroupMemberDTO {
35 | id: number;
36 |
37 | groupId: number;
38 |
39 | userId: number;
40 |
41 | role: EGroupMemberRole;
42 |
43 | isDel: boolean;
44 |
45 | createdAt: number;
46 |
47 | updatedAt: number;
48 |
49 | user: {
50 | id: number;
51 | name: string;
52 | };
53 | }
54 |
--------------------------------------------------------------------------------
/packages/client-web/src/interface/client-api/project.interface.ts:
--------------------------------------------------------------------------------
1 | export interface IGetProjectsQuery {
2 | type: "self" | "all";
3 | page: number;
4 | size: number;
5 | }
6 |
7 | export interface IProjectDTO {
8 | id: number;
9 |
10 | name: string;
11 |
12 | zhName: string;
13 |
14 | description: string;
15 |
16 | ownerId: number;
17 |
18 | groupId: number;
19 |
20 | createUserId: number;
21 |
22 | createUser: {
23 | id: number;
24 |
25 | name: string;
26 | };
27 | }
28 |
29 | export interface ICreateProjectDTO {
30 | name: string;
31 |
32 | zhName: string;
33 |
34 | description: string;
35 |
36 | /**
37 | * 创建群组项目时,添加此字段
38 | */
39 | groupId?: number;
40 | }
41 |
42 | export interface IProjectEnvDTO {
43 | id: number;
44 |
45 | projectId: number;
46 |
47 | name: string;
48 |
49 | envType: EProjectEnvType;
50 |
51 | createUserId: number;
52 |
53 | createUser: {
54 | id: number;
55 |
56 | name: string;
57 | };
58 | }
59 |
60 | export enum EProjectEnvType {
61 | Test = 0,
62 | Beta = 1,
63 | Gray = 2,
64 | Prod = 3,
65 | }
66 |
67 | export interface IProjectDeployDTO {
68 | id: number;
69 |
70 | projectId: number;
71 |
72 | projectEnvId: number;
73 |
74 | remark: string;
75 |
76 | targetType: EDeployTargetType;
77 |
78 | target: string;
79 |
80 | createUserId: number;
81 |
82 | createUser: {
83 | id: number;
84 |
85 | name: string;
86 | };
87 |
88 | actionUserId: number;
89 |
90 | actionUser: {
91 | id: number;
92 |
93 | name: string;
94 | };
95 |
96 | isActive: boolean;
97 |
98 | createdAt: number;
99 |
100 | updatedAt: number;
101 | }
102 |
103 | export enum EDeployTargetType {
104 | Local = 0,
105 | Cloud = 1,
106 | }
107 |
--------------------------------------------------------------------------------
/packages/client-web/src/interface/constants.ts:
--------------------------------------------------------------------------------
1 | // 工作区类型
2 | export const EnvType = {
3 | TEST: 0,
4 | BETA: 1,
5 | GRAY: 2,
6 | PROD: 3,
7 | };
8 |
9 | // 成员角色
10 | export const Role = {
11 | OWNER: 1 << 7,
12 | MASTER: 1 << 6,
13 | DEVELOPER: 1 << 5,
14 | GUEST: 1 << 4,
15 | };
16 |
17 | export const RoleDesc = {
18 | [Role.GUEST]: {
19 | name: "Guest",
20 | desc: "可浏览项目,不支持任何修改",
21 | },
22 | [Role.DEVELOPER]: {
23 | name: "Developer",
24 | desc: "日常项目操作权限,比如发布,创建工作区等",
25 | },
26 | [Role.MASTER]: {
27 | name: "Master",
28 | desc: "可添加修改项目成员与角色,生产审批变更",
29 | },
30 | [Role.OWNER]: {
31 | name: "Owner",
32 | desc: "可以删除、删除项目成员、转移项目、修改项目详情",
33 | },
34 | };
35 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/component/create-project-modal/create-project-modal.tsx:
--------------------------------------------------------------------------------
1 | import { Form, Input, message, Modal } from "antd";
2 | import { useForm } from "antd/es/form/Form";
3 | import { isNil } from "lodash-es";
4 | import React, { useCallback } from "react";
5 | import Api, { EApiCode } from "../../../api";
6 |
7 | interface IProps {
8 | groupId?: number;
9 | onOk: () => void;
10 | onCancel: () => void;
11 | }
12 |
13 | const CreateProjectModal: React.FC = (props) => {
14 | const [form] = useForm();
15 |
16 | const createProject = useCallback(async () => {
17 | const { groupId } = props;
18 | const values = form.getFieldsValue() as {
19 | name: string;
20 | zhName: string;
21 | description: string;
22 | groupId?: number;
23 | };
24 |
25 | if (!isNil(groupId)) {
26 | values.groupId = groupId;
27 | }
28 |
29 | const res = await Api.project.createProject(values);
30 | if (res.code !== EApiCode.Success) {
31 | message.error(`创建项目失败:${res.message}`);
32 | throw new Error(`创建项目失败:${res.message}`);
33 | }
34 | }, [props.groupId]);
35 |
36 | return (
37 | {
42 | await createProject();
43 | props.onOk();
44 | }}
45 | onCancel={() => {
46 | props.onCancel();
47 | }}
48 | >
49 |
61 |
62 |
63 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 | );
76 | };
77 |
78 | export default CreateProjectModal;
79 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/component/project-list-card/project-item/project-item.module.less:
--------------------------------------------------------------------------------
1 | @import (reference) "src/style/variable.less";
2 |
3 | .project-item {
4 | display: flex;
5 | justify-content: flex-start;
6 | margin: 0;
7 |
8 | .title {
9 | color: @sub-color;
10 | font-size: 14px;
11 | }
12 |
13 | .title:hover {
14 | text-decoration: underline;
15 | cursor: pointer;
16 | color: @main-color;
17 | }
18 |
19 | .project-info {
20 | flex: 0 0 70%;
21 | margin-bottom: 6px;
22 | }
23 |
24 | .tag-list > span {
25 | font-size: 10px;
26 | }
27 |
28 | :global {
29 | .ant-list-item-meta-title {
30 | margin-bottom: 0;
31 | }
32 |
33 | .ant-list-item-meta-description {
34 | height: 16px;
35 | overflow: hidden;
36 | text-overflow: ellipsis;
37 | font-size: 12px;
38 | white-space: nowrap;
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/component/project-list-card/project-item/project-item.tsx:
--------------------------------------------------------------------------------
1 | import { List } from "antd";
2 | import { observer } from "mobx-react";
3 | import React from "react";
4 | import { IProjectDTO } from "../../../../interface/client-api/project.interface";
5 | import { routerStore } from "../../../../store/router-store";
6 | import styles from "./project-item.module.less";
7 |
8 | interface IProps {
9 | project: IProjectDTO;
10 | }
11 |
12 | const ProjectItem: React.FC = observer((props) => {
13 | const { project } = props;
14 |
15 | return (
16 |
21 | {project.ownerId}
22 |
23 | }
24 | >
25 | {
30 | routerStore.push(`/projects/${project.id}/workspaces`);
31 | }}
32 | >
33 | {`${project.name}(${project.zhName})`}
34 |
35 | }
36 | description={project.description || "no description"}
37 | className={styles.projectInfo}
38 | />
39 |
40 | );
41 | });
42 |
43 | export default ProjectItem;
44 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/component/project-list-card/project-list-card.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Card, Empty, Input, List, Pagination, Row, Spin } from "antd";
2 | import { isEmpty } from "lodash-es";
3 | import React from "react";
4 | import { IProjectDTO } from "../../../interface/client-api/project.interface";
5 | import ProjectItem from "./project-item/project-item";
6 |
7 | interface IProps {
8 | isLoading?: boolean;
9 | searchWord?: string;
10 |
11 | projects: IProjectDTO[];
12 | projectsTotal: number;
13 | curPage: number;
14 |
15 | onSearch: (searchVal: string) => Promise | void;
16 | onPageChange: (page: number) => Promise | void;
17 |
18 | onClickCreate: () => void;
19 | }
20 |
21 | const ProjectListCard: React.FC = (props) => {
22 | const {
23 | isLoading = false,
24 | searchWord,
25 | projects,
26 | projectsTotal,
27 | curPage,
28 | onSearch,
29 | onPageChange,
30 | onClickCreate,
31 | } = props;
32 |
33 | return (
34 |
35 |
36 | {
42 | await onSearch(value);
43 | }}
44 | />
45 |
48 |
49 |
50 |
51 |
57 | {!isEmpty(projects) && (
58 | }
64 | />
65 | )}
66 | {isEmpty(projects) && }
67 |
68 |
69 |
{
76 | await onPageChange(page);
77 | }}
78 | />
79 |
80 | );
81 | };
82 |
83 | export default ProjectListCard;
84 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/group/group-detail/group-holder/group-holder.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Redirect, useParams } from "react-router-dom";
3 |
4 | const GroupHolder: React.FC = (props) => {
5 | const { groupId } = useParams() as { groupId: string };
6 |
7 | return ;
8 | };
9 |
10 | export default GroupHolder;
11 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/group/group-detail/group-layout-store.ts:
--------------------------------------------------------------------------------
1 | import { observable } from "mobx";
2 | import Api, { EApiCode } from "../../../api";
3 | import { IGroupDTO } from "../../../interface/client-api/group.interface";
4 | import { routerStore } from "../../../store/router-store";
5 | import { BasicStore } from "../../../util/basic-store";
6 |
7 | class Status {
8 | isLoading: boolean = true;
9 | curActiveKey: "projects" | "settings" | string = null;
10 |
11 | group: IGroupDTO = null;
12 | }
13 |
14 | export class GroupLayoutStore extends BasicStore {
15 | @observable.ref status = new Status();
16 |
17 | params: {
18 | groupId: number;
19 | } = null;
20 |
21 | async init() {
22 | try {
23 | await this.fetchGroupInfo();
24 | } finally {
25 | this.setStatus({ isLoading: false });
26 | }
27 | }
28 |
29 | destroy() {
30 | this.status = new Status();
31 | this.params = null;
32 | }
33 |
34 | async fetchGroupInfo() {
35 | const { groupId } = this.params;
36 | const res = await Api.group.getGroupDetail(groupId);
37 | if (res.code === EApiCode.Success) {
38 | this.setStatus({ group: res.data });
39 | }
40 | }
41 |
42 | switchGroupTab(key: "projects" | "settings" | string) {
43 | const { groupId } = this.params;
44 | this.setStatus({ curActiveKey: key });
45 |
46 | switch (key) {
47 | case "projects":
48 | routerStore.push(`/groups/${groupId}/projects`);
49 | break;
50 | case "settings":
51 | routerStore.push(`/groups/${groupId}/settings/common`);
52 | break;
53 | }
54 | }
55 | }
56 |
57 | const groupLayoutStore = new GroupLayoutStore();
58 | export default groupLayoutStore;
59 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/group/group-detail/group-layout.tsx:
--------------------------------------------------------------------------------
1 | import { Card, PageHeader } from "antd";
2 | import { isNil } from "lodash-es";
3 | import { toJS } from "mobx";
4 | import { observer } from "mobx-react";
5 | import React, { useEffect, useRef } from "react";
6 | import { RouteComponentProps } from "react-router-dom";
7 | import { WideLayout } from "../../../component/layout/wide-layout/wide-layout";
8 | import groupLayoutStore from "./group-layout-store";
9 |
10 | const GroupLayout: React.FC> =
11 | observer((props) => {
12 | const storeRef = useRef(groupLayoutStore);
13 |
14 | useEffect(() => {
15 | const { groupId } = props.match.params;
16 | storeRef.current.params = {
17 | groupId: +groupId,
18 | };
19 |
20 | storeRef.current.init();
21 |
22 | return () => {
23 | storeRef.current.destroy();
24 | };
25 | }, []);
26 |
27 | const status = toJS(storeRef.current.status);
28 | const { isLoading, curActiveKey, group } = status;
29 |
30 | if (isLoading) {
31 | return null;
32 | }
33 |
34 | if (isNil(group)) {
35 | return null;
36 | }
37 |
38 | return (
39 |
40 |
48 | {
55 | storeRef.current.switchGroupTab(key);
56 | }}
57 | >
58 | {props.children}
59 |
60 |
61 |
62 | );
63 | });
64 |
65 | export default GroupLayout;
66 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/group/group-detail/group-project/group-project-store.ts:
--------------------------------------------------------------------------------
1 | import { observable, toJS } from "mobx";
2 | import Api, { EApiCode } from "../../../../api";
3 | import { IProjectDTO } from "../../../../interface/client-api/project.interface";
4 | import { BasicStore } from "../../../../util/basic-store";
5 |
6 | class Status {
7 | isLoading: boolean = true;
8 |
9 | projects: IProjectDTO[] = [];
10 | searchWord: string = "";
11 | curPage: number = 1;
12 |
13 | isShowCreateProjectModal: boolean = false;
14 | }
15 |
16 | export class GroupProjectStore extends BasicStore {
17 | @observable.ref status = new Status();
18 |
19 | params: {
20 | groupId: number;
21 | } = null;
22 |
23 | async init() {
24 | try {
25 | await this.fetchProjectList();
26 | } finally {
27 | this.setStatus({ isLoading: false });
28 | }
29 | }
30 |
31 | destroy() {
32 | this.status = new Status();
33 | this.params = null;
34 | }
35 |
36 | async fetchProjectList() {
37 | const { groupId } = this.params;
38 | const res = await Api.group.getGroupProjects(groupId);
39 | if (res.code === EApiCode.Success) {
40 | this.setStatus({ projects: res.data });
41 | }
42 | }
43 |
44 | get filteredProjects() {
45 | const { projects, searchWord } = toJS(this.status);
46 | return projects.filter((project) => project.name.includes(searchWord));
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/group/group-detail/group-project/group-project.tsx:
--------------------------------------------------------------------------------
1 | import { toJS } from "mobx";
2 | import { observer } from "mobx-react";
3 | import React, { useEffect, useRef } from "react";
4 | import { RouteComponentProps } from "react-router-dom";
5 | import CreateProjectModal from "../../../component/create-project-modal/create-project-modal";
6 | import ProjectListCard from "../../../component/project-list-card/project-list-card";
7 | import groupLayoutStore from "../group-layout-store";
8 | import { GroupProjectStore } from "./group-project-store";
9 |
10 | const GroupProject: React.FC<
11 | RouteComponentProps<{
12 | groupId: string;
13 | }>
14 | > = observer((props) => {
15 | const storeRef = useRef(new GroupProjectStore());
16 |
17 | useEffect(() => {
18 | groupLayoutStore.setStatus({ curActiveKey: "projects" });
19 | }, []);
20 |
21 | useEffect(() => {
22 | const { groupId } = props.match.params;
23 | storeRef.current.params = {
24 | groupId: +groupId,
25 | };
26 |
27 | storeRef.current.init();
28 |
29 | return () => {
30 | storeRef.current.destroy();
31 | };
32 | }, []);
33 |
34 | const store = storeRef.current;
35 | const status = toJS(storeRef.current.status);
36 | const { filteredProjects } = store;
37 | const { isLoading } = status;
38 |
39 | if (isLoading) {
40 | return null;
41 | }
42 |
43 | return (
44 | <>
45 | {
50 | store.setStatus({
51 | searchWord: searchVal,
52 | });
53 | }}
54 | onPageChange={(page) => {
55 | store.setStatus({ curPage: page });
56 | }}
57 | onClickCreate={() => {
58 | store.setStatus({ isShowCreateProjectModal: true });
59 | }}
60 | />
61 | {status.isShowCreateProjectModal && (
62 | {
65 | store.setStatus({ isShowCreateProjectModal: false });
66 | await store.fetchProjectList();
67 | }}
68 | onCancel={() => {
69 | store.setStatus({ isShowCreateProjectModal: false });
70 | }}
71 | />
72 | )}
73 | >
74 | );
75 | });
76 |
77 | export default GroupProject;
78 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/group/group-detail/group-settiing/group-setting-advance/group-setting-advance-store.ts:
--------------------------------------------------------------------------------
1 | import { message } from "antd";
2 | import { observable } from "mobx";
3 | import Api, { EApiCode } from "../../../../../api";
4 | import { routerStore } from "../../../../../store/router-store";
5 | import { BasicStore } from "../../../../../util/basic-store";
6 |
7 | class Status {}
8 |
9 | export class GroupSettingAdvanceStore extends BasicStore {
10 | @observable.ref status = new Status();
11 |
12 | params: {
13 | groupId: number;
14 | } = null;
15 |
16 | async init() {}
17 |
18 | async destroy() {
19 | this.params = null;
20 | this.status = new Status();
21 | }
22 |
23 | async deleteGroup() {
24 | const { groupId } = this.params;
25 | const res = await Api.group.deleteGroup(groupId);
26 | if (res.code === EApiCode.Success) {
27 | message.success("空间删除成功");
28 | routerStore.push("/groups");
29 | } else {
30 | message.error("空间删除失败,请稍候再试");
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/group/group-detail/group-settiing/group-setting-advance/group-setting-advance.tsx:
--------------------------------------------------------------------------------
1 | import { ExclamationCircleOutlined } from "@ant-design/icons";
2 | import { Button, Card, Popconfirm, Space, Tag } from "antd";
3 | import { observer } from "mobx-react";
4 | import React, { useEffect, useRef } from "react";
5 | import { useParams } from "react-router-dom";
6 | import CategoryDetail from "../../../../../component/category-detail/category-detail";
7 | import { GroupSettingAdvanceStore } from "./group-setting-advance-store";
8 |
9 | const GroupSettingAdvance: React.FC = observer(() => {
10 | const storeRef = useRef(new GroupSettingAdvanceStore());
11 | const { groupId } = useParams() as { groupId: string };
12 |
13 | useEffect(() => {
14 | storeRef.current.params = {
15 | groupId: +groupId,
16 | };
17 |
18 | storeRef.current.init();
19 |
20 | return () => {
21 | storeRef.current.destroy();
22 | };
23 | }, [groupId]);
24 |
25 | const store = storeRef.current;
26 |
27 | return (
28 |
29 |
30 | 删除空间将会连同其相关的所有数据一起删除。此操作无法恢复!
31 | }>
32 | 删除空间前,请确保空间下已无项目
33 |
34 |
35 | {
38 | await store.deleteGroup();
39 | }}
40 | >
41 |
44 |
45 |
46 |
47 |
48 | );
49 | });
50 |
51 | export default GroupSettingAdvance;
52 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/group/group-detail/group-settiing/group-setting-common/group-setting-common-store.ts:
--------------------------------------------------------------------------------
1 | import { observable } from "mobx";
2 | import Api, { EApiCode } from "../../../../../api";
3 | import { IGroupDTO } from "../../../../../interface/client-api/group.interface";
4 | import { BasicStore } from "../../../../../util/basic-store";
5 |
6 | class Status {
7 | isLoading: boolean = true;
8 |
9 | group: IGroupDTO = null;
10 | }
11 |
12 | export class GroupSettingCommonStore extends BasicStore {
13 | @observable.ref status = new Status();
14 |
15 | params: { groupId: number } = null;
16 |
17 | async init() {
18 | try {
19 | await this.fetchGroupInfo();
20 | } finally {
21 | this.setStatus({ isLoading: false });
22 | }
23 | }
24 |
25 | destroy() {
26 | this.status = new Status();
27 | this.params = null;
28 | }
29 |
30 | async fetchGroupInfo() {
31 | const res = await Api.group.getGroupDetail(this.params.groupId);
32 | if (res.code === EApiCode.Success) {
33 | this.setStatus({
34 | group: res.data,
35 | });
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/group/group-detail/group-settiing/group-setting-common/group-setting-common.tsx:
--------------------------------------------------------------------------------
1 | import { Card, Input } from "antd";
2 | import { toJS } from "mobx";
3 | import { observer } from "mobx-react";
4 | import React, { useEffect, useRef } from "react";
5 | import { useParams } from "react-router-dom";
6 | import CategoryDetail from "../../../../../component/category-detail/category-detail";
7 | import { GroupSettingCommonStore } from "./group-setting-common-store";
8 |
9 | const GroupSettingCommon: React.FC = observer(() => {
10 | const storeRef = useRef(new GroupSettingCommonStore());
11 | const { groupId } = useParams() as { groupId: string };
12 |
13 | useEffect(() => {
14 | storeRef.current.params = {
15 | groupId: +groupId,
16 | };
17 | storeRef.current.init();
18 |
19 | return () => {
20 | storeRef.current.destroy();
21 | };
22 | }, [groupId]);
23 |
24 | const status = toJS(storeRef.current.status);
25 |
26 | if (status.isLoading) {
27 | return null;
28 | }
29 |
30 | return (
31 |
32 |
33 |
34 |
35 |
36 | );
37 | });
38 |
39 | export default GroupSettingCommon;
40 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/group/group-detail/group-settiing/group-setting-member/group-role-select/group-role-select.tsx:
--------------------------------------------------------------------------------
1 | import { Select, Space } from "antd";
2 | import React from "react";
3 | import { EGroupMemberRole } from "../../../../../../interface/client-api/member.interface";
4 |
5 | interface IProps {
6 | value: EGroupMemberRole;
7 | onChange: (value, option) => Promise;
8 | }
9 |
10 | const GroupRoleSelect: React.FC = (props) => {
11 | return (
12 |
13 |
40 |
41 | );
42 | };
43 |
44 | export default GroupRoleSelect;
45 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/group/group-detail/group-settiing/group-setting-member/group-setting-member-store.ts:
--------------------------------------------------------------------------------
1 | import { message } from "antd";
2 | import { isNil } from "lodash-es";
3 | import { observable } from "mobx";
4 | import Api, { EApiCode } from "../../../../../api";
5 | import {
6 | EGroupMemberRole,
7 | IGroupMemberDTO,
8 | } from "../../../../../interface/client-api/member.interface";
9 | import { BasicStore } from "../../../../../util/basic-store";
10 |
11 | class Status {
12 | isLoading: boolean = true;
13 |
14 | members: IGroupMemberDTO[] = [];
15 |
16 | preAddUsername: string = null;
17 | preAddUserRole: EGroupMemberRole = null;
18 | }
19 |
20 | export class GroupSettingMemberStore extends BasicStore {
21 | @observable.ref status = new Status();
22 |
23 | params: {
24 | groupId: number;
25 | } = null;
26 |
27 | async init() {
28 | try {
29 | await this.fetchGroupMembers();
30 | } finally {
31 | this.setStatus({ isLoading: false });
32 | }
33 | }
34 |
35 | async destroy() {}
36 |
37 | async fetchGroupMembers() {
38 | const res = await Api.member.getGroupMembers(this.params.groupId);
39 | if (res.code === EApiCode.Success) {
40 | this.setStatus({ members: res.data });
41 | }
42 | }
43 |
44 | async addGroupMember() {
45 | const { preAddUsername, preAddUserRole } = this.status;
46 |
47 | if (isNil(preAddUsername) || isNil(preAddUserRole)) {
48 | return message.warning("请先选择要添加的人员和权限");
49 | }
50 |
51 | const { groupId } = this.params;
52 | const res = await Api.member.addGroupMember(groupId, {
53 | name: preAddUsername,
54 | role: preAddUserRole,
55 | });
56 | if (res.code === EApiCode.Success) {
57 | message.success("成员添加成功");
58 |
59 | this.setStatus({
60 | preAddUsername: null,
61 | preAddUserRole: null,
62 | });
63 | await this.fetchGroupMembers();
64 | } else {
65 | message.error("成员添加失败,请稍候再试");
66 | }
67 | }
68 |
69 | async deleteGroupMember(memberId: number) {
70 | const { groupId } = this.params;
71 | const res = await Api.member.deleteGroupMember(groupId, memberId);
72 | if (res.code === EApiCode.Success) {
73 | message.success("成员移除成功");
74 |
75 | this.setStatus({
76 | preAddUsername: null,
77 | preAddUserRole: null,
78 | });
79 | await this.fetchGroupMembers();
80 | } else {
81 | message.error("成员移除失败,请稍候再试");
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/group/group-detail/group-settiing/group-setting-store.ts:
--------------------------------------------------------------------------------
1 | import { observable } from "mobx";
2 | import { routerStore } from "../../../../store/router-store";
3 | import { BasicStore } from "../../../../util/basic-store";
4 |
5 | export enum ESettingType {
6 | Common = "common",
7 | Members = "members",
8 | Advance = "advance",
9 | }
10 |
11 | class Status {
12 | curActiveType: ESettingType = null;
13 | }
14 |
15 | export class GroupSettingStore extends BasicStore {
16 | @observable.ref status = new Status();
17 |
18 | params: {
19 | groupId: number;
20 | type: ESettingType;
21 | } = null;
22 |
23 | async init() {
24 | this.setStatus({ curActiveType: this.params.type });
25 | this.switchSettingType(this.params.type || ESettingType.Common);
26 | }
27 |
28 | async destroy() {
29 | this.setStatus({
30 | curActiveType: ESettingType.Common,
31 | });
32 | }
33 |
34 | switchSettingType(key: ESettingType) {
35 | const { groupId } = this.params;
36 |
37 | this.setStatus({ curActiveType: key });
38 |
39 | routerStore.push(`/groups/${groupId}/settings/${key}`);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/group/group-detail/group-settiing/group-setting.module.less:
--------------------------------------------------------------------------------
1 | .group-setting {
2 | :global {
3 | .group-setting-tabs > .ant-tabs-nav {
4 | .ant-tabs-tab-active {
5 | background-color: rgba(98, 71, 170, 0.5);
6 |
7 | .ant-tabs-tab-btn {
8 | color: #fff;
9 | }
10 | }
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/group/group-detail/group-settiing/group-setting.tsx:
--------------------------------------------------------------------------------
1 | import { Tabs } from "antd";
2 | import { toJS } from "mobx";
3 | import { observer } from "mobx-react";
4 | import React, { useEffect, useRef } from "react";
5 | import { useParams } from "react-router-dom";
6 | import groupLayoutStore from "../group-layout-store";
7 | import GroupSettingAdvance from "./group-setting-advance/group-setting-advance";
8 | import GroupSettingCommon from "./group-setting-common/group-setting-common";
9 | import GroupSettingMember from "./group-setting-member/group-setting-member";
10 | import { ESettingType, GroupSettingStore } from "./group-setting-store";
11 | import styles from "./group-setting.module.less";
12 |
13 | const GroupSetting: React.FC = observer((props) => {
14 | const storeRef = useRef(new GroupSettingStore());
15 | const { groupId, type } = useParams() as {
16 | groupId: string;
17 | type: ESettingType;
18 | };
19 |
20 | useEffect(() => {
21 | groupLayoutStore.setStatus({ curActiveKey: "settings" });
22 | }, []);
23 |
24 | useEffect(() => {
25 | storeRef.current.params = {
26 | groupId: +groupId,
27 | type: type,
28 | };
29 |
30 | storeRef.current.init();
31 |
32 | return () => {
33 | storeRef.current.destroy();
34 | };
35 | }, []);
36 |
37 | const status = toJS(storeRef.current.status);
38 | const { curActiveType } = status;
39 |
40 | return (
41 |
42 | {
52 | storeRef.current.switchSettingType(key as ESettingType);
53 | }}
54 | >
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | );
67 | });
68 |
69 | export default GroupSetting;
70 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/group/group-list/components/create-group-modal/create-group-modal.tsx:
--------------------------------------------------------------------------------
1 | import { Form, Input, message, Modal } from "antd";
2 | import { useForm } from "antd/es/form/Form";
3 | import React, { useCallback } from "react";
4 | import Api, { EApiCode } from "../../../../../api";
5 |
6 | interface IProps {
7 | onOk: () => void;
8 | onCancel: () => void;
9 | }
10 |
11 | const CreateGroupModal: React.FC = (props) => {
12 | const [form] = useForm();
13 |
14 | const createGroup = useCallback(async () => {
15 | const values = form.getFieldsValue() as {
16 | name: string;
17 | description: string;
18 | };
19 |
20 | const res = await Api.group.createGroup(values);
21 | if (res.code === EApiCode.Success) {
22 | message.success("创建空间成功 ");
23 | } else {
24 | message.error(`创建空间失败:${res.message}`);
25 | }
26 | }, []);
27 |
28 | return (
29 | {
34 | await createGroup();
35 | props.onOk();
36 | }}
37 | onCancel={() => {
38 | props.onCancel();
39 | }}
40 | >
41 |
47 |
48 |
49 |
54 |
55 |
56 |
57 |
58 | );
59 | };
60 |
61 | export default CreateGroupModal;
62 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/group/group-list/group-list.store.ts:
--------------------------------------------------------------------------------
1 | import { observable } from "mobx";
2 | import Api, { EApiCode } from "../../../api";
3 | import { IGroupDTO } from "../../../interface/client-api/group.interface";
4 | import { BasicStore } from "../../../util/basic-store";
5 |
6 | class Status {
7 | groups: IGroupDTO[] = null;
8 | isShowCreateModal: boolean = false;
9 | }
10 |
11 | class GroupListStore extends BasicStore {
12 | @observable status = new Status();
13 |
14 | async fetchGroupList() {
15 | const res = await Api.group.getGroups();
16 |
17 | if (res.code === EApiCode.Success) {
18 | this.setStatus({
19 | groups: res.data,
20 | });
21 | }
22 | }
23 | }
24 |
25 | const groupListStore = new GroupListStore();
26 | export default groupListStore;
27 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/group/group-list/group-list.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Card, Empty, List, Row, Spin } from "antd";
2 | import { isEmpty } from "lodash-es";
3 | import { toJS } from "mobx";
4 | import { observer } from "mobx-react";
5 | import React, { useEffect, useRef } from "react";
6 | import { WideLayout } from "../../../component/layout/wide-layout/wide-layout";
7 | import { routerStore } from "../../../store/router-store";
8 | import CreateGroupModal from "./components/create-group-modal/create-group-modal";
9 | import groupListStore from "./group-list.store";
10 |
11 | const GroupList = observer(() => {
12 | const storeRef = useRef(groupListStore);
13 |
14 | useEffect(() => {
15 | storeRef.current.fetchGroupList();
16 | }, []);
17 |
18 | const store = storeRef.current;
19 | const status = toJS(store.status);
20 |
21 | return (
22 |
23 |
24 | {/* 搜索新建区 */}
25 |
26 |
32 |
33 |
34 |
35 |
36 | {!isEmpty(status.groups) && (
37 | (
43 | {
46 | routerStore.push(`/groups/${group.id}/projects`);
47 | }}
48 | >
49 |
53 |
54 | )}
55 | />
56 | )}
57 | {isEmpty(status.groups) && (
58 |
59 | )}
60 |
61 |
62 | {status.isShowCreateModal && (
63 | {
65 | store.setStatus({ isShowCreateModal: false });
66 | await store.fetchGroupList();
67 | }}
68 | onCancel={() => {
69 | store.setStatus({ isShowCreateModal: false });
70 | }}
71 | />
72 | )}
73 |
74 |
75 | );
76 | });
77 | export default GroupList;
78 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/project/project-detail/project-holder/project-holder.tsx:
--------------------------------------------------------------------------------
1 | import { isEmpty } from "lodash-es";
2 | import React, { useCallback, useEffect, useState } from "react";
3 | import { Redirect, useParams } from "react-router-dom";
4 | import Api, { EApiCode } from "../../../../api";
5 |
6 | const ProjectHolder: React.FC = (props) => {
7 | const { projectId } = useParams() as { projectId: string };
8 | const [envAreas, setEnvAreas] = useState([]);
9 |
10 | const fetchEnvAreas = useCallback(async () => {
11 | const res = await Api.project.getProjectEnvs(+projectId);
12 | if (res.code === EApiCode.Success) {
13 | setEnvAreas(res.data);
14 | }
15 | }, [projectId]);
16 |
17 | useEffect(() => {
18 | fetchEnvAreas();
19 | }, []);
20 |
21 | if (isEmpty(envAreas)) {
22 | return null;
23 | }
24 |
25 | const envId = envAreas[0].id;
26 | return ;
27 | };
28 |
29 | export default ProjectHolder;
30 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/project/project-detail/project-layout-store.ts:
--------------------------------------------------------------------------------
1 | import { observable } from "mobx";
2 | import Api, { EApiCode } from "../../../api";
3 | import { IProjectDTO } from "../../../interface/client-api/project.interface";
4 | import { routerStore } from "../../../store/router-store";
5 | import { BasicStore } from "../../../util/basic-store";
6 |
7 | class Status {
8 | isLoading: boolean = true;
9 | curActiveKey: "workspaces" | "settings" | string = null;
10 |
11 | project: IProjectDTO = null;
12 | }
13 |
14 | export class ProjectLayoutStore extends BasicStore {
15 | @observable.ref status = new Status();
16 |
17 | params: {
18 | projectId: number;
19 | } = null;
20 |
21 | async init() {
22 | try {
23 | await this.fetchProjectInfo();
24 | } finally {
25 | this.setStatus({ isLoading: false });
26 | }
27 | }
28 |
29 | destroy() {
30 | this.status = new Status();
31 | this.params = null;
32 | }
33 |
34 | async fetchProjectInfo() {
35 | const { projectId } = this.params;
36 | const res = await Api.project.getProjectInfo(projectId);
37 | if (res.code === EApiCode.Success) {
38 | this.setStatus({
39 | project: res.data,
40 | });
41 | }
42 | }
43 |
44 | switchProjectTab(key: "workspaces" | "settings" | string) {
45 | const { projectId } = this.params;
46 | this.setStatus({ curActiveKey: key });
47 |
48 | switch (key) {
49 | case "workspaces":
50 | routerStore.replace(`/projects/${projectId}/workspaces`);
51 | break;
52 | case "settings":
53 | routerStore.replace(`/projects/${projectId}/settings/common`);
54 | break;
55 | }
56 | }
57 | }
58 |
59 | const projectLayoutStore = new ProjectLayoutStore();
60 | export default projectLayoutStore;
61 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/project/project-detail/project-layout.tsx:
--------------------------------------------------------------------------------
1 | import { Card, PageHeader } from "antd";
2 | import { isNil } from "lodash-es";
3 | import { toJS } from "mobx";
4 | import { observer } from "mobx-react";
5 | import React, { useEffect, useRef } from "react";
6 | import { RouteComponentProps } from "react-router-dom";
7 | import { WideLayout } from "../../../component/layout/wide-layout/wide-layout";
8 | import projectLayoutStore from "./project-layout-store";
9 |
10 | const ProjectLayout: React.FC> =
11 | observer((props) => {
12 | const storeRef = useRef(projectLayoutStore);
13 |
14 | useEffect(() => {
15 | const { projectId } = props.match.params;
16 |
17 | storeRef.current.params = {
18 | projectId: +projectId,
19 | };
20 |
21 | storeRef.current.init();
22 |
23 | return () => {
24 | storeRef.current.destroy();
25 | };
26 | }, []);
27 |
28 | const status = toJS(storeRef.current.status);
29 | const { isLoading, project } = status;
30 |
31 | if (isLoading) {
32 | return null;
33 | }
34 |
35 | if (isNil(project)) {
36 | return null;
37 | }
38 |
39 | return (
40 |
41 |
49 | {
56 | storeRef.current.switchProjectTab(key);
57 | }}
58 | >
59 | {props.children}
60 |
61 |
62 |
63 | );
64 | });
65 |
66 | export default ProjectLayout;
67 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/project/project-detail/project-setting/project-setting-advance/project-setting-advance-store.ts:
--------------------------------------------------------------------------------
1 | import { message } from "antd";
2 | import { observable } from "mobx";
3 | import Api, { EApiCode } from "../../../../../api";
4 | import { routerStore } from "../../../../../store/router-store";
5 | import { BasicStore } from "../../../../../util/basic-store";
6 |
7 | class Status {}
8 |
9 | export class ProjectSettingAdvanceStore extends BasicStore {
10 | @observable.ref status = new Status();
11 |
12 | params: {
13 | projectId: number;
14 | } = null;
15 |
16 | async deleteProject() {
17 | const { projectId } = this.params;
18 | const res = await Api.project.deleteProject(projectId);
19 | if (res.code === EApiCode.Success) {
20 | message.success("项目删除成功");
21 | routerStore.push("/projects");
22 | } else {
23 | message.error(`项目删除失败,请稍候再试,${res.message}`);
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/project/project-detail/project-setting/project-setting-advance/project-setting-advance.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Card, Popconfirm, Space } from "antd";
2 | import { observer } from "mobx-react";
3 | import React, { useEffect, useRef } from "react";
4 | import { useParams } from "react-router-dom";
5 | import CategoryDetail from "../../../../../component/category-detail/category-detail";
6 | import { ProjectSettingAdvanceStore } from "./project-setting-advance-store";
7 |
8 | const ProjectSettingAdvance: React.FC = observer(() => {
9 | const storeRef = useRef(new ProjectSettingAdvanceStore());
10 | const { projectId } = useParams() as { projectId: string };
11 |
12 | useEffect(() => {
13 | storeRef.current.params = {
14 | projectId: +projectId,
15 | };
16 | }, [projectId]);
17 |
18 | const store = storeRef.current;
19 |
20 | return (
21 |
22 |
23 | 删除项目将会连同其相关的所有数据一起删除。此操作无法恢复!
24 |
25 | {
28 | await store.deleteProject();
29 | }}
30 | >
31 |
34 |
35 |
36 |
37 |
38 | );
39 | });
40 |
41 | export default ProjectSettingAdvance;
42 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/project/project-detail/project-setting/project-setting-common/project-setting-common-store.ts:
--------------------------------------------------------------------------------
1 | import { message } from "antd";
2 | import { observable } from "mobx";
3 | import Api from "../../../../../api";
4 | import { IProjectDTO } from "../../../../../interface/client-api/project.interface";
5 | import { BasicStore } from "../../../../../util/basic-store";
6 | import projectLayoutStore from "../../project-layout-store";
7 |
8 | class Status {
9 | project: IProjectDTO = null;
10 |
11 | description: string = "";
12 | isAllowOuterHostnameVisit: boolean = null;
13 | supportPublicPath: boolean = null;
14 | }
15 |
16 | export class ProjectSettingCommonStore extends BasicStore {
17 | @observable.ref status = new Status();
18 |
19 | params: {
20 | projectId: number;
21 | } = null;
22 |
23 | async init() {
24 | await this.fetchProjectInfo();
25 | }
26 |
27 | destroy() {
28 | this.status = new Status();
29 | this.params = null;
30 | }
31 |
32 | async fetchProjectInfo() {
33 | const { projectId } = this.params;
34 | const res = await Api.project.getProjectInfo(projectId);
35 | const projectInfo = res.data;
36 | this.setStatus({
37 | project: projectInfo,
38 | description: projectInfo.description,
39 | isAllowOuterHostnameVisit: false,
40 | supportPublicPath: false,
41 | });
42 | }
43 |
44 | async updateProjectDescription() {
45 | message.warn("功能待实现");
46 | // TODO
47 | // const { projectId } = this.params
48 | // const res = await ApiV2.project.updateProjectInfo(projectId, {
49 | // description: this.status.description,
50 | // })
51 | // if (res.code === EApiCode.Success) {
52 | // message.success('项目描述更新成功')
53 | // await this.refreshProjectLayout()
54 | // } else {
55 | // message.error('项目描述更新失败,请稍候再试')
56 | // }
57 | }
58 |
59 | private async refreshProjectLayout() {
60 | await projectLayoutStore.fetchProjectInfo();
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/project/project-detail/project-setting/project-setting-domain/create-domain-modal/create-domain-modal.tsx:
--------------------------------------------------------------------------------
1 | import { Form, Input, Modal, Select } from "antd";
2 | import { useForm } from "antd/es/form/Form";
3 | import React from "react";
4 | import { IProjectEnvDTO } from "../../../../../../interface/client-api/project.interface";
5 | import { ProjectSettingDomainStore } from "../project-setting-domain-store";
6 |
7 | interface IProps {
8 | store: ProjectSettingDomainStore;
9 | envs: IProjectEnvDTO[];
10 | }
11 |
12 | const CreateDomainModal: React.FC = (props) => {
13 | const { store, envs } = props;
14 | const [form] = useForm();
15 |
16 | return (
17 | {
22 | await form.validateFields();
23 | const { projectEnvId, host } = form.getFieldsValue() as {
24 | projectEnvId: string;
25 | host: string;
26 | };
27 | await store.createProjectDomain({
28 | projectEnvId: +projectEnvId,
29 | host: host,
30 | });
31 | }}
32 | onCancel={() => store.setStatus({ isShowCreateDomainModal: false })}
33 | >
34 |
40 |
47 |
48 |
66 |
67 |
68 |
69 |
70 | );
71 | };
72 |
73 | export default CreateDomainModal;
74 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/project/project-detail/project-setting/project-setting-domain/project-setting-domain-store.ts:
--------------------------------------------------------------------------------
1 | import { message } from "antd";
2 | import { observable } from "mobx";
3 | import Api, { EApiCode } from "../../../../../api";
4 | import { IDomainDTO } from "../../../../../interface/client-api/domain.interface";
5 | import { IProjectEnvDTO } from "../../../../../interface/client-api/project.interface";
6 | import { BasicStore } from "../../../../../util/basic-store";
7 |
8 | class Status {
9 | isLoading: boolean = true;
10 |
11 | envs: IProjectEnvDTO[] = [];
12 | domains: IDomainDTO[] = [];
13 |
14 | isShowCreateDomainModal: boolean = false;
15 | }
16 |
17 | export class ProjectSettingDomainStore extends BasicStore {
18 | @observable.ref status = new Status();
19 |
20 | params: {
21 | projectId: number;
22 | } = null;
23 |
24 | async init() {
25 | try {
26 | await this.fetchEnvs();
27 | await this.fetchProjectDomains();
28 | } finally {
29 | this.setStatus({ isLoading: false });
30 | }
31 | }
32 |
33 | destroy() {
34 | this.status = new Status();
35 | this.params = null;
36 | }
37 |
38 | async refreshDomains() {
39 | this.setStatus({ isLoading: true });
40 | try {
41 | await this.fetchEnvs();
42 | await this.fetchProjectDomains();
43 | } finally {
44 | this.setStatus({ isLoading: false });
45 | }
46 | }
47 |
48 | /**
49 | * 获取工作区信息
50 | */
51 | async fetchEnvs() {
52 | const { projectId } = this.params;
53 | const envsRes = await Api.project.getProjectEnvs(projectId);
54 |
55 | if (envsRes.code === EApiCode.Success) {
56 | this.setStatus({ envs: envsRes.data });
57 | }
58 | }
59 |
60 | /**
61 | * 获取个性化域名列表
62 | */
63 | async fetchProjectDomains() {
64 | const { projectId } = this.params;
65 | const domainsRes = await Api.domain.getProjectDomains(projectId);
66 |
67 | if (domainsRes.code === EApiCode.Success) {
68 | this.setStatus({
69 | domains: domainsRes.data,
70 | });
71 | }
72 | }
73 |
74 | /**
75 | * 删除个性化域名
76 | * @param domainId domainId
77 | */
78 | async deleteProjectDomain(domainId: number) {
79 | const { projectId } = this.params;
80 | const res = await Api.domain.deleteProjectDomain(projectId, domainId);
81 | if (res.code === EApiCode.Success) {
82 | message.success("删除成功");
83 | await this.refreshDomains();
84 | } else {
85 | message.error(res.message || "删除失败");
86 | }
87 | }
88 |
89 | /**
90 | * 创建个性化域名
91 | */
92 | async createProjectDomain(params: { projectEnvId: number; host: string }) {
93 | const { projectEnvId, host } = params;
94 | const { projectId } = this.params;
95 |
96 | const res = await Api.domain.createProjectDomain(projectId, {
97 | projectEnvId,
98 | host,
99 | });
100 | if (res.code === EApiCode.Success) {
101 | message.success("添加个性化域名成功");
102 | this.setStatus({ isShowCreateDomainModal: false });
103 | await this.refreshDomains();
104 | } else {
105 | message.error(res.message || "添加个性化域名失败");
106 | }
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/project/project-detail/project-setting/project-setting-member/project-setting-member-store.ts:
--------------------------------------------------------------------------------
1 | import { message } from "antd";
2 | import { isNil } from "lodash-es";
3 | import { observable } from "mobx";
4 | import Api, { EApiCode } from "../../../../../api";
5 | import {
6 | EProjectMemberRole,
7 | IProjectMemberDTO,
8 | } from "../../../../../interface/client-api/member.interface";
9 | import { BasicStore } from "../../../../../util/basic-store";
10 |
11 | class Status {
12 | members: IProjectMemberDTO[] = [];
13 |
14 | preAddUsername: string = null;
15 | preAddUserRole: EProjectMemberRole = null;
16 | }
17 |
18 | export class ProjectSettingMemberStore extends BasicStore {
19 | @observable.ref status = new Status();
20 |
21 | params: {
22 | projectId: number;
23 | } = null;
24 |
25 | async init() {
26 | await this.fetchProjectMembers();
27 | }
28 |
29 | destroy() {
30 | this.status = new Status();
31 | this.params = null;
32 | }
33 |
34 | async fetchProjectMembers() {
35 | const { projectId } = this.params;
36 | const res = await Api.member.getProjectMembers(projectId);
37 | if (res.code === EApiCode.Success) {
38 | this.setStatus({
39 | members: res.data,
40 | });
41 | }
42 | }
43 |
44 | async addProjectMember() {
45 | const { preAddUsername, preAddUserRole } = this.status;
46 | if (isNil(preAddUsername) || isNil(preAddUserRole)) {
47 | return message.warning("请先选择要添加的人员和权限");
48 | }
49 |
50 | const { projectId } = this.params;
51 | const res = await Api.member.addProjectMember(projectId, {
52 | name: preAddUsername,
53 | role: preAddUserRole,
54 | });
55 | if (res.code === EApiCode.Success) {
56 | message.success("成员添加成功");
57 |
58 | this.setStatus({
59 | preAddUsername: null,
60 | preAddUserRole: null,
61 | });
62 | await this.fetchProjectMembers();
63 | } else {
64 | message.error("成员添加失败,请稍候再试");
65 | }
66 | }
67 |
68 | async removeProjectMember(member: IProjectMemberDTO) {
69 | const { projectId } = this.params;
70 | const res = await Api.member.deleteProjectMember(projectId, member.id);
71 | if (res.code === EApiCode.Success) {
72 | message.success("成员移除成功");
73 |
74 | this.setStatus({
75 | preAddUsername: null,
76 | preAddUserRole: null,
77 | });
78 | await this.fetchProjectMembers();
79 | } else {
80 | message.error("成员移除失败,请稍候再试");
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/project/project-detail/project-setting/project-setting-member/project-setting-member.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Card, Input, Popconfirm, Space, Table } from "antd";
2 | import { toJS } from "mobx";
3 | import { observer } from "mobx-react";
4 | import React, { useEffect, useRef } from "react";
5 | import { useParams } from "react-router-dom";
6 | import {
7 | EProjectMemberRole,
8 | IProjectMemberDTO,
9 | } from "../../../../../interface/client-api/member.interface";
10 | import { ProjectSettingMemberStore } from "./project-setting-member-store";
11 | import RoleSelect from "./role-select/role-select";
12 |
13 | interface IProps {}
14 |
15 | const ProjectSettingMember: React.FC = observer((props) => {
16 | const storeRef = useRef(new ProjectSettingMemberStore());
17 | const { projectId } = useParams() as { projectId: string };
18 |
19 | useEffect(() => {
20 | storeRef.current.params = {
21 | projectId: +projectId,
22 | };
23 | storeRef.current.init();
24 |
25 | return () => {
26 | storeRef.current.destroy();
27 | };
28 | }, []);
29 |
30 | const store = storeRef.current;
31 | const status = toJS(storeRef.current.status);
32 |
33 | return (
34 |
35 |
36 | {
40 | store.setStatus({
41 | preAddUsername: event.target.value,
42 | });
43 | }}
44 | />
45 | {
48 | store.setStatus({
49 | preAddUserRole: value,
50 | });
51 | }}
52 | />
53 |
61 |
62 |
63 | size={"small"}
64 | style={{ marginTop: 20 }}
65 | rowKey={(record) => record.user.id}
66 | dataSource={status.members}
67 | columns={[
68 | {
69 | title: "成员",
70 | render: (value, object) => {
71 | return {object.user.name}
;
72 | },
73 | },
74 | {
75 | title: "项目权限",
76 | dataIndex: "role",
77 | width: 200,
78 | render: (value, object) => {
79 | return {EProjectMemberRole[object.role]}
;
80 | },
81 | },
82 | {
83 | title: "操作",
84 | width: 200,
85 | align: "center",
86 | render: (value, object) => {
87 | return (
88 |
89 | store.removeProjectMember(object)}
92 | okText="确认"
93 | cancelText="取消"
94 | >
95 |
98 |
99 |
100 | );
101 | },
102 | },
103 | ]}
104 | />
105 |
106 | );
107 | });
108 |
109 | export default ProjectSettingMember;
110 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/project/project-detail/project-setting/project-setting-member/role-select/role-select.tsx:
--------------------------------------------------------------------------------
1 | import { Select, Space } from "antd";
2 | import React from "react";
3 | import { EProjectMemberRole } from "../../../../../../interface/client-api/member.interface";
4 |
5 | interface IProps {
6 | value: EProjectMemberRole;
7 | onChange: (value, option) => Promise;
8 | }
9 |
10 | const RoleSelect: React.FC = (props) => {
11 | return (
12 |
13 |
40 |
41 | );
42 | };
43 |
44 | export default RoleSelect;
45 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/project/project-detail/project-setting/project-setting-member/role-select/role.ts:
--------------------------------------------------------------------------------
1 | import { isNil } from "lodash-es";
2 |
3 | export enum ERole {
4 | Owner = 1 << 7, // 128
5 | Master = 1 << 6, // 64
6 | Developer = 1 << 5, // 32
7 | Guest = 1 << 4, // 16
8 | }
9 |
10 | export default class Role {
11 | static roleDesc = {
12 | [ERole.Owner]: {
13 | name: "Owner",
14 | desc: "可以删除、删除项目成员、转移项目、修改项目详情",
15 | },
16 | [ERole.Master]: {
17 | name: "Master",
18 | desc: "可添加修改项目成员与角色,生产审批变更",
19 | },
20 | [ERole.Developer]: {
21 | name: "Developer",
22 | desc: "日常项目操作权限,比如发布,创建工作区等",
23 | },
24 | [ERole.Guest]: {
25 | name: "Guest",
26 | desc: "可浏览项目,不支持任何修改",
27 | },
28 | };
29 |
30 | static getCanSelectRoles = () => {
31 | return [ERole.Guest, ERole.Developer, ERole.Master];
32 | };
33 |
34 | /**
35 | * 获取用户角色名称,一个用户可以有多个角色
36 | * @param role
37 | */
38 | static getMemberRoleName = (role: ERole) => {
39 | const desc = Role.roleDesc[role];
40 | if (!isNil(desc)) {
41 | return desc.name;
42 | }
43 | if (role === ERole.Owner + ERole.Master) {
44 | return `${Role.roleDesc[ERole.Owner].name} & ${
45 | Role.roleDesc[ERole.Master].name
46 | }`;
47 | }
48 | return "";
49 | };
50 |
51 | /**
52 | * 获取角色说明
53 | * @param role
54 | * @returns
55 | */
56 | static getRoleDesc = (role: ERole) => {
57 | const desc = Role.roleDesc[role];
58 | if (isNil(desc)) {
59 | return null;
60 | }
61 | return desc;
62 | };
63 | }
64 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/project/project-detail/project-setting/project-setting-store.ts:
--------------------------------------------------------------------------------
1 | import { observable } from "mobx";
2 | import { routerStore } from "../../../../store/router-store";
3 | import { BasicStore } from "../../../../util/basic-store";
4 |
5 | export enum ESettingType {
6 | Common = "common",
7 | Members = "members",
8 | Domain = "domain",
9 | Webhooks = "webhooks",
10 | Advance = "advance",
11 | }
12 |
13 | interface IParams {
14 | projectId: number;
15 | type: ESettingType;
16 | }
17 |
18 | class Status {
19 | curActiveType: ESettingType = null;
20 | }
21 |
22 | export class ProjectSettingStore extends BasicStore {
23 | @observable.ref status = new Status();
24 |
25 | params: IParams = null;
26 |
27 | async init() {
28 | this.setStatus({ curActiveType: this.params.type });
29 | this.switchSettingType(this.params.type || ESettingType.Common);
30 | }
31 |
32 | destroy() {
33 | this.status = new Status();
34 | this.params = null;
35 | }
36 |
37 | switchSettingType(key: ESettingType) {
38 | const { projectId } = this.params;
39 |
40 | this.setStatus({ curActiveType: key });
41 |
42 | routerStore.push(`/projects/${projectId}/settings/${key}`);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/project/project-detail/project-setting/project-setting.module.less:
--------------------------------------------------------------------------------
1 | .project-setting {
2 | :global {
3 | .project-setting-tabs > .ant-tabs-nav {
4 | .ant-tabs-tab-active {
5 | background-color: rgba(98, 71, 170, 0.5);
6 |
7 | .ant-tabs-tab-btn {
8 | color: #fff;
9 | }
10 | }
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/project/project-detail/project-setting/project-setting.tsx:
--------------------------------------------------------------------------------
1 | import { Tabs } from "antd";
2 | import { isNil } from "lodash-es";
3 | import { toJS } from "mobx";
4 | import { observer } from "mobx-react";
5 | import React, { useEffect, useRef } from "react";
6 | import { RouteComponentProps } from "react-router-dom";
7 | import projectLayoutStore from "../project-layout-store";
8 | import ProjectSettingAdvance from "./project-setting-advance/project-setting-advance";
9 | import ProjectSettingCommon from "./project-setting-common/project-setting-common";
10 | import ProjectSettingDomain from "./project-setting-domain/project-setting-domain";
11 | import ProjectSettingMember from "./project-setting-member/project-setting-member";
12 | import { ESettingType, ProjectSettingStore } from "./project-setting-store";
13 | import styles from "./project-setting.module.less";
14 |
15 | const ProjectSetting: React.FC<
16 | RouteComponentProps<{
17 | projectId: string;
18 | type: ESettingType;
19 | }>
20 | > = observer((props) => {
21 | const storeRef = useRef(new ProjectSettingStore());
22 |
23 | useEffect(() => {
24 | projectLayoutStore.setStatus({ curActiveKey: "settings" });
25 | }, []);
26 |
27 | useEffect(() => {
28 | const { projectId, type } = props.match.params;
29 | storeRef.current.params = {
30 | projectId: +projectId,
31 | type: type,
32 | };
33 |
34 | storeRef.current.init();
35 |
36 | return () => {
37 | storeRef.current.destroy();
38 | };
39 | }, []);
40 |
41 | const status = toJS(storeRef.current.status);
42 |
43 | if (isNil(storeRef.current.params)) {
44 | return null;
45 | }
46 |
47 | return (
48 |
49 |
59 | storeRef.current.switchSettingType(key as ESettingType)
60 | }
61 | >
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | );
80 | });
81 |
82 | export default ProjectSetting;
83 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/project/project-detail/project-workspace/create-workspace-modal/create-workspace-modal.tsx:
--------------------------------------------------------------------------------
1 | import { Form, Input, Modal, Select } from "antd";
2 | import { useForm } from "antd/es/form/Form";
3 | import React from "react";
4 | import { EProjectEnvType } from "../../../../../interface/client-api/project.interface";
5 | import { ProjectWorkspaceStore } from "../project-workspace-store";
6 |
7 | interface IProps {
8 | store: ProjectWorkspaceStore;
9 | }
10 |
11 | const CreateWorkspaceModal: React.FC = (props) => {
12 | const { store } = props;
13 | const [form] = useForm();
14 |
15 | return (
16 | {
21 | await form.validateFields();
22 | const values = form.getFieldsValue() as {
23 | name: string;
24 | envType: EProjectEnvType;
25 | };
26 | await store.createWorkspace(values.envType, values.name);
27 | }}
28 | onCancel={() => {
29 | store.setStatus({
30 | isShowCreateModal: false,
31 | });
32 | }}
33 | >
34 |
46 |
47 |
48 |
49 |
54 |
58 |
59 |
60 |
61 | );
62 | };
63 |
64 | export default CreateWorkspaceModal;
65 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/project/project-detail/project-workspace/project-workspace-store.ts:
--------------------------------------------------------------------------------
1 | import { message } from "antd";
2 | import { observable } from "mobx";
3 | import Api, { EApiCode } from "../../../../api";
4 | import {
5 | EProjectEnvType,
6 | IProjectEnvDTO,
7 | } from "../../../../interface/client-api/project.interface";
8 | import { routerStore } from "../../../../store/router-store";
9 | import { BasicStore } from "../../../../util/basic-store";
10 |
11 | interface IParams {
12 | projectId: number;
13 | workspaceId: number;
14 | }
15 |
16 | class Status {
17 | envAreas: IProjectEnvDTO[] = [];
18 |
19 | curEnvAreaId: number = null;
20 | isShowCreateModal: boolean = false;
21 | }
22 |
23 | export class ProjectWorkspaceStore extends BasicStore {
24 | @observable.ref status = new Status();
25 |
26 | params: IParams = null;
27 |
28 | destroy() {
29 | this.status = new Status();
30 | this.params = null;
31 | }
32 |
33 | configParams(params: IParams) {
34 | this.params = params;
35 | this.setStatus({
36 | curEnvAreaId: params.workspaceId,
37 | });
38 | }
39 |
40 | async fetchEnvAreas() {
41 | const { projectId } = this.params;
42 | const envAreasRes = await Api.project.getProjectEnvs(projectId);
43 | if (envAreasRes.code === EApiCode.Success) {
44 | const envAreas = envAreasRes.data.sort((a, b) => a.envType - b.envType);
45 | this.setStatus({
46 | envAreas: envAreas,
47 | });
48 | }
49 | }
50 |
51 | switchEnvArea(envAreaIdStr: string) {
52 | const { projectId } = this.params;
53 | routerStore.replace(`/projects/${projectId}/workspaces/${envAreaIdStr}`);
54 | }
55 |
56 | async createWorkspace(envType: EProjectEnvType, name: string) {
57 | const { projectId } = this.params;
58 | const res = await Api.project.createProjectEnv(projectId, {
59 | name: name,
60 | type: envType,
61 | });
62 | if (res.code === EApiCode.Success) {
63 | this.setStatus({
64 | isShowCreateModal: false,
65 | });
66 | message.success("新建工作区成功");
67 | await this.fetchEnvAreas();
68 | } else {
69 | message.error(`新建工作区失败,请稍候再试: ${res.message}`);
70 | }
71 | }
72 | }
73 |
74 | const projectWorkspaceStore = new ProjectWorkspaceStore();
75 | export default projectWorkspaceStore;
76 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/project/project-detail/project-workspace/project-workspace.module.less:
--------------------------------------------------------------------------------
1 | .project-workspace {
2 | :global {
3 | .project-env-area-tabs > .ant-tabs-nav {
4 | .ant-tabs-tab-active {
5 | background-color: rgba(98, 71, 170, 0.5);
6 |
7 | .ant-tabs-tab-btn {
8 | color: #fff;
9 | }
10 | }
11 | }
12 | }
13 |
14 | .tab-pane-gray {
15 | display: inline-block;
16 | background: #8c8c8c;
17 | color: #fff;
18 | font-size: 12px;
19 | line-height: 15px;
20 | padding: 2px 3px;
21 | border-radius: 2px;
22 | margin-right: 3px;
23 | }
24 |
25 | .tab-pane-pro {
26 | display: inline-block;
27 | background: #13c2c2;
28 | color: #fff;
29 | font-size: 12px;
30 | line-height: 15px;
31 | padding: 2px 3px;
32 | border-radius: 2px;
33 | margin-right: 3px;
34 | }
35 |
36 | .tab-pane-beta {
37 | display: inline-block;
38 | background: #faad14;
39 | color: #fff;
40 | font-size: 12px;
41 | line-height: 15px;
42 | padding: 2px 3px;
43 | border-radius: 2px;
44 | margin-right: 3px;
45 | }
46 |
47 | .tab-pane-test {
48 | display: inline-block;
49 | background: #eb2f96;
50 | color: #fff;
51 | font-size: 12px;
52 | line-height: 15px;
53 | padding: 2px 3px;
54 | border-radius: 2px;
55 | margin-right: 3px;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/project/project-detail/project-workspace/workspace-single/create-deploy-file/create-deploy-file.tsx:
--------------------------------------------------------------------------------
1 | import { InboxOutlined } from "@ant-design/icons";
2 | import { Modal, Upload } from "antd";
3 | import React, { useState } from "react";
4 | import { WorkspaceSingleStore } from "../workspace-single-store";
5 |
6 | interface IProps {
7 | store: WorkspaceSingleStore;
8 | }
9 |
10 | const CreateDeployFile: React.FC = (props) => {
11 | const { store } = props;
12 | const [file, setFile] = useState(null);
13 |
14 | return (
15 | {
21 | await store.handleCreateDeployFile(file);
22 | }}
23 | onCancel={() => {
24 | store.setStatus({
25 | isShowCreateDeployFile: false,
26 | });
27 | }}
28 | >
29 | {
33 | setFile(options.file);
34 | }}
35 | >
36 |
37 |
38 |
39 |
40 | {file
41 | ? file.name
42 | : "点击或者拖拽 .zip 文件到此处(支持单独上传 html 文件)"}
43 |
44 |
45 |
46 | );
47 | };
48 |
49 | export default CreateDeployFile;
50 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/project/project-detail/project-workspace/workspace-single/create-deploy-url/create-deploy-url.tsx:
--------------------------------------------------------------------------------
1 | import { Form, Input, Modal } from "antd";
2 | import { useForm } from "antd/es/form/Form";
3 | import React from "react";
4 | import { WorkspaceSingleStore } from "../workspace-single-store";
5 |
6 | interface IProps {
7 | store: WorkspaceSingleStore;
8 | }
9 |
10 | const CreateDeployUrl: React.FC = (props) => {
11 | const { store } = props;
12 | const [form] = useForm();
13 |
14 | return (
15 | {
21 | const { target, remark } = form.getFieldsValue();
22 | await store.handleCreateDeployUrl(target, remark);
23 | }}
24 | onCancel={() => {
25 | store.setStatus({
26 | isShowCreateDeployUrl: false,
27 | });
28 | }}
29 | >
30 |
36 |
37 |
38 |
43 |
44 |
45 |
46 |
47 | );
48 | };
49 |
50 | export default CreateDeployUrl;
51 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/project/project-detail/project-workspace/workspace-single/deploy-info-descriptions/deploy-info-descriptions.tsx:
--------------------------------------------------------------------------------
1 | import { Descriptions, Empty } from "antd";
2 | import { isNil } from "lodash-es";
3 | import React, { useMemo } from "react";
4 | import QrCode from "../../../../../../component/qrcode/qrcode";
5 | import {
6 | IProjectDeployDTO,
7 | IProjectDTO,
8 | IProjectEnvDTO,
9 | } from "../../../../../../interface/client-api/project.interface";
10 | import { getNormalUrl } from "../../../../../../util/get-normal-url";
11 | import { timeFormat } from "../../../../../../util/time-format";
12 |
13 | interface IProps {
14 | project: IProjectDTO;
15 | env: IProjectEnvDTO;
16 | deploy: IProjectDeployDTO;
17 | }
18 |
19 | const DeployInfoDescriptions: React.FC = (props) => {
20 | const { project, env, deploy } = props;
21 |
22 | if (isNil(deploy)) {
23 | return ;
24 | }
25 |
26 | const httpsLink = useMemo(() => {
27 | return getNormalUrl(project, env);
28 | }, [project, env]);
29 |
30 | return (
31 |
32 |
33 | {deploy.id}
34 |
35 |
36 |
37 | {httpsLink}
38 |
39 |
40 |
41 |
42 |
43 |
44 | {deploy.createUser.name || "--"}
45 |
46 |
47 | {timeFormat(deploy.createdAt)}
48 |
49 |
50 | {deploy.actionUser.name || "--"}
51 |
52 |
53 | {timeFormat(deploy.updatedAt)}
54 |
55 |
56 |
57 | {deploy.target || "--"}
58 |
59 |
60 |
61 | {deploy.remark || "--"}
62 |
63 |
64 | );
65 | };
66 |
67 | export default DeployInfoDescriptions;
68 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/project/project-detail/project-workspace/workspace-single/deploy-info-modal/deploy-info-modal.tsx:
--------------------------------------------------------------------------------
1 | import { Modal } from "antd";
2 | import React from "react";
3 | import {
4 | IProjectDeployDTO,
5 | IProjectDTO,
6 | IProjectEnvDTO,
7 | } from "../../../../../../interface/client-api/project.interface";
8 | import DeployInfoDescriptions from "../deploy-info-descriptions/deploy-info-descriptions";
9 |
10 | interface IProps {
11 | project: IProjectDTO;
12 | env: IProjectEnvDTO;
13 | deploy: IProjectDeployDTO;
14 | onCancel: () => void;
15 | }
16 |
17 | const DeployInfoModal: React.FC = (props) => {
18 | const { project, env, deploy, onCancel } = props;
19 | return (
20 | {
26 | await onCancel();
27 | }}
28 | >
29 |
30 |
31 | );
32 | };
33 |
34 | export default DeployInfoModal;
35 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/project/project-detail/project-workspace/workspace-single/tabs-deploy-info/tabs-deploy-info.tsx:
--------------------------------------------------------------------------------
1 | import { ReloadOutlined } from "@ant-design/icons";
2 | import { Button, Empty, Space, Spin } from "antd";
3 | import { isNil } from "lodash-es";
4 | import { toJS } from "mobx";
5 | import React, { useMemo } from "react";
6 | import {
7 | EProjectEnvType,
8 | IProjectEnvDTO,
9 | } from "../../../../../../interface/client-api/project.interface";
10 | import projectLayoutStore from "../../../project-layout-store";
11 | import DeployInfoDescriptions from "../deploy-info-descriptions/deploy-info-descriptions";
12 | import { WorkspaceSingleStore } from "../workspace-single-store";
13 |
14 | interface IProps {
15 | store: WorkspaceSingleStore;
16 | envAreas: IProjectEnvDTO[];
17 | }
18 |
19 | const TabsDeployInfo: React.FC = (props) => {
20 | const { store, envAreas } = props;
21 | const { curActiveDeploy } = store;
22 |
23 | const status = toJS(store.status);
24 |
25 | const {
26 | status: { project },
27 | } = projectLayoutStore;
28 |
29 | if (isNil(curActiveDeploy)) {
30 | return ;
31 | }
32 |
33 | return (
34 | <>
35 |
36 | }
39 | loading={status.isLoadingDeploys}
40 | onClick={async () => {
41 | await store.fetchDeployList();
42 | }}
43 | />
44 |
45 |
46 |
47 |
52 |
53 |
54 | >
55 | );
56 | };
57 |
58 | export default TabsDeployInfo;
59 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/project/project-detail/project-workspace/workspace-single/workspace-single.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Card, Popover, Space } from "antd";
2 | import { toJS } from "mobx";
3 | import { observer } from "mobx-react";
4 | import React, { useEffect, useMemo, useRef } from "react";
5 | import QrCode from "../../../../../component/qrcode/qrcode";
6 | import { IProjectEnvDTO } from "../../../../../interface/client-api/project.interface";
7 | import projectWorkspaceStore from "../project-workspace-store";
8 | import CreateDeployFile from "./create-deploy-file/create-deploy-file";
9 | import CreateDeployUrl from "./create-deploy-url/create-deploy-url";
10 | import TabsDeployInfo from "./tabs-deploy-info/tabs-deploy-info";
11 | import TabsDeploysTable from "./tabs-deploys-table/tabs-deploys-table";
12 | import { WorkspaceSingleStore } from "./workspace-single-store";
13 |
14 | interface IProps {
15 | env: IProjectEnvDTO;
16 | }
17 |
18 | const WorkspaceSingle: React.FC = observer((props) => {
19 | const { env } = props;
20 | const storeRef = useRef(new WorkspaceSingleStore());
21 | const projectWorkspaceStoreRef = useRef(projectWorkspaceStore);
22 |
23 | useEffect(() => {
24 | storeRef.current.params = {
25 | projectId: props.env.projectId,
26 | workspaceId: props.env.id,
27 | };
28 |
29 | storeRef.current.setStatus({
30 | env: props.env,
31 | });
32 |
33 | const curEnvAreaId = toJS(
34 | projectWorkspaceStoreRef.current.status.curEnvAreaId
35 | );
36 |
37 | if (props.env.id === curEnvAreaId) {
38 | storeRef.current.init();
39 |
40 | return () => {
41 | storeRef.current.destroy();
42 | };
43 | }
44 | }, [props.env, projectWorkspaceStoreRef.current.status.curEnvAreaId]);
45 |
46 | const envUrl = useMemo(() => {
47 | return "https://baidu.com";
48 | }, [env]);
49 |
50 | const store = storeRef.current;
51 | const status = toJS(storeRef.current.status);
52 |
53 | if (status.isLoading) {
54 | return null;
55 | }
56 |
57 | return (
58 | <>
59 | v)}
64 | tabBarExtraContent={
65 |
66 | }>
67 |
70 |
71 |
72 | }
73 | activeTabKey={status.curActiveTab}
74 | onTabChange={(key) => {
75 | store.setStatus({
76 | curActiveTab: key,
77 | });
78 | }}
79 | >
80 | {status.curActiveTab === "info" && (
81 |
85 | )}
86 | {status.curActiveTab === "deploy" && }
87 | {status.isShowCreateDeployUrl && }
88 | {status.isShowCreateDeployFile && }
89 |
90 | >
91 | );
92 | });
93 |
94 | export default WorkspaceSingle;
95 |
--------------------------------------------------------------------------------
/packages/client-web/src/page/project/project-list/project-list.tsx:
--------------------------------------------------------------------------------
1 | import { Card } from "antd";
2 | import { toJS } from "mobx";
3 | import { observer } from "mobx-react";
4 | import React, { useEffect, useRef } from "react";
5 | import { WideLayout } from "../../../component/layout/wide-layout/wide-layout";
6 | import CreateProjectModal from "../../component/create-project-modal/create-project-modal";
7 | import ProjectListCard from "../../component/project-list-card/project-list-card";
8 | import { ProjectListStore } from "./project-list-store";
9 |
10 | const ProjectList: React.FC = observer(() => {
11 | const storeRef = useRef(new ProjectListStore());
12 |
13 | useEffect(() => {
14 | storeRef.current.init();
15 |
16 | return () => {
17 | storeRef.current.destroy();
18 | };
19 | }, []);
20 |
21 | const store = storeRef.current;
22 | const status = toJS(store.status);
23 |
24 | return (
25 |
26 | {
33 | await store.onCardTabChange(key);
34 | }}
35 | >
36 | {status.curActiveTab === "my" && (
37 | {
44 | store.setStatus({ mySearchWord: searchVal, myCurPage: 1 });
45 | await store.fetchMimeProjects();
46 | }}
47 | onPageChange={async (page) => {
48 | store.setStatus({ myCurPage: page });
49 | await store.fetchMimeProjects();
50 | }}
51 | onClickCreate={() => store.onClickCreateProjectButton()}
52 | />
53 | )}
54 |
55 | {status.curActiveTab === "all" && (
56 | {
63 | store.setStatus({ allSearchWord: searchVal, allCurPage: 1 });
64 | await store.fetchAllProjects();
65 | }}
66 | onPageChange={async (page) => {
67 | store.setStatus({ allCurPage: page });
68 | await store.fetchAllProjects();
69 | }}
70 | onClickCreate={() => store.onClickCreateProjectButton()}
71 | />
72 | )}
73 |
74 |
75 | {storeRef.current.status.isShowCreateProjectModal && (
76 | {
78 | storeRef.current.setStatus({
79 | isShowCreateProjectModal: false,
80 | });
81 | await store.fetchProjects();
82 | }}
83 | onCancel={() => {
84 | storeRef.current.setStatus({
85 | isShowCreateProjectModal: false,
86 | });
87 | }}
88 | />
89 | )}
90 |
91 | );
92 | });
93 |
94 | export default ProjectList;
95 |
--------------------------------------------------------------------------------
/packages/client-web/src/routes.tsx:
--------------------------------------------------------------------------------
1 | import { observer } from "mobx-react";
2 | import React, { useEffect, useState } from "react";
3 | import { Redirect, Route, Switch } from "react-router-dom";
4 | import GroupHolder from "./page/group/group-detail/group-holder/group-holder";
5 | import GroupLayout from "./page/group/group-detail/group-layout";
6 | import GroupProject from "./page/group/group-detail/group-project/group-project";
7 | import GroupSetting from "./page/group/group-detail/group-settiing/group-setting";
8 | import NewGroupList from "./page/group/group-list/group-list";
9 | import ProjectHolder from "./page/project/project-detail/project-holder/project-holder";
10 | import ProjectLayout from "./page/project/project-detail/project-layout";
11 | import ProjectSetting from "./page/project/project-detail/project-setting/project-setting";
12 | import ProjectWorkspace from "./page/project/project-detail/project-workspace/project-workspace";
13 | import ProjectList from "./page/project/project-list/project-list";
14 | import userStore from "./store/user-store";
15 |
16 | const Routes: React.FC = observer((props) => {
17 | const [isLoading, setIsLoading] = useState(true);
18 |
19 | useEffect(() => {
20 | (async () => {
21 | try {
22 | await userStore.login();
23 | } finally {
24 | setIsLoading(false);
25 | }
26 | })();
27 | }, []);
28 |
29 | if (isLoading) {
30 | return null;
31 | }
32 |
33 | return (
34 | <>
35 |
36 |
37 | (
40 |
41 |
42 |
47 |
52 |
56 |
57 |
58 | )}
59 | />
60 |
61 |
62 | (
65 |
66 |
67 |
72 |
77 |
78 |
79 |
80 | )}
81 | />
82 |
83 |
84 |
85 |
86 |
87 | >
88 | );
89 | });
90 |
91 | export default Routes;
92 |
--------------------------------------------------------------------------------
/packages/client-web/src/store/router-store.ts:
--------------------------------------------------------------------------------
1 | // react-router-dom 下的 react-router 中 history
2 | import { createBrowserHistory } from "history";
3 |
4 | export const routerStore = createBrowserHistory();
5 |
--------------------------------------------------------------------------------
/packages/client-web/src/store/user-store.ts:
--------------------------------------------------------------------------------
1 | import { message } from "antd";
2 | import { isNil } from "lodash-es";
3 | import { observable } from "mobx";
4 | import Api, { EApiCode } from "../api";
5 | import { BasicStore } from "../util/basic-store";
6 |
7 | class Status {
8 | userinfo: { id: number; name: string } = null;
9 | }
10 |
11 | export class UserStore extends BasicStore {
12 | @observable.ref status = new Status();
13 |
14 | login = async () => {
15 | if (isNil(this.status.userinfo)) {
16 | return await this.getUserInfo();
17 | }
18 | };
19 |
20 | getUserInfo = async () => {
21 | try {
22 | const res = await Api.account.accountInfo();
23 | if (res.code === EApiCode.Success) {
24 | this.setStatus({
25 | userinfo: res.data,
26 | });
27 | }
28 | if (res.code === EApiCode.Unauthorized) {
29 | // TODO
30 | alert("JumpToLogin");
31 | }
32 | } catch (err) {
33 | message.error(`校验身份信息失败,请自行排查当前网络环境或联系咚咚答疑群`);
34 | }
35 | };
36 |
37 | logout = async () => {
38 | // TODO
39 | };
40 |
41 | get isAdministrator(): boolean {
42 | return false;
43 | }
44 | }
45 |
46 | const userStore = new UserStore();
47 | export default userStore;
48 |
--------------------------------------------------------------------------------
/packages/client-web/src/style/style.less:
--------------------------------------------------------------------------------
1 | @import "node_modules/antd/dist/antd.less";
2 | @import "node_modules/antd/lib/style/themes/default.less";
3 | @import "variable.less";
4 |
5 | html {
6 | height: auto;
7 |
8 | body {
9 | //noinspection CssInvalidPropertyValue
10 | overflow-y: overlay;
11 | }
12 | }
13 |
14 | * {
15 | word-break: break-all;
16 | }
17 |
18 | // antd 全局覆盖
19 | .ant-popover-buttons {
20 | text-align: center;
21 | }
22 |
23 | .ant-btn-dangerous.ant-btn-primary[disabled],
24 | .ant-btn-dangerous.ant-btn-primary[disabled]:hover,
25 | .ant-btn-dangerous.ant-btn-primary[disabled]:focus,
26 | .ant-btn-dangerous.ant-btn-primary[disabled]:active {
27 | color: #f9a7a7;
28 | background-color: #fef0f0;
29 | border-color: #fde2e2;
30 | }
31 |
32 | .danger {
33 | color: #ff4d4f;
34 | }
35 |
--------------------------------------------------------------------------------
/packages/client-web/src/style/variable.less:
--------------------------------------------------------------------------------
1 | // 自定义变量
2 | @main-color: #6247aa;
3 | @secondary-color: #815ac0;
4 | @sub-color: #a06cd5;
5 |
6 | // antd变量
7 | @layout-header-background: @main-color;
8 | @heading-color: @main-color;
9 | @primary-color: @main-color;
10 | @link-color: @main-color;
11 | @layout-header-height: 50px;
12 |
--------------------------------------------------------------------------------
/packages/client-web/src/util/basic-store.ts:
--------------------------------------------------------------------------------
1 | import { observable, runInAction } from "mobx";
2 |
3 | /**
4 | * 基础 store 类,用来给单页面的 store 继承
5 | */
6 | export class BasicStore {
7 | /**
8 | * 被监听的状态值
9 | */
10 | @observable status: T = null;
11 |
12 | /**
13 | * 封装 runInAction 统一更新状态值
14 | * @param object
15 | */
16 | setStatus(object: Partial) {
17 | runInAction(() => {
18 | this.status = Object.assign({}, this.status, object);
19 | });
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/packages/client-web/src/util/define-property.ts:
--------------------------------------------------------------------------------
1 | declare var USE_V_CONSOLE: boolean;
2 | declare var SERVER_URL: string;
3 | declare var DOMAIN_SUFFIX: string;
4 |
5 | console.log("--- loading global constants ---");
6 | console.log(`SERVER_URL: ${SERVER_URL}`);
7 | console.log(`USE_V_CONSOLE: ${USE_V_CONSOLE}`);
8 | console.log(`DOMAIN_SUFFIX: ${DOMAIN_SUFFIX}`);
9 |
10 | export const DefineProperty = {
11 | UseVConsole: USE_V_CONSOLE,
12 | ServerUrl: SERVER_URL,
13 | DomainSuffix: DOMAIN_SUFFIX,
14 | };
15 |
--------------------------------------------------------------------------------
/packages/client-web/src/util/get-normal-url.ts:
--------------------------------------------------------------------------------
1 | import {
2 | IProjectDTO,
3 | IProjectEnvDTO,
4 | } from "../interface/client-api/project.interface";
5 | import { DefineProperty } from "./define-property";
6 |
7 | export const getNormalUrl = (project: IProjectDTO, env: IProjectEnvDTO) => {
8 | return `http://${project.name}.${env.name}${DefineProperty.DomainSuffix}`;
9 | };
10 |
--------------------------------------------------------------------------------
/packages/client-web/src/util/get-preview-url.ts:
--------------------------------------------------------------------------------
1 | import {
2 | IProjectDeployDTO,
3 | IProjectDTO,
4 | IProjectEnvDTO,
5 | } from "../interface/client-api/project.interface";
6 | import { DefineProperty } from "./define-property";
7 |
8 | export const getPreviewUrl = (
9 | project: IProjectDTO,
10 | env: IProjectEnvDTO,
11 | deploy: IProjectDeployDTO
12 | ) => {
13 | return `http://${deploy.id}.${project.name}.${env.name}${DefineProperty.DomainSuffix}`;
14 | };
15 |
--------------------------------------------------------------------------------
/packages/client-web/src/util/get-query.ts:
--------------------------------------------------------------------------------
1 | import { isArray, isNil } from "lodash-es";
2 |
3 | /**
4 | * 获取当前 query 中的参数
5 | * @param name 参数名
6 | * @param url 查询字符串,默认为当前页面路由
7 | */
8 | export const getQuery = (name, url?) => {
9 | if (isNil(url)) {
10 | url = window.location.href;
11 | }
12 |
13 | url = decodeURIComponent(url);
14 | const reg = new RegExp(`(^|[&?])${name}=([^]*)`);
15 | const result = url.match(reg);
16 |
17 | if (isArray(result)) {
18 | return result[2];
19 | } else {
20 | return null;
21 | }
22 | };
23 |
--------------------------------------------------------------------------------
/packages/client-web/src/util/time-format.ts:
--------------------------------------------------------------------------------
1 | export const timeFormat = (time) => {
2 | if (!time) {
3 | return "";
4 | }
5 |
6 | const d = new Date(time);
7 | const year = fillZero(d.getFullYear()),
8 | month = fillZero(d.getMonth() + 1),
9 | day = fillZero(d.getDate()),
10 | hour = fillZero(d.getHours()),
11 | minute = fillZero(d.getMinutes()),
12 | second = fillZero(d.getSeconds());
13 | return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
14 | };
15 |
16 | const fillZero = (num) => {
17 | if (num < 10) {
18 | return "0" + num;
19 | }
20 | return num;
21 | };
22 |
--------------------------------------------------------------------------------
/packages/client-web/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": false,
5 | "lib": [
6 | "DOM",
7 | "DOM.Iterable",
8 | "ESNext"
9 | ],
10 | "allowJs": false,
11 | "skipLibCheck": true,
12 | "esModuleInterop": false,
13 | "allowSyntheticDefaultImports": true,
14 | "experimentalDecorators": true,
15 | "strict": true,
16 | "strictNullChecks": false,
17 | "noImplicitAny": false,
18 | "module": "ESNext",
19 | "moduleResolution": "Node",
20 | "resolveJsonModule": true,
21 | "isolatedModules": true,
22 | "noEmit": true,
23 | "jsx": "react-jsx"
24 | },
25 | "include": [
26 | "src",
27 | "typings"
28 | ],
29 | "exclude": [
30 | "node_modules"
31 | ],
32 | "references": [
33 | {
34 | "path": "./tsconfig.node.json"
35 | }
36 | ]
37 | }
38 |
--------------------------------------------------------------------------------
/packages/client-web/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "ESNext",
5 | "moduleResolution": "Node",
6 | "allowSyntheticDefaultImports": true
7 | },
8 | "include": [
9 | "vite.config.ts"
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/packages/client-web/typings/declaration.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.module.less" {
2 | const classes: {[className: string]: string};
3 | export default classes
4 | }
--------------------------------------------------------------------------------
/packages/client-web/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | define: {
7 | USE_V_CONSOLE: true,
8 | SERVER_URL: JSON.stringify('http://127.0.0.1:7001'),
9 | SERVER_DOMAIN: JSON.stringify('beta-pf.jd.com'),
10 | DOMAIN_SUFFIX: JSON.stringify('.pubfree.jd.com'),
11 | },
12 | plugins: [react()],
13 | css: {
14 | modules: {
15 | localsConvention: 'camelCaseOnly',
16 | },
17 | preprocessorOptions: {
18 | less: {
19 | javascriptEnabled: true,
20 | },
21 | },
22 | },
23 | })
24 |
--------------------------------------------------------------------------------
/packages/server-api/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "extends": [
4 | "prettier/@typescript-eslint",
5 | "plugin:prettier/recommended"
6 | ],
7 | "plugins": [
8 | "@typescript-eslint"
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/packages/server-api/.gitignore:
--------------------------------------------------------------------------------
1 | logs
2 | npm-debug.log
3 | yarn-error.log
4 | node_modules/
5 | package-lock.json
6 |
7 | coverage/
8 | dist/
9 | .idea/
10 | run/
11 | .DS_Store
12 | *.sw*
13 | *.un~
14 | .tsbuildinfo
15 | .tsbuildinfo.*
16 |
17 | resource/config.default.json
18 | .vscode/
19 |
--------------------------------------------------------------------------------
/packages/server-api/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | printWidth: 120,
3 | }
4 |
--------------------------------------------------------------------------------
/packages/server-api/bootstrap.js:
--------------------------------------------------------------------------------
1 | const { Bootstrap } = require('@midwayjs/bootstrap')
2 | const { createLogger } = require('@midwayjs/logger')
3 | const WebFramework = require('@midwayjs/koa').Framework
4 |
5 | const logger = createLogger('logger', {
6 | dir: process.env.LOG_DIR || __dirname + '/logs',
7 | fileLogName: 'pubfree-server.log',
8 | })
9 | const webFramework = new WebFramework().configure({
10 | port: process.env.PORT || 7001,
11 | logger: logger,
12 | })
13 |
14 | Bootstrap.load(webFramework).run()
--------------------------------------------------------------------------------
/packages/server-api/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | testEnvironment: 'node',
4 | testPathIgnorePatterns: ['/test/fixtures'],
5 | coveragePathIgnorePatterns: ['/test/'],
6 | setupFilesAfterEnv: [],
7 | }
8 |
--------------------------------------------------------------------------------
/packages/server-api/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@midwayjs-examples/applicaiton-express",
3 | "version": "1.0.0",
4 | "scripts": {
5 | "start": "NODE_ENV=production node bootstrap.js",
6 | "start-prof": "NODE_ENV=production node --prof bootstrap.js",
7 | "dev": "cross-env NODE_ENV=local midway-bin dev --ts",
8 | "test": "midway-bin test --ts",
9 | "cov": "midway-bin cov --ts",
10 | "lint": "eslint src --ext .ts,.tsx",
11 | "ci": "npm run cov",
12 | "build": "midway-bin build -c",
13 | "check": "luckyeye",
14 | "prettier": "prettier -l --write './src/**/*.{ts,tsx,less,css}'",
15 | "prettier-check": "prettier -l './src/**/*.{ts,tsx,less,css}'",
16 | "unittest": "jest --silent"
17 | },
18 | "engines": {
19 | "node": ">=12.0.0"
20 | },
21 | "dependencies": {
22 | "@koa/cors": "3.1.0",
23 | "@midwayjs/bootstrap": "^2.12.3",
24 | "@midwayjs/core": "^2.12.3",
25 | "@midwayjs/decorator": "^2.12.3",
26 | "@midwayjs/koa": "^2.12.3",
27 | "@midwayjs/logger": "^2.16.3",
28 | "@midwayjs/orm": "^2.12.3",
29 | "axios": "0.21.1",
30 | "joi": "^17.2.1",
31 | "jsonwebtoken": "^8.5.1",
32 | "koa-bodyparser": "4.3.0",
33 | "lodash": "4.17.21",
34 | "multer": "1.4.4",
35 | "mysql": "2.18.1",
36 | "reflect-metadata": "0.1.13",
37 | "typeorm": "0.2.31"
38 | },
39 | "devDependencies": {
40 | "@midwayjs/cli": "1.2.78",
41 | "@midwayjs/luckyeye": "1.0.2",
42 | "@midwayjs/mock": "2.12.3",
43 | "@types/jest": "26.0.20",
44 | "@types/jsonwebtoken": "^8.5.8",
45 | "@types/lodash": "4.14.168",
46 | "@types/lru-cache": "^5.1.1",
47 | "@types/multer": "1.4.7",
48 | "@types/node": "14",
49 | "@typescript-eslint/eslint-plugin": "^4.29.1",
50 | "@typescript-eslint/parser": "^4.29.1",
51 | "cross-env": "6.0.0",
52 | "eslint-plugin-prettier": "^3.4.0",
53 | "jest": "26.6.3",
54 | "mwts": "1.1.2",
55 | "prettier": "^2.7.1",
56 | "ts-jest": "26.5.2",
57 | "typescript": "4.1.6"
58 | },
59 | "midway-bin-clean": [
60 | ".vscode/.tsbuildinfo",
61 | "dist"
62 | ],
63 | "midway-luckyeye": {
64 | "packages": [
65 | "midway_v2"
66 | ]
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/packages/server-api/src/configuration.ts:
--------------------------------------------------------------------------------
1 | import * as cors from "@koa/cors";
2 | import { ILifeCycle } from "@midwayjs/core";
3 | import { ALL, App, Config, Configuration, Logger } from "@midwayjs/decorator";
4 | import { Application } from "@midwayjs/koa";
5 | import { ILogger } from "@midwayjs/logger";
6 | import * as bodyParser from "koa-bodyparser";
7 | import * as path from "path";
8 | import { createConnection, getConnection } from "typeorm";
9 | import { IAppConfig } from "./interface/config.interface";
10 |
11 | @Configuration({
12 | importConfigs: [path.join(__dirname, "../resource/config.default.json")],
13 | })
14 | export class ContainerLifeCycle implements ILifeCycle {
15 | @App()
16 | app: Application;
17 |
18 | @Logger()
19 | logger: ILogger;
20 |
21 | private config: IAppConfig = null;
22 |
23 | async onReady() {
24 | this.logger.info("Configuration onReady");
25 | this.config = require(path.join(__dirname, "../resource/config.default.json"));
26 |
27 | await this.configOrm();
28 | await this.configMiddlewares();
29 | }
30 |
31 | async onStop() {
32 | await getConnection().close();
33 | }
34 |
35 | private async configOrm() {
36 | const { host, port, database, username, password } = this.config.orm;
37 | await createConnection({
38 | type: "mysql",
39 | host: host,
40 | port: port,
41 | username: username,
42 | password: password,
43 | database: database,
44 | entities: [path.resolve(__dirname, "./entity/*{.js,.ts}")],
45 | synchronize: false,
46 | bigNumberStrings: false,
47 | logging: false,
48 | });
49 | }
50 |
51 | private async configMiddlewares() {
52 | this.logger.info("Starting config middlewares.");
53 | this.app.use(
54 | cors({
55 | origin: (ctx) => {
56 | return ctx.get("origin");
57 | },
58 | credentials: true,
59 | allowMethods: ["GET", "HEAD", "PUT", "POST", "DELETE", "PATCH", "OPTIONS"],
60 | })
61 | );
62 | this.app.use(bodyParser());
63 | this.app.use(await this.app.generateMiddleware("RequestMiddleware"));
64 | this.app.use(await this.app.generateMiddleware("ValidateMiddleware"));
65 | this.logger.info("Config middlewares success.");
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/packages/server-api/src/controller/account-ctr.ts:
--------------------------------------------------------------------------------
1 | import { ALL, Body, Controller, Get, Inject, Logger, Post, Provide } from "@midwayjs/decorator";
2 | import { Context } from "@midwayjs/koa";
3 | import { ILogger } from "@midwayjs/logger";
4 | import * as jwt from "jsonwebtoken";
5 | import { AccountSrv } from "../service/account-srv";
6 | import { WebUtil } from "./common/web-util";
7 |
8 | @Provide()
9 | @Controller("/api/account")
10 | export class AccountCtr {
11 | @Inject()
12 | ctx: Context;
13 |
14 | @Logger()
15 | logger: ILogger;
16 |
17 | @Inject()
18 | userSrv: AccountSrv;
19 |
20 | @Get("/info", { middleware: ["AuthMiddleWare"] })
21 | async getUserInfo() {
22 | return WebUtil.result(this.ctx.loginUser);
23 | }
24 |
25 | @Post("/login")
26 | async login(@Body(ALL) body: { name: string; password: string }) {
27 | const user = await this.userSrv.login({ name: body.name, password: body.password });
28 | // TODO load "pubfree" from config
29 | const token = jwt.sign({ id: user.id, name: user.name }, "pubfree");
30 | return WebUtil.result({
31 | token: token,
32 | });
33 | }
34 |
35 | @Post("/logout")
36 | async logout() {
37 | return WebUtil.notImplemented();
38 | }
39 |
40 | @Post("/register")
41 | async register(@Body(ALL) body: { name: string; password: string }) {
42 | const res = this.userSrv.register({ name: body.name, password: body.password });
43 | return WebUtil.result(res);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/packages/server-api/src/controller/common/api-response.ts:
--------------------------------------------------------------------------------
1 | export class ApiResponse {
2 | private data: T;
3 | private code: number;
4 | private message: string;
5 |
6 | constructor(code?: number, message?: string, data?: T) {
7 | this.code = code;
8 | this.message = message;
9 | this.data = data;
10 | }
11 |
12 | static newInstance(code = 200, message?: string, data?: T) {
13 | return new ApiResponse(code, message, data);
14 | }
15 |
16 | public setData(data: T): ApiResponse {
17 | this.data = data;
18 | return this;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/server-api/src/controller/common/web-util.ts:
--------------------------------------------------------------------------------
1 | import { ApiResponse } from "./api-response";
2 |
3 | export class WebUtil {
4 | static result(data: T) {
5 | return ApiResponse.newInstance(200).setData(data);
6 | }
7 |
8 | static success() {
9 | return ApiResponse.newInstance(200, "success");
10 | }
11 |
12 | static warning(code: number, message: string) {
13 | return ApiResponse.newInstance(code, message);
14 | }
15 |
16 | static error(message?: string) {
17 | return ApiResponse.newInstance(-1, message || "Internal Server Error");
18 | }
19 |
20 | static userNoLogin() {
21 | return ApiResponse.newInstance(401, "Unauthorized");
22 | }
23 |
24 | static userNoAuthority() {
25 | return ApiResponse.newInstance(403, "Unauthorized");
26 | }
27 |
28 | static notImplemented() {
29 | return this.warning(-1, "NotImplemented");
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/packages/server-api/src/controller/group-ctr.ts:
--------------------------------------------------------------------------------
1 | import { ALL, Body, Controller, Del, Get, Inject, Param, Post, Provide, Validate } from "@midwayjs/decorator";
2 | import { GroupPathParams, IGroupCreateBody, IGroupMemberCreateBody } from "../interface/group.interface";
3 | import { GroupMemberSrv } from "../service/group-member-srv";
4 | import { GroupSrv } from "../service/group-srv";
5 | import { parseStrToNum } from "../util/parse-str-to-num";
6 | import { WebUtil } from "./common/web-util";
7 |
8 | @Provide()
9 | @Controller("/api/groups", { middleware: ["AuthMiddleWare"] })
10 | export class GroupCtr {
11 | @Inject()
12 | groupSrv: GroupSrv;
13 |
14 | @Inject()
15 | groupMemberSrv: GroupMemberSrv;
16 |
17 | @Get("", { description: "获取空间列表" })
18 | @Validate()
19 | async getGroups() {
20 | const res = await this.groupSrv.getGroups();
21 | return WebUtil.result(res);
22 | }
23 |
24 | @Get("/:groupId", { description: "获取空间详情" })
25 | @Validate()
26 | async getGroupDetail(@Param(ALL) params: GroupPathParams) {
27 | const group = await this.groupSrv.getGroupDetail(parseStrToNum(params.groupId));
28 | return WebUtil.result(group);
29 | }
30 |
31 | @Post("", { description: "创建空间" })
32 | @Validate()
33 | async createGroup(@Body(ALL) params: IGroupCreateBody) {
34 | const group = await this.groupSrv.createGroup({
35 | name: params.name,
36 | description: params.description,
37 | });
38 | return WebUtil.result(group);
39 | }
40 |
41 | @Post("/:groupId", { description: "更新空间" })
42 | @Validate()
43 | async updateGroup() {
44 | return WebUtil.notImplemented();
45 | }
46 |
47 | @Del("/:groupId", { description: "删除空间" })
48 | @Validate()
49 | async deleteGroup(@Param(ALL) params: GroupPathParams) {
50 | await this.groupSrv.deleteGroup(parseStrToNum(params.groupId));
51 | return WebUtil.success();
52 | }
53 |
54 | @Get("/:groupId/projects", { description: "获取某个群组下的所有项目" })
55 | @Validate()
56 | async getGroupProjects(@Param(ALL) params: GroupPathParams) {
57 | const projects = await this.groupSrv.getGroupProjects(parseStrToNum(params.groupId));
58 | return WebUtil.result(projects);
59 | }
60 |
61 | @Get("/:groupId/members", { description: "获取空间成员列表" })
62 | @Validate()
63 | async getGroupMembers(@Param(ALL) params: GroupPathParams) {
64 | const groupMembers = await this.groupMemberSrv.getAllMembers(parseStrToNum(params.groupId));
65 | return WebUtil.result(groupMembers);
66 | }
67 |
68 | @Post("/:groupId/members", { description: "空间添加成员" })
69 | @Validate()
70 | async addGroupMember(@Param(ALL) params: GroupPathParams, @Body(ALL) body: IGroupMemberCreateBody) {
71 | const result = await this.groupMemberSrv.addGroupMember(parseStrToNum(params.groupId), {
72 | name: body.name,
73 | role: body.role,
74 | });
75 | return WebUtil.result(result);
76 | }
77 |
78 | @Del("/:groupId/members/:memberId", { description: "空间删除成员" })
79 | @Validate()
80 | async deleteGroupMember(@Param(ALL) params: GroupPathParams) {
81 | await this.groupMemberSrv.deleteGroupMember(parseStrToNum(params.groupId), parseStrToNum(params.memberId));
82 | return WebUtil.success();
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/packages/server-api/src/controller/project-ctr.ts:
--------------------------------------------------------------------------------
1 | import { ALL, Body, Controller, Del, Get, Inject, Param, Post, Provide, Query, Validate } from "@midwayjs/decorator";
2 | import { IProjectCreateBody, IProjectGetQuery, IProjectPathParams } from "../interface/project.interface";
3 | import { ProjectSrv } from "../service/project-srv";
4 | import { parseStrToNum } from "../util/parse-str-to-num";
5 | import { WebUtil } from "./common/web-util";
6 |
7 | @Provide()
8 | @Controller("/api/projects", { middleware: ["AuthMiddleWare"] })
9 | export class ProjectCtr {
10 | @Inject()
11 | projectSrv: ProjectSrv;
12 |
13 | @Get("", { description: "获取项目列表" })
14 | @Validate()
15 | async getProjects(@Query(ALL) query: IProjectGetQuery) {
16 | let { type, page: pageStr, size: sizeStr } = query;
17 | const page = +pageStr;
18 | const size = +sizeStr;
19 |
20 | const res = await this.projectSrv.getProjects(type, {
21 | page: page,
22 | size: size,
23 | });
24 | return WebUtil.result(res);
25 | }
26 |
27 | @Get("/:projectId", { description: "查询单个项目详情" })
28 | async getProject(@Param(ALL) params: IProjectPathParams) {
29 | const res = await this.projectSrv.getProject(+params.projectId);
30 | return WebUtil.result(res);
31 | }
32 |
33 | @Post("", { description: "创建项目" })
34 | @Validate()
35 | async createProject(@Body(ALL) body: IProjectCreateBody) {
36 | const res = await this.projectSrv.createProject({
37 | name: body.name,
38 | zhName: body.zhName,
39 | description: body.description,
40 | groupId: body.groupId,
41 | });
42 | return WebUtil.result(res);
43 | }
44 |
45 | @Post("/:projectId", { description: "更新项目" })
46 | @Validate()
47 | async updateProject() {
48 | return WebUtil.notImplemented();
49 | }
50 |
51 | @Del("/:projectId", { description: "删除项目" })
52 | @Validate()
53 | async deleteProject(@Param(ALL) params: IProjectPathParams) {
54 | await this.projectSrv.deleteProject(parseStrToNum(params.projectId));
55 | return WebUtil.success();
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/packages/server-api/src/controller/project-domain-ctr.ts:
--------------------------------------------------------------------------------
1 | import { ALL, Body, Controller, Del, Get, Inject, Param, Post, Provide, Validate } from "@midwayjs/decorator";
2 | import { IProjectDomainCreateDTO, IProjectDomainPathParams } from "../interface/project-domain.interface";
3 | import { ProjectDomainSrv } from "../service/project-domain-srv";
4 | import { parseStrToNum } from "../util/parse-str-to-num";
5 | import { WebUtil } from "./common/web-util";
6 |
7 | @Provide()
8 | @Controller("/api/projects/:projectId/domains", { middleware: ["AuthMiddleWare"] })
9 | export class ProjectDomainCtr {
10 | @Inject()
11 | projectDomainSrv: ProjectDomainSrv;
12 |
13 | @Get("")
14 | @Validate()
15 | async getProjectDomains(@Param(ALL) params: IProjectDomainPathParams) {
16 | const domains = await this.projectDomainSrv.getProjectDomains(parseStrToNum(params.projectId));
17 | return WebUtil.result(domains);
18 | }
19 |
20 | @Post("")
21 | @Validate()
22 | async createProjectDomain(@Param(ALL) params: IProjectDomainPathParams, @Body(ALL) body: IProjectDomainCreateDTO) {
23 | const { projectId } = params;
24 | const domain = await this.projectDomainSrv.createProjectDomain(parseStrToNum(projectId), {
25 | projectEnvId: body.projectEnvId,
26 | host: body.host,
27 | });
28 | return WebUtil.result(domain);
29 | }
30 |
31 | @Del("/:domainId")
32 | @Validate()
33 | async deleteProjectDomain(@Param(ALL) params: IProjectDomainPathParams) {
34 | const { projectId, domainId } = params;
35 | await this.projectDomainSrv.deleteProjectDomain(parseStrToNum(projectId), parseStrToNum(domainId));
36 | return WebUtil.success();
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/packages/server-api/src/controller/project-env-ctr.ts:
--------------------------------------------------------------------------------
1 | import { ALL, Body, Controller, Del, Get, Inject, Param, Post, Provide, Validate } from "@midwayjs/decorator";
2 | import { IProjectEnvCreateBody, IProjectEnvPathParams } from "../interface/project-env.interface";
3 | import { ProjectEnvSrv } from "../service/project-env-srv";
4 | import { parseStrToNum } from "../util/parse-str-to-num";
5 | import { WebUtil } from "./common/web-util";
6 |
7 | @Provide()
8 | @Controller("/api/projects/:projectId/envs", { middleware: ["AuthMiddleWare"] })
9 | export class ProjectEnvCtr {
10 | @Inject()
11 | projectEnvSrv: ProjectEnvSrv;
12 |
13 | @Get("", { description: "获取工作区列表" })
14 | @Validate()
15 | async getProjectEnvs(@Param(ALL) params: IProjectEnvPathParams) {
16 | const projectEnvs = await this.projectEnvSrv.getProjectEnvs(parseStrToNum(params.projectId));
17 | return WebUtil.result(projectEnvs);
18 | }
19 |
20 | @Post("", { description: "新建工作区" })
21 | @Validate()
22 | async createProjectEnv(@Param(ALL) params: IProjectEnvPathParams, @Body(ALL) body: IProjectEnvCreateBody) {
23 | const res = await this.projectEnvSrv.createProjectEnv(parseStrToNum(params.projectId), {
24 | name: body.name,
25 | envType: body.type,
26 | });
27 | return WebUtil.result(res);
28 | }
29 |
30 | @Post("/:envId", { description: "更新工作区信息" })
31 | @Validate()
32 | async updateProjectEnv(@Param(ALL) params: IProjectEnvPathParams) {
33 | return WebUtil.notImplemented();
34 | }
35 |
36 | @Del("/:envId", { description: "删除工作区" })
37 | @Validate()
38 | async deleteProjectEnv(@Param(ALL) params: IProjectEnvPathParams) {
39 | await this.projectEnvSrv.deleteProjectEnv(parseStrToNum(params.envId));
40 | return WebUtil.success();
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/packages/server-api/src/controller/project-env-deploy-ctr.ts:
--------------------------------------------------------------------------------
1 | import { ALL, Body, Controller, Del, Get, Inject, Param, Post, Provide, Validate } from "@midwayjs/decorator";
2 | import { ICreateEnvDeployBody, IProjectEnvDeployPathParams } from "../interface/project-env-deploy.interface";
3 | import { ProjectEnvDeploySrv } from "../service/project-env-deploy-srv";
4 | import { parseStrToNum } from "../util/parse-str-to-num";
5 | import { WebUtil } from "./common/web-util";
6 |
7 | @Provide()
8 | @Controller("/api/projects/:projectId/envs/:envId/deploys", { middleware: ["AuthMiddleWare"] })
9 | export class ProjectEnvDeployCtr {
10 | @Inject()
11 | projectEnvDeploySrv: ProjectEnvDeploySrv;
12 |
13 | @Get("", { description: "获取部署记录列表" })
14 | @Validate()
15 | async getDeploys(@Param(ALL) params: IProjectEnvDeployPathParams) {
16 | const res = await this.projectEnvDeploySrv.getEnvDeploys(parseStrToNum(params.envId));
17 | return WebUtil.result(res);
18 | }
19 |
20 | @Post("", { description: "创建部署记录" })
21 | @Validate()
22 | async createDeploy(@Param(ALL) params: IProjectEnvDeployPathParams, @Body(ALL) body: ICreateEnvDeployBody) {
23 | const deploy = await this.projectEnvDeploySrv.createEnvDeploy(
24 | parseStrToNum(params.projectId),
25 | parseStrToNum(params.envId),
26 | {
27 | type: body.type,
28 | options: body.options,
29 | }
30 | );
31 | return WebUtil.result(deploy);
32 | }
33 |
34 | @Del("/:deployId", { description: "删除部署记录" })
35 | @Validate()
36 | async deleteDeploy(@Param(ALL) params: IProjectEnvDeployPathParams) {
37 | await this.projectEnvDeploySrv.deleteEnvDeploy(parseStrToNum(params.deployId));
38 | return WebUtil.success();
39 | }
40 |
41 | @Post("/:deployId/activate", { description: "部署记录上线" })
42 | @Validate()
43 | async activateDeploy(@Param(ALL) params: IProjectEnvDeployPathParams) {
44 | await this.projectEnvDeploySrv.activateEnvDeploy(parseStrToNum(params.deployId));
45 | return WebUtil.success();
46 | }
47 |
48 | @Post("/:deployId/deactivate", { description: "部署记录下线" })
49 | @Validate()
50 | async deactivateDeploy(@Param(ALL) params: IProjectEnvDeployPathParams) {
51 | await this.projectEnvDeploySrv.deactivateEnvDeploy(parseStrToNum(params.deployId));
52 | return WebUtil.success();
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/packages/server-api/src/controller/project-member-ctr.ts:
--------------------------------------------------------------------------------
1 | import { ALL, Body, Controller, Del, Get, Inject, Param, Post, Provide, Validate } from "@midwayjs/decorator";
2 | import { ProjectMember } from "../entity/project-member";
3 | import {
4 | IProjectMemberCreateBody,
5 | IProjectMemberDTO,
6 | IProjectMemberPathParams,
7 | } from "../interface/project-member.interface";
8 | import { ProjectMemberSrv } from "../service/project-member-srv";
9 | import { parseStrToNum } from "../util/parse-str-to-num";
10 | import { ApiResponse } from "./common/api-response";
11 | import { WebUtil } from "./common/web-util";
12 |
13 | @Provide()
14 | @Controller("/api/projects/:projectId/members", { middleware: ["AuthMiddleWare"] })
15 | export class ProjectMemberCtr {
16 | @Inject()
17 | projectMemberSrv: ProjectMemberSrv;
18 |
19 | @Get("", { description: "获取项目成员" })
20 | @Validate()
21 | async getProjectMembers(@Param(ALL) params: IProjectMemberPathParams): Promise> {
22 | const projectMembers = await this.projectMemberSrv.getProjectMembers(parseStrToNum(params.projectId));
23 | return WebUtil.result(projectMembers);
24 | }
25 |
26 | @Post("", { description: "添加项目成员" })
27 | @Validate()
28 | async addProjectMembers(
29 | @Param(ALL) params: IProjectMemberPathParams,
30 | @Body(ALL) body: IProjectMemberCreateBody
31 | ): Promise> {
32 | const saveRes = await this.projectMemberSrv.addProjectMember(parseStrToNum(params.projectId), {
33 | name: body.name,
34 | role: body.role,
35 | });
36 | return WebUtil.result(saveRes);
37 | }
38 |
39 | @Del("/:memberId", { description: "删除项目成员" })
40 | @Validate()
41 | async deleteProjectMember(@Param(ALL) params: IProjectMemberPathParams): Promise> {
42 | await this.projectMemberSrv.deleteProjectMember(parseStrToNum(params.projectId), parseStrToNum(params.memberId));
43 | return WebUtil.success();
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/packages/server-api/src/entity/base/base-column.ts:
--------------------------------------------------------------------------------
1 | import { Column } from "typeorm";
2 | import { BoolTransform } from "./bool-transform";
3 |
4 | export class BaseColumn {
5 | @Column({
6 | name: "is_del",
7 | type: "tinyint",
8 | transformer: BoolTransform.defaultFalse(),
9 | })
10 | isDel: boolean;
11 |
12 | @Column({
13 | name: "created_at",
14 | transformer: {
15 | to(value: any): any {
16 | return value;
17 | },
18 | from(value: any): any {
19 | return new Date(value).valueOf();
20 | },
21 | },
22 | })
23 | createdAt: number;
24 |
25 | @Column({
26 | name: "updated_at",
27 | transformer: {
28 | to(value: any): any {
29 | return value;
30 | },
31 | from(value: any): any {
32 | return new Date(value).valueOf();
33 | },
34 | },
35 | })
36 | updatedAt: number;
37 | }
38 |
--------------------------------------------------------------------------------
/packages/server-api/src/entity/base/bool-transform.ts:
--------------------------------------------------------------------------------
1 | import * as _ from "lodash";
2 |
3 | export class BoolTransform {
4 | public static defaultTrue() {
5 | return {
6 | to(value: boolean): 1 | 0 {
7 | if (_.isNil(value)) {
8 | return 1;
9 | }
10 | return value === true ? 1 : 0;
11 | },
12 | from(value: 1 | 0): boolean {
13 | if (_.isNil(value)) {
14 | return true;
15 | }
16 | return value === 1;
17 | },
18 | };
19 | }
20 |
21 | public static defaultFalse() {
22 | return {
23 | to(value: boolean): 1 | 0 {
24 | if (_.isNil(value)) {
25 | return 0;
26 | }
27 | return value === true ? 1 : 0;
28 | },
29 | from(value: 1 | 0): boolean {
30 | if (_.isNil(value)) {
31 | return false;
32 | }
33 | return value === 1;
34 | },
35 | };
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/packages/server-api/src/entity/group-member.ts:
--------------------------------------------------------------------------------
1 | import { Column, Entity, EntityRepository, PrimaryGeneratedColumn, Repository } from "typeorm";
2 | import { BaseColumn } from "./base/base-column";
3 |
4 | export enum EGroupMemberRole {
5 | Guest = 0,
6 | Developer = 1,
7 | Master = 2,
8 | }
9 |
10 | @Entity({ name: "group_member" })
11 | export class GroupMember extends BaseColumn {
12 | @PrimaryGeneratedColumn({ name: "id" })
13 | id: number;
14 |
15 | @Column({ name: "group_id" })
16 | groupId: number;
17 |
18 | @Column({ name: "user_id" })
19 | userId: number;
20 |
21 | @Column({ name: "role" })
22 | role: EGroupMemberRole;
23 | }
24 |
25 | @EntityRepository(GroupMember)
26 | export class GroupMemberRepo extends Repository {}
27 |
--------------------------------------------------------------------------------
/packages/server-api/src/entity/group.ts:
--------------------------------------------------------------------------------
1 | import { Column, Entity, EntityRepository, PrimaryGeneratedColumn, Repository } from "typeorm";
2 | import { BaseColumn } from "./base/base-column";
3 |
4 | @Entity({ name: "group" })
5 | export class Group extends BaseColumn {
6 | @PrimaryGeneratedColumn({ name: "id" })
7 | id: number;
8 |
9 | @Column({ name: "name" })
10 | name: string;
11 |
12 | @Column({ name: "description" })
13 | description: string;
14 |
15 | @Column({ name: "owner_id" })
16 | ownerId: number;
17 |
18 | @Column({ name: "create_user_id" })
19 | createUserId: number;
20 | }
21 |
22 | @EntityRepository(Group)
23 | export class GroupRepo extends Repository {}
24 |
--------------------------------------------------------------------------------
/packages/server-api/src/entity/project-domain.ts:
--------------------------------------------------------------------------------
1 | import { Column, Entity, EntityRepository, PrimaryGeneratedColumn, Repository } from "typeorm";
2 | import { BaseColumn } from "./base/base-column";
3 |
4 | @Entity({ name: "project_domain" })
5 | export class ProjectDomain extends BaseColumn {
6 | @PrimaryGeneratedColumn({ name: "id" })
7 | id: number;
8 |
9 | @Column({ name: "project_id" })
10 | projectId: number;
11 |
12 | @Column({ name: "project_env_id" })
13 | projectEnvId: number;
14 |
15 | @Column({ name: "host" })
16 | host: string;
17 | }
18 |
19 | @EntityRepository(ProjectDomain)
20 | export class ProjectDomainRepo extends Repository {}
21 |
--------------------------------------------------------------------------------
/packages/server-api/src/entity/project-env-deploy.ts:
--------------------------------------------------------------------------------
1 | import * as _ from "lodash";
2 | import { Column, Entity, EntityRepository, PrimaryGeneratedColumn, Repository } from "typeorm";
3 | import { BaseColumn } from "./base/base-column";
4 | import { BoolTransform } from "./base/bool-transform";
5 |
6 | export enum EDeployTargetType {
7 | Local = 0,
8 | Cloud = 1,
9 | }
10 |
11 | @Entity({ name: "project_env_deploy" })
12 | export class ProjectEnvDeploy extends BaseColumn {
13 | @PrimaryGeneratedColumn({ name: "id" })
14 | id: number;
15 |
16 | @Column({ name: "project_id" })
17 | projectId: number;
18 |
19 | @Column({ name: "project_env_id" })
20 | projectEnvId: number;
21 |
22 | @Column({ name: "remark" })
23 | remark: string;
24 |
25 | @Column({ name: "target_type" })
26 | targetType: EDeployTargetType;
27 |
28 | @Column({ name: "target" })
29 | target: string;
30 |
31 | @Column({ name: "create_user_id" })
32 | createUserId: number;
33 |
34 | @Column({ name: "action_user_id" })
35 | actionUserId: number;
36 |
37 | @Column({
38 | name: "is_active",
39 | type: "tinyint",
40 | transformer: BoolTransform.defaultFalse(),
41 | })
42 | isActive: boolean;
43 |
44 | static purify(projectEnvDeploy: ProjectEnvDeploy) {
45 | if (_.isNil(projectEnvDeploy)) {
46 | return null;
47 | }
48 | return {
49 | id: projectEnvDeploy.id,
50 | projectId: projectEnvDeploy.projectId,
51 | projectEnvId: projectEnvDeploy.projectEnvId,
52 | remark: projectEnvDeploy.remark,
53 | targetType: projectEnvDeploy.targetType,
54 | target: projectEnvDeploy.target,
55 | createUserId: projectEnvDeploy.createUserId,
56 | actionUserId: projectEnvDeploy.actionUserId,
57 | isActive: projectEnvDeploy.isActive,
58 | };
59 | }
60 | }
61 |
62 | @EntityRepository(ProjectEnvDeploy)
63 | export class ProjectEnvDeployRepo extends Repository {}
64 |
--------------------------------------------------------------------------------
/packages/server-api/src/entity/project-env.ts:
--------------------------------------------------------------------------------
1 | import { Column, Entity, EntityRepository, PrimaryGeneratedColumn, Repository } from "typeorm";
2 | import { BaseColumn } from "./base/base-column";
3 |
4 | export enum EProjectEnvType {
5 | Test = 0,
6 | Beta = 1,
7 | Gray = 2,
8 | Prod = 3,
9 | }
10 |
11 | @Entity({ name: "project_env" })
12 | export class ProjectEnv extends BaseColumn {
13 | @PrimaryGeneratedColumn({ name: "id" })
14 | id: number;
15 |
16 | @Column({ name: "project_id" })
17 | projectId: number;
18 |
19 | @Column({ name: "name" })
20 | name: string;
21 |
22 | @Column({ name: "env_type" })
23 | envType: EProjectEnvType;
24 |
25 | @Column({ name: "create_user_id" })
26 | createUserId: number;
27 | }
28 |
29 | @EntityRepository(ProjectEnv)
30 | export class ProjectEnvRepo extends Repository {}
31 |
--------------------------------------------------------------------------------
/packages/server-api/src/entity/project-member.ts:
--------------------------------------------------------------------------------
1 | import { Column, Entity, EntityRepository, PrimaryGeneratedColumn, Repository } from "typeorm";
2 | import { BaseColumn } from "./base/base-column";
3 |
4 | export enum EProjectMemberRole {
5 | Guest = 0,
6 | Developer = 1,
7 | Master = 2,
8 | }
9 |
10 | @Entity({ name: "project_member" })
11 | export class ProjectMember extends BaseColumn {
12 | @PrimaryGeneratedColumn({ name: "id" })
13 | id: number;
14 |
15 | @Column({ name: "project_id" })
16 | projectId: number;
17 |
18 | @Column({ name: "user_id" })
19 | userId: number;
20 |
21 | @Column({ name: "role" })
22 | role: EProjectMemberRole;
23 | }
24 |
25 | @EntityRepository(ProjectMember)
26 | export class ProjectMemberRepo extends Repository {}
27 |
--------------------------------------------------------------------------------
/packages/server-api/src/entity/project.ts:
--------------------------------------------------------------------------------
1 | import { Column, Entity, EntityRepository, PrimaryGeneratedColumn, Repository } from "typeorm";
2 | import { BaseColumn } from "./base/base-column";
3 |
4 | @Entity({ name: "project" })
5 | export class Project extends BaseColumn {
6 | @PrimaryGeneratedColumn({ name: "id" })
7 | id: number;
8 |
9 | @Column({ name: "name" })
10 | name: string;
11 |
12 | @Column({ name: "zh_name" })
13 | zhName: string;
14 |
15 | @Column({ name: "description" })
16 | description: string;
17 |
18 | @Column({ name: "owner_id" })
19 | ownerId: number;
20 |
21 | @Column({ name: "group_id" })
22 | groupId: number;
23 |
24 | @Column({ name: "create_user_id" })
25 | createUserId: number;
26 | }
27 |
28 | @EntityRepository(Project)
29 | export class ProjectRepo extends Repository {}
30 |
--------------------------------------------------------------------------------
/packages/server-api/src/entity/user.ts:
--------------------------------------------------------------------------------
1 | import * as _ from "lodash";
2 | import { Column, Entity, EntityRepository, PrimaryGeneratedColumn, Repository } from "typeorm";
3 | import { BaseColumn } from "./base/base-column";
4 |
5 | @Entity({ name: "user" })
6 | export class User extends BaseColumn {
7 | @PrimaryGeneratedColumn({ name: "id" })
8 | id: number;
9 |
10 | @Column({ name: "name" })
11 | name: string;
12 |
13 | @Column({ name: "password" })
14 | password: string;
15 |
16 | static purify(user: User): Partial {
17 | if (_.isNil(user)) {
18 | return null;
19 | }
20 | return {
21 | id: user.id,
22 | name: user.name,
23 | };
24 | }
25 | }
26 |
27 | @EntityRepository(User)
28 | export class UserRepo extends Repository {}
29 |
--------------------------------------------------------------------------------
/packages/server-api/src/error/custom-error.ts:
--------------------------------------------------------------------------------
1 | export class CustomError extends Error {
2 | code: number;
3 |
4 | constructor(props) {
5 | super(props);
6 | }
7 |
8 | public static new(message: string, code: number = -1) {
9 | const error = new CustomError(message);
10 | error.code = code;
11 | return error;
12 | }
13 |
14 | public static noAuthority(message?: string) {
15 | const error = new CustomError(message || "没有操作权限");
16 | error.code = 403;
17 | return error;
18 | }
19 |
20 | public static notImplemented() {
21 | const error = new CustomError("功能暂未实现");
22 | error.code = 501;
23 | return error;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/packages/server-api/src/interface/config.interface.ts:
--------------------------------------------------------------------------------
1 | export interface IAppConfig {
2 | orm: IOrmConfig;
3 | }
4 |
5 | export interface IOrmConfig {
6 | host: string;
7 | port: number;
8 | database: string;
9 | username: string;
10 | password: string;
11 | }
12 |
--------------------------------------------------------------------------------
/packages/server-api/src/interface/group.interface.ts:
--------------------------------------------------------------------------------
1 | import { Rule } from "@midwayjs/decorator";
2 | import { RuleType } from "@midwayjs/decorator/dist/annotation/rule";
3 | import { Group } from "../entity/group";
4 | import { EGroupMemberRole, GroupMember } from "../entity/group-member";
5 | import { User } from "../entity/user";
6 |
7 | export class GroupPathParams {
8 | @Rule(
9 | RuleType.string()
10 | .pattern(/^[0-9]+$/)
11 | .required()
12 | )
13 | groupId: string;
14 |
15 | @Rule(RuleType.string().pattern(/^[0-9]+$/))
16 | memberId: string;
17 | }
18 |
19 | export class IGroupCreateBody {
20 | @Rule(
21 | RuleType.string()
22 | .pattern(/^[a-zA-Z0-9-]+$/)
23 | .max(128)
24 | .required()
25 | )
26 | name: string;
27 |
28 | @Rule(RuleType.string())
29 | description: string;
30 | }
31 |
32 | export interface IGroupDTO extends Group {}
33 |
34 | export interface IGroupMemberDTO extends GroupMember {
35 | user: Partial;
36 | }
37 |
38 | export class IGroupMemberCreateBody {
39 | @Rule(
40 | RuleType.string()
41 | .pattern(/^[a-zA-Z0-9-]+$/)
42 | .max(128)
43 | .required()
44 | )
45 | name: string;
46 |
47 | @Rule(RuleType.number().valid(EGroupMemberRole.Guest, EGroupMemberRole.Developer, EGroupMemberRole.Master).required())
48 | role: EGroupMemberRole;
49 | }
50 |
--------------------------------------------------------------------------------
/packages/server-api/src/interface/project-domain.interface.ts:
--------------------------------------------------------------------------------
1 | import { Rule } from "@midwayjs/decorator";
2 | import { RuleType } from "@midwayjs/decorator/dist/annotation/rule";
3 |
4 | export class IProjectDomainPathParams {
5 | @Rule(
6 | RuleType.string()
7 | .pattern(/^[0-9]+$/)
8 | .required()
9 | )
10 | projectId: string;
11 |
12 | @Rule(RuleType.string().pattern(/^[0-9]+$/))
13 | domainId: string;
14 | }
15 |
16 | export class IProjectDomainCreateDTO {
17 | @Rule(RuleType.number().required())
18 | projectEnvId: number;
19 |
20 | @Rule(
21 | RuleType.string()
22 | .pattern(/^(?=^.{3,255}$)[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+$/)
23 | .required()
24 | )
25 | host: string;
26 | }
27 |
--------------------------------------------------------------------------------
/packages/server-api/src/interface/project-env-deploy.interface.ts:
--------------------------------------------------------------------------------
1 | import { Rule } from "@midwayjs/decorator";
2 | import { RuleType } from "@midwayjs/decorator/dist/annotation/rule";
3 | import { EDeployTargetType, ProjectEnvDeploy } from "../entity/project-env-deploy";
4 | import { User } from "../entity/user";
5 |
6 | export class IProjectEnvDeployPathParams {
7 | @Rule(
8 | RuleType.string()
9 | .pattern(/^[0-9]+$/)
10 | .required()
11 | )
12 | projectId: string;
13 |
14 | @Rule(
15 | RuleType.string()
16 | .pattern(/^[0-9]+$/)
17 | .required()
18 | )
19 | envId?: string;
20 |
21 | @Rule(RuleType.string().pattern(/^[0-9]+$/))
22 | deployId?: string;
23 | }
24 |
25 | export interface IProjectEnvDeployDTO extends ProjectEnvDeploy {
26 | createUser: Partial;
27 |
28 | actionUser: Partial;
29 | }
30 |
31 | export interface ICreateEnvDeployBody {
32 | type: "url" | "zip";
33 | options: IUrlCreateDTO | IZipCreateDTO;
34 | }
35 |
36 | export interface IUrlCreateDTO {
37 | target: string;
38 |
39 | remark: string;
40 | }
41 |
42 | export interface IZipCreateDTO {}
43 |
--------------------------------------------------------------------------------
/packages/server-api/src/interface/project-env.interface.ts:
--------------------------------------------------------------------------------
1 | import { Rule } from "@midwayjs/decorator";
2 | import { RuleType } from "@midwayjs/decorator/dist/annotation/rule";
3 | import { EProjectEnvType, ProjectEnv } from "../entity/project-env";
4 | import { User } from "../entity/user";
5 |
6 | export interface IProjectEnvDTO extends ProjectEnv {
7 | createUser: Partial;
8 | }
9 |
10 | export class IProjectEnvPathParams {
11 | @Rule(
12 | RuleType.string()
13 | .pattern(/^[0-9]+$/)
14 | .required()
15 | )
16 | projectId: string;
17 |
18 | @Rule(RuleType.string().pattern(/^[0-9]+$/))
19 | envId?: string;
20 | }
21 |
22 | export class IProjectEnvCreateBody {
23 | @Rule(
24 | RuleType.string()
25 | .pattern(/^[a-z0-9]+$/)
26 | .max(32)
27 | .required()
28 | )
29 | name: string;
30 |
31 | @Rule(RuleType.allow(EProjectEnvType.Test, EProjectEnvType.Gray).required())
32 | type: EProjectEnvType;
33 | }
34 |
--------------------------------------------------------------------------------
/packages/server-api/src/interface/project-member.interface.ts:
--------------------------------------------------------------------------------
1 | import { Rule } from "@midwayjs/decorator";
2 | import { RuleType } from "@midwayjs/decorator/dist/annotation/rule";
3 | import { EProjectMemberRole, ProjectMember } from "../entity/project-member";
4 | import { User } from "../entity/user";
5 |
6 | export interface IProjectMemberDTO extends ProjectMember {
7 | user: Partial;
8 | }
9 |
10 | export class IProjectMemberCreateBody {
11 | @Rule(
12 | RuleType.string()
13 | .pattern(/^[a-zA-Z0-9-]+$/)
14 | .max(128)
15 | .required()
16 | )
17 | name: string;
18 |
19 | @Rule(
20 | RuleType.number()
21 | .valid(EProjectMemberRole.Guest, EProjectMemberRole.Developer, EProjectMemberRole.Master)
22 | .required()
23 | )
24 | role: EProjectMemberRole;
25 | }
26 |
27 | export class IProjectMemberPathParams {
28 | @Rule(
29 | RuleType.string()
30 | .pattern(/^[0-9]+$/)
31 | .required()
32 | )
33 | projectId: string;
34 |
35 | @Rule(RuleType.string().pattern(/^[0-9]+$/))
36 | memberId: string;
37 | }
38 |
--------------------------------------------------------------------------------
/packages/server-api/src/interface/project.interface.ts:
--------------------------------------------------------------------------------
1 | import { Rule } from "@midwayjs/decorator";
2 | import { RuleType } from "@midwayjs/decorator/dist/annotation/rule";
3 | import { Project } from "../entity/project";
4 | import { User } from "../entity/user";
5 |
6 | export class IProjectPathParams {
7 | @Rule(
8 | RuleType.string()
9 | .pattern(/^[0-9]+$/)
10 | .required()
11 | )
12 | projectId: string;
13 | }
14 |
15 | export class IProjectGetQuery {
16 | @Rule(RuleType.string().valid("self", "all").default("self"))
17 | type: "self" | "all";
18 |
19 | @Rule(
20 | RuleType.string()
21 | .pattern(/^[0-9]+$/)
22 | .default("1")
23 | )
24 | page: string;
25 |
26 | @Rule(
27 | RuleType.string()
28 | .pattern(/^[0-9]+$/)
29 | .default("20")
30 | )
31 | size: string;
32 | }
33 |
34 | export class IProjectDTO extends Project {
35 | createUser: Partial;
36 | }
37 |
38 | export class IProjectCreateBody {
39 | @Rule(
40 | RuleType.string()
41 | .pattern(/^[a-z0-9-]+$/)
42 | .max(128)
43 | .required()
44 | )
45 | name: string;
46 |
47 | @Rule(RuleType.string().max(128).required())
48 | zhName: string;
49 |
50 | @Rule(RuleType.string())
51 | description: string;
52 |
53 | @Rule(RuleType.number())
54 | groupId?: number;
55 | }
56 |
--------------------------------------------------------------------------------
/packages/server-api/src/middleware/auth.ts:
--------------------------------------------------------------------------------
1 | import { Provide } from "@midwayjs/decorator";
2 | import { Context, IMidwayKoaNext, IWebMiddleware } from "@midwayjs/koa";
3 |
4 | interface IJwtPayload {
5 | id: number;
6 | name: string;
7 | iat: number;
8 | exp: number;
9 | }
10 |
11 | @Provide("AuthMiddleWare")
12 | export class AuthMiddleWare implements IWebMiddleware {
13 | resolve() {
14 | return async (ctx: Context, next: IMidwayKoaNext) => {
15 | ctx.loginUser = {
16 | id: 1,
17 | name: "admin",
18 | };
19 | await next();
20 | // const authorization: string = ctx.headers["authorization"] as string;
21 | // const token = authorization.split(" ")?.[1];
22 | // try {
23 | // // TODO get secret key from config
24 | // const decoded = jwt.verify(token, "pubfree") as IJwtPayload;
25 | // const { id, name } = decoded;
26 | // ctx.loginUser = {
27 | // id: id,
28 | // name: name,
29 | // };
30 | // await next();
31 | // } catch (err) {
32 | // if (err instanceof TokenExpiredError) {
33 | // ctx.body = WebUtil.userNoLogin();
34 | // return;
35 | // }
36 | // throw err;
37 | // }
38 | };
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/packages/server-api/src/middleware/request.ts:
--------------------------------------------------------------------------------
1 | import { App, Provide } from "@midwayjs/decorator";
2 | import { Application, Context, IMidwayKoaNext, IWebMiddleware } from "@midwayjs/koa";
3 | import { WebUtil } from "../controller/common/web-util";
4 | import { CustomError } from "../error/custom-error";
5 |
6 | @Provide("RequestMiddleware")
7 | export class RequestMiddleware implements IWebMiddleware {
8 | @App()
9 | app: Application;
10 |
11 | resolve() {
12 | return async (ctx: Context, next: IMidwayKoaNext) => {
13 | try {
14 | await next();
15 | } catch (err) {
16 | if (err instanceof CustomError) {
17 | ctx.body = WebUtil.warning(err.code, err.message);
18 | return;
19 | }
20 |
21 | this.app.getLogger().error(err.stack);
22 | ctx.body = WebUtil.error();
23 | }
24 | };
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/packages/server-api/src/middleware/validate.ts:
--------------------------------------------------------------------------------
1 | import { Provide } from "@midwayjs/decorator";
2 | import { Context, IMidwayKoaNext, IWebMiddleware } from "@midwayjs/koa";
3 |
4 | @Provide("ValidateMiddleware")
5 | export class ValidateMiddleware implements IWebMiddleware {
6 | resolve() {
7 | return async (ctx: Context, next: IMidwayKoaNext) => {
8 | try {
9 | await next();
10 | } catch (err) {
11 | // RuleType 校验失败
12 | if (err?.name === "ValidationError") {
13 | return (ctx.body = {
14 | code: -1,
15 | message: err?.message || "Unknown validation error.",
16 | });
17 | }
18 | throw err;
19 | }
20 | };
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/packages/server-api/src/plugin/noop.ts:
--------------------------------------------------------------------------------
1 | export const noop = (noopInstance: string) =>
2 | new Proxy(
3 | {},
4 | {
5 | get: function (obj, prop) {
6 | console.log(`Noop function: ${noopInstance}.${String(prop)}`);
7 | return () => null;
8 | },
9 | }
10 | );
11 |
--------------------------------------------------------------------------------
/packages/server-api/src/service/account-srv.ts:
--------------------------------------------------------------------------------
1 | import { App, Inject, Provide } from "@midwayjs/decorator";
2 | import { Application, Context } from "@midwayjs/koa";
3 | import { ILogger } from "@midwayjs/logger";
4 | import * as assert from "assert";
5 | import * as _ from "lodash";
6 | import { getConnection } from "typeorm";
7 | import { User, UserRepo } from "../entity/user";
8 | import { CustomError } from "../error/custom-error";
9 | import { queryRunnerRepo } from "../util/query-runner-repo";
10 |
11 | @Provide()
12 | export class AccountSrv {
13 | @App()
14 | private app: Application;
15 |
16 | @Inject()
17 | private logger: ILogger;
18 |
19 | @Inject()
20 | private ctx: Context;
21 |
22 | /**
23 | * 用户注册
24 | */
25 | async register(user: Partial): Promise {
26 | const queryRunner = getConnection().createQueryRunner();
27 | await queryRunner.startTransaction("SERIALIZABLE");
28 |
29 | try {
30 | const userRepo = queryRunnerRepo(UserRepo, queryRunner);
31 |
32 | const existed = await userRepo.findOne({
33 | where: {
34 | name: user.name,
35 | isDel: false,
36 | },
37 | });
38 | assert(_.isNil(existed), CustomError.new(`user with name ${user.name} already exists`));
39 |
40 | const result = await userRepo.save({
41 | name: user.name,
42 | // TODO encryption here
43 | password: user.password,
44 | });
45 | await queryRunner.commitTransaction();
46 | return result;
47 | } catch (err) {
48 | await queryRunner.rollbackTransaction();
49 | throw err;
50 | } finally {
51 | await queryRunner.release();
52 | }
53 | }
54 |
55 | /**
56 | * 用户登录
57 | */
58 | async login(user: Partial): Promise> {
59 | const userRepo = queryRunnerRepo(UserRepo);
60 |
61 | const matchedUser = await userRepo.findOne({
62 | name: user.name,
63 | // TODO encryption here
64 | password: user.password,
65 | isDel: false,
66 | });
67 | assert(!_.isNil(matchedUser), CustomError.new(`user with name ${user.name} was not registered.`));
68 |
69 | return User.purify(matchedUser);
70 | }
71 |
72 | /**
73 | * 当前用户是否是管理员
74 | */
75 | async isAdmin(): Promise {
76 | return false;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/packages/server-api/src/service/group-member-srv.ts:
--------------------------------------------------------------------------------
1 | import { Context } from "@midwayjs/core";
2 | import { Inject, Provide } from "@midwayjs/decorator";
3 | import { ILogger } from "@midwayjs/logger";
4 | import * as assert from "assert";
5 | import * as _ from "lodash";
6 | import { isNil } from "lodash";
7 | import { In } from "typeorm";
8 | import { EGroupMemberRole, GroupMember, GroupMemberRepo } from "../entity/group-member";
9 | import { User, UserRepo } from "../entity/user";
10 | import { CustomError } from "../error/custom-error";
11 | import { IGroupMemberDTO } from "../interface/group.interface";
12 | import { queryRunnerRepo } from "../util/query-runner-repo";
13 |
14 | @Provide()
15 | export class GroupMemberSrv {
16 | @Inject()
17 | logger: ILogger;
18 |
19 | @Inject()
20 | ctx: Context;
21 |
22 | async getAllMembers(groupId: number): Promise {
23 | const groupMemberRepo = queryRunnerRepo(GroupMemberRepo);
24 | const groupMembers = await groupMemberRepo.find({
25 | groupId: groupId,
26 | isDel: false,
27 | });
28 |
29 | const userIds = groupMembers.map((member) => member.userId);
30 | const userRepo = queryRunnerRepo(UserRepo);
31 | const users = await userRepo.find({
32 | id: In(userIds),
33 | isDel: false,
34 | });
35 | const usersMap = new Map(users.map((user) => [user.id, user]));
36 |
37 | return groupMembers.map((member) => {
38 | return {
39 | ...member,
40 | user: User.purify(usersMap.get(member.userId)),
41 | };
42 | });
43 | }
44 |
45 | async addGroupMember(
46 | groupId: number,
47 | params: {
48 | name: string;
49 | role: EGroupMemberRole;
50 | }
51 | ): Promise {
52 | const { name, role } = params;
53 | const userRepo = queryRunnerRepo(UserRepo);
54 | const user = await userRepo.findOne({ name: name, isDel: false });
55 | assert(!isNil(user), CustomError.new(`用户 ${name} 不存在`));
56 |
57 | const groupMemberRepo = queryRunnerRepo(GroupMemberRepo);
58 | const exist = await groupMemberRepo.findOne({ groupId: groupId, userId: user.id, role: role, isDel: false });
59 | assert(_.isNil(exist), CustomError.new(`用户 ${name} 已在成员列表中`));
60 |
61 | const groupMember = await groupMemberRepo.save({
62 | groupId: groupId,
63 | userId: user.id,
64 | role: role,
65 | });
66 | return groupMember;
67 | }
68 |
69 | async deleteGroupMember(groupId: number, memberId: number) {
70 | const groupMemberRepo = queryRunnerRepo(GroupMemberRepo);
71 | const exist = await groupMemberRepo.findOne({
72 | id: memberId,
73 | groupId: groupId,
74 | isDel: false,
75 | });
76 | assert(!_.isNil(exist), CustomError.new(`群组中未找到该成员`));
77 |
78 | const updateRes = await groupMemberRepo.update(
79 | {
80 | id: exist.id,
81 | isDel: false,
82 | },
83 | {
84 | isDel: true,
85 | }
86 | );
87 | assert(updateRes.affected > 0, CustomError.new("群组成员移除失败"));
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/packages/server-api/src/service/project-domain-srv.ts:
--------------------------------------------------------------------------------
1 | import { Inject, Provide } from "@midwayjs/decorator";
2 | import { Context } from "@midwayjs/koa";
3 | import { ILogger } from "@midwayjs/logger";
4 | import * as assert from "assert";
5 | import { isNil } from "lodash";
6 | import { ProjectDomain, ProjectDomainRepo } from "../entity/project-domain";
7 | import { CustomError } from "../error/custom-error";
8 | import { queryRunnerRepo } from "../util/query-runner-repo";
9 |
10 | @Provide()
11 | export class ProjectDomainSrv {
12 | @Inject()
13 | logger: ILogger;
14 |
15 | @Inject()
16 | ctx: Context;
17 |
18 | async getProjectDomains(projectId: number): Promise {
19 | const projectDomainRepo = queryRunnerRepo(ProjectDomainRepo);
20 | const projectDomains = await projectDomainRepo.find({
21 | where: {
22 | projectId: projectId,
23 | isDel: false,
24 | },
25 | });
26 | return projectDomains;
27 | }
28 |
29 | async createProjectDomain(projectId: number, params: { projectEnvId: number; host: string }): Promise {
30 | const { projectEnvId, host } = params;
31 | const projectDomainRepo = queryRunnerRepo(ProjectDomainRepo);
32 |
33 | // 确保该域名未被用过
34 | const existedDomain = await projectDomainRepo.findOne({
35 | host: host,
36 | isDel: false,
37 | });
38 |
39 | assert(isNil(existedDomain), CustomError.new("当前域名已占用,请解除该域名配置或更换新域名"));
40 |
41 | const domain = await projectDomainRepo.save({
42 | projectId: projectId,
43 | projectEnvId: projectEnvId,
44 | host: host,
45 | });
46 | return domain;
47 | }
48 |
49 | async deleteProjectDomain(projectId: number, domainId: number): Promise {
50 | const projectDomainRepo = queryRunnerRepo(ProjectDomainRepo);
51 | const updateRes = await projectDomainRepo.update(
52 | {
53 | id: domainId,
54 | isDel: false,
55 | },
56 | {
57 | isDel: true,
58 | }
59 | );
60 | assert(updateRes.affected > 0, CustomError.new("域名删除失败"));
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/packages/server-api/src/service/project-env-srv.ts:
--------------------------------------------------------------------------------
1 | import { Inject, Provide } from "@midwayjs/decorator";
2 | import { Context } from "@midwayjs/koa";
3 | import { ILogger } from "@midwayjs/logger";
4 | import * as assert from "assert";
5 | import * as _ from "lodash";
6 | import { In } from "typeorm";
7 | import { EProjectEnvType, ProjectEnv, ProjectEnvRepo } from "../entity/project-env";
8 | import { User, UserRepo } from "../entity/user";
9 | import { CustomError } from "../error/custom-error";
10 | import { IProjectEnvDTO } from "../interface/project-env.interface";
11 | import { queryRunnerRepo } from "../util/query-runner-repo";
12 |
13 | @Provide()
14 | export class ProjectEnvSrv {
15 | @Inject()
16 | private ctx: Context;
17 |
18 | @Inject()
19 | private logger: ILogger;
20 |
21 | async getProjectEnvs(projectId: number): Promise {
22 | const projectEnvRepo = queryRunnerRepo(ProjectEnvRepo);
23 | const projectEnvs = await projectEnvRepo.find({
24 | projectId: projectId,
25 | isDel: false,
26 | });
27 |
28 | const userRepo = queryRunnerRepo(UserRepo);
29 | const users = await userRepo.find({ id: In(projectEnvs.map((v) => v.createUserId)) });
30 | const usersMap = new Map(users.map((user) => [user.id, user]));
31 |
32 | return projectEnvs.map((projectEnv) => {
33 | return {
34 | ...projectEnv,
35 | createUser: User.purify(usersMap.get(projectEnv.createUserId)),
36 | };
37 | });
38 | }
39 |
40 | /**
41 | * 创建项目环境
42 | */
43 | async createProjectEnv(
44 | projectId: number,
45 | params: {
46 | name: string;
47 | envType: EProjectEnvType;
48 | }
49 | ): Promise {
50 | const { id: userId } = this.ctx.loginUser;
51 |
52 | const projectEnvRepo = queryRunnerRepo(ProjectEnvRepo);
53 | const existed = await projectEnvRepo.findOne({
54 | projectId: projectId,
55 | name: params.name,
56 | });
57 | assert(_.isNil(existed), CustomError.new(`当前环境 ${params.name} 已存在`));
58 |
59 | const saveRes = await projectEnvRepo.save({
60 | projectId: projectId,
61 | name: params.name,
62 | envType: params.envType,
63 | createUserId: userId,
64 | });
65 | return saveRes;
66 | }
67 |
68 | /**
69 | * 删除项目环境
70 | */
71 | async deleteProjectEnv(envId: number): Promise {
72 | const projectEnvRepo = queryRunnerRepo(ProjectEnvRepo);
73 | const existed = await projectEnvRepo.findOne({
74 | id: envId,
75 | isDel: false,
76 | });
77 | assert(!_.isNil(existed), CustomError.new(`当前环境 ${envId} 不存在`));
78 |
79 | const updateRes = await projectEnvRepo.update(
80 | {
81 | id: envId,
82 | isDel: false,
83 | },
84 | {
85 | isDel: true,
86 | }
87 | );
88 |
89 | assert(updateRes.affected > 0, "环境删除失败");
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/packages/server-api/src/service/project-member-srv.ts:
--------------------------------------------------------------------------------
1 | import { Inject, Provide } from "@midwayjs/decorator";
2 | import { Context } from "@midwayjs/koa";
3 | import { ILogger } from "@midwayjs/logger";
4 | import * as assert from "assert";
5 | import * as _ from "lodash";
6 | import { In } from "typeorm";
7 | import { GroupRepo } from "../entity/group";
8 | import { GroupMember, GroupMemberRepo } from "../entity/group-member";
9 | import { ProjectRepo } from "../entity/project";
10 | import { EProjectMemberRole, ProjectMember, ProjectMemberRepo } from "../entity/project-member";
11 | import { User, UserRepo } from "../entity/user";
12 | import { CustomError } from "../error/custom-error";
13 | import { IProjectMemberDTO } from "../interface/project-member.interface";
14 | import { queryRunnerRepo } from "../util/query-runner-repo";
15 |
16 | @Provide()
17 | export class ProjectMemberSrv {
18 | @Inject()
19 | logger: ILogger;
20 |
21 | @Inject()
22 | ctx: Context;
23 |
24 | /**
25 | * 获取所有成员
26 | */
27 | async getProjectMembers(projectId: number): Promise {
28 | const projectRepo = queryRunnerRepo(ProjectRepo);
29 | const project = await projectRepo.findOne({
30 | id: projectId,
31 | isDel: false,
32 | });
33 | assert(!_.isNil(project), CustomError.new(`project with id ${project} doesn't exist.`));
34 |
35 | // 获取项目成员
36 | const projectMemberRepo = queryRunnerRepo(ProjectMemberRepo);
37 | const projectMembers = await projectMemberRepo.find({
38 | projectId: projectId,
39 | isDel: false,
40 | });
41 |
42 | // 获取用户身份信息
43 | const userIds = projectMembers.map((v) => v.userId);
44 | const userRepo = queryRunnerRepo(UserRepo);
45 | const users = await userRepo.find({ id: In(userIds), isDel: false });
46 | const usersMap = new Map(users.map((user) => [user.id, user]));
47 |
48 | return projectMembers.map((member) => {
49 | return {
50 | ...member,
51 | user: User.purify(usersMap.get(member.userId)),
52 | };
53 | });
54 | }
55 |
56 | /**
57 | * 添加项目成员
58 | */
59 | async addProjectMember(
60 | projectId: number,
61 | params: {
62 | name: string;
63 | role: EProjectMemberRole;
64 | }
65 | ): Promise {
66 | const { name, role } = params;
67 | const userRepo = queryRunnerRepo(UserRepo);
68 | const user = await userRepo.findOne({ name: name, isDel: false });
69 | assert(!_.isNil(user), CustomError.new(`用户 ${name} 不存在`));
70 |
71 | const projectMemberRepo = queryRunnerRepo(ProjectMemberRepo);
72 | const exist = await projectMemberRepo.findOne({ projectId: projectId, userId: user.id, role: role, isDel: false });
73 | assert(_.isNil(exist), CustomError.new(`用户 ${name} 已在成员列表中`));
74 |
75 | return await projectMemberRepo.save({
76 | projectId: projectId,
77 | userId: user.id,
78 | role: role,
79 | });
80 | }
81 |
82 | /**
83 | * 删除项目成员
84 | */
85 | async deleteProjectMember(projectId: number, memberId: number) {
86 | const projectMemberRepo = queryRunnerRepo(ProjectMemberRepo);
87 | const exist = await projectMemberRepo.findOne({ id: memberId, projectId: projectId, isDel: false });
88 | assert(!_.isNil(exist), CustomError.new(`未查询到匹配的项目权限记录`));
89 |
90 | const updateRes = await projectMemberRepo.update(
91 | {
92 | id: exist.id,
93 | isDel: false,
94 | },
95 | {
96 | isDel: true,
97 | }
98 | );
99 | assert(updateRes.affected > 0, `项目成员删除失败`);
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/packages/server-api/src/util/parse-str-to-num.ts:
--------------------------------------------------------------------------------
1 | import * as _ from "lodash";
2 |
3 | export function parseStrToNum(value: string) {
4 | if (!_.isNil(value)) {
5 | return +value;
6 | }
7 | return undefined;
8 | }
9 |
--------------------------------------------------------------------------------
/packages/server-api/src/util/query-runner-repo.ts:
--------------------------------------------------------------------------------
1 | import { getManager, QueryRunner } from "typeorm";
2 | import { ObjectType } from "typeorm/common/ObjectType";
3 |
4 | export function queryRunnerRepo(customRepo: ObjectType, queryRunner?: QueryRunner) {
5 | if (queryRunner) {
6 | return queryRunner.manager.getCustomRepository(customRepo);
7 | } else {
8 | return getManager().getCustomRepository(customRepo);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/packages/server-api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": true,
3 | "compilerOptions": {
4 | "target": "ES2018",
5 | "module": "commonjs",
6 | "moduleResolution": "node",
7 | "experimentalDecorators": true,
8 | "emitDecoratorMetadata": true,
9 | "inlineSourceMap": true,
10 | "noImplicitThis": true,
11 | "noUnusedLocals": false,
12 | "stripInternal": true,
13 | "pretty": true,
14 | "declaration": true,
15 | "outDir": "dist",
16 | "allowSyntheticDefaultImports": true
17 | },
18 | "include": [
19 | "src/**/*",
20 | "typings/**/*"
21 | ],
22 | "exclude": [
23 | "node_modules",
24 | "dist",
25 | "test"
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------
/packages/server-api/typings/index.d.ts:
--------------------------------------------------------------------------------
1 | import { IMidwayKoaContext } from '@midwayjs/koa'
2 |
3 | declare module '@midwayjs/koa' {
4 | interface Context extends IMidwayKoaContext {
5 | loginUser: {
6 | id: number
7 | name: string
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/packages/server-client/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .vs_code
3 |
4 | # 生产配置不入 git 记录
5 | deploy/config/config.prd.json
6 | resource/config.default.json
7 |
8 | node_modules
9 | dist
10 | logs
11 | temp
12 | tmp
13 | data
14 |
15 | *.log
16 |
--------------------------------------------------------------------------------
/packages/server-client/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | printWidth: 120,
3 | }
4 |
--------------------------------------------------------------------------------
/packages/server-client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pubfree-client",
3 | "version": "1.0.0",
4 | "scripts": {
5 | "dev": "nodemon --exec ts-node -T src/index.ts",
6 | "build": "rm -rf dist && tsc",
7 | "start": "NODE_ENV=production node dist/index.js",
8 | "prettier": "prettier -l --write 'src/**/*.{js,ts}'",
9 | "prettier-check": "prettier -l 'src/**/*.{js,ts}'",
10 | "lint": "tsc -p tsconfig.json --noEmit"
11 | },
12 | "engines": {
13 | "node": ">=12"
14 | },
15 | "dependencies": {
16 | "axios": "0.21.1",
17 | "dayjs": "^1.11.2",
18 | "fs-extra": "^9.1.0",
19 | "lodash": "4.17.21",
20 | "lru-cache": "6.0.0",
21 | "mime": "3.0.0",
22 | "mysql": "2.18.1",
23 | "node-schedule": "^2.1.0",
24 | "parseurl": "^1.3.3",
25 | "reflect-metadata": "0.1.13",
26 | "typeorm": "0.2.31"
27 | },
28 | "devDependencies": {
29 | "@types/fs-extra": "^9.0.13",
30 | "@types/lodash": "^4.14.178",
31 | "@types/lru-cache": "^5.1.1",
32 | "@types/node": "12.12.6",
33 | "nodemon": "^2.0.15",
34 | "prettier": "2.5.1",
35 | "ts-node": "10.4.0",
36 | "typescript": "4.3.5"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/packages/server-client/resource/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 404
6 |
34 |
35 |
36 |
37 |

38 |
404 Page Not Found
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/packages/server-client/resource/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jd-opensource/FEPubfree/f88ce53e800b8abd34750ba6a643164f7515fd8f/packages/server-client/resource/favicon.ico
--------------------------------------------------------------------------------
/packages/server-client/src/cache/memory-cache.ts:
--------------------------------------------------------------------------------
1 | import { isNil } from "lodash";
2 | import * as LRUCache from "lru-cache";
3 | import * as parseurl from "parseurl";
4 | import { IContext } from "../interface/context";
5 |
6 | export interface IMemoryCacheValue {
7 | envId: number;
8 | buffer: Buffer;
9 | respHeaders: { [key: string]: string };
10 | }
11 |
12 | export class MemoryCache {
13 | private static instance: MemoryCache = null;
14 |
15 | cache: LRUCache = null;
16 |
17 | constructor() {
18 | this.cache = new LRUCache({
19 | // count
20 | max: 1000,
21 | // ms
22 | maxAge: 5 * 60 * 1000,
23 | updateAgeOnGet: false,
24 | });
25 | }
26 |
27 | public static getInstance() {
28 | if (isNil(MemoryCache.instance)) {
29 | MemoryCache.instance = new MemoryCache();
30 | }
31 |
32 | return MemoryCache.instance;
33 | }
34 |
35 | /**
36 | * 请求的域名以及路径作为缓存 key
37 | * 示例
38 | * 请求地址:https://abc.com/account?name=x
39 | * 缓存 Key 为:abc.com/account
40 | */
41 | public static getCacheKey(ctx: IContext) {
42 | return `${ctx.req.headers.host}${parseurl(ctx.req).pathname}`;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/packages/server-client/src/config/index.ts:
--------------------------------------------------------------------------------
1 | export { TypeormConfiguration } from "./typeorm/typeorm-configuration";
2 |
--------------------------------------------------------------------------------
/packages/server-client/src/config/typeorm/typeorm-configuration.ts:
--------------------------------------------------------------------------------
1 | import * as path from "path";
2 | import { createConnection } from "typeorm";
3 | import { Logger } from "../../util/logger";
4 | import { AppConfig } from "../../util/app-config";
5 |
6 | export class TypeormConfiguration {
7 | private logger = Logger.create();
8 |
9 | async config() {
10 | this.logger.info("Starting config typeorm.");
11 |
12 | const {
13 | mysql: {
14 | enable,
15 | options: { host, port, username, password, database },
16 | },
17 | // @ts-ignore
18 | } = AppConfig.config();
19 |
20 | if (enable === false) {
21 | this.logger.error("Typeorm config enable is false, skipped.");
22 | return;
23 | }
24 |
25 | try {
26 | await createConnection({
27 | bigNumberStrings: false,
28 | type: "mysql",
29 | host: host,
30 | port: port,
31 | username: username,
32 | password: password,
33 | database: database,
34 | entities: [path.resolve(__dirname, `../../entity/*{.js,.ts}`)],
35 | synchronize: false,
36 | logging: false,
37 | });
38 | this.logger.info("Typeorm config success.");
39 | } catch (err) {
40 | this.logger.error(`Typeorm config failed.`);
41 | this.logger.error(err.stack);
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/packages/server-client/src/entity/base/base-column.ts:
--------------------------------------------------------------------------------
1 | import { Column } from "typeorm";
2 | import { BoolTransform } from "./bool-transform";
3 |
4 | export class BaseColumn {
5 | @Column({
6 | name: "is_del",
7 | type: "tinyint",
8 | transformer: BoolTransform.defaultFalse(),
9 | })
10 | isDel: boolean;
11 |
12 | @Column({
13 | name: "created_at",
14 | transformer: {
15 | to(value: any): any {
16 | return value;
17 | },
18 | from(value: any): any {
19 | return new Date(value).valueOf();
20 | },
21 | },
22 | })
23 | createdAt: number;
24 |
25 | @Column({
26 | name: "updated_at",
27 | transformer: {
28 | to(value: any): any {
29 | return value;
30 | },
31 | from(value: any): any {
32 | return new Date(value).valueOf();
33 | },
34 | },
35 | })
36 | updatedAt: number;
37 | }
38 |
--------------------------------------------------------------------------------
/packages/server-client/src/entity/base/bool-transform.ts:
--------------------------------------------------------------------------------
1 | import * as _ from "lodash";
2 |
3 | export class BoolTransform {
4 | public static defaultTrue() {
5 | return {
6 | to(value: boolean): 1 | 0 {
7 | if (_.isNil(value)) {
8 | return 1;
9 | }
10 | return value === true ? 1 : 0;
11 | },
12 | from(value: 1 | 0): boolean {
13 | if (_.isNil(value)) {
14 | return true;
15 | }
16 | return value === 1;
17 | },
18 | };
19 | }
20 |
21 | public static defaultFalse() {
22 | return {
23 | to(value: boolean): 1 | 0 {
24 | if (_.isNil(value)) {
25 | return 0;
26 | }
27 | return value === true ? 1 : 0;
28 | },
29 | from(value: 1 | 0): boolean {
30 | if (_.isNil(value)) {
31 | return false;
32 | }
33 | return value === 1;
34 | },
35 | };
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/packages/server-client/src/entity/project-domain.ts:
--------------------------------------------------------------------------------
1 | import { Column, Entity, EntityRepository, PrimaryGeneratedColumn, Repository } from "typeorm";
2 | import { BaseColumn } from "./base/base-column";
3 |
4 | @Entity({ name: "project_domain" })
5 | export class ProjectDomain extends BaseColumn {
6 | @PrimaryGeneratedColumn({ name: "id" })
7 | id: number;
8 |
9 | @Column({ name: "project_id" })
10 | projectId: number;
11 |
12 | @Column({ name: "project_env_id" })
13 | projectEnvId: number;
14 |
15 | @Column({ name: "host" })
16 | host: string;
17 | }
18 |
19 | @EntityRepository(ProjectDomain)
20 | export class ProjectDomainRepo extends Repository {}
21 |
--------------------------------------------------------------------------------
/packages/server-client/src/entity/project-env-deploy.ts:
--------------------------------------------------------------------------------
1 | import * as _ from "lodash";
2 | import * as dayjs from "dayjs";
3 | import { Column, Entity, EntityRepository, PrimaryGeneratedColumn, Repository } from "typeorm";
4 | import { BaseColumn } from "./base/base-column";
5 | import { BoolTransform } from "./base/bool-transform";
6 |
7 | export enum EDeployTargetType {
8 | Local = 0,
9 | Cloud = 1,
10 | }
11 |
12 | @Entity({ name: "project_env_deploy" })
13 | export class ProjectEnvDeploy extends BaseColumn {
14 | @PrimaryGeneratedColumn({ name: "id" })
15 | id: number;
16 |
17 | @Column({ name: "project_id" })
18 | projectId: number;
19 |
20 | @Column({ name: "project_env_id" })
21 | projectEnvId: number;
22 |
23 | @Column({ name: "remark" })
24 | remark: string;
25 |
26 | @Column({ name: "target_type" })
27 | targetType: EDeployTargetType;
28 |
29 | @Column({ name: "target" })
30 | target: string;
31 |
32 | @Column({ name: "create_user_id" })
33 | createUserId: number;
34 |
35 | @Column({ name: "action_user_id" })
36 | actionUserId: number;
37 |
38 | @Column({
39 | name: "is_active",
40 | type: "tinyint",
41 | transformer: BoolTransform.defaultFalse(),
42 | })
43 | isActive: boolean;
44 |
45 | static purify(projectEnvDeploy: ProjectEnvDeploy) {
46 | if (_.isNil(projectEnvDeploy)) {
47 | return null;
48 | }
49 | return {
50 | id: projectEnvDeploy.id,
51 | projectId: projectEnvDeploy.projectId,
52 | projectEnvId: projectEnvDeploy.projectEnvId,
53 | remark: projectEnvDeploy.remark,
54 | targetType: projectEnvDeploy.targetType,
55 | target: projectEnvDeploy.target,
56 | createUserId: projectEnvDeploy.createUserId,
57 | actionUserId: projectEnvDeploy.actionUserId,
58 | isActive: projectEnvDeploy.isActive,
59 | };
60 | }
61 | }
62 |
63 | @EntityRepository(ProjectEnvDeploy)
64 | export class ProjectEnvDeployRepo extends Repository {
65 | findLatestUpdatedEnvIds(timestamp: number): Promise> {
66 | return this.query(
67 | "SELECT DISTINCT project_env_id as envId FROM project_env_deploy WHERE is_del = 0 AND updated_at >= ?",
68 | [dayjs(timestamp).format("YYYY-MM-DD HH:mm:ss")]
69 | );
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/packages/server-client/src/entity/project-env.ts:
--------------------------------------------------------------------------------
1 | import * as dayjs from "dayjs";
2 | import { Column, Entity, EntityRepository, PrimaryGeneratedColumn, Repository } from "typeorm";
3 | import { BaseColumn } from "./base/base-column";
4 |
5 | export enum EProjectEnvType {
6 | Test = 0,
7 | Beta = 1,
8 | Gray = 2,
9 | Prod = 3,
10 | }
11 |
12 | @Entity({ name: "project_env" })
13 | export class ProjectEnv extends BaseColumn {
14 | @PrimaryGeneratedColumn({ name: "id" })
15 | id: number;
16 |
17 | @Column({ name: "project_id" })
18 | projectId: number;
19 |
20 | @Column({ name: "name" })
21 | name: string;
22 |
23 | @Column({ name: "env_type" })
24 | envType: EProjectEnvType;
25 |
26 | @Column({ name: "create_user_id" })
27 | createUserId: number;
28 | }
29 |
30 | @EntityRepository(ProjectEnv)
31 | export class ProjectEnvRepo extends Repository {
32 | findLatestUpdatedEnvAreas(timestamp: number): Promise> {
33 | return this.query("SELECT DISTINCT id FROM project_env WHERE is_del = 0 AND updated_at >= ?", [
34 | dayjs(timestamp).format("YYYY-MM-DD HH:mm:ss"),
35 | ]);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/packages/server-client/src/entity/project.ts:
--------------------------------------------------------------------------------
1 | import { Column, Entity, EntityRepository, PrimaryGeneratedColumn, Repository } from "typeorm";
2 | import { BaseColumn } from "./base/base-column";
3 |
4 | @Entity({ name: "project" })
5 | export class Project extends BaseColumn {
6 | @PrimaryGeneratedColumn({ name: "id" })
7 | id: number;
8 |
9 | @Column({ name: "name" })
10 | name: string;
11 |
12 | @Column({ name: "zh_name" })
13 | zhName: string;
14 |
15 | @Column({ name: "description" })
16 | description: string;
17 |
18 | @Column({ name: "owner_id" })
19 | ownerId: number;
20 |
21 | @Column({ name: "group_id" })
22 | groupId: number;
23 |
24 | @Column({ name: "create_user_id" })
25 | createUserId: number;
26 | }
27 |
28 | @EntityRepository(Project)
29 | export class ProjectRepo extends Repository {}
30 |
--------------------------------------------------------------------------------
/packages/server-client/src/error/InvalidPreviewError.ts:
--------------------------------------------------------------------------------
1 | export class InvalidPreviewError extends Error {
2 | constructor(props) {
3 | super(props);
4 | this.name = "InvalidPreviewError";
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/packages/server-client/src/error/ResourceFetchFailedError.ts:
--------------------------------------------------------------------------------
1 | export class ResourceFetchFailedError extends Error {
2 | constructor(props) {
3 | super(props);
4 | this.name = "ResourceFetchFailedError";
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/packages/server-client/src/error/UnMatchedHostError.ts:
--------------------------------------------------------------------------------
1 | export class UnMatchedHostError extends Error {
2 | constructor(props) {
3 | super(props);
4 | this.name = "UnMatchedHostError";
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/packages/server-client/src/error/UnMatchedProjectEnvDeployError.ts:
--------------------------------------------------------------------------------
1 | export class UnMatchedProjectEnvDeployError extends Error {
2 | constructor(props) {
3 | super(props);
4 | this.name = "UnMatchedProjectEnvDeployError";
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/packages/server-client/src/error/UnMatchedProjectEnvError.ts:
--------------------------------------------------------------------------------
1 | export class UnMatchedProjectEnvError extends Error {
2 | constructor(props) {
3 | super(props);
4 | this.name = "UnMatchedProjectEnvError";
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/packages/server-client/src/error/UnMatchedProjectError.ts:
--------------------------------------------------------------------------------
1 | export class UnMatchedProjectError extends Error {
2 | constructor(props) {
3 | super(props);
4 | this.name = "UnMatchedProjectError";
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/packages/server-client/src/index.ts:
--------------------------------------------------------------------------------
1 | import * as http from "http";
2 | import * as path from "path";
3 | import * as fse from "fs-extra";
4 | import { Logger } from "./util/logger";
5 | import { TypeormConfiguration } from "./config";
6 | import { AppConfig } from "./util/app-config";
7 | import { DispatchController } from "./controller/dispatch-controller";
8 | import { IApplication } from "./interface/application";
9 | import { ScheduleManager } from "./schedule";
10 |
11 | const bootstrap = async () => {
12 | const appConfig = AppConfig.load();
13 | const logger = Logger.create();
14 |
15 | const application: IApplication = {
16 | config: appConfig,
17 | faviconBuffer: await fse.readFileSync(path.resolve(__dirname, "../resource/favicon.ico")),
18 | notFoundHtmlBuffer: await fse.readFileSync(path.resolve(__dirname, "../resource/404.html")),
19 | };
20 |
21 | await new TypeormConfiguration().config();
22 | new ScheduleManager(application).start();
23 |
24 | const dispatchController = new DispatchController();
25 | const server = http.createServer(async (req, resp) => {
26 | logger.info(`receive: ${req.headers.host + req.url}`);
27 |
28 | if (req.url === "/favicon.ico") {
29 | return resp.end(application.faviconBuffer);
30 | }
31 |
32 | const ctx = { req: req, resp: resp, app: application, respHeaders: {} };
33 | return await dispatchController.dispatchWithErrorHandler(ctx);
34 | });
35 |
36 | server.listen(3000, () => {
37 | logger.info(`Server now is listening on http://127.0.0.1:3000`);
38 | });
39 | };
40 |
41 | bootstrap();
42 |
--------------------------------------------------------------------------------
/packages/server-client/src/interface/application.ts:
--------------------------------------------------------------------------------
1 | import { IAppConfig } from "../util/app-config";
2 |
3 | export interface IApplication {
4 | // 应用配置信息
5 | config: IAppConfig;
6 |
7 | // favicon
8 | faviconBuffer: Buffer;
9 | // 404 页面 buffer
10 | notFoundHtmlBuffer: Buffer;
11 | }
12 |
--------------------------------------------------------------------------------
/packages/server-client/src/interface/context.ts:
--------------------------------------------------------------------------------
1 | import { IncomingMessage, ServerResponse } from "http";
2 | import { IApplication } from "./application";
3 |
4 | export interface IContext {
5 | req: IncomingMessage;
6 | resp: ServerResponse;
7 | app: IApplication;
8 |
9 | // 过程中临时存放 Header 字段
10 | respHeaders?: any;
11 | }
12 |
13 | declare module "http" {
14 | interface IncomingHttpHeaders {
15 | originHost?: string;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/packages/server-client/src/schedule/base/BaseLoop.ts:
--------------------------------------------------------------------------------
1 | import { Logger } from "../../util/logger";
2 | import { IApplication } from "../../interface/application";
3 |
4 | export abstract class BaseLoop {
5 | protected logger = Logger.create();
6 | protected app: IApplication = null;
7 |
8 | abstract interval: number;
9 | abstract identifier: string;
10 |
11 | abstract runner();
12 |
13 | protected constructor(app: IApplication) {
14 | this.app = app;
15 | }
16 |
17 | start() {
18 | setTimeout(async () => {
19 | try {
20 | await Promise.resolve(this.runner());
21 | } catch (err) {
22 | this.logger.error(`${this.identifier} execute error`, err.stack);
23 | }
24 | this.start();
25 | }, this.interval);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/packages/server-client/src/schedule/base/BaseSchedule.ts:
--------------------------------------------------------------------------------
1 | import * as schedule from "node-schedule";
2 | import { IApplication } from "../../interface/application";
3 | import { Logger } from "../../util/logger";
4 |
5 | export abstract class BaseSchedule {
6 | protected logger = Logger.create();
7 | protected app: IApplication = null;
8 |
9 | /**
10 | * * * * * * *
11 | * ┬ ┬ ┬ ┬ ┬ ┬
12 | * │ │ │ │ │ │
13 | * │ │ │ │ │ └ day of week (0 - 7) (0 or 7 is Sun)
14 | * │ │ │ │ └───── month (1 - 12)
15 | * │ │ │ └────────── day of month (1 - 31)
16 | * │ │ └─────────────── hour (0 - 23)
17 | * │ └──────────────────── minute (0 - 59)
18 | * └───────────────────────── second (0 - 59, OPTIONAL)
19 | */
20 | abstract cron: string;
21 | abstract identifier: string;
22 |
23 | abstract runner();
24 |
25 | protected constructor(app: IApplication) {
26 | this.app = app;
27 | }
28 |
29 | start() {
30 | schedule.scheduleJob(this.cron, () => {
31 | this.runner();
32 | });
33 | this.logger.info(`Schedule ${this.identifier} is started`);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/packages/server-client/src/schedule/index.ts:
--------------------------------------------------------------------------------
1 | import { IApplication } from "../interface/application";
2 | import { Logger } from "../util/logger";
3 | import { ResourceUpdateLoop } from "./resource-update-loop";
4 |
5 | export class ScheduleManager {
6 | private logger = Logger.create();
7 | private app: IApplication = null;
8 |
9 | constructor(app: IApplication) {
10 | this.app = app;
11 | }
12 |
13 | start() {
14 | const { enable = false } = this.app.config.schedule || {};
15 | if (enable === true) {
16 | new ResourceUpdateLoop(this.app).start();
17 | } else {
18 | this.logger.info("Schedule enable is false, skipped.");
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/packages/server-client/src/schedule/resource-update-loop.ts:
--------------------------------------------------------------------------------
1 | import { IApplication } from "../interface/application";
2 | import { queryRunnerRepo } from "../util/query-runner-repo";
3 | import { ProjectEnvDeployRepo } from "../entity/project-env-deploy";
4 | import { MemoryCache } from "../cache/memory-cache";
5 | import { BaseLoop } from "./base/BaseLoop";
6 | import { ProjectEnvRepo } from "../entity/project-env";
7 |
8 | export class ResourceUpdateLoop extends BaseLoop {
9 | interval: number = 5000;
10 | identifier: string = "ResourceUpdateLoop";
11 |
12 | private startTimestamp = new Date().valueOf();
13 |
14 | private memoryCache = MemoryCache.getInstance();
15 |
16 | constructor(app: IApplication) {
17 | super(app);
18 | }
19 |
20 | async runner() {
21 | const lastTimestamp = this.startTimestamp;
22 | this.startTimestamp = new Date().valueOf();
23 |
24 | const deployRepo = queryRunnerRepo(ProjectEnvDeployRepo);
25 | const latestDeploys = await deployRepo.findLatestUpdatedEnvIds(lastTimestamp);
26 |
27 | const envRepo = queryRunnerRepo(ProjectEnvRepo);
28 | const latestEnvs = await envRepo.findLatestUpdatedEnvAreas(lastTimestamp);
29 |
30 | let envIds = latestDeploys.map((v) => v.envId).concat(latestEnvs.map((env) => env.id));
31 |
32 | this.memoryCache.cache.forEach((value, key) => {
33 | if (envIds.includes(value.envId)) {
34 | this.logger.debug(`EnvId: ${value.envId} matched, will delete key: ${key}`);
35 | this.memoryCache.cache.del(key);
36 | }
37 | });
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/packages/server-client/src/service/project-service.ts:
--------------------------------------------------------------------------------
1 | import * as assert from "assert";
2 | import { isNil } from "lodash";
3 | import { ProjectDomain, ProjectDomainRepo } from "../entity/project-domain";
4 | import { UnMatchedHostError } from "../error/UnMatchedHostError";
5 | import { queryRunnerRepo } from "../util/query-runner-repo";
6 | import { Project, ProjectRepo } from "../entity/project";
7 | import { ProjectEnv, ProjectEnvRepo } from "../entity/project-env";
8 | import { ProjectEnvDeployRepo } from "../entity/project-env-deploy";
9 |
10 | export class ProjectService {
11 | async getProjectDomain(host: string): Promise<{ domain: ProjectDomain; project: Project; env: ProjectEnv }> {
12 | // TODO 添加缓存机制
13 |
14 | const projectDomainRepo = queryRunnerRepo(ProjectDomainRepo);
15 | const domain = await projectDomainRepo.findOne({ host: host, isDel: false });
16 | assert(!isNil(domain), new UnMatchedHostError(`个性化域名配置不存在 ${host}`));
17 |
18 | const projectRepo = queryRunnerRepo(ProjectRepo);
19 | const project = await projectRepo.findOne({ id: domain.projectId, isDel: false });
20 | assert(!isNil(project), new UnMatchedHostError(`个性化域名配置对应项目不存在 ${host} ${domain.projectId}`));
21 |
22 | const envRepo = queryRunnerRepo(ProjectEnvRepo);
23 | const env = await envRepo.findOne({ id: domain.projectEnvId, isDel: false });
24 | assert(!isNil(env), new UnMatchedHostError(`个性化域名配置对应环境不存在 ${host} ${domain.projectEnvId}`));
25 |
26 | return {
27 | domain: domain,
28 | project: project,
29 | env: env,
30 | };
31 | }
32 |
33 | async getProjectById(projectId: number) {
34 | const projectRepo = queryRunnerRepo(ProjectRepo);
35 | return await projectRepo.findOne({ id: projectId, isDel: false });
36 | }
37 |
38 | async getProjectByName(projectName: string) {
39 | const projectRepo = queryRunnerRepo(ProjectRepo);
40 | return await projectRepo.findOne({ name: projectName, isDel: false });
41 | }
42 |
43 | async getProjectEnvByName(projectId: number, name: string) {
44 | const projectEnvRepo = queryRunnerRepo(ProjectEnvRepo);
45 | return await projectEnvRepo.findOne({ projectId: projectId, name: name, isDel: false });
46 | }
47 |
48 | async getProjectEnvDeployById(deployId: number) {
49 | const projectEnvDeployRepo = queryRunnerRepo(ProjectEnvDeployRepo);
50 | return await projectEnvDeployRepo.findOne({ id: deployId, isDel: false });
51 | }
52 |
53 | /**
54 | * 获取某个环境下生效中的部署记录
55 | */
56 | async getProjectEnvActiveDeploy(projectEnvId: number) {
57 | const projectEnvDeployRepo = queryRunnerRepo(ProjectEnvDeployRepo);
58 | return await projectEnvDeployRepo.findOne({
59 | projectEnvId: projectEnvId,
60 | isActive: true,
61 | isDel: false,
62 | });
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/packages/server-client/src/service/resource-service.ts:
--------------------------------------------------------------------------------
1 | import * as mime from "mime";
2 | import axios from "axios";
3 | import { ResourceFetchFailedError } from "../error/ResourceFetchFailedError";
4 | import { IContext } from "../interface/context";
5 | import { IMemoryCacheValue, MemoryCache } from "../cache/memory-cache";
6 | import { Logger } from "../util/logger";
7 |
8 | export class ResourceService {
9 | private logger = Logger.create();
10 |
11 | private memoryCacheIns = MemoryCache.getInstance();
12 |
13 | private axiosClient = axios.create({
14 | timeout: 5000,
15 | });
16 |
17 | public getResourceFromLocalCache(ctx: IContext) {
18 | const cacheKey = MemoryCache.getCacheKey(ctx);
19 | return this.memoryCacheIns.cache.get(cacheKey);
20 | }
21 |
22 | public addResourceToLocalCache(ctx: IContext, cache: IMemoryCacheValue) {
23 | const cacheKey = MemoryCache.getCacheKey(ctx);
24 | this.memoryCacheIns.cache.set(cacheKey, cache);
25 | }
26 |
27 | public getContentType(url: string) {
28 | return mime.getType(url);
29 | }
30 |
31 | public responseSuccessBuffer(ctx: IContext, buffer: Buffer) {
32 | Object.getOwnPropertyNames(ctx.respHeaders).forEach((key) => {
33 | ctx.resp.setHeader(key, ctx.respHeaders[key]);
34 | });
35 |
36 | ctx.resp.end(buffer);
37 | return;
38 | }
39 |
40 | public response404Buffer(ctx: IContext, identifier: string = "default") {
41 | ctx.resp.setHeader("Content-Type", "text/html; charset=UTF-8");
42 | ctx.resp.setHeader("Pubfree-Error-Code", identifier);
43 | ctx.resp.end(ctx.app.notFoundHtmlBuffer);
44 | return;
45 | }
46 |
47 | public async getResourceBufferFromCloud(ctx: IContext, url: string) {
48 | ctx.respHeaders["Content-Type"] = this.getContentType(url);
49 |
50 | try {
51 | // TODO 添加本地缓存策略
52 | const resp = await this.axiosClient.get(url, { responseType: "arraybuffer" });
53 | return resp.data;
54 | } catch (err) {
55 | this.logger.error(`远程拉取资源失败 ${url}`, err.message, err.stack);
56 | throw new ResourceFetchFailedError(`远程拉取资源失败 ${url}`);
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/packages/server-client/src/util/app-config.ts:
--------------------------------------------------------------------------------
1 | import * as path from "path";
2 | import { Logger } from "./logger";
3 |
4 | export interface IAppConfig {
5 | mysql: {
6 | enable: boolean;
7 | options: {
8 | host: string;
9 | port: number;
10 | database: string;
11 | username: string;
12 | password: string;
13 | };
14 | };
15 |
16 | schedule: {
17 | enable: boolean;
18 | };
19 | }
20 |
21 | export class AppConfig {
22 | private static logger = Logger.create();
23 | private static _config: IAppConfig = null;
24 |
25 | public static load(): IAppConfig {
26 | if (this._config === null) {
27 | this._config = require(path.resolve(__dirname, "../../resource/config.default.json"));
28 | this.logger.info("Load resource/config.default.json success");
29 | }
30 |
31 | return this._config;
32 | }
33 |
34 | public static config(): IAppConfig {
35 | return this._config;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/packages/server-client/src/util/ctx-print.ts:
--------------------------------------------------------------------------------
1 | import { IContext } from "../interface/context";
2 | import { MemoryCache } from "../cache/memory-cache";
3 |
4 | export function ctxPrint(ctx: IContext) {
5 | return MemoryCache.getCacheKey(ctx);
6 | }
7 |
--------------------------------------------------------------------------------
/packages/server-client/src/util/logger.ts:
--------------------------------------------------------------------------------
1 | export interface ILogger {
2 | error(...args: any[]): void;
3 |
4 | warn(...args: any[]): void;
5 |
6 | info(...args: any[]): void;
7 |
8 | verbose(...args: any[]): void;
9 |
10 | debug(...args: any[]): void;
11 | }
12 |
13 | /**
14 | * 日志工具
15 | * TODO 后期替换
16 | */
17 | export class Logger {
18 | private static instance: ILogger = {
19 | error: console.error,
20 | warn: console.warn,
21 | info: console.warn,
22 | verbose: console.debug,
23 | debug: console.debug,
24 | };
25 |
26 | public static create() {
27 | return this.instance;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/packages/server-client/src/util/query-runner-repo.ts:
--------------------------------------------------------------------------------
1 | import { getManager, QueryRunner } from "typeorm";
2 | import { ObjectType } from "typeorm/common/ObjectType";
3 |
4 | export function queryRunnerRepo(customRepo: ObjectType, queryRunner?: QueryRunner) {
5 | if (queryRunner) {
6 | return queryRunner.manager.getCustomRepository(customRepo);
7 | } else {
8 | return getManager().getCustomRepository(customRepo);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/packages/server-client/src/util/req-hostname.ts:
--------------------------------------------------------------------------------
1 | export const reqHostname = (host: string): string => {
2 | const offset = host[0] === "[" ? host.indexOf("]") + 1 : 0;
3 | const index = host.indexOf(":", offset);
4 | return index !== -1 ? host.substring(0, index) : host;
5 | };
6 |
--------------------------------------------------------------------------------
/packages/server-client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "buildOnSave": false,
3 | "compileOnSave": false,
4 | "compilerOptions": {
5 | "rootDir": "src",
6 | "outDir": "dist",
7 | "allowSyntheticDefaultImports": true,
8 | "allowJs": false,
9 | "declaration": true,
10 | "emitDecoratorMetadata": true,
11 | "experimentalDecorators": true,
12 | "target": "ES2015",
13 | "module": "commonjs",
14 | "moduleResolution": "node",
15 | "noUnusedLocals": true,
16 | "strictNullChecks": false,
17 | "skipLibCheck": true,
18 | "skipDefaultLibCheck": true
19 | },
20 | "include": [
21 | "src",
22 | "typings"
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - "packages/client-web"
3 | - "packages/server-api"
4 | - "packages/server-client"
--------------------------------------------------------------------------------