├── .github └── ISSUE_TEMPLATE │ ├── bug-report.md │ ├── feature-request.md │ └── security-issue-report.md ├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── LANGS.md ├── LICENSE ├── README.md ├── README_cn.md ├── _config.yml ├── api ├── appconfig.json ├── gulpfile.js ├── logconfig.json ├── mail.json ├── package.json ├── pm2.json ├── sample collection.json ├── src │ ├── controllers │ │ ├── collection_controller.ts │ │ ├── environment_controller.ts │ │ ├── mock_api_controller.ts │ │ ├── mock_controller.ts │ │ ├── project_controller.ts │ │ ├── record_controller.ts │ │ ├── sample_controller.ts │ │ ├── schedule_controller.ts │ │ ├── stress_controller.ts │ │ └── user_controller.ts │ ├── global_data │ │ ├── data │ │ │ └── test.xlsx │ │ └── lib │ │ │ ├── crypto-js │ │ │ └── index.js │ │ │ ├── lodash │ │ │ └── index.js │ │ │ ├── request │ │ │ └── index.js │ │ │ ├── uuid │ │ │ └── index.js │ │ │ └── xlsx │ │ │ └── index.js │ ├── index.ts │ ├── interfaces │ │ ├── invite_project_token.ts │ │ ├── reg_token.ts │ │ ├── res_object.ts │ │ ├── stress_case_info.ts │ │ └── user_data.ts │ ├── locales │ │ ├── en.json │ │ └── zh.json │ ├── mail │ │ ├── mail.ts │ │ ├── template_setting.ts │ │ └── templates │ │ │ ├── accept_invitation_cn.html │ │ │ ├── accept_invitation_en.html │ │ │ ├── find_pwd_cn.html │ │ │ ├── find_pwd_en.html │ │ │ ├── invite_cn.html │ │ │ ├── invite_en.html │ │ │ ├── invite_project_cn.html │ │ │ ├── invite_project_en.html │ │ │ ├── register_cn.html │ │ │ ├── register_en.html │ │ │ ├── reject_invitation_cn.html │ │ │ ├── reject_invitation_en.html │ │ │ ├── schedule_fail_cn.html │ │ │ ├── schedule_fail_en.html │ │ │ ├── schedule_success_cn.html │ │ │ ├── schedule_success_en.html │ │ │ ├── user_info_cn.html │ │ │ └── user_info_en.html │ ├── middlewares │ │ ├── async_init.ts │ │ ├── error_handle.ts │ │ ├── middleware.ts │ │ ├── route_failed.ts │ │ └── session_handle.ts │ ├── models │ │ ├── body_form_data.ts │ │ ├── collection.ts │ │ ├── environment.ts │ │ ├── header.ts │ │ ├── localhost_mapping.ts │ │ ├── mock.ts │ │ ├── mock_collection.ts │ │ ├── project.ts │ │ ├── query_string.ts │ │ ├── record.ts │ │ ├── record_doc.ts │ │ ├── record_doc_history.ts │ │ ├── record_history.ts │ │ ├── schedule.ts │ │ ├── schedule_record.ts │ │ ├── stress.ts │ │ ├── stress_failed_info.ts │ │ ├── stress_record.ts │ │ ├── user.ts │ │ └── variable.ts │ ├── run_engine │ │ ├── assert_runner.ts │ │ ├── process │ │ │ ├── base_process_handler.ts │ │ │ ├── child_process_manager.ts │ │ │ ├── schedule_process.ts │ │ │ ├── schedule_process_handler.ts │ │ │ ├── stress_nodejs_process.ts │ │ │ ├── stress_nodejs_process_handler.ts │ │ │ ├── stress_nodejs_worker.ts │ │ │ ├── stress_nodejs_worker_handler.ts │ │ │ ├── stress_process.ts │ │ │ └── stress_process_handler.ts │ │ ├── record_runner.ts │ │ ├── request_option_adapter.ts │ │ ├── sandbox.ts │ │ ├── schedule_runner.ts │ │ └── script_runner.ts │ ├── services │ │ ├── backup_service.ts │ │ ├── base │ │ │ ├── request_import.ts │ │ │ └── web_socket_handler.ts │ │ ├── collection_service.ts │ │ ├── connection_manager.ts │ │ ├── console_message.ts │ │ ├── environment_service.ts │ │ ├── form_data_service.ts │ │ ├── header_service.ts │ │ ├── importer │ │ │ ├── curl_import.ts │ │ │ ├── postman_import.ts │ │ │ └── swagger_import.ts │ │ ├── mail_service.ts │ │ ├── mock_collection_service.ts │ │ ├── mock_service.ts │ │ ├── project_data_service.ts │ │ ├── project_service.ts │ │ ├── query_string_service.ts │ │ ├── record_service.ts │ │ ├── sample_service.ts │ │ ├── schedule_on_demand_service.ts │ │ ├── schedule_record_service.ts │ │ ├── schedule_service.ts │ │ ├── session_service.ts │ │ ├── stress_record_service.ts │ │ ├── stress_service.ts │ │ ├── stress_test_ws_service.ts │ │ ├── token_service.ts │ │ ├── user_collection_service.ts │ │ ├── user_project_service.ts │ │ ├── user_service.ts │ │ ├── user_variable_manager.ts │ │ ├── variable_service.ts │ │ └── web_socket_service.ts │ ├── setup.ts │ └── utils │ │ ├── func_util.ts │ │ ├── log.ts │ │ ├── message.ts │ │ ├── script_transform.ts │ │ ├── setting.ts │ │ ├── string_util.ts │ │ └── validate_util.ts ├── tsconfig.json ├── tslint.json ├── typings.json └── yarn.lock ├── client ├── .babelrc ├── .gitignore ├── README.md ├── config │ ├── env.js │ ├── jest │ │ ├── cssTransform.js │ │ ├── fileTransform.js │ │ └── typescriptTransform.js │ ├── paths.js │ ├── polyfills.js │ ├── postcss.config.js │ ├── webpack.config.dev.js │ ├── webpack.config.prod.js │ └── webpack_dll.config.dev.js ├── debugwebpack.js ├── package.json ├── public │ ├── favicon.ico │ ├── hitchhiker-title-dark.png │ ├── hitchhiker-title-dark.svg │ ├── hitchhiker-title.svg │ ├── hitchhiker.svg │ ├── index.html │ ├── index_dev.html │ ├── jq.easing.js │ ├── jq.js │ ├── puff.svg │ ├── rings.svg │ ├── setup.html │ ├── setup.js │ └── setup_cn.html ├── scripts │ ├── build.js │ ├── start.js │ └── test.js ├── src │ ├── App.tsx │ ├── action │ │ ├── collection.ts │ │ ├── document.ts │ │ ├── index.ts │ │ ├── local_data.ts │ │ ├── project.ts │ │ ├── record.ts │ │ ├── schedule.ts │ │ ├── stress.ts │ │ ├── ui.ts │ │ └── user.ts │ ├── common │ │ ├── enum │ │ │ ├── data_mode.ts │ │ │ ├── metadata_type.ts │ │ │ ├── mock_mode.ts │ │ │ ├── notification_mode.ts │ │ │ ├── parameter_type.ts │ │ │ ├── period.ts │ │ │ ├── record_category.ts │ │ │ ├── stress_type.ts │ │ │ └── string_type.ts │ │ ├── interfaces │ │ │ ├── dto_assert.ts │ │ │ ├── dto_collection.ts │ │ │ ├── dto_environment.ts │ │ │ ├── dto_error.ts │ │ │ ├── dto_header.ts │ │ │ ├── dto_mail.ts │ │ │ ├── dto_mock.ts │ │ │ ├── dto_mock_collection.ts │ │ │ ├── dto_project.ts │ │ │ ├── dto_project_data.ts │ │ │ ├── dto_project_quit.ts │ │ │ ├── dto_record.ts │ │ │ ├── dto_record_run.ts │ │ │ ├── dto_record_sort.ts │ │ │ ├── dto_res.ts │ │ │ ├── dto_run_result.ts │ │ │ ├── dto_schedule.ts │ │ │ ├── dto_schedule_record.ts │ │ │ ├── dto_stress.ts │ │ │ ├── dto_stress_record.ts │ │ │ ├── dto_stress_setting.ts │ │ │ ├── dto_user.ts │ │ │ ├── dto_variable.ts │ │ │ ├── password.ts │ │ │ └── postman_v1.ts │ │ ├── link.bat │ │ ├── link.sh │ │ └── utils │ │ │ ├── date_util.ts │ │ │ ├── math_util.ts │ │ │ └── pairwise.ts │ ├── components │ │ ├── assert_json_view │ │ │ ├── assert_funcs.ts │ │ │ ├── assert_item.tsx │ │ │ ├── index.tsx │ │ │ └── style │ │ │ │ └── index.less │ │ ├── code_snippet_dialog │ │ │ ├── index.tsx │ │ │ └── style │ │ │ │ └── index.less │ │ ├── collection_tree │ │ │ ├── collection_item.tsx │ │ │ ├── index.tsx │ │ │ ├── record_folder.tsx │ │ │ ├── record_item.tsx │ │ │ ├── selector.ts │ │ │ └── style │ │ │ │ └── index.less │ │ ├── common_setting_dialog │ │ │ └── index.tsx │ │ ├── confirm_dialog │ │ │ └── index.ts │ │ ├── diff_dialog │ │ │ ├── index.tsx │ │ │ └── style │ │ │ │ └── index.less │ │ ├── editable_cell │ │ │ ├── index.tsx │ │ │ └── style │ │ │ │ └── index.less │ │ ├── editor │ │ │ ├── index.tsx │ │ │ └── style │ │ │ │ └── index.less │ │ ├── environment_select │ │ │ ├── index.tsx │ │ │ ├── selector.ts │ │ │ └── style │ │ │ │ └── index.less │ │ ├── font_icon │ │ │ ├── font_icon.tsx │ │ │ ├── http_method_icon.tsx │ │ │ └── style │ │ │ │ └── index.less │ │ ├── highlight_code │ │ │ └── index.tsx │ │ ├── indicator │ │ │ ├── index.tsx │ │ │ └── style │ │ │ │ └── index.less │ │ ├── item_with_menu │ │ │ ├── index.tsx │ │ │ └── style │ │ │ │ └── index.less │ │ ├── key_value │ │ │ ├── index.tsx │ │ │ ├── key_value_list.tsx │ │ │ └── style │ │ │ │ └── index.less │ │ ├── name_with_tag │ │ │ ├── index.tsx │ │ │ └── style │ │ │ │ └── index.less │ │ ├── record_timeline │ │ │ ├── index.tsx │ │ │ └── style │ │ │ │ └── index.less │ │ ├── res_error_panel │ │ │ ├── index.tsx │ │ │ └── style │ │ │ │ └── index.less │ │ ├── script_dialog │ │ │ └── index.tsx │ │ ├── sider_layout │ │ │ ├── index.tsx │ │ │ └── styles │ │ │ │ └── index.less │ │ ├── sortable_list │ │ │ ├── index.tsx │ │ │ └── style │ │ │ │ └── index.less │ │ ├── splitter │ │ │ ├── index.tsx │ │ │ └── style │ │ │ │ └── index.less │ │ ├── tab_dot │ │ │ ├── index.tsx │ │ │ └── style │ │ │ │ └── index.less │ │ └── validated_name │ │ │ ├── index.tsx │ │ │ └── style │ │ │ └── index.less │ ├── index.tsx │ ├── locales │ │ ├── en.json │ │ ├── index.tsx │ │ ├── input.tsx │ │ ├── lang.ts │ │ ├── string.ts │ │ └── zh.json │ ├── misc │ │ ├── body_type.ts │ │ ├── code_snippet_type.ts │ │ ├── conflict_type.ts │ │ ├── constants.ts │ │ ├── custom_type.ts │ │ ├── har.ts │ │ ├── http_method.ts │ │ ├── key_value_pair.ts │ │ ├── notification_mode.ts │ │ ├── parameter_type.ts │ │ ├── period.ts │ │ ├── record_category.ts │ │ ├── request_status.ts │ │ ├── schedule_statistics.ts │ │ ├── stress_type.ts │ │ ├── tab_action.ts │ │ └── test_snippet.ts │ ├── modules │ │ ├── api_mock │ │ │ └── index.tsx │ │ ├── collection │ │ │ ├── index.tsx │ │ │ └── req_res_panel │ │ │ │ ├── index.tsx │ │ │ │ ├── request_name_panel.tsx │ │ │ │ ├── request_option_panel.tsx │ │ │ │ ├── request_query_string_panel.tsx │ │ │ │ ├── request_tab_extra.tsx │ │ │ │ ├── request_url_panel.tsx │ │ │ │ ├── response_loading_panel.tsx │ │ │ │ ├── response_panel.tsx │ │ │ │ ├── selector.ts │ │ │ │ └── style │ │ │ │ └── index.less │ │ ├── document │ │ │ ├── document_content.tsx │ │ │ ├── index.tsx │ │ │ ├── style │ │ │ │ └── index.less │ │ │ └── templates │ │ │ │ └── default.ts │ │ ├── header │ │ │ ├── change_password_dialog.tsx │ │ │ ├── index.tsx │ │ │ └── style │ │ │ │ └── index.less │ │ ├── login │ │ │ ├── find_password.tsx │ │ │ ├── index.tsx │ │ │ ├── loading_screen.tsx │ │ │ ├── login.tsx │ │ │ ├── register.tsx │ │ │ └── style │ │ │ │ └── index.less │ │ ├── project │ │ │ ├── environments.tsx │ │ │ ├── index.tsx │ │ │ ├── members.tsx │ │ │ ├── project_data_dialog.tsx │ │ │ ├── project_item.tsx │ │ │ ├── project_list.tsx │ │ │ └── style │ │ │ │ └── index.less │ │ ├── schedule │ │ │ ├── index.tsx │ │ │ ├── schedule_edit_dialog.tsx │ │ │ ├── schedule_item.tsx │ │ │ ├── schedule_list.tsx │ │ │ ├── schedule_run_console.tsx │ │ │ ├── schedule_run_history_grid.tsx │ │ │ └── style │ │ │ │ └── index.less │ │ └── stress_test │ │ │ ├── index.tsx │ │ │ ├── stress_edit_dialog.tsx │ │ │ ├── stress_item.tsx │ │ │ ├── stress_list.tsx │ │ │ ├── stress_run_diagram.tsx │ │ │ ├── stress_run_history_grid.tsx │ │ │ ├── stress_worker_status.tsx │ │ │ └── style │ │ │ └── index.less │ ├── reducer │ │ ├── __tests__ │ │ │ ├── collection.test.ts │ │ │ ├── data.ts │ │ │ ├── environment.test.ts │ │ │ ├── index.test.ts │ │ │ ├── local_data.test.ts │ │ │ ├── project.test.ts │ │ │ ├── record.test.ts │ │ │ ├── schedule.test.ts │ │ │ ├── stress.test.ts │ │ │ └── ui.test.ts │ │ ├── collection.ts │ │ ├── document.ts │ │ ├── environment.ts │ │ ├── index.ts │ │ ├── local_data.ts │ │ ├── project.ts │ │ ├── schedule.ts │ │ ├── stress.ts │ │ ├── ui.ts │ │ └── user.ts │ ├── state │ │ ├── collection.ts │ │ ├── document.ts │ │ ├── environment.ts │ │ ├── index.ts │ │ ├── local_data.ts │ │ ├── project.ts │ │ ├── request.ts │ │ ├── schedule.ts │ │ ├── stress.ts │ │ ├── ui.ts │ │ └── user.ts │ ├── store.ts │ ├── style │ │ ├── App.less │ │ ├── colors.less │ │ ├── fonts │ │ │ ├── SourceCodePro-Bold.woff │ │ │ ├── SourceCodePro-Medium.woff │ │ │ ├── SourceCodePro-Regular.woff │ │ │ ├── SourceCodePro-Semibold.woff │ │ │ ├── iconfont.eot │ │ │ ├── iconfont.svg │ │ │ ├── iconfont.ttf │ │ │ └── iconfont.woff │ │ ├── perfect-scrollbar.min.css │ │ └── theme.tsx │ └── utils │ │ ├── beautify.ts │ │ ├── compare_util.ts │ │ ├── curl_import.ts │ │ ├── date_util.ts │ │ ├── download_util.ts │ │ ├── global_var.ts │ │ ├── local_store.ts │ │ ├── pairwise.ts │ │ ├── request_manager.ts │ │ ├── string_util.ts │ │ ├── template_util.ts │ │ └── urls.ts ├── tsconfig.json └── tslint.json ├── cn ├── README.md ├── SUMMARY.md ├── Schedule │ ├── Create_Schedule.md │ ├── README.md │ └── Run.md ├── Script │ ├── API.md │ ├── Common_Pre_Script.md │ ├── Global_Func.md │ ├── Pre_Script.md │ ├── README.md │ ├── Test.md │ ├── custom-data-file.md │ └── custom-javascript-lib.md ├── Simple_Tutorial │ ├── Assert_Base_On_UI.md │ ├── Create_Collection.md │ ├── Create_Project.md │ ├── Create_Request.md │ ├── README.md │ ├── Use_Env_Var.md │ └── Use_Param.md ├── Stress │ ├── Create_Stress.md │ ├── Node.md │ ├── README.md │ └── Run.md ├── TODO.md ├── Variable │ ├── Dynamic_Var.md │ ├── Env_Var.md │ ├── Param_Var.md │ └── README.md ├── change_log.md ├── import.md ├── installation │ ├── Mail_Interface.md │ ├── README.md │ ├── StepByStep.md │ ├── configuration.md │ ├── docker.md │ ├── linux.md │ └── win.md ├── introduction.md └── other.md ├── deploy ├── docker │ ├── Dockerfile │ ├── hitchhiker │ │ └── docker-compose.yml │ ├── hitchhiker_and_mysql │ │ ├── docker-compose.yml │ │ └── hitchhiker-mysql.cnf │ ├── mysql │ │ └── docker-compose.yml │ ├── nginx │ │ ├── docker-compose.yml │ │ └── nginx.conf │ ├── private │ │ ├── docker-compose.yml │ │ └── update.sh │ └── push_docker.sh ├── linux_deploy.sh ├── package.bat └── win_deploy.bat ├── doc ├── ReuqestWorkflow.gliffy ├── hitchhiker.gliffy ├── howtouse-cn.md ├── howtouse-en.md └── images │ ├── collection.png │ ├── env.png │ ├── header.gif │ ├── history.png │ └── schedule.png ├── en ├── README.md ├── SUMMARY.md ├── Schedule │ ├── Create_Schedule.md │ ├── README.md │ └── Run.md ├── Script │ ├── API.md │ ├── Common_Pre_Script.md │ ├── Global_Func.md │ ├── Pre_Script.md │ ├── README.md │ ├── Test.md │ ├── custom-data-file.md │ └── custom-javascript-lib.md ├── Simple_Tutorial │ ├── Assert_Base_On_UI.md │ ├── Create_Collection.md │ ├── Create_Project.md │ ├── Create_Request.md │ ├── README.md │ ├── Use_Env_Var.md │ └── Use_Param.md ├── Stress │ ├── Create_Stress.md │ ├── Node.md │ ├── README.md │ └── Run.md ├── TODO.md ├── Variable │ ├── Dynamic_Var.md │ ├── Env_Var.md │ ├── Param_Var.md │ └── README.md ├── change_log.md ├── import.md ├── installation │ ├── Mail_Interface.md │ ├── README.md │ ├── StepByStep.md │ ├── configuration.md │ ├── docker.md │ ├── linux.md │ └── win.md ├── introduction.md └── other.md ├── tasks.todo └── test case.xlsx /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report (提交Bug) 3 | about: File a bug to help us improve. 4 | 5 | --- 6 | 7 | **1. What went wrong `(哪里出错)`?** 8 | Please describe what went wrong (请描述bug的具体表现) 9 | 10 | **2. Steps to reproduce the problem `(重现步骤)`:** 11 | Clear and concise reproduction instructions are important for us to be able to triage your issue in a timely manner (简洁清晰的重现步骤能够帮助我们更迅速地定位问题所在) 12 | 13 | **3. What is the expected behavior `(期望表现)`?** 14 | Please describe the expected behavior (请描述期望表现) 15 | 16 | **4. Did this work before `(之前是否正常)`?** 17 | [Yes, No, N/A] [是,否,之前没用过] 18 | 19 | **5. Environment `(环境)`:** 20 | - Hitchhiker Version (Hitchhiker版本): [e.g. V0.14] 21 | - Use Docker or Installation Package (使用Docker还是安装包): [e.g. Docker/Installation Package] 22 | - Node Version (If you use the installation package) (如果使用安装包的Node版本): [e.g. 8.11.3] 23 | - Server OS (部署的系统): [e.g. Ubuntu, Windows, MacOS] 24 | - Browser (浏览器): [e.g. Chrome, Firefox, Safari] 25 | 26 | **6. Additional Information (e.g. Screenshots, Links) `(补充信息,截图,链接等)`** 27 | Add any other context about the problem here (补充具体的上下文信息) 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request (功能要求) 3 | about: Suggest an idea for this project. 4 | 5 | --- 6 | 7 | **1. Is your feature request related to a problem? Please describe. `(这个功能是为解决什么问题)`** 8 | A clear and concise description of what the problem is. Explain your use case, context, and rationale behind this feature request (请尽可能详尽地说明这个需求的用例和场景) 9 | 10 | **2. Describe the solution you'd like`(请描述你期望的结果)`** 11 | A clear and concise description of what you want to happen (简单清楚的描述你期望的结果) 12 | 13 | **3. Additional context `(补充信息)`** 14 | Add any other context or screenshots about the feature request here (截图,链接等) 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/security-issue-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Security Issue report (提交安全性问题) 3 | about: File a security issue report. 4 | 5 | --- 6 | 7 | **1. Describe the Security Issue as detailed as possible `(问题描述)`** 8 | A clear and concise description of what the bug is (请详细描述问题) 9 | 10 | **2. Steps to reproduce the problem `(重现步骤)`:** 11 | Clear and concise reproduction instructions are important for us to be able to triage your issue in a timely manner (简洁清晰的重现步骤能够帮助我们更迅速地定位问题所在) 12 | 13 | **3. What is the expected behavior `(期望表现)`?** 14 | Please describe the expected behavior (请描述期望表现) 15 | 16 | **4. Did this work before `(之前是否正常)`?** 17 | [Yes/No/NaN] [是,否,之前没用过] 18 | 19 | **5. Environment `(环境)`:** 20 | - Hitchhiker Version (Hitchhiker版本): [e.g. V0.14] 21 | - Use Docker or Installation Package (使用Docker还是安装包): [e.g. Docker/Installation Package] 22 | - Node Version (If you use the installation package) (如果使用安装包的Node版本): [e.g. 8.11.3] 23 | - Server OS (部署的系统): [e.g. Ubuntu, Windows, MacOS] 24 | - Browser (浏览器): [e.g. Chrome, Firefox, Safari] 25 | 26 | **6. Additional Information (e.g. Screenshots, Links) `(补充信息,截图,链接等)`** 27 | Add any other context about the problem here (补充具体的上下文信息) 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | bin 40 | typings 41 | build 42 | ~$test case.xlsx 43 | staging 44 | 45 | # symbolic link folder 46 | api/src/common 47 | 48 | .awcache 49 | stats.json -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "vsicons.presets.angular": false, 3 | "tslint.run": "onSave", 4 | "todohighlight.include": "{**/*.ts,**/*.html,**/*.php,**/*.css}", 5 | "auto-close-tag.SublimeText3Mode": true, 6 | "typescript.tsdk": "client\\node_modules\\typescript\\lib" 7 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [{ 6 | "taskName": "build", 7 | "command": "tsc", 8 | "isShellCommand": true, 9 | "dependsOn": ["gulp"], 10 | "args": [ 11 | "-p", 12 | "api" 13 | ], 14 | "problemMatcher": "$tsc-watch" 15 | }] 16 | } -------------------------------------------------------------------------------- /LANGS.md: -------------------------------------------------------------------------------- 1 | # Languages 2 | 3 | * [中文版](cn/) 4 | * [English](en/) -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /api/logconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "appenders": [{ 3 | "type": "file", 4 | "filename": "logs/log_file.log", 5 | "maxLogSize": 20480000, 6 | "backups": 10, 7 | "category": "default" 8 | }, 9 | { 10 | "type": "console", 11 | "category": "console" 12 | } 13 | ], 14 | "levels": { 15 | "relative-logger": "ALL", 16 | "console": "ALL" 17 | }, 18 | "replaceConsole": true 19 | } -------------------------------------------------------------------------------- /api/pm2.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [{ 3 | "name": "hitchhiker", 4 | "script": "./build/index.js", 5 | "watch": false, 6 | "env": { 7 | "HITCHHIKER_APP_HOST": "myhost" 8 | } 9 | }] 10 | } -------------------------------------------------------------------------------- /api/src/controllers/environment_controller.ts: -------------------------------------------------------------------------------- 1 | import { POST, DELETE, PUT, PathParam, BodyParam, BaseController } from 'webapi-router'; 2 | import { ResObject } from '../interfaces/res_object'; 3 | import { EnvironmentService } from '../services/environment_service'; 4 | import { DtoEnvironment } from '../common/interfaces/dto_environment'; 5 | 6 | export default class EnvironmentController extends BaseController { 7 | 8 | @POST('/environment') 9 | async create(@BodyParam env: DtoEnvironment): Promise { 10 | return await EnvironmentService.create(env); 11 | } 12 | 13 | @PUT('/environment') 14 | async update(@BodyParam env: DtoEnvironment): Promise { 15 | return await EnvironmentService.update(env); 16 | } 17 | 18 | @DELETE('/environment/:id') 19 | async delete(@PathParam('id') id: string) { 20 | return await EnvironmentService.delete(id); 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /api/src/controllers/mock_api_controller.ts: -------------------------------------------------------------------------------- 1 | import { GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, PathParam, BaseController } from 'webapi-router'; 2 | import { MockService } from '../services/mock_service'; 3 | 4 | export default class MockApiController extends BaseController { 5 | 6 | // url rule: /mockapi/:collectionid/.. 7 | @GET('/mockapi/:path*') 8 | async get(@PathParam('path') path: any) { 9 | return await this.getMockData('GET', path); 10 | } 11 | 12 | @POST('/mockapi/:path*') 13 | async post(@PathParam('path') path: any) { 14 | return await this.getMockData('POST', path); 15 | } 16 | 17 | @PUT('/mockapi/:path*') 18 | async put(@PathParam('path') path: any) { 19 | return await this.getMockData('PUT', path); 20 | } 21 | 22 | @DELETE('/mockapi/:path*') 23 | async delete(@PathParam('path') path: any) { 24 | return await this.getMockData('DELETE', path); 25 | } 26 | 27 | @PATCH('/mockapi/:path*') 28 | async patch(@PathParam('path') path: any) { 29 | return await this.getMockData('PATCH', path); 30 | } 31 | 32 | @HEAD('/mockapi/:path*') 33 | async head(@PathParam('path') path: any) { 34 | return await this.getMockData('HEAD', path); 35 | } 36 | 37 | @OPTIONS('/mockapi/:path*') 38 | async options(@PathParam('path') path: any) { 39 | return await this.getMockData('OPTIONS', path); 40 | } 41 | 42 | async getMockData(method: string, path: string) { 43 | return await MockService.getMockRes(method, `mockapi/${path}`); 44 | } 45 | } -------------------------------------------------------------------------------- /api/src/controllers/sample_controller.ts: -------------------------------------------------------------------------------- 1 | import { GET, POST, PUT, DELETE, BodyParam, PathParam, BaseController } from 'webapi-router'; 2 | import { ResObject } from '../interfaces/res_object'; 3 | 4 | export default class SampleController extends BaseController { 5 | 6 | @GET('/sample/:id') 7 | async getById(@PathParam('id') id: any): Promise { 8 | return { 9 | success: true, 10 | message: '', 11 | result: { 12 | id, 13 | name: 'sample' 14 | } 15 | }; 16 | } 17 | 18 | @POST('/sample') 19 | addSample(@BodyParam body: any): ResObject { 20 | return { 21 | success: true, 22 | message: 'add sample success.', 23 | result: body 24 | }; 25 | } 26 | 27 | @PUT('/sample') 28 | changeSample(@BodyParam body: any): ResObject { 29 | return { 30 | success: true, 31 | message: 'update sample success.', 32 | result: body 33 | }; 34 | } 35 | 36 | @DELETE('/sample/:id') 37 | delete(@PathParam('id') id: any): ResObject { 38 | return { 39 | success: true, 40 | message: `delete sample ${id} success` 41 | }; 42 | } 43 | 44 | @GET('/sample/action/assert') 45 | assert() { 46 | return { 47 | root: { 48 | array: [100, 102, 104], 49 | boolean: true, 50 | number: 10000, 51 | string: 'hitchhiker', 52 | objArr: [ 53 | { name: '111' }, 54 | { name: '222' } 55 | ] 56 | } 57 | }; 58 | } 59 | } -------------------------------------------------------------------------------- /api/src/controllers/stress_controller.ts: -------------------------------------------------------------------------------- 1 | import { GET, POST, PUT, DELETE, BodyParam, PathParam, BaseController } from 'webapi-router'; 2 | import { ResObject } from '../interfaces/res_object'; 3 | import * as Koa from 'koa'; 4 | import { DtoStress } from '../common/interfaces/dto_stress'; 5 | import { StressService } from '../services/stress_service'; 6 | 7 | export default class StressController extends BaseController { 8 | 9 | @POST('/stress') 10 | async createNew(ctx: Koa.Context, @BodyParam stress: DtoStress): Promise { 11 | return StressService.createNew(stress, (ctx).session.user); 12 | } 13 | 14 | @PUT('/stress') 15 | async update(@BodyParam stress: DtoStress): Promise { 16 | return StressService.update(stress); 17 | } 18 | 19 | @DELETE('/stress/:id') 20 | async delete(@PathParam('id') id: string): Promise { 21 | return StressService.delete(id); 22 | } 23 | 24 | @GET('/stresses') 25 | async getStresses(ctx: Koa.Context): Promise { 26 | const stresses = await StressService.getByUserId((ctx).session.userId); 27 | return { success: true, message: '', result: stresses }; 28 | } 29 | } -------------------------------------------------------------------------------- /api/src/global_data/data/test.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brookshi/Hitchhiker/318bc47d4041b32d0e2ffb1716be053abd28b456/api/src/global_data/data/test.xlsx -------------------------------------------------------------------------------- /api/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as Koa from 'koa'; 2 | import Middleware from './middlewares/middleware'; 3 | import { Log } from './utils/log'; 4 | import { ChildProcessManager } from './run_engine/process/child_process_manager'; 5 | import 'reflect-metadata'; 6 | import { WebSocketService } from './services/web_socket_service'; 7 | import { Setting } from './utils/setting'; 8 | import { ProjectDataService } from './services/project_data_service'; 9 | 10 | let app = new Koa(); 11 | 12 | Log.init(); 13 | 14 | process.on('uncaughtException', (err) => { 15 | Log.error(err); 16 | }); 17 | 18 | Setting.instance.init(); 19 | 20 | ProjectDataService.instance.init(); 21 | 22 | ChildProcessManager.default.init(); 23 | 24 | app.use(Middleware()); 25 | 26 | const server = app.listen(Setting.instance.appPort); 27 | 28 | server.timeout = 30 * 60 * 1000; 29 | 30 | new WebSocketService(server).start(); -------------------------------------------------------------------------------- /api/src/interfaces/invite_project_token.ts: -------------------------------------------------------------------------------- 1 | export interface InviteToProjectToken { 2 | 3 | userEmail: string; 4 | 5 | inviterId: string; 6 | 7 | inviterEmail: string; 8 | 9 | date: Date; 10 | 11 | uid: string; 12 | 13 | projectId: string; 14 | } -------------------------------------------------------------------------------- /api/src/interfaces/reg_token.ts: -------------------------------------------------------------------------------- 1 | export interface RegToken { 2 | 3 | host: string; 4 | 5 | date: Date; 6 | 7 | uid: string; 8 | } -------------------------------------------------------------------------------- /api/src/interfaces/res_object.ts: -------------------------------------------------------------------------------- 1 | export interface ResObject { 2 | 3 | message: string; 4 | 5 | success: boolean; 6 | 7 | result?: any; 8 | } -------------------------------------------------------------------------------- /api/src/interfaces/stress_case_info.ts: -------------------------------------------------------------------------------- 1 | import { StressMessageType } from '../common/enum/stress_type'; 2 | import { RecordEx } from '../models/record'; 3 | 4 | export interface StressUser { 5 | 6 | id: string; 7 | } 8 | 9 | export interface StressRequest extends StressUser { 10 | 11 | type: StressMessageType; 12 | 13 | stressId: string; 14 | 15 | stressName: string; 16 | 17 | testCase: TestCase; 18 | 19 | fileData: Buffer; 20 | } 21 | 22 | export interface TestCase { 23 | 24 | records: RecordEx[]; 25 | 26 | envId: string; 27 | 28 | requestBodyList?: RequestBody[]; 29 | 30 | envVariables: _.Dictionary; 31 | 32 | repeat: number; 33 | 34 | concurrencyCount: number; 35 | 36 | qps: number; 37 | 38 | timeout: number; 39 | 40 | keepAlive: boolean; 41 | } 42 | 43 | export interface RequestBody { 44 | 45 | id: string; 46 | 47 | name: string; 48 | 49 | param: string; 50 | 51 | method: string; 52 | 53 | url: string; 54 | 55 | body?: string; 56 | 57 | headers?: _.Dictionary; 58 | 59 | test?: string; 60 | 61 | prescript?: string; 62 | } -------------------------------------------------------------------------------- /api/src/interfaces/user_data.ts: -------------------------------------------------------------------------------- 1 | import { User } from '../models/user'; 2 | import { Project } from '../models/project'; 3 | import { Environment } from '../models/environment'; 4 | import { DtoSchedule } from '../common/interfaces/dto_schedule'; 5 | import { DtoStress } from '../common/interfaces/dto_stress'; 6 | import { DtoCollectionWithRecord } from '../common/interfaces/dto_collection'; 7 | import { ProjectFiles } from '../common/interfaces/dto_project_data'; 8 | import { DtoCollectionWithMock } from '../common/interfaces/dto_mock_collection'; 9 | 10 | export interface UserData { 11 | 12 | user: User; 13 | 14 | collection: DtoCollectionWithRecord; 15 | 16 | projects: _.Dictionary; 17 | 18 | environments: _.Dictionary; 19 | 20 | schedules: _.Dictionary; 21 | 22 | schedulePageSize: number; 23 | 24 | stresses: _.Dictionary; 25 | 26 | mockCollections: DtoCollectionWithMock; 27 | 28 | projectFiles: ProjectFiles; 29 | 30 | defaultHeaders: string; 31 | 32 | syncInterval: number; 33 | 34 | sync: boolean; 35 | 36 | enableUpload: boolean; 37 | } -------------------------------------------------------------------------------- /api/src/mail/template_setting.ts: -------------------------------------------------------------------------------- 1 | export class TemplateSetting { 2 | private _setting: any; 3 | 4 | static readonly instance = new TemplateSetting(); 5 | 6 | private constructor() { 7 | this._setting = require('../../mail.json'); 8 | } 9 | 10 | get templates() { 11 | return this._setting.templates; 12 | } 13 | } -------------------------------------------------------------------------------- /api/src/mail/templates/accept_invitation_cn.html: -------------------------------------------------------------------------------- 1 | {{user}} 接受了您的邀请,加入 {{project}} 项目。 -------------------------------------------------------------------------------- /api/src/mail/templates/accept_invitation_en.html: -------------------------------------------------------------------------------- 1 | {{user}} accept your invitation to join {{project}} project. -------------------------------------------------------------------------------- /api/src/mail/templates/find_pwd_cn.html: -------------------------------------------------------------------------------- 1 | 您已重置您的 [HitchhikerAPI] 密码,新密码为:{{pwd}} ,请尽快登录并修改密码。 -------------------------------------------------------------------------------- /api/src/mail/templates/find_pwd_en.html: -------------------------------------------------------------------------------- 1 | you reset your [HitchhikerAPI] password,new password is:{{pwd}} ,please login and change password。 -------------------------------------------------------------------------------- /api/src/mail/templates/reject_invitation_cn.html: -------------------------------------------------------------------------------- 1 | {{user}} 拒绝了您的邀请,没有加入{{project}}项目。 -------------------------------------------------------------------------------- /api/src/mail/templates/reject_invitation_en.html: -------------------------------------------------------------------------------- 1 | {{user}} reject your invitation to join {{project}} project. -------------------------------------------------------------------------------- /api/src/mail/templates/user_info_cn.html: -------------------------------------------------------------------------------- 1 | 成功加入 {{project}} 项目,您可以用您的email为用户名,密码:{{password}} 来登录系统,欢迎使用。 -------------------------------------------------------------------------------- /api/src/mail/templates/user_info_en.html: -------------------------------------------------------------------------------- 1 | You had join {{project}} project, you can use your email as account and password: {{password}} to login. -------------------------------------------------------------------------------- /api/src/middlewares/async_init.ts: -------------------------------------------------------------------------------- 1 | import { ConnectionManager } from '../services/connection_manager'; 2 | 3 | export default function asyncInit(): (ctx: any, next: Function) => Promise { 4 | let isAsyncInit = false; 5 | return async (_ctx, next) => { 6 | if (!isAsyncInit) { 7 | isAsyncInit = true; 8 | await ConnectionManager.init(); 9 | } 10 | await next(); 11 | }; 12 | } -------------------------------------------------------------------------------- /api/src/middlewares/error_handle.ts: -------------------------------------------------------------------------------- 1 | import { Log } from '../utils/log'; 2 | 3 | export default function errorHandle(): (ctx: any, next: Function) => Promise { 4 | return async (ctx, next) => { 5 | try { 6 | await next(); 7 | } catch (err) { 8 | Log.error(err); 9 | ctx.status = err.status || 500; 10 | ctx.body = err.message; 11 | ctx.app.emit('error', err, ctx); 12 | } 13 | }; 14 | } -------------------------------------------------------------------------------- /api/src/middlewares/middleware.ts: -------------------------------------------------------------------------------- 1 | import * as Compose from 'koa-compose'; 2 | import * as Bodyparser from 'koa-bodyparser'; 3 | import * as Session from 'koa-session-minimal'; 4 | import { WebApiRouter } from 'webapi-router'; 5 | import sessionHandle from './session_handle'; 6 | import { SessionService } from '../services/session_service'; 7 | import routeFailed from './route_failed'; 8 | import errorHandle from './error_handle'; 9 | import * as KoaStatic from 'koa-static'; 10 | import * as Path from 'path'; 11 | import asyncInit from './async_init'; 12 | import * as Compress from 'koa-compress'; 13 | 14 | export default function middleware() { 15 | const ctrlRouter = new WebApiRouter(); 16 | return Compose( 17 | [ 18 | asyncInit(), 19 | errorHandle(), 20 | KoaStatic(Path.join(__dirname, '../public'), { gzip: true, maxage: 60 * 60 * 24 * 30 * 12 }), 21 | Session({ 22 | cookie: { 23 | maxAge: SessionService.maxAge 24 | } 25 | }), 26 | sessionHandle(), 27 | Compress(), 28 | Bodyparser({ jsonLimit: '50mb' }), 29 | ctrlRouter.router('../build/controllers', 'api'), 30 | routeFailed(), 31 | ] 32 | ); 33 | } -------------------------------------------------------------------------------- /api/src/middlewares/route_failed.ts: -------------------------------------------------------------------------------- 1 | import { Message } from '../utils/message'; 2 | 3 | export default function routeFailed(): (ctx: any, next: Function) => Promise { 4 | return async (ctx, next) => { 5 | ctx.body = { success: false, message: Message.get('apiNotExist') }; 6 | return await next(); 7 | }; 8 | } -------------------------------------------------------------------------------- /api/src/middlewares/session_handle.ts: -------------------------------------------------------------------------------- 1 | import { SessionService } from '../services/session_service'; 2 | import { Message } from '../utils/message'; 3 | 4 | export default function sessionHandle(): (ctx: any, next: Function) => Promise { 5 | return async (ctx, next) => { 6 | const isSessionValid = await SessionService.isSessionValid(ctx); 7 | if (false) { 8 | ctx.body = { success: false, message: Message.get('sessionInvalid') }; 9 | ctx.status = 403; 10 | // ctx.redirect(Setting.instance.host); 11 | return; 12 | } 13 | SessionService.rollDate(ctx); 14 | return await next(); 15 | }; 16 | } -------------------------------------------------------------------------------- /api/src/models/body_form_data.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, ManyToOne, PrimaryColumn } from 'typeorm'; 2 | import { Record } from './record'; 3 | import { Mock } from './mock'; 4 | 5 | @Entity() 6 | export class BodyFormData { 7 | @PrimaryColumn() 8 | id: string; 9 | 10 | @Column({ nullable: true }) 11 | key: string; 12 | 13 | @Column('text', { nullable: true }) 14 | value: string; 15 | 16 | @Column({ default: true }) 17 | isActive: boolean; 18 | 19 | @Column() 20 | sort: number; 21 | 22 | @Column('text', { nullable: true }) 23 | description: string; 24 | 25 | @ManyToOne(_type => Record, record => record.id) 26 | record: Record; 27 | 28 | @ManyToOne(_type => Mock, mock => mock.id) 29 | mock: Mock; 30 | } -------------------------------------------------------------------------------- /api/src/models/environment.ts: -------------------------------------------------------------------------------- 1 | import { ManyToOne, OneToMany, Entity, PrimaryColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'; 2 | import { Variable } from './variable'; 3 | import { Project } from './project'; 4 | 5 | @Entity() 6 | export class Environment { 7 | @PrimaryColumn() 8 | id: string; 9 | 10 | @Column() 11 | name: string; 12 | 13 | @OneToMany(_type => Variable, variable => variable.environment, { 14 | cascadeInsert: true, 15 | cascadeUpdate: true 16 | }) 17 | variables: Variable[] = []; 18 | 19 | @ManyToOne(_type => Project, project => project.environments) 20 | project: Project; 21 | 22 | @CreateDateColumn() 23 | createDate: Date; 24 | 25 | @UpdateDateColumn() 26 | updateDate: Date; 27 | } 28 | -------------------------------------------------------------------------------- /api/src/models/header.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, ManyToOne, PrimaryColumn } from 'typeorm'; 2 | import { Record } from './record'; 3 | import { Mock } from './mock'; 4 | import { MockCollection } from './mock_collection'; 5 | 6 | @Entity() 7 | export class Header { 8 | @PrimaryColumn() 9 | id: string; 10 | 11 | @Column({ nullable: true }) 12 | key: string; 13 | 14 | @Column('text', { nullable: true }) 15 | value: string; 16 | 17 | @Column({ default: true }) 18 | isActive: boolean; 19 | 20 | @Column({ default: false }) 21 | isFav: boolean; 22 | 23 | @Column() 24 | sort: number; 25 | 26 | @Column('text', { nullable: true }) 27 | description: string; 28 | 29 | @ManyToOne(_type => Record, record => record.id) 30 | record: Record; 31 | 32 | @ManyToOne(_type => Mock, mock => mock.id) 33 | mock?: Mock; 34 | } -------------------------------------------------------------------------------- /api/src/models/localhost_mapping.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, ManyToOne, PrimaryColumn } from 'typeorm'; 2 | import { Project } from './project'; 3 | 4 | @Entity() 5 | export class LocalhostMapping { 6 | @PrimaryColumn() 7 | id: string; 8 | 9 | @Column() 10 | userId: string; 11 | 12 | @Column({ default: 'localhost' }) 13 | ip: string; 14 | 15 | @ManyToOne(_type => Project, project => project.localhosts) 16 | project: Project; 17 | } -------------------------------------------------------------------------------- /api/src/models/mock_collection.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryColumn, Column, CreateDateColumn, OneToOne, OneToMany, JoinColumn, ManyToOne, UpdateDateColumn } from 'typeorm'; 2 | import { User } from './user'; 3 | import { Project } from './project'; 4 | import { Mock } from './mock'; 5 | import { DtoHeader } from '../common/interfaces/dto_header'; 6 | 7 | @Entity() 8 | export class MockCollection { 9 | 10 | @PrimaryColumn() 11 | id: string; 12 | 13 | @Column() 14 | name: string; 15 | 16 | @OneToMany(_type => Mock, mock => mock.collection, { 17 | cascadeInsert: true 18 | }) 19 | mocks: Mock[]; 20 | 21 | @Column('text', { nullable: true }) 22 | description: string; 23 | 24 | @JoinColumn() 25 | @OneToOne(_type => User) 26 | owner: User; 27 | 28 | @ManyToOne(_type => Project, project => project.collections) 29 | project: Project; 30 | 31 | @Column({ default: false }) 32 | recycle: boolean; 33 | 34 | @Column({ default: true }) 35 | public: boolean; 36 | 37 | @Column('json', { nullable: true }) 38 | headers: DtoHeader[]; 39 | 40 | @CreateDateColumn() 41 | createDate: Date; 42 | 43 | @UpdateDateColumn() 44 | updateDate: Date; 45 | } -------------------------------------------------------------------------------- /api/src/models/project.ts: -------------------------------------------------------------------------------- 1 | import { OneToOne, JoinColumn, OneToMany, Entity, PrimaryColumn, Column, ManyToMany, CreateDateColumn, JoinTable } from 'typeorm'; 2 | import { Collection } from './collection'; 3 | import { LocalhostMapping } from './localhost_mapping'; 4 | import { User } from './user'; 5 | import { Environment } from './environment'; 6 | 7 | @Entity() 8 | export class Project { 9 | 10 | @PrimaryColumn() 11 | id: string; 12 | 13 | @Column() 14 | name: string; 15 | 16 | @JoinTable() 17 | @ManyToMany(_type => User, user => user.projects) 18 | members: User[] = []; 19 | 20 | @JoinTable() 21 | @OneToMany(_type => LocalhostMapping, mapping => mapping.project) 22 | localhosts: LocalhostMapping[]; 23 | 24 | @OneToMany(_type => Collection, collection => collection.project) 25 | collections: Collection[] = []; 26 | 27 | @OneToMany(_type => Environment, environment => environment.project) 28 | environments: Environment[] = []; 29 | 30 | @Column('text', { nullable: true }) 31 | globalFunction: string; 32 | 33 | @Column({ nullable: true }) 34 | note: string; 35 | 36 | @Column({ default: false }) 37 | isMe: boolean; 38 | 39 | @JoinColumn() 40 | @OneToOne(_type => User) 41 | owner: User; 42 | 43 | @CreateDateColumn() 44 | createDate: Date; 45 | } -------------------------------------------------------------------------------- /api/src/models/query_string.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, ManyToOne, PrimaryColumn } from 'typeorm'; 2 | import { Record } from './record'; 3 | import { Mock } from './mock'; 4 | 5 | @Entity() 6 | export class QueryString { 7 | @PrimaryColumn() 8 | id: string; 9 | 10 | @Column({ nullable: true }) 11 | key: string; 12 | 13 | @Column('text', { nullable: true }) 14 | value: string; 15 | 16 | @Column({ default: true }) 17 | isActive: boolean; 18 | 19 | @Column() 20 | sort: number; 21 | 22 | @Column('text', { nullable: true }) 23 | description: string; 24 | 25 | @ManyToOne(_type => Record, record => record.id) 26 | record: Record; 27 | 28 | @ManyToOne(_type => Mock, mock => mock.id) 29 | mock: Mock; 30 | } -------------------------------------------------------------------------------- /api/src/models/record_doc.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryColumn, Column, UpdateDateColumn, CreateDateColumn, OneToOne, OneToMany } from 'typeorm'; 2 | import { Record } from './record'; 3 | import { RecordDocHistory } from './record_doc_history'; 4 | 5 | @Entity() 6 | export class RecordDoc { 7 | 8 | @PrimaryColumn() 9 | id: string; 10 | 11 | @OneToOne(_type => Record, record => record.doc) 12 | record: Record; 13 | 14 | @OneToMany(_type => RecordDocHistory, history => history.target) 15 | history: RecordDocHistory[]; 16 | 17 | @Column({ nullable: true }) 18 | version: number; // TODO: need increase for each changing 19 | 20 | @CreateDateColumn() 21 | createDate: Date; 22 | 23 | @UpdateDateColumn() 24 | updateDate: Date; 25 | } -------------------------------------------------------------------------------- /api/src/models/record_doc_history.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, ManyToOne } from 'typeorm'; 2 | import { RecordDoc } from './record_doc'; 3 | import { User } from './user'; 4 | 5 | @Entity() 6 | export class RecordDocHistory { 7 | 8 | @PrimaryGeneratedColumn() 9 | id: number; 10 | 11 | @ManyToOne(_type => RecordDoc, doc => doc.history) 12 | target: RecordDoc; 13 | 14 | @Column('json') 15 | doc: RecordDoc; 16 | 17 | @ManyToOne(_type => User) 18 | user: User; 19 | 20 | @CreateDateColumn() 21 | createDate: Date; 22 | } -------------------------------------------------------------------------------- /api/src/models/record_history.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, ManyToOne, JoinColumn } from 'typeorm'; 2 | import { Record } from './record'; 3 | import { User } from './user'; 4 | 5 | @Entity() 6 | export class RecordHistory { 7 | 8 | @PrimaryGeneratedColumn() 9 | id: number; 10 | 11 | @ManyToOne(_type => Record, record => record.history) 12 | target: Record; 13 | 14 | @Column('json') 15 | record: Record; 16 | 17 | @ManyToOne(_type => User) 18 | @JoinColumn({ name: 'userId' }) 19 | user: User; 20 | 21 | @Column({ nullable: true }) 22 | userId: string; 23 | 24 | @CreateDateColumn() 25 | createDate: Date; 26 | } -------------------------------------------------------------------------------- /api/src/models/schedule_record.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, CreateDateColumn } from 'typeorm'; 2 | import { Schedule } from './schedule'; 3 | import { RunResult } from '../common/interfaces/dto_run_result'; 4 | 5 | @Entity() 6 | export class ScheduleRecord { 7 | 8 | @PrimaryGeneratedColumn() 9 | id: number; 10 | 11 | @ManyToOne(_type => Schedule, schedule => schedule.scheduleRecords) 12 | schedule: Schedule; 13 | 14 | @Column() 15 | duration: number; 16 | 17 | @Column('json') 18 | result: { origin: Array>, compare: Array> }; 19 | 20 | @Column() 21 | success: boolean; 22 | 23 | @Column() 24 | isScheduleRun: boolean; 25 | 26 | @Column({ default: () => `'1949-10-01'` }) 27 | runDate: Date; 28 | 29 | @CreateDateColumn() 30 | createDate: Date; 31 | } -------------------------------------------------------------------------------- /api/src/models/stress.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryColumn, Column, UpdateDateColumn, CreateDateColumn, OneToMany } from 'typeorm'; 2 | import { NotificationMode } from '../common/enum/notification_mode'; 3 | import { StressRecord } from './stress_record'; 4 | 5 | @Entity() 6 | export class Stress { 7 | 8 | @PrimaryColumn() 9 | id: string; 10 | 11 | @Column() 12 | name: string; 13 | 14 | @Column() 15 | collectionId: string; 16 | 17 | @Column({ nullable: true }) 18 | environmentId: string; 19 | 20 | @Column() 21 | concurrencyCount: number; 22 | 23 | @Column() 24 | repeat: number; 25 | 26 | @Column() 27 | qps: number; 28 | 29 | @Column() 30 | timeout: number; 31 | 32 | @Column() 33 | keepAlive: boolean; 34 | 35 | @Column('json') 36 | requests: string[]; 37 | 38 | @Column('int', { default: 2 }) 39 | notification: NotificationMode; 40 | 41 | @Column() 42 | emails: string; 43 | 44 | @Column() 45 | ownerId: string; 46 | 47 | @OneToMany(_type => StressRecord, stressRecord => stressRecord.stress) 48 | stressRecords: StressRecord[]; 49 | 50 | @Column({ nullable: true }) 51 | lastRunDate: Date; 52 | 53 | @CreateDateColumn() 54 | createDate: Date; 55 | 56 | @UpdateDateColumn() 57 | updateDate: Date; 58 | } -------------------------------------------------------------------------------- /api/src/models/stress_failed_info.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; 2 | 3 | @Entity() 4 | export class StressFailedInfo { 5 | 6 | @PrimaryGeneratedColumn() 7 | id: number; 8 | 9 | @Column('longtext') 10 | info: string; 11 | } -------------------------------------------------------------------------------- /api/src/models/stress_record.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, CreateDateColumn } from 'typeorm'; 2 | import { Stress } from './stress'; 3 | import { StressRunResult } from '../common/interfaces/dto_stress_setting'; 4 | 5 | @Entity() 6 | export class StressRecord { 7 | 8 | @PrimaryGeneratedColumn() 9 | id: number; 10 | 11 | @ManyToOne(_type => Stress, stress => stress.stressRecords) 12 | stress: Stress; 13 | 14 | @Column('json') 15 | result: StressRunResult; 16 | 17 | @CreateDateColumn() 18 | createDate: Date; 19 | } -------------------------------------------------------------------------------- /api/src/models/user.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryColumn, Column, UpdateDateColumn, CreateDateColumn, ManyToMany } from 'typeorm'; 2 | import { Project } from './project'; 3 | 4 | @Entity() 5 | export class User { 6 | 7 | @PrimaryColumn() 8 | id: string; 9 | 10 | @Column() 11 | name: string; 12 | 13 | @Column() 14 | password: string; 15 | 16 | @Column() 17 | email: string; 18 | 19 | @ManyToMany(_type => Project, project => project.members) 20 | projects: Project[] = []; 21 | 22 | @Column() 23 | isActive: boolean; 24 | 25 | @Column({ default: false }) 26 | isTemp: boolean; 27 | 28 | @CreateDateColumn() 29 | createDate: Date; 30 | 31 | @UpdateDateColumn() 32 | updateDate: Date; 33 | } -------------------------------------------------------------------------------- /api/src/models/variable.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryColumn, Column, ManyToOne } from 'typeorm'; 2 | import { Environment } from './environment'; 3 | 4 | @Entity() 5 | export class Variable { 6 | @PrimaryColumn() 7 | id: string; 8 | 9 | @Column({ nullable: true }) 10 | key: string; 11 | 12 | @Column('text', { nullable: true }) 13 | value: string; 14 | 15 | @Column({ default: false }) 16 | isActive: boolean; 17 | 18 | @Column('int') 19 | sort: number; 20 | 21 | @ManyToOne(_type => Environment, env => env.variables) 22 | environment: Environment; 23 | 24 | } -------------------------------------------------------------------------------- /api/src/run_engine/process/base_process_handler.ts: -------------------------------------------------------------------------------- 1 | import * as childProcess from 'child_process'; 2 | 3 | export abstract class BaseProcessHandler { 4 | 5 | process: childProcess.ChildProcess; 6 | 7 | call: (data?: any) => void; 8 | 9 | abstract handleMessage(msg: any); 10 | 11 | abstract afterProcessCreated(); 12 | } -------------------------------------------------------------------------------- /api/src/run_engine/process/schedule_process.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { Setting } from '../../utils/setting'; 3 | import { Log } from '../../utils/log'; 4 | import { ScheduleRunner } from '../schedule_runner'; 5 | import { ProjectDataService } from '../../services/project_data_service'; 6 | 7 | Log.init(); 8 | 9 | process.on('uncaughtException', (err) => { 10 | Log.error(err); 11 | }); 12 | 13 | process.on('message', (msg) => { 14 | if (msg === 'start') { 15 | startScheduleProcess(); 16 | } else if (msg === 'reload_project_data') { 17 | Log.info('schedule: reload libs'); 18 | ProjectDataService.instance.reload(); 19 | } 20 | }); 21 | 22 | function startScheduleProcess() { 23 | new ScheduleRunner().run(); 24 | setInterval(() => { 25 | new ScheduleRunner().run(); 26 | }, Setting.instance.scheduleDuration * 1000); 27 | } -------------------------------------------------------------------------------- /api/src/run_engine/process/schedule_process_handler.ts: -------------------------------------------------------------------------------- 1 | import { BaseProcessHandler } from './base_process_handler'; 2 | 3 | export class ScheduleProcessHandler extends BaseProcessHandler { 4 | 5 | handleMessage() { } 6 | 7 | afterProcessCreated() { 8 | this.process.send('start'); 9 | } 10 | 11 | reloadLib() { 12 | this.process.send('reload_project_data'); 13 | } 14 | } -------------------------------------------------------------------------------- /api/src/run_engine/process/stress_nodejs_process_handler.ts: -------------------------------------------------------------------------------- 1 | import { BaseProcessHandler } from './base_process_handler'; 2 | 3 | export class StressNodejsProcessHandler extends BaseProcessHandler { 4 | 5 | handleMessage() { } 6 | 7 | afterProcessCreated() { } 8 | } -------------------------------------------------------------------------------- /api/src/run_engine/process/stress_nodejs_worker_handler.ts: -------------------------------------------------------------------------------- 1 | import { BaseProcessHandler } from './base_process_handler'; 2 | import { StressMessageType } from '../../common/enum/stress_type'; 3 | import { Log } from '../../utils/log'; 4 | 5 | export class StressNodejsWorkerHandler extends BaseProcessHandler { 6 | 7 | isFinish: boolean; 8 | 9 | handleMessage(msg: any) { 10 | Log.info(`stress nodejs worker handle msg`); 11 | if (msg === 'ready') { 12 | this.process.send({ type: StressMessageType.start }); 13 | } else if (msg === 'finish' || msg === 'error') { 14 | this.isFinish = true; 15 | } 16 | 17 | if (this.call) { 18 | this.call(msg); 19 | } 20 | } 21 | 22 | afterProcessCreated() { 23 | Log.info(`stress nodejs worker process created`); 24 | } 25 | } -------------------------------------------------------------------------------- /api/src/run_engine/process/stress_process_handler.ts: -------------------------------------------------------------------------------- 1 | import { BaseProcessHandler } from './base_process_handler'; 2 | import { StressResponse } from '../../common/interfaces/dto_stress_setting'; 3 | import { StressMessageType } from '../../common/enum/stress_type'; 4 | import { Log } from '../../utils/log'; 5 | import { StressRequest } from '../../interfaces/stress_case_info'; 6 | 7 | export class StressProcessHandler extends BaseProcessHandler { 8 | 9 | private stressHandlers: _.Dictionary<(data: StressResponse) => void> = {}; 10 | 11 | handleMessage(msg: any) { 12 | if (this.stressHandlers[msg.id]) { 13 | this.stressHandlers[msg.id](msg.data); 14 | } 15 | } 16 | 17 | afterProcessCreated() { 18 | this.process.send({ type: StressMessageType.start }); 19 | } 20 | 21 | initStressUser(id: string, dataHandler: (data: StressResponse) => void) { 22 | this.stressHandlers[id] = dataHandler; 23 | this.process.send({ type: StressMessageType.init, id }); 24 | } 25 | 26 | closeStressUser(id: string) { 27 | Reflect.deleteProperty(this.stressHandlers, id); 28 | this.process.send({ type: StressMessageType.close, id }); 29 | } 30 | 31 | sendStressTask(request: StressRequest) { 32 | Log.info('send stress test task.'); 33 | this.process.send(request); 34 | } 35 | 36 | stopStressTask(id: string) { 37 | Log.info('stop stress test task.'); 38 | this.process.send({ type: StressMessageType.stop, id }); 39 | } 40 | } -------------------------------------------------------------------------------- /api/src/services/base/request_import.ts: -------------------------------------------------------------------------------- 1 | import { Record } from '../../models/record'; 2 | import { SwaggerImport } from '../importer/swagger_import'; 3 | import { ImportType } from '../../common/enum/string_type'; 4 | import { User } from '../../models/user'; 5 | import { PostmanImport } from '../importer/postman_import'; 6 | 7 | export interface RequestImport { 8 | 9 | convert(target: T, collectionId: string): Record; 10 | } 11 | 12 | export interface RequestsImport { 13 | 14 | import(data: any, projectId: string, user: User): Promise; 15 | } 16 | 17 | export class Importer { 18 | 19 | static async do(data: any, projectId: string, user: User): Promise { 20 | const type = Importer.getType(data); 21 | await Importer.get(type).import(data, projectId, user); 22 | } 23 | 24 | static get(type: ImportType): RequestsImport { 25 | switch (type) { 26 | case 'swagger': 27 | return new SwaggerImport(); 28 | case 'postman': 29 | return new PostmanImport(); 30 | default: 31 | throw new Error(`not support this type: ${type}`); 32 | } 33 | } 34 | 35 | static getType(data: any): ImportType { 36 | if (data.swagger) { 37 | return 'swagger'; 38 | } else { 39 | return 'postman'; 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /api/src/services/base/web_socket_handler.ts: -------------------------------------------------------------------------------- 1 | import * as WS from 'ws'; 2 | import { Log } from '../../utils/log'; 3 | 4 | export abstract class WebSocketHandler { 5 | 6 | protected socket: WS; 7 | 8 | handle = (socket: WS) => { 9 | this.socket = socket; 10 | this.init(); 11 | socket.on('message', data => this.onReceive(data as string)); 12 | socket.on('close', () => this.onClose()); 13 | } 14 | 15 | init() { 16 | Log.info('ws init'); 17 | } 18 | 19 | abstract onReceive(data: string); 20 | 21 | abstract onClose(); 22 | 23 | send = (data: string) => { 24 | this.socket.send(data); 25 | } 26 | 27 | close = (data?: string) => { 28 | this.socket.close(1000, data); 29 | } 30 | } -------------------------------------------------------------------------------- /api/src/services/console_message.ts: -------------------------------------------------------------------------------- 1 | import { ConsoleMsg } from '../common/interfaces/dto_res'; 2 | 3 | export class ConsoleMessage { 4 | 5 | private valid: boolean; 6 | 7 | private msgs: ConsoleMsg[] = []; 8 | 9 | get messages() { 10 | return this.msgs; 11 | } 12 | 13 | static create(valid: boolean) { 14 | var cm = new ConsoleMessage(); 15 | cm.valid = valid; 16 | return cm; 17 | } 18 | 19 | push(message: string, type: string = 'info', force?: boolean) { 20 | if (force || this.valid) { 21 | this.msgs.push({ time: new Date(), message, type, custom: false }); 22 | } 23 | } 24 | 25 | pushArray(msgs: ConsoleMsg[], _isCustom?: boolean, force?: boolean) { 26 | if (force || this.valid) { 27 | this.msgs.push(...msgs); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /api/src/services/form_data_service.ts: -------------------------------------------------------------------------------- 1 | import { ConnectionManager } from './connection_manager'; 2 | import { StringUtil } from '../utils/string_util'; 3 | import { DtoBodyFormData } from '../common/interfaces/dto_variable'; 4 | import { BodyFormData } from '../models/body_form_data'; 5 | 6 | export class FormDataService { 7 | static fromDto(dtoFormData: DtoBodyFormData): BodyFormData { 8 | let formData = new BodyFormData(); 9 | formData.key = dtoFormData.key; 10 | formData.value = dtoFormData.value; 11 | formData.isActive = dtoFormData.isActive; 12 | formData.sort = dtoFormData.sort; 13 | formData.id = dtoFormData.id || StringUtil.generateUID(); 14 | formData.description = dtoFormData.description; 15 | return formData; 16 | } 17 | 18 | static clone(formData: BodyFormData): BodyFormData { 19 | const target = Object.create(formData); 20 | target.id = undefined; 21 | return target; 22 | } 23 | 24 | static async deleteForHost(hostName: string, hostId: string) { 25 | const connection = await ConnectionManager.getInstance(); 26 | await connection.getRepository(BodyFormData) 27 | .createQueryBuilder('formData') 28 | .delete() 29 | .where(`${hostName}Id=:id`, { id: hostId }) 30 | .execute(); 31 | } 32 | } -------------------------------------------------------------------------------- /api/src/services/header_service.ts: -------------------------------------------------------------------------------- 1 | import { Header } from '../models/header'; 2 | import { DtoHeader } from '../common/interfaces/dto_header'; 3 | import { ConnectionManager } from './connection_manager'; 4 | import { StringUtil } from '../utils/string_util'; 5 | 6 | export class HeaderService { 7 | static fromDto(dtoHeader: DtoHeader): Header { 8 | let header = new Header(); 9 | header.key = dtoHeader.key; 10 | header.value = dtoHeader.value; 11 | header.isActive = dtoHeader.isActive; 12 | header.isFav = dtoHeader.isFav; 13 | header.sort = dtoHeader.sort; 14 | header.description = dtoHeader.description; 15 | header.id = dtoHeader.id || StringUtil.generateUID(); 16 | return header; 17 | } 18 | 19 | static clone(header: Header): Header { 20 | const target =
Object.create(header); 21 | target.id = undefined; 22 | return target; 23 | } 24 | 25 | static async deleteForHost(hostName: string, hostId: string) { 26 | const connection = await ConnectionManager.getInstance(); 27 | await connection.getRepository(Header) 28 | .createQueryBuilder('header') 29 | .delete() 30 | .where(`${hostName}Id=:id`, { id: hostId }) 31 | .execute(); 32 | } 33 | } -------------------------------------------------------------------------------- /api/src/services/query_string_service.ts: -------------------------------------------------------------------------------- 1 | import { ConnectionManager } from './connection_manager'; 2 | import { StringUtil } from '../utils/string_util'; 3 | import { DtoQueryString } from '../common/interfaces/dto_variable'; 4 | import { QueryString } from '../models/query_string'; 5 | 6 | export class QueryStringService { 7 | static fromDto(dtoQueryString: DtoQueryString): QueryString { 8 | let queryString = new QueryString(); 9 | queryString.key = dtoQueryString.key; 10 | queryString.value = dtoQueryString.value; 11 | queryString.isActive = dtoQueryString.isActive; 12 | queryString.sort = dtoQueryString.sort; 13 | queryString.id = dtoQueryString.id || StringUtil.generateUID(); 14 | queryString.description = dtoQueryString.description; 15 | return queryString; 16 | } 17 | 18 | static clone(queryString: QueryString): QueryString { 19 | const target = Object.create(queryString); 20 | target.id = undefined; 21 | return target; 22 | } 23 | 24 | static async deleteForHost(hostName: string, hostId: string) { 25 | const connection = await ConnectionManager.getInstance(); 26 | await connection.getRepository(QueryString) 27 | .createQueryBuilder('queryString') 28 | .delete() 29 | .where(`${hostName}Id=:id`, { id: hostId }) 30 | .execute(); 31 | } 32 | } -------------------------------------------------------------------------------- /api/src/services/sample_service.ts: -------------------------------------------------------------------------------- 1 | import { CollectionService } from './collection_service'; 2 | import { User } from '../models/user'; 3 | import { EnvironmentService } from './environment_service'; 4 | import { DtoEnvironment } from '../common/interfaces/dto_environment'; 5 | import { StringUtil } from '../utils/string_util'; 6 | import { RecordService } from './record_service'; 7 | import { PostmanImport } from './importer/postman_import'; 8 | import * as _ from 'lodash'; 9 | 10 | export class SampleService { 11 | 12 | private static sampleCollection: any; 13 | 14 | private static init() { 15 | if (!!SampleService.sampleCollection) { 16 | return; 17 | } 18 | SampleService.sampleCollection = require('../../sample collection.json'); 19 | } 20 | 21 | static async createSampleForUser(owner: User, projectId: string) { 22 | 23 | SampleService.init(); 24 | 25 | const collection = await new PostmanImport().parsePostmanCollectionV1(owner, projectId, _.cloneDeep(SampleService.sampleCollection)); 26 | 27 | await CollectionService.save(collection); 28 | 29 | await Promise.all(collection.records.map(r => RecordService.saveRecordHistory(RecordService.createRecordHistory(r, owner)))); 30 | 31 | const dtoEnv: DtoEnvironment = { id: StringUtil.generateUID(), name: 'Sample Env', project: { id: projectId }, variables: [{ id: StringUtil.generateUID(), key: 'apihost', value: 'http://httpbin.org', isActive: true, sort: 0 }, { id: StringUtil.generateUID(), key: 'string', value: 'test', isActive: true, sort: 1 }] }; 32 | 33 | await EnvironmentService.create(dtoEnv); 34 | } 35 | } -------------------------------------------------------------------------------- /api/src/services/schedule_on_demand_service.ts: -------------------------------------------------------------------------------- 1 | import { ScheduleService } from './schedule_service'; 2 | import { ScheduleRunner } from '../run_engine/schedule_runner'; 3 | import { WebSocketHandler } from './base/web_socket_handler'; 4 | import { Log } from '../utils/log'; 5 | import { Message } from '../utils/message'; 6 | 7 | export class ScheduleOnDemandService extends WebSocketHandler { 8 | 9 | onReceive(data: string) { 10 | Log.info(`receive data: ${data}`); 11 | if (!data) { 12 | this.close('invalid schedule id'); 13 | } 14 | 15 | this.run(data).then(() => this.close()); 16 | } 17 | 18 | onClose() { 19 | Log.info('client close'); 20 | this.close(); 21 | } 22 | 23 | async run(id: string): Promise { 24 | const schedule = await ScheduleService.getById(id); 25 | if (!schedule) { 26 | this.close(Message.get('scheduleNotExist')); 27 | return; 28 | } 29 | 30 | await new ScheduleRunner().runSchedule(schedule, null, false, data => this.send(data)); 31 | } 32 | } -------------------------------------------------------------------------------- /api/src/services/user_variable_manager.ts: -------------------------------------------------------------------------------- 1 | export class UserVariableManager { 2 | 3 | static variables: _.Dictionary = {}; 4 | 5 | static cookies: _.Dictionary<_.Dictionary<_.Dictionary>> = {}; 6 | 7 | static getVariables(vid: string, envId: string) { 8 | if (vid && !UserVariableManager.variables[vid]) { 9 | UserVariableManager.variables[vid] = {}; 10 | } 11 | if (envId && !UserVariableManager.variables[vid][envId]) { 12 | UserVariableManager.variables[vid][envId] = {}; 13 | } 14 | return vid ? UserVariableManager.variables[vid][envId] : {}; 15 | } 16 | 17 | static clearVariables(vid: string) { 18 | if (!vid) { 19 | return; 20 | } 21 | Reflect.deleteProperty(UserVariableManager.variables, vid); 22 | } 23 | 24 | static getCookies(vid: string, envId: string) { 25 | if (vid && !UserVariableManager.cookies[vid]) { 26 | UserVariableManager.cookies[vid] = {}; 27 | } 28 | if (envId && !UserVariableManager.cookies[vid][envId]) { 29 | UserVariableManager.cookies[vid][envId] = {}; 30 | } 31 | return vid ? UserVariableManager.cookies[vid][envId] : {}; 32 | } 33 | 34 | static clearCookies(vid: string) { 35 | if (!vid) { 36 | return; 37 | } 38 | Reflect.deleteProperty(UserVariableManager.cookies, vid); 39 | } 40 | } -------------------------------------------------------------------------------- /api/src/services/web_socket_service.ts: -------------------------------------------------------------------------------- 1 | import * as WS from 'ws'; 2 | import * as http from 'http'; 3 | import { ScheduleOnDemandService } from './schedule_on_demand_service'; 4 | import { WebSocketHandler } from './base/web_socket_handler'; 5 | import { StressTestWSService } from './stress_test_ws_service'; 6 | 7 | export class WebSocketService { 8 | 9 | private wsServer: WS.Server; 10 | 11 | private routes: { [key: string]: { new(): WebSocketHandler } } = {}; 12 | 13 | constructor(server: http.Server) { 14 | this.wsServer = new WS.Server({ server }); 15 | this.use('/schedule', ScheduleOnDemandService); 16 | this.use('/stresstest', StressTestWSService); 17 | } 18 | 19 | use(path: string, handler: { new(): WebSocketHandler }) { 20 | this.routes[path] = handler; 21 | } 22 | 23 | start() { 24 | this.wsServer.on('connection', (socket, req) => { 25 | const route = this.routes[req.url]; 26 | if (!route) { 27 | socket.close(1000, `no handler for ${req.url}`); 28 | return; 29 | } 30 | new route().handle(socket); 31 | }); 32 | } 33 | } -------------------------------------------------------------------------------- /api/src/setup.ts: -------------------------------------------------------------------------------- 1 | import * as Koa from 'koa'; 2 | import 'reflect-metadata'; 3 | import * as KoaRouter from 'koa-router'; 4 | import * as Bodyparser from 'koa-bodyparser'; 5 | import { execSync } from 'child_process'; 6 | import * as fs from 'fs'; 7 | import * as path from 'path'; 8 | import * as KoaStatic from 'koa-static'; 9 | 10 | let app = new Koa(); 11 | const router = new KoaRouter(); 12 | 13 | router.get('/setup/env', (ctx) => { 14 | ctx.body = getPm2Obj().apps[0].env; 15 | }); 16 | 17 | router.post('/setup/env', (ctx) => { 18 | const pm2Obj = getPm2Obj(); 19 | pm2Obj.apps[0].env = ctx.request.body; 20 | pm2Obj.apps[0].script = 'index.js'; 21 | fs.writeFileSync(getPm2File(), JSON.stringify(pm2Obj), { encoding: 'utf8' }); 22 | try { 23 | execSync('pm2 -V', { encoding: 'utf8' }); 24 | } catch (e) { 25 | execSync(`npm install pm2 -g`); 26 | } 27 | const stdout = execSync(`pm2 start ${getPm2File()}`, { encoding: 'utf8' }); 28 | ctx.body = stdout; 29 | }); 30 | 31 | app.use(KoaStatic(path.join(__dirname, 'public'), { gzip: true })) 32 | .use(Bodyparser()) 33 | .use(router.routes()) 34 | .use(router.allowedMethods()); 35 | 36 | app.listen(9527); 37 | 38 | function getPm2File() { 39 | return path.join(__dirname, 'pm2.json'); 40 | } 41 | 42 | function getPm2Obj() { 43 | const pm2Content = fs.readFileSync(getPm2File(), 'utf8'); 44 | return JSON.parse(pm2Content); 45 | } -------------------------------------------------------------------------------- /api/src/utils/func_util.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | import { StringUtil } from './string_util'; 3 | 4 | export class FuncUtil { 5 | 6 | static formatKeyValue(keyValues: { key: string, value: string, isActive: boolean }[]) { 7 | let objs: { [key: string]: string } = {}; 8 | keyValues.forEach(o => { 9 | if (o.isActive) { 10 | objs[o.key] = o.value; 11 | } 12 | }); 13 | return objs; 14 | } 15 | 16 | static restoreKeyValue(obj: { [key: string]: string }, fromDto: (dto: { isActive: boolean, key: string, value: string, id: string, sort: number }) => T) { 17 | const keyValues = []; 18 | _.keys(obj || {}).forEach(k => { 19 | keyValues.push(fromDto({ 20 | isActive: true, 21 | key: k, 22 | value: obj[k], 23 | id: '', 24 | sort: 0 25 | })); 26 | }); 27 | return keyValues; 28 | } 29 | 30 | static adjustAttachs(attachs: T[]) { 31 | if (!attachs) { 32 | return; 33 | } 34 | attachs.forEach((attach, index) => { 35 | attach.id = attach.id || StringUtil.generateUID(); 36 | attach.sort = index; 37 | }); 38 | } 39 | } -------------------------------------------------------------------------------- /api/src/utils/log.ts: -------------------------------------------------------------------------------- 1 | import * as Log4js from 'log4js'; 2 | import { Logger, getLogger } from 'log4js'; 3 | import * as Path from 'path'; 4 | 5 | export class Log { 6 | 7 | private static logger: Logger; 8 | 9 | static init() { 10 | Log4js.configure(Path.join(__dirname, '../../logconfig.json')); 11 | Log.logger = getLogger('default'); 12 | Log.logger.setLevel(Log4js.levels.DEBUG); 13 | } 14 | 15 | static info(info: string) { 16 | Log.logger.info(info); 17 | } 18 | 19 | static debug(debug: string) { 20 | Log.logger.debug(debug); 21 | } 22 | 23 | static warn(warn: string) { 24 | Log.logger.warn(warn); 25 | } 26 | 27 | static error(error: string) { 28 | Log.logger.error(error); 29 | } 30 | } -------------------------------------------------------------------------------- /api/src/utils/message.ts: -------------------------------------------------------------------------------- 1 | import { Setting } from './setting'; 2 | 3 | export class Message { 4 | 5 | static en = require('../locales/en.json'); 6 | 7 | static zh = require('../locales/zh.json'); 8 | 9 | static get(id: string) { 10 | return (this[Setting.instance.appLanguage] || this['en'])[id]; 11 | } 12 | } -------------------------------------------------------------------------------- /api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "emitDecoratorMetadata": true, 5 | "module": "commonjs", 6 | "target": "ES6", 7 | "outDir": "build", 8 | "rootDir": "src", 9 | "sourceMap": true, 10 | "watch": true 11 | }, 12 | "exclude": [ 13 | "node_modules" 14 | ] 15 | } -------------------------------------------------------------------------------- /api/typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "globalDependencies": { 3 | "cookies": "registry:dt/cookies#0.5.1+20160316171810", 4 | "koa": "registry:dt/koa#2.0.0+20161025101853", 5 | "koa-bodyparser": "registry:dt/koa-bodyparser#3.0.0+20160928122441", 6 | "koa-router": "registry:dt/koa-router#7.0.0+20160907102352", 7 | "koa-session-minimal": "registry:dt/koa-session-minimal#3.0.0+20161019121247", 8 | "node": "registry:dt/node#6.0.0+20161121110008" 9 | }, 10 | "dependencies": { 11 | "koa-compose": "registry:npm/koa-compose#3.0.0+20160723033700" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react", 4 | "es2015" 5 | ], 6 | "plugins": [ 7 | [ 8 | "import", 9 | { 10 | "libraryName": "antd", 11 | "style": true 12 | } 13 | ] 14 | ] 15 | } -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | -------------------------------------------------------------------------------- /client/config/env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 4 | // injected into the application via DefinePlugin in Webpack configuration. 5 | 6 | var REACT_APP = /^REACT_APP_/i; 7 | 8 | function getClientEnvironment(publicUrl) { 9 | var raw = Object 10 | .keys(process.env) 11 | .filter(key => REACT_APP.test(key)) 12 | .reduce((env, key) => { 13 | env[key] = process.env[key]; 14 | return env; 15 | }, { 16 | // Useful for determining whether we’re running in production mode. 17 | // Most importantly, it switches React into the correct mode. 18 | 'NODE_ENV': process.env.NODE_ENV || 'development', 19 | // Useful for resolving the correct path to static assets in `public`. 20 | // For example, . 21 | // This should only be used as an escape hatch. Normally you would put 22 | // images into the `src` and `import` them in code to get their paths. 23 | 'PUBLIC_URL': publicUrl 24 | }); 25 | // Stringify all values so we can feed into Webpack DefinePlugin 26 | var stringified = { 27 | 'process.env': Object 28 | .keys(raw) 29 | .reduce((env, key) => { 30 | env[key] = JSON.stringify(raw[key]); 31 | return env; 32 | }, {}) 33 | }; 34 | 35 | return { raw, stringified }; 36 | } 37 | 38 | module.exports = getClientEnvironment; 39 | -------------------------------------------------------------------------------- /client/config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /client/config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | return 'module.exports = ' + JSON.stringify(path.basename(filename)) + ';'; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /client/config/jest/typescriptTransform.js: -------------------------------------------------------------------------------- 1 | // Copyright 2004-present Facebook. All Rights Reserved. 2 | 3 | const fs = require('fs'); 4 | const tsc = require('typescript'); 5 | const tsconfigPath = require('app-root-path').resolve('/tsconfig.json'); 6 | 7 | let compilerConfig = { 8 | module: tsc.ModuleKind.CommonJS, 9 | jsx: tsc.JsxEmit.React, 10 | } 11 | 12 | if (fs.existsSync(tsconfigPath)) { 13 | try { 14 | const tsconfig = require(tsconfigPath); 15 | 16 | if (tsconfig && tsconfig.compilerOptions) { 17 | compilerConfig = tsconfig.compilerOptions; 18 | } 19 | } catch (e) { /* Do nothing - default is set */ } 20 | } 21 | 22 | module.exports = { 23 | process(src, path) { 24 | if (path.endsWith('.ts') || path.endsWith('.tsx')) { 25 | return tsc.transpile( 26 | src, 27 | compilerConfig, 28 | path, [] 29 | ); 30 | } 31 | return src; 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /client/config/polyfills.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (typeof Promise === 'undefined') { 4 | // Rejection tracking prevents a common issue where React gets into an 5 | // inconsistent state due to an error, but it gets swallowed by a Promise, 6 | // and the user has no idea what causes React's erratic future behavior. 7 | require('promise/lib/rejection-tracking').enable(); 8 | window.Promise = require('promise/lib/es6-extensions.js'); 9 | } 10 | 11 | // fetch() polyfill for making API calls. 12 | require('whatwg-fetch'); 13 | 14 | // Object.assign() is commonly used with React. 15 | // It will use the native implementation if it's present and isn't buggy. 16 | Object.assign = require('object-assign'); 17 | -------------------------------------------------------------------------------- /client/config/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('autoprefixer')({ 4 | browsers: [ 5 | '>1%', 6 | 'last 4 versions', 7 | 'Firefox ESR', 8 | 'not ie < 9', // React doesn't support IE8 anyway 9 | ] 10 | }) 11 | ] 12 | } -------------------------------------------------------------------------------- /client/debugwebpack.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var WebpackDevServer = require('webpack-dev-server'); 3 | var config = require('./config/webpack.config.dev'); 4 | 5 | 6 | new WebpackDevServer(webpack(config), { 7 | publicPath: config.output.publicPath, 8 | hot: true, 9 | noInfo: false, 10 | historyApiFallback: true 11 | }).listen(3000, '127.0.0.1', function (err, result) { 12 | if (err) { 13 | console.log(err); 14 | } 15 | console.log('Listening at localhost:3000'); 16 | }); -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brookshi/Hitchhiker/318bc47d4041b32d0e2ffb1716be053abd28b456/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/hitchhiker-title-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brookshi/Hitchhiker/318bc47d4041b32d0e2ffb1716be053abd28b456/client/public/hitchhiker-title-dark.png -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 17 | Hitchhiker API 18 | 21 | 22 | 23 | 24 | 25 |
26 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /client/public/puff.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 19 | 20 | 21 | 28 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /client/scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process.env.NODE_ENV = 'test'; 4 | process.env.PUBLIC_URL = ''; 5 | 6 | // Load environment variables from .env file. Suppress warnings using silent 7 | // if this file is missing. dotenv will never modify any environment variables 8 | // that have already been set. 9 | // https://github.com/motdotla/dotenv 10 | require('dotenv').config({silent: true}); 11 | 12 | const jest = require('jest'); 13 | const argv = process.argv.slice(2); 14 | 15 | // Watch unless on CI or in coverage mode 16 | if (!process.env.CI && argv.indexOf('--coverage') < 0) { 17 | argv.push('--watch'); 18 | } 19 | 20 | 21 | jest.run(argv); 22 | -------------------------------------------------------------------------------- /client/src/action/document.ts: -------------------------------------------------------------------------------- 1 | 2 | export const DocumentActiveRecordType = 'active document record'; 3 | 4 | export const DocumentCollectionOpenKeysType = 'document collection open keys'; 5 | 6 | export const DocumentSelectedProjectChangedType = 'document selected project changed'; 7 | 8 | export const ScrollDocumentType = 'scroll document'; 9 | 10 | export const DocumentActiveCollectionType = 'document active collection'; 11 | 12 | export const DocumentActiveEnvIdType = 'document active env id'; -------------------------------------------------------------------------------- /client/src/action/local_data.ts: -------------------------------------------------------------------------------- 1 | import { takeLatest, call, put } from 'redux-saga/effects'; 2 | import { actionCreator } from './index'; 3 | import LocalStore from '../utils/local_store'; 4 | import { delay } from 'redux-saga'; 5 | 6 | export const FetchLocalDataType = 'fetch local data'; 7 | 8 | export const FetchLocalDataPendingType = 'fetch local data pending'; 9 | 10 | export const FetchLocalDataSuccessType = 'fetch local data success'; 11 | 12 | export const FetchLocalDataFailedType = 'fetch local data failed'; 13 | 14 | export const StoreLocalDataType = 'store local data'; 15 | 16 | export function* fetchLocalData() { 17 | yield takeLatest(FetchLocalDataType, function* (action: any) { 18 | try { 19 | yield put(actionCreator(FetchLocalDataPendingType)); 20 | const state = yield call(LocalStore.getState, action.value); 21 | yield put(actionCreator(FetchLocalDataSuccessType, state)); 22 | } catch (err) { 23 | console.error(err.toString()); 24 | } 25 | }); 26 | } 27 | 28 | export function* storeLocalData() { 29 | yield takeLatest(StoreLocalDataType, function* (action: any) { 30 | try { 31 | yield delay(1000); 32 | const state = action.value.state; 33 | yield call(LocalStore.setState, action.value.userId, { ...state, uiState: { ...state.uiState, syncState: { syncCount: 0, syncItems: [] } } }); 34 | } catch (err) { 35 | console.log(action.value.state); 36 | console.error(err); 37 | } 38 | }); 39 | } -------------------------------------------------------------------------------- /client/src/action/ui.ts: -------------------------------------------------------------------------------- 1 | export const SelectReqTabType = 'select req panel tab'; 2 | 3 | export const SelectResTabType = 'select res panel tab'; 4 | 5 | export const ToggleReqPanelVisibleType = 'toggle req panel visible'; 6 | 7 | export const ResizeResHeightType = 'resize res panel height'; 8 | 9 | export const ResizeLeftPanelType = 'resize left panel'; 10 | 11 | export const UpdateLeftPanelType = 'collapse left panel'; 12 | 13 | export const SwitchHeadersEditModeType = 'switch headers edit mode'; 14 | 15 | export const ShowTimelineType = 'show timeline'; 16 | 17 | export const CloseTimelineType = 'close timeline'; 18 | 19 | export const DisplayQueryStringType = 'display query string'; 20 | 21 | export const ToggleRequestDescriptionType = 'toggle request description'; 22 | 23 | export const BatchCloseType = 'batch close type'; 24 | 25 | export const TableDisplayType = 'table display type'; -------------------------------------------------------------------------------- /client/src/common/enum/data_mode.ts: -------------------------------------------------------------------------------- 1 | export enum DataMode { 2 | 3 | urlencoded = 0, 4 | 5 | raw = 1, 6 | 7 | form = 2, 8 | 9 | binary = 3 10 | } -------------------------------------------------------------------------------- /client/src/common/enum/metadata_type.ts: -------------------------------------------------------------------------------- 1 | export enum MetadataType { 2 | 3 | PostmanAllV1, 4 | 5 | PostmanCollectionV1, 6 | 7 | PostmanCollectionV2 8 | } -------------------------------------------------------------------------------- /client/src/common/enum/mock_mode.ts: -------------------------------------------------------------------------------- 1 | export enum MockMode { 2 | 3 | template = 0, 4 | 5 | nativelData = 1 6 | } -------------------------------------------------------------------------------- /client/src/common/enum/notification_mode.ts: -------------------------------------------------------------------------------- 1 | export enum NotificationMode { 2 | 3 | none = 0, 4 | 5 | me = 1, 6 | 7 | project = 2, 8 | 9 | custom = 3, 10 | } 11 | 12 | export class NotificationStr { 13 | 14 | static none = 'None'; 15 | 16 | static me = 'Me'; 17 | 18 | static project = 'Project Members'; 19 | 20 | static custom = 'Custom'; 21 | 22 | static convert(mode: NotificationMode) { 23 | switch (mode) { 24 | case NotificationMode.none: 25 | return NotificationStr.none; 26 | case NotificationMode.me: 27 | return NotificationStr.me; 28 | case NotificationMode.project: 29 | return NotificationStr.project; 30 | case NotificationMode.custom: 31 | return NotificationStr.custom; 32 | default: 33 | return NotificationStr.none; 34 | } 35 | } 36 | } 37 | 38 | export enum MailMode { 39 | 40 | mailAlways = 0, 41 | 42 | mailWhenFail = 1, 43 | } -------------------------------------------------------------------------------- /client/src/common/enum/parameter_type.ts: -------------------------------------------------------------------------------- 1 | export enum ParameterType { 2 | 3 | ManyToMany = 0, 4 | 5 | OneToOne = 1 6 | } 7 | 8 | export enum ReduceAlgorithmType { 9 | 10 | none = 0, 11 | 12 | pairwise = 1, 13 | 14 | orthogonalArray = 2 15 | } -------------------------------------------------------------------------------- /client/src/common/enum/period.ts: -------------------------------------------------------------------------------- 1 | export enum TimerType { 2 | 3 | Minute = 1, 4 | 5 | Hour = 2, 6 | 7 | Day = 3 8 | } 9 | 10 | export enum Period { 11 | 12 | daily = 1, 13 | 14 | monday = 2, 15 | 16 | tuesday = 3, 17 | 18 | wednesday = 4, 19 | 20 | thursday = 5, 21 | 22 | friday = 6, 23 | 24 | saturday = 7, 25 | 26 | sunday = 8 27 | } -------------------------------------------------------------------------------- /client/src/common/enum/record_category.ts: -------------------------------------------------------------------------------- 1 | export enum RecordCategory { 2 | 3 | folder = 10, 4 | 5 | record = 20, 6 | } -------------------------------------------------------------------------------- /client/src/common/enum/stress_type.ts: -------------------------------------------------------------------------------- 1 | export const noEnvironment = 'No environment'; 2 | 3 | export enum WorkerStatus { 4 | 5 | idle = 0, 6 | 7 | ready = 1, 8 | 9 | working = 2, 10 | 11 | finish = 3, 12 | 13 | down = 4, 14 | 15 | fileReady = 5, 16 | } 17 | 18 | export enum StressMessageType { 19 | 20 | hardware = 0, 21 | 22 | task = 1, 23 | 24 | start = 2, 25 | 26 | runResult = 3, 27 | 28 | stop = 4, 29 | 30 | status = 5, 31 | 32 | fileStart = 6, 33 | 34 | fileFinish = 7, 35 | 36 | init, 37 | 38 | close, 39 | 40 | wait, 41 | 42 | error, 43 | 44 | finish, 45 | 46 | noWorker 47 | } 48 | 49 | export class StressFailedType { 50 | 51 | static noRes = 'noRes'; 52 | 53 | static m500 = 'm500'; 54 | 55 | static testFailed = 'testFailed'; 56 | } -------------------------------------------------------------------------------- /client/src/common/enum/string_type.ts: -------------------------------------------------------------------------------- 1 | export type BodyType = 'json' | 'xml' | 'text'; 2 | 3 | export type ProjectFolderType = 'lib' | 'data'; 4 | 5 | export type ImportType = 'swagger' | 'postman'; -------------------------------------------------------------------------------- /client/src/common/interfaces/dto_assert.ts: -------------------------------------------------------------------------------- 1 | export interface DtoAssert { 2 | 3 | name: string; 4 | 5 | env?: string; 6 | 7 | target: string[]; 8 | 9 | function: string; 10 | 11 | value: string; 12 | } -------------------------------------------------------------------------------- /client/src/common/interfaces/dto_collection.ts: -------------------------------------------------------------------------------- 1 | import { DtoRecord } from './dto_record'; 2 | import { DtoHeader } from './dto_header'; 3 | import * as _ from 'lodash'; 4 | 5 | export interface DtoCollection { 6 | 7 | id: string; 8 | 9 | name: string; 10 | 11 | commonPreScript: string; 12 | 13 | reqStrictSSL?: boolean; 14 | 15 | reqFollowRedirect?: boolean; 16 | 17 | projectId: string; 18 | 19 | commonSetting: DtoCommonSetting; 20 | 21 | description: string; 22 | } 23 | 24 | export interface DtoCommonSetting { 25 | 26 | prescript: string; 27 | 28 | test: string; 29 | 30 | headers: DtoHeader[]; 31 | } 32 | 33 | export interface DtoCollectionWithRecord { 34 | 35 | collections: _.Dictionary; 36 | 37 | records: _.Dictionary<_.Dictionary>; 38 | } -------------------------------------------------------------------------------- /client/src/common/interfaces/dto_environment.ts: -------------------------------------------------------------------------------- 1 | import { DtoVariable } from './dto_variable'; 2 | import { DtoProject } from './dto_project'; 3 | 4 | export interface DtoEnvironment { 5 | 6 | id: string; 7 | 8 | name: string; 9 | 10 | project: Partial; 11 | 12 | variables: DtoVariable[]; 13 | } -------------------------------------------------------------------------------- /client/src/common/interfaces/dto_error.ts: -------------------------------------------------------------------------------- 1 | export interface DtoError { 2 | 3 | message: string; 4 | 5 | stack: string; 6 | } -------------------------------------------------------------------------------- /client/src/common/interfaces/dto_header.ts: -------------------------------------------------------------------------------- 1 | export interface DtoHeader { 2 | 3 | id?: string; 4 | 5 | key?: string; 6 | 7 | value?: string; 8 | 9 | isActive: boolean; 10 | 11 | isFav?: boolean; 12 | 13 | description?: string; 14 | 15 | sort?: number; 16 | } -------------------------------------------------------------------------------- /client/src/common/interfaces/dto_mail.ts: -------------------------------------------------------------------------------- 1 | import { DtoError } from './dto_error'; 2 | 3 | export interface MailScheduleRecord { 4 | 5 | scheduleName: string; 6 | 7 | success: boolean; 8 | 9 | duration: number; 10 | 11 | runResults: MailRunResult[]; 12 | } 13 | 14 | export interface MailRunResult { 15 | 16 | recordName: string; 17 | 18 | parameter: string; 19 | 20 | envName: string; 21 | 22 | tests: { [key: string]: boolean }; 23 | 24 | duration: number; 25 | 26 | error?: DtoError; 27 | 28 | isSuccess: boolean; 29 | } -------------------------------------------------------------------------------- /client/src/common/interfaces/dto_mock.ts: -------------------------------------------------------------------------------- 1 | import { MockMode } from '../enum/mock_mode'; 2 | import { DtoBaseItem } from './dto_record'; 3 | 4 | export interface DtoMock extends DtoBaseItem { 5 | 6 | mode: MockMode; 7 | 8 | res?: string; 9 | } -------------------------------------------------------------------------------- /client/src/common/interfaces/dto_mock_collection.ts: -------------------------------------------------------------------------------- 1 | import { DtoHeader } from './dto_header'; 2 | import * as _ from 'lodash'; 3 | import { DtoMock } from './dto_mock'; 4 | 5 | export interface DtoMockCollection { 6 | 7 | id: string; 8 | 9 | name: string; 10 | 11 | projectId: string; 12 | 13 | headers: DtoHeader[]; 14 | 15 | description: string; 16 | } 17 | 18 | export interface DtoCollectionWithMock { 19 | 20 | collections: _.Dictionary; 21 | 22 | mocks: _.Dictionary<_.Dictionary>; 23 | } -------------------------------------------------------------------------------- /client/src/common/interfaces/dto_project.ts: -------------------------------------------------------------------------------- 1 | import { DtoUser } from './dto_user'; 2 | import { DtoEnvironment } from './dto_environment'; 3 | 4 | export interface LocalhostMapping { 5 | 6 | id: string; 7 | 8 | userId: string; 9 | 10 | ip: string; 11 | } 12 | 13 | export interface DtoProject { 14 | 15 | id: string; 16 | 17 | name: string; 18 | 19 | note?: string; 20 | 21 | members?: DtoUser[]; 22 | 23 | localhosts?: Partial[]; 24 | 25 | environments?: DtoEnvironment[]; 26 | 27 | globalFunction?: string; 28 | 29 | isMe?: boolean; 30 | 31 | owner: Partial; 32 | 33 | createDate?: Date; 34 | } -------------------------------------------------------------------------------- /client/src/common/interfaces/dto_project_data.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | 3 | export interface ProjectData { 4 | 5 | name: string; 6 | 7 | path: string; 8 | 9 | size: number; 10 | 11 | createdDate: Date; 12 | } 13 | 14 | export interface ProjectFiles { 15 | 16 | globalJS: _.Dictionary; 17 | 18 | projectJS: _.Dictionary<_.Dictionary>; 19 | 20 | globalData: _.Dictionary; 21 | 22 | projectData: _.Dictionary<_.Dictionary>; 23 | } -------------------------------------------------------------------------------- /client/src/common/interfaces/dto_project_quit.ts: -------------------------------------------------------------------------------- 1 | export class DtoProjectQuit { 2 | 3 | projectId: string; 4 | 5 | userId: string; 6 | } -------------------------------------------------------------------------------- /client/src/common/interfaces/dto_record.ts: -------------------------------------------------------------------------------- 1 | import { DtoHeader } from './dto_header'; 2 | import { RecordCategory } from '../enum/record_category'; 3 | import { BodyType } from '../enum/string_type'; 4 | import { DtoUser } from './dto_user'; 5 | import { ParameterType, ReduceAlgorithmType } from '../enum/parameter_type'; 6 | import { DtoAssert } from './dto_assert'; 7 | import { DtoQueryString, DtoBodyFormData } from './dto_variable'; 8 | import { DataMode } from '../enum/data_mode'; 9 | import * as _ from 'lodash'; 10 | 11 | export interface DtoBaseItem { 12 | 13 | id: string; 14 | 15 | collectionId: string; 16 | 17 | pid?: string; 18 | 19 | category: RecordCategory; 20 | 21 | name: string; 22 | 23 | url?: string; 24 | 25 | method?: string; 26 | 27 | queryStrings?: DtoQueryString[]; 28 | 29 | formDatas?: DtoBodyFormData[]; 30 | 31 | headers?: DtoHeader[]; 32 | 33 | body?: string; 34 | 35 | dataMode?: DataMode; 36 | 37 | sort?: number; 38 | 39 | description?: string; 40 | 41 | children?: DtoBaseItem[]; 42 | } 43 | 44 | export interface DtoRecord extends DtoBaseItem { 45 | 46 | history?: DtoRecordHistory[]; 47 | 48 | assertInfos?: _.Dictionary; 49 | 50 | bodyType?: BodyType; 51 | 52 | parameters?: string; 53 | 54 | reduceAlgorithm?: ReduceAlgorithmType; 55 | 56 | parameterType: ParameterType; 57 | 58 | prescript?: string; 59 | 60 | test?: string; 61 | } 62 | 63 | export interface DtoRecordHistory { 64 | 65 | id: number; 66 | 67 | record: DtoRecord; 68 | 69 | user: DtoUser; 70 | 71 | createDate: Date; 72 | } -------------------------------------------------------------------------------- /client/src/common/interfaces/dto_record_run.ts: -------------------------------------------------------------------------------- 1 | import { DtoRecord } from './dto_record'; 2 | 3 | export interface DtoRecordRun { 4 | 5 | environment: string; 6 | 7 | record: DtoRecord; 8 | } -------------------------------------------------------------------------------- /client/src/common/interfaces/dto_record_sort.ts: -------------------------------------------------------------------------------- 1 | export interface DtoRecordSort { 2 | 3 | recordId: string; 4 | 5 | folderId: string; 6 | 7 | collectionId: string; 8 | 9 | newSort: number; 10 | } -------------------------------------------------------------------------------- /client/src/common/interfaces/dto_res.ts: -------------------------------------------------------------------------------- 1 | import { DtoProject } from './dto_project'; 2 | 3 | export interface DtoResUser { 4 | 5 | id: string; 6 | 7 | name: string; 8 | 9 | password: string; 10 | 11 | email: string; 12 | 13 | projects: DtoProject[]; 14 | 15 | isActive: boolean; 16 | 17 | isTemp: boolean; 18 | 19 | createDate: Date; 20 | 21 | updateDate: Date; 22 | } 23 | 24 | export interface ConsoleMsg { 25 | 26 | time: Date; 27 | 28 | type: 'log' | 'info' | 'warn' | 'error' | any; 29 | 30 | message: string; 31 | 32 | custom: boolean; 33 | } -------------------------------------------------------------------------------- /client/src/common/interfaces/dto_run_result.ts: -------------------------------------------------------------------------------- 1 | import { DtoError } from './dto_error'; 2 | import { Duration } from './dto_stress_setting'; 3 | import { ConsoleMsg } from './dto_res'; 4 | 5 | export interface RunResult { 6 | 7 | id: string; 8 | 9 | envId: string; 10 | 11 | param?: string; 12 | 13 | error?: DtoError; 14 | 15 | body: any; 16 | 17 | tests: { [key: string]: boolean }; 18 | 19 | variables: {}; 20 | 21 | export: {}; 22 | 23 | status: number; 24 | 25 | statusMessage: string; 26 | 27 | elapsed: number; 28 | 29 | duration?: Duration; 30 | 31 | headers: { [key: string]: string | string[] }; 32 | 33 | cookies: string[]; 34 | 35 | host: string; 36 | 37 | consoleMsgQueue: ConsoleMsg[]; 38 | } -------------------------------------------------------------------------------- /client/src/common/interfaces/dto_schedule.ts: -------------------------------------------------------------------------------- 1 | import { DtoScheduleRecord } from './dto_schedule_record'; 2 | import { Period, TimerType } from '../enum/period'; 3 | import { NotificationMode, MailMode } from '../enum/notification_mode'; 4 | 5 | export interface DtoSchedule { 6 | 7 | id: string; 8 | 9 | name: string; 10 | 11 | collectionId: string; 12 | 13 | environmentId: string; 14 | 15 | needCompare: boolean; 16 | 17 | compareEnvironmentId: string; 18 | 19 | timer: TimerType; 20 | 21 | period: Period; 22 | 23 | hour: number; 24 | 25 | notification: NotificationMode; 26 | 27 | emails?: string; 28 | 29 | mailMode: MailMode; 30 | 31 | mailIncludeSuccessReq: boolean; 32 | 33 | needOrder: boolean; 34 | 35 | recordsOrder: string; 36 | 37 | suspend: boolean; 38 | 39 | scheduleRecords: DtoScheduleRecord[]; 40 | 41 | ownerId: string; 42 | 43 | lastRunDate?: Date; 44 | 45 | recordCount: number; 46 | } -------------------------------------------------------------------------------- /client/src/common/interfaces/dto_schedule_record.ts: -------------------------------------------------------------------------------- 1 | import { RunResult } from './dto_run_result'; 2 | import * as _ from 'lodash'; 3 | 4 | export interface DtoScheduleRecord { 5 | 6 | id: number; 7 | 8 | scheduleId: string; 9 | 10 | duration: number; 11 | 12 | result: { origin: Array>, compare: Array> }; 13 | 14 | success: boolean; 15 | 16 | isScheduleRun: boolean; 17 | 18 | runDate: Date; 19 | 20 | createDate: Date; 21 | } -------------------------------------------------------------------------------- /client/src/common/interfaces/dto_stress.ts: -------------------------------------------------------------------------------- 1 | import { NotificationMode } from '../enum/notification_mode'; 2 | import { DtoStressRecord } from './dto_stress_record'; 3 | 4 | export interface DtoStress { 5 | 6 | id: string; 7 | 8 | name: string; 9 | 10 | collectionId: string; 11 | 12 | environmentId: string; 13 | 14 | concurrencyCount: number; 15 | 16 | repeat: number; 17 | 18 | qps: number; 19 | 20 | timeout: number; 21 | 22 | keepAlive: boolean; 23 | 24 | requests: string[]; 25 | 26 | notification: NotificationMode; 27 | 28 | emails?: string; 29 | 30 | ownerId: string; 31 | 32 | stressRecords: DtoStressRecord[]; 33 | 34 | lastRunDate?: Date; 35 | } -------------------------------------------------------------------------------- /client/src/common/interfaces/dto_stress_record.ts: -------------------------------------------------------------------------------- 1 | import { StressRunResult } from './dto_stress_setting'; 2 | 3 | export interface DtoStressRecord { 4 | 5 | id: number; 6 | 7 | stressId: string; 8 | 9 | result: StressRunResult; 10 | 11 | createDate: Date; 12 | } -------------------------------------------------------------------------------- /client/src/common/interfaces/dto_user.ts: -------------------------------------------------------------------------------- 1 | export interface DtoUser { 2 | 3 | id: string; 4 | 5 | name: string; 6 | 7 | email: string; 8 | 9 | password: string; 10 | } -------------------------------------------------------------------------------- /client/src/common/interfaces/dto_variable.ts: -------------------------------------------------------------------------------- 1 | export interface DtoVariable { 2 | 3 | id: string; 4 | 5 | key?: string; 6 | 7 | value?: string; 8 | 9 | isActive: boolean; 10 | 11 | sort: number; 12 | } 13 | 14 | export interface DtoQueryString extends DtoVariable { 15 | 16 | description?: string; 17 | } 18 | 19 | export interface DtoBodyFormData extends DtoVariable { 20 | 21 | description?: string; 22 | } -------------------------------------------------------------------------------- /client/src/common/interfaces/password.ts: -------------------------------------------------------------------------------- 1 | export interface Password { 2 | 3 | oldPassword: string; 4 | 5 | newPassword: string; 6 | } -------------------------------------------------------------------------------- /client/src/common/interfaces/postman_v1.ts: -------------------------------------------------------------------------------- 1 | import { DtoRecord } from './dto_record'; 2 | import { DtoEnvironment } from './dto_environment'; 3 | import { DtoVariable } from './dto_variable'; 4 | import { DataMode } from '../enum/data_mode'; 5 | 6 | export interface PostmanAllV1 { 7 | 8 | collections: PostmanCollectionV1[]; 9 | 10 | environments: PostmanEnvironments[]; 11 | } 12 | 13 | export interface PostmanCollectionV1 { 14 | 15 | id: string; 16 | 17 | name: string; 18 | 19 | description: string; 20 | 21 | commonPreScript: string; 22 | 23 | folders: PostmanRecord[]; 24 | 25 | requests: PostmanRecord[]; 26 | } 27 | 28 | export interface PostmanRecord extends DtoRecord { 29 | 30 | tests: string; 31 | 32 | folder: string; 33 | 34 | rawModeData: string; 35 | 36 | dataMode: DataMode & string; 37 | 38 | data: any; 39 | 40 | preRequestScript: string; 41 | 42 | order: string[]; 43 | } 44 | 45 | export interface PostmanEnvironments extends DtoEnvironment { 46 | 47 | values: PostmanEnv[]; 48 | } 49 | 50 | export interface PostmanEnv extends DtoVariable { 51 | 52 | enabled: boolean; 53 | 54 | type: string; 55 | } -------------------------------------------------------------------------------- /client/src/common/link.bat: -------------------------------------------------------------------------------- 1 | mklink /D "%~dp0..\..\..\api\src\common" "%~dp0..\common" 2 | pause -------------------------------------------------------------------------------- /client/src/common/link.sh: -------------------------------------------------------------------------------- 1 | work_path=$(dirname $(readlink -f $0)) 2 | ln -sf "${work_path}..\..\..\api\src\common" "${work_path}..\common" 3 | -------------------------------------------------------------------------------- /client/src/common/utils/date_util.ts: -------------------------------------------------------------------------------- 1 | export class DateUtil { 2 | 3 | static readonly MINUTE = 60 * 1000; 4 | 5 | static readonly HOUR = 60 * DateUtil.MINUTE; 6 | 7 | static readonly DAY = 24 * DateUtil.HOUR; 8 | 9 | static diff(start: Date, end: Date, unit: 'h' | 'm' = 'h', offset: number = 0): number { 10 | const timeDiff = Math.abs(end.getTime() - start.getTime() + offset); 11 | return parseInt((timeDiff / (unit === 'h' ? DateUtil.HOUR : DateUtil.MINUTE)) + ''); 12 | } 13 | 14 | static getUTCDate(date?: Date): Date { 15 | date = date || new Date(); 16 | return new Date( 17 | date.getUTCFullYear(), 18 | date.getUTCMonth(), 19 | date.getUTCDate(), 20 | date.getUTCHours(), 21 | date.getUTCMinutes(), 22 | date.getUTCSeconds(), 23 | date.getUTCMilliseconds() 24 | ); 25 | } 26 | } -------------------------------------------------------------------------------- /client/src/common/utils/math_util.ts: -------------------------------------------------------------------------------- 1 | export class MathUtil { 2 | 3 | static distribute(total: number, cores: number): number[] { 4 | const result: number[] = []; 5 | let distributed = 0; 6 | for (let i = 0; i < cores; i++) { 7 | const max = parseInt((total * (i + 1) / cores) + ''); 8 | const current = i === 0 ? max : max - distributed; 9 | distributed += current; 10 | result.push(current); 11 | } 12 | return result; 13 | } 14 | 15 | static stddev(data: number[]): number { 16 | if (!data || data.length === 0) { 17 | return 0; 18 | } 19 | var mean = data.reduce((p, c) => p + c, 0) / data.length; 20 | var deviations = data.map(x => x - mean); 21 | return Math.sqrt(deviations.map(x => x * x).reduce((p, c) => p + c, 0) / data.length); 22 | } 23 | } -------------------------------------------------------------------------------- /client/src/components/assert_json_view/assert_funcs.ts: -------------------------------------------------------------------------------- 1 | export type AssertType = 'array' | 'string' | 'number' | 'boolean'; 2 | 3 | export const arrayFuncs = ['length >', 'length ==', 'length <', 'includes', 'every', 'some', 'custom']; 4 | 5 | export const stringFuncs = ['length >', 'length ==', 'length <', '==', 'includes', 'startsWith', 'endsWith', 'custom']; 6 | 7 | export const numberFuncs = ['>', '>=', '==', '<', '<=']; 8 | 9 | export const booleanFuncs = ['true', 'false']; 10 | 11 | export const AssertTypeFuncMapping = { 12 | array: arrayFuncs, 13 | string: stringFuncs, 14 | number: numberFuncs, 15 | boolean: booleanFuncs 16 | }; -------------------------------------------------------------------------------- /client/src/components/code_snippet_dialog/style/index.less: -------------------------------------------------------------------------------- 1 | 2 | .codesnippet-dropdownicon{ 3 | float: right; 4 | margin-top: 4px; 5 | } 6 | 7 | .codesnippet-dropdownbtn{ 8 | width: 200px; 9 | text-align: left; 10 | } 11 | 12 | .codesnippet-toolbar{ 13 | margin-bottom: 8px; 14 | } 15 | 16 | .codesnippet-copy{ 17 | float: right; 18 | } -------------------------------------------------------------------------------- /client/src/components/confirm_dialog/index.ts: -------------------------------------------------------------------------------- 1 | import { Modal } from 'antd'; 2 | import LocalesString from '../../locales/string'; 3 | 4 | export function confirmDlg(title: string | React.ReactNode, onOk: (func: Function) => any, action: string | React.ReactNode = 'delete') { 5 | Modal.confirm({ 6 | title, 7 | content: LocalesString.get('Common.ConfirmContent', { action }), 8 | okText: 'Yes', 9 | cancelText: 'No', 10 | onOk, 11 | }); 12 | } -------------------------------------------------------------------------------- /client/src/components/diff_dialog/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Diff2Html } from 'diff2html'; 3 | import 'diff2html/dist/diff2html.css'; 4 | import { Modal } from 'antd'; 5 | import * as JsDiff from 'diff'; 6 | import './style/index.less'; 7 | 8 | interface DiffDialogProps { 9 | 10 | title: string | React.ReactNode; 11 | 12 | isOpen?: boolean; 13 | 14 | originContent: string; 15 | 16 | originTitle: string; 17 | 18 | targetContent: string; 19 | 20 | targetTitle: string; 21 | 22 | onClose(); 23 | } 24 | 25 | interface DiffDialogState { 26 | 27 | } 28 | 29 | class DiffDialog extends React.Component { 30 | 31 | public render() { 32 | 33 | const { title, isOpen, onClose, originTitle, targetTitle, originContent, targetContent } = this.props; 34 | const diffContent = JsDiff.createTwoFilesPatch(originTitle || '', targetTitle || '', originContent || '', targetContent || '', '', ''); 35 | const diffHtml = Diff2Html.getPrettyHtml(diffContent, { inputFormat: 'diff', outputFormat: 'side-by-side', matching: 'lines' }); 36 | 37 | return ( 38 | 48 |
49 | 50 | ); 51 | } 52 | } 53 | 54 | export default DiffDialog; -------------------------------------------------------------------------------- /client/src/components/diff_dialog/style/index.less: -------------------------------------------------------------------------------- 1 | 2 | .d2h-file-name-wrapper{ 3 | .d2h-tag{ 4 | display: none; 5 | } 6 | } 7 | 8 | -------------------------------------------------------------------------------- /client/src/components/editable_cell/style/index.less: -------------------------------------------------------------------------------- 1 | .editable-cell { 2 | position: relative; 3 | } 4 | 5 | .editable-cell-input-wrapper { 6 | padding-right: 24px; 7 | } 8 | 9 | .editable-cell-text-wrapper { 10 | padding: 5px 24px 5px 5px; 11 | } 12 | 13 | .editable-cell-icon, 14 | .editable-cell-icon-check { 15 | position: absolute; 16 | right: 0; 17 | width: 20px; 18 | cursor: pointer; 19 | } 20 | 21 | .editable-cell-icon-check { 22 | line-height: 28px; 23 | } 24 | 25 | .editable-cell:hover .editable-cell-icon { 26 | display: inline-block; 27 | } 28 | 29 | .editable-cell-icon:hover, 30 | .editable-cell-icon-check:hover { 31 | color: #108ee9; 32 | } -------------------------------------------------------------------------------- /client/src/components/editor/style/index.less: -------------------------------------------------------------------------------- 1 | @import '../../../style/colors.less'; 2 | .req-editor { 3 | border: 1px solid @border-color; 4 | } 5 | 6 | .ace_scrollbar::-webkit-scrollbar-track { 7 | box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.1); 8 | border-radius: 8px; 9 | background-color: #F5F5F5; 10 | } 11 | 12 | .ace_scrollbar::-webkit-scrollbar { 13 | width: 8px; 14 | background-color: #F5F5F5; 15 | } 16 | 17 | .ace_scrollbar::-webkit-scrollbar-thumb { 18 | border-radius: 8px; 19 | box-shadow: inset 0 0 6px rgba(0, 0, 0, .1); 20 | background-color: #ccc; 21 | } -------------------------------------------------------------------------------- /client/src/components/environment_select/style/index.less: -------------------------------------------------------------------------------- 1 | 2 | @import '../../../style/colors.less'; 3 | 4 | .req-tab-extra-env{ 5 | padding: 0px 8px; 6 | border-left: 1px solid @border-color; 7 | background: @panel-background; 8 | display: inline-block; 9 | } 10 | 11 | .req-tab-extra-env-select{ 12 | width: 180px; 13 | margin-right: 4px; 14 | } 15 | 16 | .record-add-btn { 17 | border-radius: 0px; 18 | position: relative; 19 | right: 2px; 20 | top: 2px; 21 | height: 26px; 22 | border: 0px; 23 | font-size: 18px; 24 | background: #fafafa; 25 | color: #108ee9; 26 | } -------------------------------------------------------------------------------- /client/src/components/font_icon/font_icon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './style/index.less'; 3 | 4 | interface FontIconProps { 5 | text: string; 6 | 7 | color: string; 8 | 9 | size?: number; 10 | } 11 | 12 | interface FontIconState { } 13 | 14 | class FontIcon extends React.Component { 15 | public render() { 16 | return ( 17 | 24 | {this.props.text} 25 | 26 | ); 27 | } 28 | } 29 | 30 | export default FontIcon; -------------------------------------------------------------------------------- /client/src/components/font_icon/http_method_icon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FontIcon from './font_icon'; 3 | import { HttpMethodType } from '../../misc/http_method'; 4 | 5 | interface HttpMethodIconProps { 6 | 7 | httpMethod: HttpMethodType; 8 | 9 | needTextMapping?: boolean; 10 | 11 | fontSize?: number; 12 | } 13 | 14 | interface HttpMethodIconState { } 15 | 16 | class HttpMethodIcon extends React.Component { 17 | static defaultColor: string = '#34515e'; 18 | 19 | static colorMapping: { [key: string]: string } = { 20 | GET: '#00e676', 21 | POST: '#00b0ff', 22 | PUT: '#651fff', 23 | DELETE: '#ff1744', 24 | PATCH: '#fbc02d' 25 | }; 26 | 27 | static textMapping: { [key: string]: string } = { 28 | DELETE: 'DEL', 29 | OPTIONS: 'OPT', 30 | CONNECT: 'CONN' 31 | }; 32 | 33 | get method() { 34 | const { httpMethod, needTextMapping } = this.props; 35 | if (!needTextMapping) { 36 | return httpMethod; 37 | } 38 | return HttpMethodIcon.textMapping[httpMethod] || httpMethod; 39 | } 40 | 41 | public render() { 42 | return ( 43 | 44 | ); 45 | } 46 | 47 | private color: () => string = () => { 48 | const color = HttpMethodIcon.colorMapping[this.props.httpMethod]; 49 | return color || HttpMethodIcon.defaultColor; 50 | } 51 | } 52 | 53 | export default HttpMethodIcon; -------------------------------------------------------------------------------- /client/src/components/font_icon/style/index.less: -------------------------------------------------------------------------------- 1 | .font-icon { 2 | font-family: SourceCodePro, Arial, "Microsoft YaHei", -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Helvetica Neue", Helvetica, "Helvetica Neue For Number", sans-serif; 3 | font-size: 10px; 4 | font-weight: bold; 5 | margin: auto; 6 | margin-top: 1px; 7 | } -------------------------------------------------------------------------------- /client/src/components/highlight_code/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import hljs from 'highlight.js/lib/highlight'; 3 | import 'highlight.js/styles/tomorrow.css'; 4 | 5 | interface HighlightCodeProps { 6 | 7 | code: string; 8 | } 9 | 10 | interface HighlightCodeState { } 11 | 12 | class HighlightCode extends React.Component { 13 | 14 | pre; 15 | 16 | public componentDidMount() { 17 | hljs.registerLanguage('javascript', require('highlight.js/lib/languages/javascript')); 18 | hljs.registerLanguage('json', require('highlight.js/lib/languages/json')); 19 | hljs.registerLanguage('xml', require('highlight.js/lib/languages/xml')); 20 | hljs.highlightBlock(this.pre); 21 | } 22 | 23 | public componentDidUpdate(_prevProps: HighlightCodeProps, _prevState: HighlightCodeState) { 24 | hljs.highlightBlock(this.pre); 25 | } 26 | 27 | public render() { 28 | return ( 29 |
 this.pre = e} style={{ background: 'transparent' }}>{this.props.code}
30 | ); 31 | } 32 | } 33 | 34 | export default HighlightCode; -------------------------------------------------------------------------------- /client/src/components/indicator/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { WorkerStatus } from '../../misc/stress_type'; 3 | import { primaryColor } from '../../style/theme'; 4 | import './style/index.less'; 5 | 6 | interface IndicatorProps { 7 | 8 | status: WorkerStatus; 9 | 10 | name: string; 11 | 12 | left?: number; 13 | } 14 | 15 | interface IndicatorState { } 16 | 17 | class Indicator extends React.Component { 18 | 19 | private getColorByStatus = () => { 20 | switch (this.props.status) { 21 | case WorkerStatus.idle: 22 | case WorkerStatus.finish: 23 | return '#3fb0f0'; 24 | default: 25 | return primaryColor; 26 | } 27 | } 28 | 29 | public render() { 30 | const { status, name, left } = this.props; 31 | const color = this.getColorByStatus(); 32 | const style = { background: color, marginLeft: left || 0 }; 33 | return ( 34 | 35 | 36 | {name} 37 | 38 | ); 39 | } 40 | } 41 | 42 | export default Indicator; -------------------------------------------------------------------------------- /client/src/components/indicator/style/index.less: -------------------------------------------------------------------------------- 1 | @import '../../../style/colors.less'; 2 | 3 | @keyframes blink { 4 | from { 5 | background: @primary-color; 6 | } 7 | to { 8 | background: transparent; 9 | } 10 | } 11 | 12 | .indicator-blink { 13 | animation-name: blink; 14 | animation-duration: 1s; 15 | animation-iteration-count: infinite; 16 | animation-timing-function: ease-in; 17 | animation-direction:alternate; 18 | } 19 | 20 | .indicator-round { 21 | height: 12px; 22 | width: 12px; 23 | display: inline-block; 24 | border-radius: 6px; 25 | margin-bottom: -2px; 26 | } 27 | 28 | .indicator-name{ 29 | font-weight: bold; 30 | margin-left: 4px; 31 | } -------------------------------------------------------------------------------- /client/src/components/key_value/style/index.less: -------------------------------------------------------------------------------- 1 | @import '../../../style/colors.less'; 2 | .keyvalue-item { 3 | list-style: none; 4 | margin-bottom: 4px; 5 | z-index: 1000; 6 | >div { 7 | display: inline-block; 8 | >label { 9 | margin-right: 4px; 10 | >span { 11 | margin-bottom: 1px; 12 | } 13 | } 14 | } 15 | .inputWithoutFav{ 16 | width: calc(33% - 19px); 17 | margin-right: 4px; 18 | border-radius: 0px; 19 | } 20 | .inputWithoutFavAndDescription{ 21 | width: calc(50% - 34px); 22 | margin-right: 4px; 23 | border-radius: 0px; 24 | } 25 | .inputWithFav{ 26 | width: calc(33% - 27px); 27 | margin-right: 4px; 28 | border-radius: 0px; 29 | } 30 | >i { 31 | font-weight: bold; 32 | cursor: pointer; 33 | margin-left: 4px; 34 | &:hover { 35 | color: red; 36 | } 37 | } 38 | } 39 | 40 | .keyvalue-dragicon { 41 | margin-right: 4px; 42 | font-size: 13px; 43 | cursor: move; 44 | &:hover { 45 | color: @primary-color; 46 | } 47 | } 48 | 49 | .keyvalue-item-fav-btn { 50 | border: 0px; 51 | background: transparent; 52 | &:hover { 53 | color: @primary-color-light; 54 | background: transparent; 55 | } 56 | &:focus { 57 | color: @primary-color; 58 | background: transparent; 59 | } 60 | } 61 | 62 | textarea.req-header { 63 | height: 300px; 64 | border-radius: 0px; 65 | font-size: 13px; 66 | } -------------------------------------------------------------------------------- /client/src/components/name_with_tag/index.tsx: -------------------------------------------------------------------------------- 1 | import './style/index.less'; 2 | import React from 'react'; 3 | 4 | export const nameWithTag = (name: string | React.ReactNode, tag: string, type: 'normal' | 'warning' = 'normal') => ( 5 |
6 | {name} 7 | { 8 | (tag !== '' && tag !== '0') ? 9 | ( 10 | 11 | {`(${tag})`} 12 | 13 | ) : '' 14 | } 15 |
16 | ); -------------------------------------------------------------------------------- /client/src/components/name_with_tag/style/index.less: -------------------------------------------------------------------------------- 1 | @import '../../../style/colors.less'; 2 | .number-tag-normal { 3 | color: @primary-color; 4 | } 5 | 6 | .number-tag-warning { 7 | color: @warning-color; 8 | } -------------------------------------------------------------------------------- /client/src/components/record_timeline/style/index.less: -------------------------------------------------------------------------------- 1 | .record-timeline-item{ 2 | pre { 3 | white-space: pre-wrap; 4 | } 5 | 6 | .record-timeline-item-detail{ 7 | margin-top: 4px; 8 | } 9 | 10 | .record-timeline-item-detailitem { 11 | border: 1px #d1d1d1 solid; 12 | border-radius: 4px; 13 | padding: 4px; 14 | margin: 4px 0px; 15 | } 16 | 17 | .record-timeline-item-detailitem-inline { 18 | margin-left: 8px; 19 | } 20 | } -------------------------------------------------------------------------------- /client/src/components/res_error_panel/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StringUtil } from '../../utils/string_util'; 3 | import './style/index.less'; 4 | import { Input } from 'antd'; 5 | import { DtoError } from '../../common/interfaces/dto_error'; 6 | import Msg from '../../locales'; 7 | 8 | const { TextArea } = Input; 9 | 10 | interface ResErrorPanelProps { 11 | url?: string; 12 | error?: DtoError; 13 | } 14 | 15 | interface ResErrorPanelState { } 16 | 17 | class ResErrorPanel extends React.Component { 18 | 19 | public render() { 20 | const { url, error } = this.props; 21 | const errorStr = StringUtil.beautify(JSON.stringify(error), 'json'); 22 | return ( 23 |
24 |
{Msg('Component.Response')}
25 |
{Msg('Component.GetNonResponse')}
26 |
{Msg('Component.GetErrorFromUrl', { url: {url} })}
27 |