├── .browserslistrc ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── babel.config.js ├── cypress.json ├── package.json ├── postcss.config.js ├── public ├── favicon.ico ├── icons │ └── app.png └── index.html ├── src ├── App.vue ├── assets │ ├── font │ │ ├── iconfont.css │ │ ├── iconfont.js │ │ ├── iconfont.json │ │ ├── iconfont.ttf │ │ ├── iconfont.woff │ │ └── iconfont.woff2 │ ├── images │ │ ├── app-logo.png │ │ ├── connections.svg │ │ ├── emq-logo-dark.png │ │ ├── emq-logo-light.png │ │ ├── mqttx-dark.png │ │ ├── mqttx-light.png │ │ └── wx_qr_code.png │ └── scss │ │ ├── base.scss │ │ ├── element │ │ ├── element-reset.scss │ │ └── element-variables.scss │ │ ├── mixins.scss │ │ ├── theme │ │ ├── dark.scss │ │ ├── editor-dark.json │ │ ├── editor-night.json │ │ ├── light.scss │ │ └── night.scss │ │ └── variable.scss ├── components │ ├── Contextmenu.vue │ ├── Editor.vue │ ├── EmptyPage.vue │ ├── Ipc.vue │ ├── LeftPanel.vue │ ├── Leftbar.vue │ ├── MsgLeftItem.vue │ ├── MsgPublish.vue │ ├── MsgRightItem.vue │ ├── MyDialog.vue │ ├── ResizeHeight.vue │ └── SubscriptionsList.vue ├── database │ └── index.ts ├── lang │ ├── about.ts │ ├── common.ts │ ├── connections.ts │ ├── index.ts │ └── settings.ts ├── main.ts ├── router │ ├── index.ts │ └── routes.ts ├── store │ ├── getter.ts │ ├── index.ts │ └── modules │ │ └── app.ts ├── types │ ├── global.d.ts │ ├── locale.d.ts │ ├── shims-tsx.d.ts │ └── shims-vue.d.ts ├── utils │ ├── api │ │ ├── connection.ts │ │ └── setting.ts │ ├── colors.ts │ ├── convertPayload.ts │ ├── deepMerge.ts │ ├── element.ts │ ├── getClientId.ts │ ├── getFiles.ts │ ├── i18n.ts │ ├── matchSearch.ts │ ├── mqttUtils.ts │ ├── time.ts │ └── topicMatch.ts └── views │ ├── Home.vue │ ├── about │ └── index.vue │ ├── connections │ ├── ConnectionForm.vue │ ├── ConnectionInfo.vue │ ├── ConnectionsDetail.vue │ ├── ConnectionsList.vue │ ├── index.vue │ └── types.ts │ └── settings │ └── index.vue ├── tests └── e2e │ ├── .eslintrc.js │ ├── plugins │ └── index.js │ ├── specs │ └── test.js │ └── support │ ├── commands.js │ └── index.js ├── tsconfig.json ├── vue.config.js └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | end_of_line = lf 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | max_line_length = 100 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | browser: true, 6 | }, 7 | extends: [ 8 | 'plugin:vue/essential', 9 | '@vue/airbnb', 10 | ], 11 | rules: { 12 | semi: 0, 13 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 14 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 15 | }, 16 | parserOptions: { 17 | parser: 'babel-eslint', 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": false, 6 | "trailingComma": "all", 7 | "printWidth": 120, 8 | "bracketSpacing": true 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mqtt-web-toolkit 2 | 3 | ## Project setup 4 | ``` 5 | yarn install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | yarn serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | yarn build 16 | ``` 17 | 18 | ### Run your end-to-end tests 19 | ``` 20 | yarn test:e2e 21 | ``` 22 | 23 | ### Lints and fixes files 24 | ``` 25 | yarn lint 26 | ``` 27 | 28 | ### Customize configuration 29 | See [Configuration Reference](https://cli.vuejs.org/config/). 30 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const plugins = [ 2 | [ 3 | 'component', 4 | { 5 | libraryName: 'element-ui', 6 | styleLibraryName: 'theme-chalk', 7 | }, 8 | ], 9 | ] 10 | if (process.env.NODE_ENV === 'development') { 11 | plugins.push('dynamic-import-node') 12 | } 13 | module.exports = { 14 | presets: [ 15 | '@vue/app', 16 | ], 17 | plugins, 18 | } 19 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginsFile": "tests/e2e/plugins/index.js" 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mqtt-web-toolkit", 3 | "version": "1.0.0", 4 | "license": "Apache", 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "test:e2e": "vue-cli-service test:e2e", 9 | "lint": "vue-cli-service lint" 10 | }, 11 | "dependencies": { 12 | "axios": "^0.21.2", 13 | "core-js": "^2.6.11", 14 | "element-ui": "^2.13.0", 15 | "fs-extra": "^8.1.0", 16 | "jump.js": "^1.0.2", 17 | "lodash-id": "^0.14.0", 18 | "lowdb": "^1.0.0", 19 | "moment": "^2.29.2", 20 | "monaco-editor": "^0.20.0", 21 | "mqtt": "^4.2.1", 22 | "vue": "^2.6.10", 23 | "vue-class-component": "^7.0.2", 24 | "vue-click-outside": "^1.1.0", 25 | "vue-clipboard2": "^0.3.1", 26 | "vue-i18n": "^8.11.2", 27 | "vue-property-decorator": "^8.1.0", 28 | "vue-router": "^3.4.9", 29 | "vuex": "^3.0.1", 30 | "vuex-class": "^0.3.2" 31 | }, 32 | "devDependencies": { 33 | "@fullhuman/postcss-purgecss": "^2.1.0", 34 | "@fullhuman/vue-cli-plugin-purgecss": "~2.2.0", 35 | "@types/chai": "^4.1.0", 36 | "@types/fs-extra": "^8.0.0", 37 | "@types/jump.js": "^1.0.3", 38 | "@types/lodash": "^4.14.142", 39 | "@types/lowdb": "^1.0.9", 40 | "@types/mocha": "^5.2.7", 41 | "@types/node": "14.0.27", 42 | "@typescript-eslint/eslint-plugin": "^3.8.0", 43 | "@typescript-eslint/parser": "^3.8.0", 44 | "@vue/cli-plugin-babel": "^3.12.1", 45 | "@vue/cli-plugin-e2e-cypress": "^3.12.1", 46 | "@vue/cli-plugin-typescript": "^3.12.1", 47 | "@vue/cli-plugin-unit-mocha": "^3.12.1", 48 | "@vue/cli-service": "^3.12.1", 49 | "@vue/eslint-config-prettier": "^6.0.0", 50 | "@vue/test-utils": "1.0.0-beta.29", 51 | "babel-plugin-component": "^1.1.1", 52 | "chai": "^4.1.2", 53 | "eslint": "6.5.1", 54 | "eslint-config-prettier": "^6.11.0", 55 | "eslint-plugin-prettier": "^3.1.4", 56 | "monaco-editor-webpack-plugin": "^1.9.0", 57 | "prettier": "^2.0.5", 58 | "sass": "1.32.13", 59 | "sass-loader": "^10.1.1", 60 | "typescript": "^3.7.4", 61 | "vue-template-compiler": "^2.6.10" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emqx/MQTT-Web-Toolkit/e740caa5ae1458ca3c49b15afb8bfd068b894a40/public/favicon.ico -------------------------------------------------------------------------------- /public/icons/app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emqx/MQTT-Web-Toolkit/e740caa5ae1458ca3c49b15afb8bfd068b894a40/public/icons/app.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | MQTT WebSocket Toolkit | EMQ 9 | 66 | 67 | 68 | 73 |
74 |
75 |
76 |
77 | 78 |
79 |
80 |
81 |
82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 14 | 21 | -------------------------------------------------------------------------------- /src/assets/font/iconfont.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "iconfont"; /* Project id 1257443 */ 3 | src: url('iconfont.woff2?t=1650782182101') format('woff2'), 4 | url('iconfont.woff?t=1650782182101') format('woff'), 5 | url('iconfont.ttf?t=1650782182101') format('truetype'); 6 | } 7 | 8 | .iconfont { 9 | font-family: "iconfont" !important; 10 | font-size: 16px; 11 | font-style: normal; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | .icon-website:before { 17 | content: "\e75a"; 18 | } 19 | 20 | .icon-faq:before { 21 | content: "\e759"; 22 | } 23 | 24 | .icon-discord:before { 25 | content: "\e66e"; 26 | } 27 | 28 | .icon-cloud-logo:before { 29 | content: "\e602"; 30 | } 31 | 32 | .icon-download:before { 33 | content: "\e6ab"; 34 | } 35 | 36 | .icon-language:before { 37 | content: "\e61b"; 38 | } 39 | 40 | .icon-youtube:before { 41 | content: "\e612"; 42 | } 43 | 44 | .icon-linkedin:before { 45 | content: "\e601"; 46 | } 47 | 48 | .icon-more:before { 49 | content: "\e6e5"; 50 | } 51 | 52 | .icon-edit:before { 53 | content: "\e6e2"; 54 | } 55 | 56 | .icon-a-stopscrip:before { 57 | content: "\e6e0"; 58 | } 59 | 60 | .icon-a-clearhistory:before { 61 | content: "\e6e4"; 62 | } 63 | 64 | .icon-a-stoptiming:before { 65 | content: "\e6df"; 66 | } 67 | 68 | .icon-collapse:before { 69 | content: "\e6e1"; 70 | } 71 | 72 | .icon-copy:before { 73 | content: "\e6e3"; 74 | } 75 | 76 | .icon-a-bytesstatistics:before { 77 | content: "\e6dd"; 78 | } 79 | 80 | .icon-delete:before { 81 | content: "\e6de"; 82 | } 83 | 84 | .icon-connect:before { 85 | content: "\e6d5"; 86 | } 87 | 88 | .icon-log:before { 89 | content: "\e6d8"; 90 | } 91 | 92 | .icon-script:before { 93 | content: "\e6dc"; 94 | } 95 | 96 | .icon-new:before { 97 | content: "\e6db"; 98 | } 99 | 100 | .icon-disconnect:before { 101 | content: "\e6c4"; 102 | } 103 | 104 | .icon-a-timedmessage:before { 105 | content: "\e6c6"; 106 | } 107 | 108 | .icon-a-exportdata:before { 109 | content: "\e6c9"; 110 | } 111 | 112 | .icon-a-importdata:before { 113 | content: "\e6cb"; 114 | } 115 | 116 | .icon-search:before { 117 | content: "\e6cc"; 118 | } 119 | 120 | .icon-about:before { 121 | content: "\e6ce"; 122 | } 123 | 124 | .icon-right:before { 125 | content: "\e6d1"; 126 | } 127 | 128 | .icon-left:before { 129 | content: "\e6d2"; 130 | } 131 | 132 | .icon-middle:before { 133 | content: "\e6d3"; 134 | } 135 | 136 | .icon-a-newwindow:before { 137 | content: "\e6d4"; 138 | } 139 | 140 | .icon-settings:before { 141 | content: "\e6d6"; 142 | } 143 | 144 | .icon-a-usescript:before { 145 | content: "\e6d7"; 146 | } 147 | 148 | .icon-fold:before { 149 | content: "\e6d9"; 150 | } 151 | 152 | .icon-unfold:before { 153 | content: "\e6da"; 154 | } 155 | 156 | .icon-triangle:before { 157 | content: "\e8e3"; 158 | } 159 | 160 | .icon-qq:before { 161 | content: "\e615"; 162 | } 163 | 164 | .icon-weibo:before { 165 | content: "\e73a"; 166 | } 167 | 168 | .icon-we-chat:before { 169 | content: "\e70e"; 170 | } 171 | 172 | .icon-ttww:before { 173 | content: "\e6c7"; 174 | } 175 | 176 | .icon-slack:before { 177 | content: "\e641"; 178 | } 179 | 180 | .icon-send:before { 181 | content: "\e62f"; 182 | } 183 | 184 | .icon-github:before { 185 | content: "\e62a"; 186 | } 187 | 188 | -------------------------------------------------------------------------------- /src/assets/font/iconfont.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "1257443", 3 | "name": "MQTTX", 4 | "font_family": "iconfont", 5 | "css_prefix_text": "icon-", 6 | "description": "", 7 | "glyphs": [ 8 | { 9 | "icon_id": "29241652", 10 | "name": "website", 11 | "font_class": "website", 12 | "unicode": "e75a", 13 | "unicode_decimal": 59226 14 | }, 15 | { 16 | "icon_id": "29237198", 17 | "name": "faq", 18 | "font_class": "faq", 19 | "unicode": "e759", 20 | "unicode_decimal": 59225 21 | }, 22 | { 23 | "icon_id": "3876491", 24 | "name": "discord", 25 | "font_class": "discord", 26 | "unicode": "e66e", 27 | "unicode_decimal": 58990 28 | }, 29 | { 30 | "icon_id": "25711546", 31 | "name": "cloud-logo", 32 | "font_class": "cloud-logo", 33 | "unicode": "e602", 34 | "unicode_decimal": 58882 35 | }, 36 | { 37 | "icon_id": "2238873", 38 | "name": "下载", 39 | "font_class": "download", 40 | "unicode": "e6ab", 41 | "unicode_decimal": 59051 42 | }, 43 | { 44 | "icon_id": "17686988", 45 | "name": "地球", 46 | "font_class": "language", 47 | "unicode": "e61b", 48 | "unicode_decimal": 58907 49 | }, 50 | { 51 | "icon_id": "2591074", 52 | "name": "youtube", 53 | "font_class": "youtube", 54 | "unicode": "e612", 55 | "unicode_decimal": 58898 56 | }, 57 | { 58 | "icon_id": "12294078", 59 | "name": "linkin", 60 | "font_class": "linkedin", 61 | "unicode": "e601", 62 | "unicode_decimal": 58881 63 | }, 64 | { 65 | "icon_id": "22444844", 66 | "name": "more", 67 | "font_class": "more", 68 | "unicode": "e6e5", 69 | "unicode_decimal": 59109 70 | }, 71 | { 72 | "icon_id": "22444281", 73 | "name": "edit", 74 | "font_class": "edit", 75 | "unicode": "e6e2", 76 | "unicode_decimal": 59106 77 | }, 78 | { 79 | "icon_id": "22420295", 80 | "name": "stop scrip", 81 | "font_class": "a-stopscrip", 82 | "unicode": "e6e0", 83 | "unicode_decimal": 59104 84 | }, 85 | { 86 | "icon_id": "22419005", 87 | "name": "clear history", 88 | "font_class": "a-clearhistory", 89 | "unicode": "e6e4", 90 | "unicode_decimal": 59108 91 | }, 92 | { 93 | "icon_id": "22418847", 94 | "name": "stop timing", 95 | "font_class": "a-stoptiming", 96 | "unicode": "e6df", 97 | "unicode_decimal": 59103 98 | }, 99 | { 100 | "icon_id": "22418849", 101 | "name": "collapse", 102 | "font_class": "collapse", 103 | "unicode": "e6e1", 104 | "unicode_decimal": 59105 105 | }, 106 | { 107 | "icon_id": "22418851", 108 | "name": "copy", 109 | "font_class": "copy", 110 | "unicode": "e6e3", 111 | "unicode_decimal": 59107 112 | }, 113 | { 114 | "icon_id": "22416556", 115 | "name": "bytes statistics", 116 | "font_class": "a-bytesstatistics", 117 | "unicode": "e6dd", 118 | "unicode_decimal": 59101 119 | }, 120 | { 121 | "icon_id": "22416557", 122 | "name": "delete", 123 | "font_class": "delete", 124 | "unicode": "e6de", 125 | "unicode_decimal": 59102 126 | }, 127 | { 128 | "icon_id": "22414556", 129 | "name": "connect", 130 | "font_class": "connect", 131 | "unicode": "e6d5", 132 | "unicode_decimal": 59093 133 | }, 134 | { 135 | "icon_id": "22414557", 136 | "name": "log", 137 | "font_class": "log", 138 | "unicode": "e6d8", 139 | "unicode_decimal": 59096 140 | }, 141 | { 142 | "icon_id": "22414310", 143 | "name": "script", 144 | "font_class": "script", 145 | "unicode": "e6dc", 146 | "unicode_decimal": 59100 147 | }, 148 | { 149 | "icon_id": "22413795", 150 | "name": "new", 151 | "font_class": "new", 152 | "unicode": "e6db", 153 | "unicode_decimal": 59099 154 | }, 155 | { 156 | "icon_id": "22406898", 157 | "name": "disconnect", 158 | "font_class": "disconnect", 159 | "unicode": "e6c4", 160 | "unicode_decimal": 59076 161 | }, 162 | { 163 | "icon_id": "22406900", 164 | "name": "timed message", 165 | "font_class": "a-timedmessage", 166 | "unicode": "e6c6", 167 | "unicode_decimal": 59078 168 | }, 169 | { 170 | "icon_id": "22406907", 171 | "name": "export data", 172 | "font_class": "a-exportdata", 173 | "unicode": "e6c9", 174 | "unicode_decimal": 59081 175 | }, 176 | { 177 | "icon_id": "22406911", 178 | "name": "import data", 179 | "font_class": "a-importdata", 180 | "unicode": "e6cb", 181 | "unicode_decimal": 59083 182 | }, 183 | { 184 | "icon_id": "22406912", 185 | "name": "search", 186 | "font_class": "search", 187 | "unicode": "e6cc", 188 | "unicode_decimal": 59084 189 | }, 190 | { 191 | "icon_id": "22407879", 192 | "name": "about", 193 | "font_class": "about", 194 | "unicode": "e6ce", 195 | "unicode_decimal": 59086 196 | }, 197 | { 198 | "icon_id": "22407882", 199 | "name": "right", 200 | "font_class": "right", 201 | "unicode": "e6d1", 202 | "unicode_decimal": 59089 203 | }, 204 | { 205 | "icon_id": "22407883", 206 | "name": "left", 207 | "font_class": "left", 208 | "unicode": "e6d2", 209 | "unicode_decimal": 59090 210 | }, 211 | { 212 | "icon_id": "22407884", 213 | "name": "middle", 214 | "font_class": "middle", 215 | "unicode": "e6d3", 216 | "unicode_decimal": 59091 217 | }, 218 | { 219 | "icon_id": "22407886", 220 | "name": "new window", 221 | "font_class": "a-newwindow", 222 | "unicode": "e6d4", 223 | "unicode_decimal": 59092 224 | }, 225 | { 226 | "icon_id": "22407889", 227 | "name": "settings", 228 | "font_class": "settings", 229 | "unicode": "e6d6", 230 | "unicode_decimal": 59094 231 | }, 232 | { 233 | "icon_id": "22407891", 234 | "name": "use script", 235 | "font_class": "a-usescript", 236 | "unicode": "e6d7", 237 | "unicode_decimal": 59095 238 | }, 239 | { 240 | "icon_id": "22407905", 241 | "name": "fold", 242 | "font_class": "fold", 243 | "unicode": "e6d9", 244 | "unicode_decimal": 59097 245 | }, 246 | { 247 | "icon_id": "22407906", 248 | "name": "unfold", 249 | "font_class": "unfold", 250 | "unicode": "e6da", 251 | "unicode_decimal": 59098 252 | }, 253 | { 254 | "icon_id": "17752358", 255 | "name": "down", 256 | "font_class": "triangle", 257 | "unicode": "e8e3", 258 | "unicode_decimal": 59619 259 | }, 260 | { 261 | "icon_id": "1878149", 262 | "name": "qq", 263 | "font_class": "qq", 264 | "unicode": "e615", 265 | "unicode_decimal": 58901 266 | }, 267 | { 268 | "icon_id": "1937032", 269 | "name": "weibo", 270 | "font_class": "weibo", 271 | "unicode": "e73a", 272 | "unicode_decimal": 59194 273 | }, 274 | { 275 | "icon_id": "9277879", 276 | "name": "we-chat", 277 | "font_class": "we-chat", 278 | "unicode": "e70e", 279 | "unicode_decimal": 59150 280 | }, 281 | { 282 | "icon_id": "12313365", 283 | "name": "twitter", 284 | "font_class": "ttww", 285 | "unicode": "e6c7", 286 | "unicode_decimal": 59079 287 | }, 288 | { 289 | "icon_id": "3876355", 290 | "name": "slack", 291 | "font_class": "slack", 292 | "unicode": "e641", 293 | "unicode_decimal": 58945 294 | }, 295 | { 296 | "icon_id": "9559673", 297 | "name": "send", 298 | "font_class": "send", 299 | "unicode": "e62f", 300 | "unicode_decimal": 58927 301 | }, 302 | { 303 | "icon_id": "9552003", 304 | "name": "github", 305 | "font_class": "github", 306 | "unicode": "e62a", 307 | "unicode_decimal": 58922 308 | } 309 | ] 310 | } 311 | -------------------------------------------------------------------------------- /src/assets/font/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emqx/MQTT-Web-Toolkit/e740caa5ae1458ca3c49b15afb8bfd068b894a40/src/assets/font/iconfont.ttf -------------------------------------------------------------------------------- /src/assets/font/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emqx/MQTT-Web-Toolkit/e740caa5ae1458ca3c49b15afb8bfd068b894a40/src/assets/font/iconfont.woff -------------------------------------------------------------------------------- /src/assets/font/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emqx/MQTT-Web-Toolkit/e740caa5ae1458ca3c49b15afb8bfd068b894a40/src/assets/font/iconfont.woff2 -------------------------------------------------------------------------------- /src/assets/images/app-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emqx/MQTT-Web-Toolkit/e740caa5ae1458ca3c49b15afb8bfd068b894a40/src/assets/images/app-logo.png -------------------------------------------------------------------------------- /src/assets/images/emq-logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emqx/MQTT-Web-Toolkit/e740caa5ae1458ca3c49b15afb8bfd068b894a40/src/assets/images/emq-logo-dark.png -------------------------------------------------------------------------------- /src/assets/images/emq-logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emqx/MQTT-Web-Toolkit/e740caa5ae1458ca3c49b15afb8bfd068b894a40/src/assets/images/emq-logo-light.png -------------------------------------------------------------------------------- /src/assets/images/mqttx-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emqx/MQTT-Web-Toolkit/e740caa5ae1458ca3c49b15afb8bfd068b894a40/src/assets/images/mqttx-dark.png -------------------------------------------------------------------------------- /src/assets/images/mqttx-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emqx/MQTT-Web-Toolkit/e740caa5ae1458ca3c49b15afb8bfd068b894a40/src/assets/images/mqttx-light.png -------------------------------------------------------------------------------- /src/assets/images/wx_qr_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emqx/MQTT-Web-Toolkit/e740caa5ae1458ca3c49b15afb8bfd068b894a40/src/assets/images/wx_qr_code.png -------------------------------------------------------------------------------- /src/assets/scss/base.scss: -------------------------------------------------------------------------------- 1 | @import './variable.scss'; 2 | @import './mixins.scss'; 3 | 4 | html, 5 | body, 6 | #app { 7 | height: 100%; 8 | margin: 0px; 9 | padding: 0px; 10 | background: var(--color-bg-primary); 11 | } 12 | 13 | body { 14 | font-family: system, -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Segoe UI', 'Microsoft YaHei', 15 | 'wenquanyi micro hei', 'Hiragino Sans GB', 'Hiragino Sans GB W3', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 16 | 'Fira Sans', 'Droid Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 17 | font-size: $font-size--body; 18 | font-weight: 400; 19 | line-height: 1.5; 20 | -webkit-font-smoothing: subpixel-antialiased; 21 | -moz-osx-font-smoothing: grayscale; 22 | color: var(--color-text-default); 23 | } 24 | 25 | body.select-none { 26 | -moz-user-select: none; 27 | -khtml-user-select: none; 28 | user-select: none; 29 | } 30 | 31 | * { 32 | box-sizing: border-box; 33 | :focus { 34 | outline: none; 35 | } 36 | } 37 | 38 | h1 { 39 | font-size: $font-size--title; 40 | @include head-title; 41 | } 42 | 43 | h2 { 44 | font-size: $font-size--subtitle; 45 | @include head-title; 46 | } 47 | 48 | h3 { 49 | font-size: $font-size--body; 50 | @include head-title; 51 | } 52 | 53 | p { 54 | font-size: $font-size--body; 55 | margin: 0px; 56 | } 57 | 58 | pre { 59 | font-size: $font-size--body; 60 | margin-bottom: 0px; 61 | font-family: Menlo, Monaco, 'Courier New', monospace; 62 | } 63 | 64 | a { 65 | color: var(--color-main-green); 66 | text-decoration: none; 67 | font-size: $font-size--body; 68 | outline: none; 69 | } 70 | 71 | img { 72 | max-width: 100%; 73 | } 74 | 75 | /* Card form */ 76 | .card-form { 77 | h3 { 78 | color: var(--color-text-light); 79 | } 80 | .info-header { 81 | @include flex-space-between; 82 | margin: 0 0 10px 0; 83 | } 84 | .item-card { 85 | margin: 0 0 30px 0; 86 | position: relative; 87 | .el-card__body { 88 | padding: 10px 20px; 89 | } 90 | } 91 | .iconfont { 92 | font-size: $font-size--title; 93 | } 94 | .el-form { 95 | .el-form-item { 96 | margin-bottom: 0px; 97 | } 98 | .el-form-item__content { 99 | line-height: 43px; 100 | } 101 | } 102 | } 103 | 104 | /* Error */ 105 | .error { 106 | color: var(--color-main-red); 107 | } 108 | 109 | /* Titlebar */ 110 | .titlebar { 111 | padding: 16px 0; 112 | -webkit-app-region: drag; 113 | } 114 | -------------------------------------------------------------------------------- /src/assets/scss/element/element-reset.scss: -------------------------------------------------------------------------------- 1 | /* Divider */ 2 | .el-divider { 3 | background-color: var(--color-border-default); 4 | } 5 | 6 | /* Radio */ 7 | .el-radio-button { 8 | outline: none; 9 | user-select: none; 10 | margin-right: -2px; 11 | } 12 | .el-radio__label { 13 | font-size: 13px; 14 | } 15 | .el-radio-button--mini .el-radio-button__inner { 16 | background: transparent; 17 | border: 2px solid var(--color-border-bold); 18 | width: 90px; 19 | color: var(--color-text-light); 20 | } 21 | .el-radio-button__orig-radio:checked + .el-radio-button__inner { 22 | background-color: var(--color-bg-item); 23 | color: var(--color-main-green); 24 | border: 2px solid var(--color-main-green) !important; 25 | z-index: 1; 26 | box-shadow: none; 27 | } 28 | .el-radio-button:first-child .el-radio-button__inner { 29 | border-left: 2px solid var(--color-border-bold); 30 | } 31 | 32 | /* Card */ 33 | .el-card { 34 | border: 1px solid var(--color-border-default); 35 | background-color: var(--color-bg-normal); 36 | } 37 | .el-card.is-hover-shadow:hover { 38 | box-shadow: 0 2px 12px 0 var(--color-shadow-card); 39 | } 40 | 41 | /* Form */ 42 | .el-form { 43 | .el-form-item__error { 44 | white-space: nowrap; 45 | } 46 | .el-form-item__label { 47 | color: var(--color-text-default); 48 | } 49 | .el-form-item:not(.will-payload-box) { 50 | .el-form-item__content { 51 | span { 52 | color: var(--color-text-default); 53 | } 54 | } 55 | } 56 | .el-select { 57 | width: 100%; 58 | } 59 | .el-form-item.is-success .el-input__inner, 60 | .el-form-item.is-success .el-input__inner:focus, 61 | .el-form-item.is-success .el-textarea__inner, 62 | .el-form-item.is-success .el-textarea__inner:focus { 63 | border-color: var(--color-main-green); 64 | } 65 | } 66 | 67 | /* Icon */ 68 | [class^='el-icon-'], 69 | [class*=' el-icon-'] { 70 | font-weight: 600; 71 | } 72 | 73 | /* Button */ 74 | .el-button--outline { 75 | background: var(--color-bg-normal); 76 | border: 2px solid var(--color-main-green); 77 | color: var(--color-main-green); 78 | } 79 | .el-button.is-plain:hover, 80 | .el-button.is-plain:focus { 81 | background: var(--color-bg-normal); 82 | } 83 | 84 | /* Cascader */ 85 | .el-cascader { 86 | width: 100%; 87 | } 88 | 89 | /* Message */ 90 | .el-message-box { 91 | border-radius: 2px; 92 | background-color: var(--color-bg-messagebox); 93 | border-color: var(--color-border-default); 94 | .el-message-box__title { 95 | color: var(--color-text-title); 96 | } 97 | .el-message-box__content { 98 | color: var(--color-text-default); 99 | } 100 | } 101 | .el-message-box__btns { 102 | font-size: 14px; 103 | .el-button--small { 104 | font-size: 14px; 105 | border: none; 106 | color: var(--color-text-light); 107 | background: transparent; 108 | &:hover { 109 | color: var(--color-minor-green); 110 | background: transparent; 111 | } 112 | } 113 | .el-button--primary { 114 | background: transparent; 115 | &:hover { 116 | background: transparent; 117 | color: var(--color-minor-green); 118 | } 119 | } 120 | button:nth-child(2) { 121 | color: var(--color-main-green); 122 | margin-left: 0px; 123 | } 124 | } 125 | .el-message { 126 | padding: 16px; 127 | border-radius: 8px; 128 | background-color: var(--color-bg-normal); 129 | border: none; 130 | .el-message__icon { 131 | font-size: 18px; 132 | } 133 | &--success { 134 | border: 1px solid var(--color-main-green); 135 | box-shadow: 3px 3px 5px 0px var(--color-shadow-message); 136 | .el-message__icon, 137 | .el-message__content { 138 | color: var(--color-main-green); 139 | } 140 | } 141 | &--warning { 142 | border: 1px solid var(--color-main-yellow); 143 | box-shadow: 3px 3px 5px 0px var(--color-shadow-message); 144 | } 145 | &--error { 146 | border: 1px solid var(--color-minor-red); 147 | box-shadow: 3px 3px 5px 0px var(--color-shadow-message); 148 | } 149 | } 150 | 151 | /* Notification */ 152 | .el-notification { 153 | border-radius: 4px; 154 | background-color: var(--color-bg-messagebox); 155 | border-color: var(--color-border-default); 156 | .el-notification__title { 157 | color: var(--color-text-title); 158 | font-weight: normal; 159 | } 160 | .el-icon-success { 161 | color: var(--color-main-green); 162 | } 163 | .el-notification__title { 164 | word-break: break-word; 165 | } 166 | } 167 | 168 | /* Dialog */ 169 | .el-dialog { 170 | background: var(--color-bg-normal); 171 | } 172 | 173 | /* Input */ 174 | .el-input { 175 | .el-input__inner { 176 | background: var(--color-bg-normal); 177 | border: 1px solid var(--color-border-default); 178 | color: var(--color-text-default); 179 | &::placeholder { 180 | color: var(--color-text-light); 181 | } 182 | } 183 | } 184 | .el-textarea { 185 | .el-textarea__inner { 186 | background: var(--color-bg-normal); 187 | border: 1px solid var(--color-border-default); 188 | color: var(--color-text-default); 189 | &::placeholder { 190 | color: var(--color-text-light); 191 | } 192 | } 193 | } 194 | 195 | /* Popper */ 196 | .el-popper { 197 | background: var(--color-bg-messagebox); 198 | border-color: var(--color-border-default); 199 | li { 200 | color: var(--color-text-default); 201 | } 202 | } 203 | .el-popper[x-placement^='bottom'] .popper__arrow { 204 | border-bottom-color: var(--color-border-default); 205 | &::after { 206 | border-bottom-color: var(--color-bg-messagebox); 207 | } 208 | } 209 | .el-popper[x-placement^='top'] .popper__arrow { 210 | border-top-color: var(--color-border-default); 211 | &:after { 212 | border-top-color: var(--color-bg-messagebox); 213 | } 214 | } 215 | .el-select-dropdown__item.hover, 216 | .el-select-dropdown__item:hover { 217 | background: var(--color-bg-item); 218 | } 219 | 220 | /* Cascader */ 221 | .el-cascader-menu { 222 | border-right: 1px solid var(--color-border-default); 223 | } 224 | .el-cascader-node:not(.is-disabled):hover, 225 | .el-cascader-node:not(.is-disabled):focus { 226 | background: var(--color-bg-item); 227 | } 228 | 229 | /* Tootip */ 230 | .el-tooltip__popper.is-light { 231 | color: #616161; 232 | } 233 | 234 | /* Color picker */ 235 | .el-color-picker__panel { 236 | background: var(--color-bg-normal); 237 | border-color: var(--color-border-default); 238 | } 239 | .el-color-dropdown__btn { 240 | border-color: var(--color-border-default); 241 | background: transparent; 242 | color: var(--color-text-default); 243 | } 244 | 245 | /* Icon */ 246 | .el-icon-refresh { 247 | cursor: pointer; 248 | color: var(--color-main-green); 249 | } 250 | 251 | /* Autocomplete */ 252 | .el-autocomplete { 253 | width: 100%; 254 | } 255 | .el-autocomplete-suggestion li:hover { 256 | color: var(--color-main-green); 257 | background-color: var(--color-bg-item); 258 | } 259 | 260 | /* Dropdown */ 261 | .el-dropdown-menu__item--divided:before { 262 | background-color: var(--color-bg-messagebox); 263 | } 264 | .el-dropdown-menu__item--divided { 265 | position: relative; 266 | margin-top: 6px; 267 | border-top: 1px solid var(--color-border-default); 268 | } 269 | -------------------------------------------------------------------------------- /src/assets/scss/element/element-variables.scss: -------------------------------------------------------------------------------- 1 | /* Change element-ui theme variable */ 2 | $--color-primary: #22bb7a; 3 | 4 | /* Change icon font path,required */ 5 | $--font-path: '~element-ui/lib/theme-chalk/fonts'; 6 | 7 | @import '~element-ui/packages/theme-chalk/src/index'; 8 | -------------------------------------------------------------------------------- /src/assets/scss/mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin head-title { 2 | margin: 0px; 3 | padding: 0px; 4 | color: var(--color-text-title); 5 | } 6 | 7 | @mixin flex-space-between { 8 | display: flex; 9 | align-items: center; 10 | justify-content: space-between; 11 | } 12 | 13 | @mixin msg-item { 14 | pre { 15 | white-space: pre-wrap; 16 | word-wrap: break-word; 17 | } 18 | p { 19 | margin: 0px; 20 | } 21 | .left-info { 22 | color: var(--color-text-left_info); 23 | } 24 | .right-info { 25 | color: var(--color-text-right_info); 26 | } 27 | .topic { 28 | margin-right: 16px; 29 | } 30 | .payload { 31 | display: inline-block; 32 | min-height: 41px; 33 | max-width: 441px; 34 | min-width: 20px; 35 | margin: 10px 0 0px 0px; 36 | padding: 10px; 37 | white-space: normal; 38 | word-break: break-all; 39 | } 40 | .time { 41 | color: var(--color-text-light); 42 | margin-bottom: 5px; 43 | } 44 | } 45 | 46 | @mixin collapse-btn-transform($top, $bottom) { 47 | a.collapse-btn { 48 | transition: all 0.3s; 49 | display: inline-block; 50 | &.top { 51 | transform: rotate($top); 52 | } 53 | &.bottom { 54 | transform: rotate($bottom); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/assets/scss/theme/dark.scss: -------------------------------------------------------------------------------- 1 | body.dark { 2 | /* Backgroud color */ 3 | --color-bg-normal: #262729; 4 | --color-bg-primary: #212223; 5 | --color-bg-topics: #484848; 6 | --color-bg-topics_active: #353535; 7 | --color-bg-topics_shadow: #1b1b1bd1; 8 | --color-bg-leftbar_top: #232325; 9 | --color-bg-leftbar_bottom: #1f2122; 10 | --color-bg-leftbar_item: #1f2021; 11 | --color-bg-item: #1f2021; 12 | --color-bg-item_status: #9ea3b1; 13 | --color-bg-input: #323232; 14 | --color-bg-messagebox: #303133; 15 | --color-bg-follows: #4a4a4a; 16 | --color-bg-code: #38404a; 17 | --color-bg-input_btn: transparent; 18 | --color-bg-radio: #37363d80; 19 | --color-bg-card-normal: #34363c; 20 | --color-bg-card-gradient: linear-gradient(33deg, #27393c 0%, #2c2f3e 100%); 21 | --color-bg-btn-gradient: linear-gradient(90deg, #35c98d 0%, #37dc85 100%); 22 | 23 | /* Font color */ 24 | --color-text-title: #ffffff; 25 | --color-text-default: #d3d3d3; 26 | --color-text-light: #a3a3a3; 27 | --color-text-tips: #b4b4b4; 28 | --color-text-right_block: #484848; 29 | --color-text-right_info: #959599; 30 | --color-text-left_info: #959599; 31 | --color-text-card_icon: #eaebec; 32 | 33 | /* Accent color */ 34 | --color-main-green: #34c388; 35 | --color-minor-green: #53daa2; 36 | --color-light-green: #ebf8f2; 37 | --color-main-red: #e86aa6; 38 | --color-minor-red: #f56c6c; 39 | --color-light-red: #fcdee4; 40 | --color-main-grey: #323232; 41 | --color-main-yellow: #e6a23cb3; 42 | --color-main-white: #fff; 43 | 44 | /* Border color */ 45 | --color-border-default: #383838; 46 | --color-border-bold: #969696; 47 | --color-shadow-card: #00000059; 48 | --color-shadow-message: #1f1f1f; 49 | } 50 | -------------------------------------------------------------------------------- /src/assets/scss/theme/editor-dark.json: -------------------------------------------------------------------------------- 1 | { 2 | "base": "vs-dark", 3 | "inherit": true, 4 | "rules": [{ 5 | "foreground": "75715e", 6 | "token": "comment" 7 | }, 8 | { 9 | "foreground": "e6db74", 10 | "token": "string" 11 | }, 12 | { 13 | "foreground": "ae81ff", 14 | "token": "constant.numeric" 15 | }, 16 | { 17 | "foreground": "ae81ff", 18 | "token": "constant.language" 19 | }, 20 | { 21 | "foreground": "ae81ff", 22 | "token": "constant.character" 23 | }, 24 | { 25 | "foreground": "ae81ff", 26 | "token": "constant.other" 27 | }, 28 | { 29 | "foreground": "f92672", 30 | "token": "keyword" 31 | }, 32 | { 33 | "foreground": "f92672", 34 | "token": "storage" 35 | }, 36 | { 37 | "foreground": "66d9ef", 38 | "fontStyle": "italic", 39 | "token": "storage.type" 40 | }, 41 | { 42 | "foreground": "a6e22e", 43 | "fontStyle": "underline", 44 | "token": "entity.name.class" 45 | }, 46 | { 47 | "foreground": "a6e22e", 48 | "fontStyle": "italic underline", 49 | "token": "entity.other.inherited-class" 50 | }, 51 | { 52 | "foreground": "a6e22e", 53 | "token": "entity.name.function" 54 | }, 55 | { 56 | "foreground": "fd971f", 57 | "fontStyle": "italic", 58 | "token": "variable.parameter" 59 | }, 60 | { 61 | "foreground": "f92672", 62 | "token": "entity.name.tag" 63 | }, 64 | { 65 | "foreground": "a6e22e", 66 | "token": "entity.other.attribute-name" 67 | }, 68 | { 69 | "foreground": "66d9ef", 70 | "token": "support.function" 71 | }, 72 | { 73 | "foreground": "66d9ef", 74 | "token": "support.constant" 75 | }, 76 | { 77 | "foreground": "66d9ef", 78 | "fontStyle": "italic", 79 | "token": "support.type" 80 | }, 81 | { 82 | "foreground": "66d9ef", 83 | "fontStyle": "italic", 84 | "token": "support.class" 85 | }, 86 | { 87 | "foreground": "f8f8f0", 88 | "background": "f92672", 89 | "token": "invalid" 90 | }, 91 | { 92 | "foreground": "f8f8f0", 93 | "background": "ae81ff", 94 | "token": "invalid.deprecated" 95 | }, 96 | { 97 | "foreground": "cfcfc2", 98 | "token": "meta.structure.dictionary.json string.quoted.double.json" 99 | }, 100 | { 101 | "foreground": "75715e", 102 | "token": "meta.diff" 103 | }, 104 | { 105 | "foreground": "75715e", 106 | "token": "meta.diff.header" 107 | }, 108 | { 109 | "foreground": "f92672", 110 | "token": "markup.deleted" 111 | }, 112 | { 113 | "foreground": "a6e22e", 114 | "token": "markup.inserted" 115 | }, 116 | { 117 | "foreground": "e6db74", 118 | "token": "markup.changed" 119 | }, 120 | { 121 | "foreground": "ae81ffa0", 122 | "token": "constant.numeric.line-number.find-in-files - match" 123 | }, 124 | { 125 | "foreground": "e6db74", 126 | "token": "entity.name.filename.find-in-files" 127 | } 128 | ], 129 | "colors": { 130 | "editor.foreground": "#F8F8F2", 131 | "editor.background": "#282828", 132 | "editor.selectionBackground": "#49483E", 133 | "editor.lineHighlightBackground": "#3E3D32", 134 | "editorCursor.foreground": "#F8F8F0", 135 | "editorWhitespace.foreground": "#3B3A32", 136 | "editorIndentGuide.activeBackground": "#9D550FB0", 137 | "editor.selectionHighlightBorder": "#222218" 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/assets/scss/theme/editor-night.json: -------------------------------------------------------------------------------- 1 | { 2 | "base": "vs-dark", 3 | "inherit": true, 4 | "rules": [{ 5 | "foreground": "75715e", 6 | "token": "comment" 7 | }, 8 | { 9 | "foreground": "e6db74", 10 | "token": "string" 11 | }, 12 | { 13 | "foreground": "ae81ff", 14 | "token": "constant.numeric" 15 | }, 16 | { 17 | "foreground": "ae81ff", 18 | "token": "constant.language" 19 | }, 20 | { 21 | "foreground": "ae81ff", 22 | "token": "constant.character" 23 | }, 24 | { 25 | "foreground": "ae81ff", 26 | "token": "constant.other" 27 | }, 28 | { 29 | "foreground": "f92672", 30 | "token": "keyword" 31 | }, 32 | { 33 | "foreground": "f92672", 34 | "token": "storage" 35 | }, 36 | { 37 | "foreground": "66d9ef", 38 | "fontStyle": "italic", 39 | "token": "storage.type" 40 | }, 41 | { 42 | "foreground": "a6e22e", 43 | "fontStyle": "underline", 44 | "token": "entity.name.class" 45 | }, 46 | { 47 | "foreground": "a6e22e", 48 | "fontStyle": "italic underline", 49 | "token": "entity.other.inherited-class" 50 | }, 51 | { 52 | "foreground": "a6e22e", 53 | "token": "entity.name.function" 54 | }, 55 | { 56 | "foreground": "fd971f", 57 | "fontStyle": "italic", 58 | "token": "variable.parameter" 59 | }, 60 | { 61 | "foreground": "f92672", 62 | "token": "entity.name.tag" 63 | }, 64 | { 65 | "foreground": "a6e22e", 66 | "token": "entity.other.attribute-name" 67 | }, 68 | { 69 | "foreground": "66d9ef", 70 | "token": "support.function" 71 | }, 72 | { 73 | "foreground": "66d9ef", 74 | "token": "support.constant" 75 | }, 76 | { 77 | "foreground": "66d9ef", 78 | "fontStyle": "italic", 79 | "token": "support.type" 80 | }, 81 | { 82 | "foreground": "66d9ef", 83 | "fontStyle": "italic", 84 | "token": "support.class" 85 | }, 86 | { 87 | "foreground": "f8f8f0", 88 | "background": "f92672", 89 | "token": "invalid" 90 | }, 91 | { 92 | "foreground": "f8f8f0", 93 | "background": "ae81ff", 94 | "token": "invalid.deprecated" 95 | }, 96 | { 97 | "foreground": "cfcfc2", 98 | "token": "meta.structure.dictionary.json string.quoted.double.json" 99 | }, 100 | { 101 | "foreground": "75715e", 102 | "token": "meta.diff" 103 | }, 104 | { 105 | "foreground": "75715e", 106 | "token": "meta.diff.header" 107 | }, 108 | { 109 | "foreground": "f92672", 110 | "token": "markup.deleted" 111 | }, 112 | { 113 | "foreground": "a6e22e", 114 | "token": "markup.inserted" 115 | }, 116 | { 117 | "foreground": "e6db74", 118 | "token": "markup.changed" 119 | }, 120 | { 121 | "foreground": "ae81ffa0", 122 | "token": "constant.numeric.line-number.find-in-files - match" 123 | }, 124 | { 125 | "foreground": "e6db74", 126 | "token": "entity.name.filename.find-in-files" 127 | } 128 | ], 129 | "colors": { 130 | "editor.foreground": "#F8F8F2", 131 | "editor.background": "#292B33", 132 | "editor.selectionBackground": "#49483E", 133 | "editor.lineHighlightBackground": "#3E3D32", 134 | "editorCursor.foreground": "#F8F8F0", 135 | "editorWhitespace.foreground": "#3B3A32", 136 | "editorIndentGuide.activeBackground": "#9D550FB0", 137 | "editor.selectionHighlightBorder": "#222218" 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/assets/scss/theme/light.scss: -------------------------------------------------------------------------------- 1 | body.light { 2 | /* Backgroud color */ 3 | --color-bg-normal: #ffffff; 4 | --color-bg-primary: #f9fafd; 5 | --color-bg-topics: #f8f8f8; 6 | --color-bg-topics_active: #efefef; 7 | --color-bg-topics_shadow: #e2e2e2d1; 8 | --color-bg-leftbar_top: #2e424a; 9 | --color-bg-leftbar_bottom: #212b3b; 10 | --color-bg-leftbar_item: #394452; 11 | --color-bg-item: #f1f8fc; 12 | --color-bg-item_status: #9ea3b1; 13 | --color-bg-input: #ffffff; 14 | --color-bg-messagebox: #ffffff; 15 | --color-bg-follows: #efefef; 16 | --color-bg-code: #e6eef7; 17 | --color-bg-input_btn: #f5f7fa; 18 | --color-bg-radio: #f6f7fb; 19 | --color-bg-card-normal: #edf4fc; 20 | --color-bg-card-gradient: linear-gradient(33deg, #e5fafc 0%, #eceeff 100%); 21 | --color-bg-btn-gradient: linear-gradient(90deg, #35c98d 0%, #37dc85 100%); 22 | 23 | /* Font color */ 24 | --color-text-title: #262626; 25 | --color-text-default: #616161; 26 | --color-text-light: #a2a9b0; 27 | --color-text-tips: #b4b4b4; 28 | --color-text-right_block: #34c388; 29 | --color-text-right_info: #ade7cf; 30 | --color-text-left_info: #00000075; 31 | --color-text-card_icon: #2f4852; 32 | 33 | /* Accent color */ 34 | --color-main-green: #34c388; 35 | --color-minor-green: #53daa2; 36 | --color-light-green: #ebf8f2; 37 | --color-main-red: #e86aa6; 38 | --color-minor-red: #f56c6c; 39 | --color-light-red: #fcdee4; 40 | --color-main-grey: #e9eaf0; 41 | --color-main-yellow: #e6a23cb3; 42 | --color-main-white: #fff; 43 | 44 | /* Border color */ 45 | --color-border-default: #e6e8f1; 46 | --color-border-bold: #979797; 47 | --color-shadow-card: #0000001a; 48 | --color-shadow-message: #dcdcdc; 49 | } 50 | -------------------------------------------------------------------------------- /src/assets/scss/theme/night.scss: -------------------------------------------------------------------------------- 1 | body.night { 2 | /* Backgroud color */ 3 | --color-bg-normal: #292b33; 4 | --color-bg-primary: #212328; 5 | --color-bg-topics: #414556; 6 | --color-bg-topics_active: #323544; 7 | --color-bg-topics_shadow: #231d1dd1; 8 | --color-bg-leftbar_top: #292b33; 9 | --color-bg-leftbar_bottom: #212328; 10 | --color-bg-leftbar_item: #394452; 11 | --color-bg-item: #31333f; 12 | --color-bg-item_status: #9ea3b1; 13 | --color-bg-input: #31333f; 14 | --color-bg-messagebox: #31333f; 15 | --color-bg-follows: #393a42; 16 | --color-bg-code: #4f5965; 17 | --color-bg-input_btn: transparent; 18 | --color-bg-radio: #262529b3; 19 | --color-bg-card-normal: #323e4f; 20 | --color-bg-card-gradient: linear-gradient(33deg, #263e4b 0%, #2b344d 100%); 21 | --color-bg-btn-gradient: linear-gradient(90deg, #35c98d 0%, #37dc85 100%); 22 | 23 | /* Font color */ 24 | --color-text-title: #ffffff; 25 | --color-text-default: #d3d3d3; 26 | --color-text-light: #a2a9b0; 27 | --color-text-tips: #b4b4b4; 28 | --color-text-right_block: #1b1d20; 29 | --color-text-right_info: #b4b4b4; 30 | --color-text-left_info: #959599; 31 | --color-text-card_icon: #eaebec; 32 | 33 | /* Accent color */ 34 | --color-main-green: #34c388; 35 | --color-minor-green: #53daa2; 36 | --color-light-green: #ebf8f2; 37 | --color-main-red: #e86aa6; 38 | --color-minor-red: #f56c6c; 39 | --color-light-red: #fcdee4; 40 | --color-main-grey: #292a33; 41 | --color-main-yellow: #e6a23cb3; 42 | --color-main-white: #fff; 43 | 44 | /* Border color */ 45 | --color-border-default: #40414e; 46 | --color-border-bold: #4f5367; 47 | --color-shadow-card: #00000085; 48 | --color-shadow-message: #151515; 49 | } 50 | -------------------------------------------------------------------------------- /src/assets/scss/variable.scss: -------------------------------------------------------------------------------- 1 | /* font */ 2 | $font-size--body: 14px; 3 | $font-size--title: 18px; 4 | $font-size--subtitle: 16px; 5 | $font-size--tips: 12px; 6 | $font-size--leftbar_title: 20px; 7 | $font-size--send: 24px; 8 | -------------------------------------------------------------------------------- /src/components/Contextmenu.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 35 | 36 | 63 | -------------------------------------------------------------------------------- /src/components/Editor.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 142 | 143 | 153 | -------------------------------------------------------------------------------- /src/components/EmptyPage.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 29 | 30 | 55 | -------------------------------------------------------------------------------- /src/components/Ipc.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 51 | -------------------------------------------------------------------------------- /src/components/LeftPanel.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 16 | 17 | 62 | -------------------------------------------------------------------------------- /src/components/Leftbar.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 69 | 70 | 135 | -------------------------------------------------------------------------------- /src/components/MsgLeftItem.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 50 | 51 | 72 | -------------------------------------------------------------------------------- /src/components/MsgPublish.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 136 | 137 | 193 | -------------------------------------------------------------------------------- /src/components/MsgRightItem.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 25 | 26 | 40 | -------------------------------------------------------------------------------- /src/components/MyDialog.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 74 | 75 | 111 | -------------------------------------------------------------------------------- /src/components/ResizeHeight.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 38 | 39 | 51 | -------------------------------------------------------------------------------- /src/components/SubscriptionsList.vue: -------------------------------------------------------------------------------- 1 | 97 | 98 | 301 | 302 | 390 | -------------------------------------------------------------------------------- /src/database/index.ts: -------------------------------------------------------------------------------- 1 | import Lowdb from 'lowdb' 2 | import LocalStorage from 'lowdb/adapters/LocalStorage' 3 | import LodashID from 'lodash-id' 4 | 5 | interface Schema { 6 | windowSize: { 7 | height: number 8 | width: number 9 | } 10 | settings: { 11 | autoCheck: boolean 12 | currentLang: string 13 | currentTheme: string 14 | } 15 | connections: [] 16 | suggestConnections: [] 17 | } 18 | 19 | class DB { 20 | private db: Lowdb.LowdbSync 21 | public constructor() { 22 | const adapter = new LocalStorage('db') 23 | this.db = Lowdb(adapter) 24 | // Use lodash-id must use insert methods 25 | this.db._.mixin(LodashID) 26 | if (!this.db.has('windowSize').value()) { 27 | this.db 28 | .set('windowSize', { 29 | width: 1025, 30 | height: 749, 31 | }) 32 | .write() 33 | } 34 | if (!this.db.has('settings').value()) { 35 | this.db 36 | .set('settings', { 37 | autoCheck: true, 38 | currentLang: 'en', 39 | currentTheme: 'light', 40 | maxReconnectTimes: 10, 41 | }) 42 | .write() 43 | } 44 | // Set max reconnection times 45 | if (!this.db.get('settings.maxReconnectTimes').value()) { 46 | this.db.set('settings.maxReconnectTimes', 10).write() 47 | } 48 | // Purple to Night 49 | if (this.db.get('settings.currentTheme').value() === 'purple') { 50 | this.db.set('settings.currentTheme', 'night').write() 51 | } 52 | if (this.db.has('brokers').value()) { 53 | this.db.unset('brokers').write() 54 | } 55 | if (this.db.has('clients').value()) { 56 | this.db.unset('clients').write() 57 | } 58 | if (!this.db.has('connections').value()) { 59 | this.db.set('connections', []).write() 60 | } 61 | if (!this.db.has('suggestConnections').value()) { 62 | this.db.set('suggestConnections', []).write() 63 | } 64 | } 65 | // read() is to keep the data of the main process and the rendering process up to date. 66 | public read() { 67 | return this.db.read() 68 | } 69 | public get(key: string): T { 70 | return this.read().get(key).value() 71 | } 72 | public find(key: string, id: string): T { 73 | const data: $TSFixed = this.read().get(key) 74 | return data.find({ id }).value() 75 | } 76 | public findChild(key: string, id: string): T { 77 | const data: $TSFixed = this.read().get(key) 78 | return data.find({ id }) 79 | } 80 | public set(key: string, value: T): T { 81 | return this.read().set(key, value).write() 82 | } 83 | public insert(key: string, value: T): T { 84 | const data: $TSFixed = this.read().get(key) 85 | return data.insert(value).write() 86 | } 87 | public update(key: string, id: string, value: T): T { 88 | const data: $TSFixed = this.read().get(key) 89 | return data.find({ id }).assign(value).write() 90 | } 91 | public remove(key: string, id: string): T { 92 | const data: $TSFixed = this.read().get(key) 93 | return data.removeById(id).write() 94 | } 95 | public filter(key: string, query: K): T { 96 | const data: $TSFixed = this.read().get(key) 97 | return data.filter(query).value() 98 | } 99 | public has(key: string): boolean { 100 | return this.read().has(key).value() 101 | } 102 | } 103 | 104 | export default new DB() 105 | -------------------------------------------------------------------------------- /src/lang/about.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | about: { 3 | zh: '关于', 4 | en: 'About', 5 | ja: 'MQTTXについて', 6 | }, 7 | update: { 8 | zh: '检查更新', 9 | en: 'Check for Updates', 10 | ja: 'バージョンチェック', 11 | }, 12 | web: { 13 | zh: '官方网站', 14 | en: 'Website', 15 | ja: '公式サイト', 16 | }, 17 | support: { 18 | zh: '支持', 19 | en: 'Support', 20 | ja: 'サポート', 21 | }, 22 | releases: { 23 | zh: '更新日志', 24 | en: 'Releases', 25 | ja: 'リリース履歴', 26 | }, 27 | mqttxDesc: { 28 | zh: 'MQTT X 是一款由 EMQ 开源的 MQTT 5.0 跨平台桌面客户端,旨在帮助开发者更快的开发、调试 MQTT 服务和应用。', 29 | en: 30 | 'MQTT X is a cross-platform MQTT 5.0 client tool open-sourced by EMQ, designed to help develop and debug MQTT services and applications faster.', 31 | ja: 32 | 'MQTT Xは、EMQによるオープンソースのMQTT 5.0クロスプラットフォーム・デスクスクライアントで、開発者がMQTTサービスおよびアプリケーションをより速く開発およびデバッグできるよう設計されています。', 33 | }, 34 | cloudTitle: { 35 | zh: '需要一个专属的 MQTT 服务器?', 36 | en: 'Need a dedicated MQTT server?', 37 | ja: '専用のMQTTサーバーが必要ですか?', 38 | }, 39 | cloudSummary: { 40 | zh: 'EMQX Cloud 是由 EMQ 提供的全托管 MQTT 云服务平台,可连接海量物联网设备并实时处理数据,且支持按量付费。', 41 | en: 42 | 'EMQX Cloud is a fully managed MQTT cloud service platform provided by EMQ that connects massive amounts of IoT devices and processes data in real-time with pay-per-use support.', 43 | ja: 44 | 'EMQX Cloudは、EMQが提供する、膨大な数のIoTデバイスを接続し、リアルタイムにデータ処理を行うフルマネージドMQTTクラウドサービスプラットフォームで、従量課金制に対応しているのが特徴です。', 45 | }, 46 | tryCloud: { 47 | zh: '免费试用 EMQX Cloud', 48 | en: 'Try EMQX Cloud for free', 49 | ja: 'EMQX Cloudの無料体験', 50 | }, 51 | } 52 | -------------------------------------------------------------------------------- /src/lang/common.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | cancel: { 3 | zh: '取消', 4 | en: 'Cancel', 5 | ja: 'キャンセル', 6 | }, 7 | confirm: { 8 | zh: '确定', 9 | en: 'Confirm', 10 | ja: '確認', 11 | }, 12 | inputRequired: { 13 | zh: '请输入', 14 | en: 'Please input', 15 | ja: '入力してください', 16 | }, 17 | selectRequired: { 18 | zh: '请选择', 19 | en: 'Please select', 20 | ja: '選択してください', 21 | }, 22 | back: { 23 | zh: '返回', 24 | en: 'Back', 25 | ja: '戻る', 26 | }, 27 | save: { 28 | zh: '保存', 29 | en: 'Save', 30 | ja: '保存', 31 | }, 32 | noData: { 33 | zh: '暂无数据', 34 | en: 'No Data', 35 | ja: 'データなし', 36 | }, 37 | createSuccess: { 38 | zh: '创建成功', 39 | en: 'Create Success', 40 | ja: '新規に成功しました', 41 | }, 42 | createfailed: { 43 | zh: '创建失败', 44 | en: 'Create Failed', 45 | ja: '新規に失敗しました', 46 | }, 47 | editSuccess: { 48 | zh: '编辑成功', 49 | en: 'Edit Success', 50 | ja: '更新に成功しました', 51 | }, 52 | editfailed: { 53 | zh: '编辑失败', 54 | en: 'Edit Failed', 55 | ja: '更新に失敗しました', 56 | }, 57 | deleteSuccess: { 58 | zh: '删除成功', 59 | en: 'Delete Success', 60 | ja: '削除に成功しました', 61 | }, 62 | deletefailed: { 63 | zh: '删除失败', 64 | en: 'Delete Failed', 65 | ja: '削除に失敗しました', 66 | }, 67 | warning: { 68 | zh: '提示', 69 | en: 'Warning', 70 | ja: 'ワーニング', 71 | }, 72 | confirmDelete: { 73 | zh: '此操作将删除 {name},是否继续?', 74 | en: 'This will delete {name}, continue?', 75 | ja: '該当操作は{name}を削除してもよろしいですか?', 76 | }, 77 | new: { 78 | zh: '新 建', 79 | en: 'New', 80 | ja: '新 規', 81 | }, 82 | delete: { 83 | zh: '删 除', 84 | en: 'Delete', 85 | ja: '削 除', 86 | }, 87 | edit: { 88 | zh: '编 辑', 89 | en: 'Edit', 90 | ja: '編 集', 91 | }, 92 | unitS: { 93 | zh: '秒', 94 | en: 's', 95 | ja: '秒', 96 | }, 97 | unitMS: { 98 | zh: '毫秒', 99 | en: 'ms', 100 | ja: 'ミリ秒', 101 | }, 102 | config: { 103 | zh: '高级配置', 104 | en: 'Advanced Config', 105 | ja: '詳細設定', 106 | }, 107 | cloud: { 108 | zh: 109 | '需要一个云原生的全托管 MQTT 服务?一键部署 EMQX Cloud!14 天免费试用!', 110 | en: 111 | 'Need a Fully Managed, Cloud-Native MQTT Messaging Service? Try EMQX Cloud now! 14-day trial, no credit card required.', 112 | ja: 113 | 'フルマネージドのクラウドネイティブMQTTメッセージングサービスが必要ですか?今すぐ EMQX Cloud をお試しください。30日間の試用版で、クレジットカードは必要ありません。', 114 | }, 115 | } 116 | -------------------------------------------------------------------------------- /src/lang/connections.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | connections: { 3 | zh: '连接', 4 | en: 'Connections', 5 | ja: '接続', 6 | }, 7 | newConnections: { 8 | zh: '新建连接', 9 | en: 'New Connection', 10 | ja: '新規接続', 11 | }, 12 | search: { 13 | zh: '搜索', 14 | en: 'Search', 15 | ja: '検索', 16 | }, 17 | searchByTopic: { 18 | zh: '按主题搜索', 19 | en: 'Search by Topic', 20 | ja: 'トピックで検索', 21 | }, 22 | topicCopied: { 23 | zh: '复制成功!', 24 | en: 'Topic copied!', 25 | ja: 'コピーが成功しました', 26 | }, 27 | clearHistory: { 28 | zh: '清除历史记录', 29 | en: 'Clear Histroy', 30 | ja: '履歴データの削除', 31 | }, 32 | notConnect: { 33 | zh: '客户端未连接', 34 | en: 'Client not connected', 35 | ja: 'クライアント未接続', 36 | }, 37 | disconnect: { 38 | zh: '断开连接', 39 | en: 'Disconnect', 40 | ja: '接続を切る', 41 | }, 42 | disconnected: { 43 | zh: '已断开连接', 44 | en: 'Disconnected', 45 | ja: '接続切れ', 46 | }, 47 | deleteConnect: { 48 | zh: '删除连接', 49 | en: 'Delete Connection', 50 | ja: '接続削除', 51 | }, 52 | all: { 53 | zh: '全部', 54 | en: 'All', 55 | ja: '全て', 56 | }, 57 | received: { 58 | zh: '已接收', 59 | en: 'Received', 60 | ja: '受信済み', 61 | }, 62 | published: { 63 | zh: '已发送', 64 | en: 'Published', 65 | ja: '送信済み', 66 | }, 67 | writeMsg: { 68 | zh: '请输入消息', 69 | en: 'Write a message', 70 | ja: 'メッセージを入力してください', 71 | }, 72 | subscriptions: { 73 | zh: '订阅列表', 74 | en: 'Subscriptions', 75 | ja: 'サブスクリプションリスト', 76 | }, 77 | subscription: { 78 | zh: '订阅', 79 | en: 'subscription', 80 | ja: 'サブスクリプション', 81 | }, 82 | newSubscription: { 83 | zh: '添加订阅', 84 | en: 'New Subscription', 85 | ja: 'サブスクリプション追加', 86 | }, 87 | subFailed: { 88 | zh: '订阅失败', 89 | en: 'Subscribe Failed', 90 | ja: 'サブスクリプションが失敗しました', 91 | }, 92 | connected: { 93 | zh: '已连接', 94 | en: 'Connected', 95 | ja: '接続済み', 96 | }, 97 | connectFailed: { 98 | zh: '连接失败', 99 | en: 'Connect Failed', 100 | ja: '接続に失敗しました', 101 | }, 102 | reconnect: { 103 | zh: '正在重连', 104 | en: 'Reconnecting', 105 | ja: '再接続中', 106 | }, 107 | connectBtn: { 108 | zh: '连 接', 109 | en: 'Connect', 110 | ja: '接 続', 111 | }, 112 | disconnectedBtn: { 113 | zh: '断开连接', 114 | en: 'Disconnect', 115 | ja: '接続を切る', 116 | }, 117 | connectionExists: { 118 | zh: '连接数据已存在', 119 | en: 'Connection already exists', 120 | ja: '接続がすでに存在しました', 121 | }, 122 | brokerIP: { 123 | zh: '服务器地址', 124 | en: 'Host', 125 | ja: 'ホスト', 126 | }, 127 | brokerPort: { 128 | zh: '端口', 129 | en: 'Port', 130 | ja: 'ポート', 131 | }, 132 | certType: { 133 | zh: '证书类型', 134 | en: 'Certificate', 135 | ja: '証明書タイプ', 136 | }, 137 | name: { 138 | zh: '名称', 139 | en: 'Name', 140 | ja: '名前', 141 | }, 142 | username: { 143 | zh: '用户名', 144 | en: 'Username', 145 | ja: 'ユーザー名', 146 | }, 147 | password: { 148 | zh: '密码', 149 | en: 'Password', 150 | ja: 'パスワード', 151 | }, 152 | ca: { 153 | zh: 'CA 文件', 154 | en: 'CA File', 155 | ja: 'CA ファイル', 156 | }, 157 | cert: { 158 | zh: '客户端证书', 159 | en: 'Client Certificate File', 160 | ja: 'クライアント証明書', 161 | }, 162 | key: { 163 | zh: '客户端 key 文件', 164 | en: 'Client key file', 165 | ja: 'クライアント キー ファイル', 166 | }, 167 | connectionTimeout: { 168 | zh: '连接超时时长', 169 | en: 'Connect Timeout', 170 | ja: '接続タイムアウト', 171 | }, 172 | cleanSession: { 173 | zh: '清除会话', 174 | en: 'Clean Session', 175 | ja: 'セッションクリア', 176 | }, 177 | autoReconnect: { 178 | zh: '自动重连', 179 | en: 'Auto Reconnect', 180 | ja: '自動再接続', 181 | }, 182 | mqttVersion: { 183 | zh: 'MQTT 版本', 184 | en: 'MQTT Version', 185 | ja: 'MQTT バージョン', 186 | }, 187 | sessionExpiryInterval: { 188 | zh: '会话过期时间', 189 | en: 'Session Expiry Interval', 190 | ja: 'セッション有効期限', 191 | }, 192 | receiveMaximum: { 193 | zh: '接收最大数值', 194 | en: 'Receive Maximum', 195 | ja: '最大受信数', 196 | }, 197 | topicAliasMaximum: { 198 | zh: '主题别名最大值', 199 | en: 'Topic Alias Maximum', 200 | ja: 'トピックエイリアスの最大値', 201 | }, 202 | requestResponseInformation: { 203 | zh: '请求响应信息', 204 | en: 'Request Response Information', 205 | ja: 'レスポンス情報をリクエストする', 206 | }, 207 | requestProblemInformation: { 208 | zh: '请求失败信息', 209 | en: 'Request Problem Information', 210 | ja: '失敗情報をリクエストする', 211 | }, 212 | topicReuired: { 213 | zh: '请输入 Topic', 214 | en: 'Topic is required', 215 | ja: 'トピックを入力してください', 216 | }, 217 | payloadReuired: { 218 | zh: '请输入 Payload', 219 | en: 'Payload is required', 220 | ja: 'Payloadを入力してください', 221 | }, 222 | color: { 223 | zh: '标记', 224 | en: 'Color', 225 | ja: 'マーク', 226 | }, 227 | willMessage: { 228 | zh: '遗嘱消息', 229 | en: 'Last Will and Testament', 230 | ja: '遺言', 231 | }, 232 | strictValidateCertificate: { 233 | zh: '严格证书验证', 234 | en: 'Strict validate Certificate', 235 | ja: 'SSL証明書', 236 | }, 237 | willTopic: { 238 | zh: '遗嘱消息主题', 239 | en: 'Last-Will Topic', 240 | ja: '遺言トピック', 241 | }, 242 | willPayload: { 243 | zh: '遗嘱消息', 244 | en: 'Last-Will Payload', 245 | ja: '遺言 Payload', 246 | }, 247 | willQos: { 248 | zh: '遗嘱消息 QoS', 249 | en: 'Last-Will QoS', 250 | ja: '遺言 QoS', 251 | }, 252 | willRetain: { 253 | zh: '遗嘱消息保留标志', 254 | en: 'Last-Will Retain', 255 | ja: '遺言 Retain', 256 | }, 257 | willDelayInterval: { 258 | zh: '遗嘱消息延迟时间', 259 | en: 'Will Delay Interval', 260 | ja: '遺言ディレイ間隔', 261 | }, 262 | messageExpiryInterval: { 263 | zh: '遗嘱消息过期时间', 264 | en: 'Message Expiry Interval', 265 | ja: '遺言有効期限', 266 | }, 267 | contentType: { 268 | zh: '遗嘱消息描述', 269 | en: 'Content Type', 270 | ja: '遺言詳細情報', 271 | }, 272 | isUTF8Data: { 273 | zh: '是否为 UTF-8 编码数据', 274 | en: 'is UTF-8 Encoded Data', 275 | ja: 'UTF-8エンコードデータフラグ', 276 | }, 277 | duplicateName: { 278 | zh: '该名称已存在,请重新命名!', 279 | en: 'Duplicate name. Please rename it!', 280 | ja: '名称が既に存在したので、リネームをお願いします!', 281 | }, 282 | nameTip: { 283 | zh: '可快速选择已创建过的连接配置', 284 | en: 'Quick selection of created connection configurations', 285 | ja: '作成された接続構成を早めに選択することができます', 286 | }, 287 | publishMsg: { 288 | zh: '发送消息', 289 | en: 'Publish message', 290 | ja: 'メッセージを送る', 291 | }, 292 | receivedMsg: { 293 | zh: '接收消息', 294 | en: 'Received message', 295 | ja: 'メッセージを受信する', 296 | }, 297 | receivedPayloadDecodedBy: { 298 | zh: '接收到的 Payload 解码', 299 | en: 'Received payload decoded by', 300 | ja: '受信したペイロードをデコードする', 301 | }, 302 | alias: { 303 | zh: '别名', 304 | en: 'Alias', 305 | ja: '別名', 306 | }, 307 | } 308 | -------------------------------------------------------------------------------- /src/lang/index.ts: -------------------------------------------------------------------------------- 1 | import VueI18n from 'vue-i18n' 2 | 3 | import zhLocale from 'element-ui/lib/locale/lang/zh-CN' 4 | import enLocale from 'element-ui/lib/locale/lang/en' 5 | import jaLocale from 'element-ui/lib/locale/lang/ja' 6 | 7 | import { formati18n } from '@/utils/i18n' 8 | 9 | const supportLang: SupportLangModel = ['zh', 'en', 'ja'] 10 | const i18nModules: i18nLocaleModel = ['connections', 'settings', 'common', 'about'] 11 | 12 | const { en, zh, ja }: VueI18n.LocaleMessages = formati18n(i18nModules, supportLang) 13 | 14 | const lang: VueI18n.LocaleMessages = { 15 | en: { ...en, ...enLocale }, 16 | zh: { ...zh, ...zhLocale }, 17 | ja: { ...ja, ...jaLocale }, 18 | } 19 | 20 | export default lang 21 | -------------------------------------------------------------------------------- /src/lang/settings.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | settings: { 3 | zh: '设置', 4 | en: 'Settings', 5 | ja: '設定', 6 | }, 7 | general: { 8 | zh: '基础', 9 | en: 'General', 10 | ja: '一般', 11 | }, 12 | language: { 13 | zh: '语言', 14 | en: 'Language', 15 | ja: '言語', 16 | }, 17 | automatically: { 18 | zh: '自动检查更新', 19 | en: 'Auto check update', 20 | ja: '自動更新チェック', 21 | }, 22 | maxReconnectTimes: { 23 | zh: '最大重连次数', 24 | en: 'Max reconnection times', 25 | ja: '最大再接続回数', 26 | }, 27 | appearance: { 28 | zh: '外观', 29 | en: 'Appearance', 30 | ja: '外観', 31 | }, 32 | theme: { 33 | zh: '主题', 34 | en: 'Theme', 35 | ja: 'テーマ', 36 | }, 37 | light: { 38 | zh: '浅色', 39 | en: 'Light', 40 | ja: 'ライト', 41 | }, 42 | dark: { 43 | zh: '深色', 44 | en: 'Dark', 45 | ja: 'ダーク', 46 | }, 47 | advanced: { 48 | zh: '高级', 49 | en: 'Advanced', 50 | ja: '詳細', 51 | }, 52 | } 53 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | import 'element-ui/lib/theme-chalk/index.css' 4 | import ElementLocale from 'element-ui/lib/locale' 5 | import App from './App.vue' 6 | import router from './router/index' 7 | import store from './store/index' 8 | import VueI18n from 'vue-i18n' 9 | import VueClipboard from 'vue-clipboard2' 10 | import Lang from './lang' 11 | import element from './utils/element' 12 | 13 | Vue.use(element) 14 | Vue.use(VueI18n) 15 | Vue.use(VueClipboard) 16 | 17 | const locale: Language = store.state.app.currentLang 18 | const vueI18n: VueI18n = new VueI18n({ 19 | locale, 20 | messages: Lang, 21 | }) 22 | const { i18n }: any = ElementLocale 23 | i18n((key: any, value: any) => vueI18n.t(key, value)) 24 | 25 | Vue.config.productionTip = false 26 | 27 | new Vue({ 28 | router, 29 | store, 30 | i18n: vueI18n, 31 | render: h => h(App), 32 | }).$mount('#app') 33 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import routes from './routes' 4 | import { loadConnections } from '@/utils/api/connection' 5 | import { ConnectionModel } from '@/views/Connections/types' 6 | 7 | Vue.use(Router) 8 | 9 | const router: Router = new Router({ 10 | mode: 'history', 11 | base: process.env.BASE_URL, 12 | scrollBehavior(to, from, savedPosition) { 13 | if (to.meta.keepAlive && savedPosition) { 14 | return savedPosition 15 | } 16 | return { x: 0, y: 0 } 17 | }, 18 | routes, 19 | }) 20 | 21 | // Fix Uncaught (in promise) NavigationDuplicated {_name: "NavigationDuplicated"} 22 | const originalPush = Router.prototype.push 23 | Router.prototype.push = function push(location: string) { 24 | const callRes: $TSFixed = originalPush.call(this, location) 25 | return callRes.catch((err: Error) => err) 26 | } 27 | 28 | router.beforeEach((to, from, next) => { 29 | if (to.name === 'Connections') { 30 | const connections: ConnectionModel[] | [] = loadConnections() || [] 31 | if (connections.length) { 32 | next({ path: `/recent_connections/${connections[0].id}` }) 33 | } else { 34 | next() 35 | } 36 | } else { 37 | next() 38 | } 39 | }) 40 | 41 | export default router 42 | -------------------------------------------------------------------------------- /src/router/routes.ts: -------------------------------------------------------------------------------- 1 | import Home from '../views/Home.vue' 2 | 3 | const routes: Routes[] = [ 4 | { 5 | path: '/', 6 | redirect: '/recent_connections', 7 | name: 'Home', 8 | component: Home, 9 | children: [ 10 | { path: '/recent_connections', name: 'Connections', component: () => import('../views/connections/index.vue') }, 11 | { 12 | path: '/recent_connections/:id', 13 | name: 'ConnectionDetails', 14 | component: () => import('../views/connections/index.vue'), 15 | }, 16 | { path: '/settings', name: 'Settings', component: () => import('../views/settings/index.vue') }, 17 | { path: '/about', name: 'About', component: () => import('../views/about/index.vue') }, 18 | ], 19 | }, 20 | ] 21 | 22 | export default routes 23 | -------------------------------------------------------------------------------- /src/store/getter.ts: -------------------------------------------------------------------------------- 1 | const getters = { 2 | currentTheme: (state: State) => state.app.currentTheme, 3 | currentLang: (state: State) => state.app.currentLang, 4 | autoCheck: (state: State) => state.app.autoCheck, 5 | maxReconnectTimes: (state: State) => state.app.maxReconnectTimes, 6 | showSubscriptions: (state: State) => state.app.showSubscriptions, 7 | activeConnection: (state: State) => state.app.activeConnection, 8 | showClientInfo: (state: State) => state.app.showClientInfo, 9 | unreadMessageCount: (state: State) => state.app.unreadMessageCount, 10 | willMessageVisible: (state: State) => state.app.willMessageVisible, 11 | advancedVisible: (state: State) => state.app.advancedVisible, 12 | allConnections: (state: State) => state.app.allConnections, 13 | } 14 | 15 | export default getters 16 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | import app from './modules/app' 5 | import getters from './getter' 6 | 7 | Vue.use(Vuex) 8 | 9 | export default new Vuex.Store({ 10 | modules: { 11 | app, 12 | }, 13 | getters, 14 | }) 15 | -------------------------------------------------------------------------------- /src/store/modules/app.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { ConnectionModel } from '../../views/connections/types' 3 | import { loadSettings, setSettings } from '@/utils/api/setting' 4 | 5 | const TOGGLE_THEME = 'TOGGLE_THEME' 6 | const TOGGLE_LANG = 'TOGGLE_LANG' 7 | const TOGGLE_AUTO_CHECK = 'TOGGLE_AUTO_CHECK' 8 | const SET_MAX_RECONNECT_TIMES = 'SET_MAX_RECONNECT_TIMES' 9 | const CHANGE_ACTIVE_CONNECTION = 'CHANGE_ACTIVE_CONNECTION' 10 | const REMOVE_ACTIVE_CONNECTION = 'REMOVE_ACTIVE_CONNECTION' 11 | const CHANGE_SUBSCRIPTIONS = 'CHANGE_SUBSCRIPTIONS' 12 | const SHOW_CLIENT_INFO = 'SHOW_CLIENT_INFO' 13 | const SHOW_SUBSCRIPTIONS = 'SHOW_SUBSCRIPTIONS' 14 | const UNREAD_MESSAGE_COUNT_INCREMENT = 'UNREAD_MESSAGE_COUNT_INCREMENT' 15 | const TOGGLE_WILL_MESSAGE_VISIBLE = 'TOGGLE_WILL_MESSAGE_VISIBLE' 16 | const TOGGLE_ADVANCED_VISIBLE = 'TOGGLE_ADVANCED_VISIBLE' 17 | const CHANGE_ALL_CONNECTIONS = 'CHANGE_ALL_CONNECTIONS' 18 | 19 | const stateRecord: App = loadSettings() 20 | 21 | const getShowSubscriptions = (): boolean => { 22 | const $showSubscriptions: string | null = localStorage.getItem('showSubscriptions') 23 | if (!$showSubscriptions) { 24 | return true 25 | } 26 | return JSON.parse($showSubscriptions) 27 | } 28 | 29 | const app = { 30 | state: { 31 | currentTheme: stateRecord.currentTheme || 'light', 32 | currentLang: stateRecord.currentLang || 'en', 33 | autoCheck: stateRecord.autoCheck, 34 | maxReconnectTimes: stateRecord.maxReconnectTimes || 10, 35 | showSubscriptions: getShowSubscriptions(), 36 | showClientInfo: {}, 37 | unreadMessageCount: {}, 38 | activeConnection: {}, 39 | advancedVisible: true, 40 | willMessageVisible: true, 41 | allConnections: [], 42 | }, 43 | mutations: { 44 | [TOGGLE_THEME](state: App, currentTheme: Theme) { 45 | state.currentTheme = currentTheme 46 | }, 47 | [TOGGLE_LANG](state: App, currentLang: Language) { 48 | state.currentLang = currentLang 49 | }, 50 | [TOGGLE_AUTO_CHECK](state: App, autoCheck: boolean) { 51 | state.autoCheck = autoCheck 52 | }, 53 | [SET_MAX_RECONNECT_TIMES](state: App, maxReconnectTimes: number) { 54 | state.maxReconnectTimes = maxReconnectTimes 55 | }, 56 | [CHANGE_ACTIVE_CONNECTION](state: App, payload: Client) { 57 | const client = payload.client 58 | const messages = payload.messages 59 | if (state.activeConnection[payload.id]) { 60 | state.activeConnection[payload.id].client = client 61 | state.activeConnection[payload.id].messages = messages 62 | } else { 63 | state.activeConnection[payload.id] = { 64 | client, 65 | messages, 66 | } 67 | } 68 | }, 69 | [REMOVE_ACTIVE_CONNECTION](state: App, id: string) { 70 | delete state.activeConnection[id] 71 | delete state.unreadMessageCount[id] 72 | delete state.showClientInfo[id] 73 | }, 74 | [CHANGE_SUBSCRIPTIONS](state: App, payload: Subscriptions) { 75 | state.activeConnection[payload.id].subscriptions = payload.subscriptions 76 | }, 77 | [SHOW_CLIENT_INFO](state: App, payload: ClientInfo) { 78 | state.showClientInfo[payload.id] = payload.showClientInfo 79 | }, 80 | [SHOW_SUBSCRIPTIONS](state: App, payload: SubscriptionsVisible) { 81 | state.showSubscriptions = payload.showSubscriptions 82 | localStorage.setItem('showSubscriptions', JSON.stringify(state.showSubscriptions)) 83 | }, 84 | [UNREAD_MESSAGE_COUNT_INCREMENT](state: App, payload: UnreadMessage) { 85 | if (payload.unreadMessageCount !== undefined) { 86 | Vue.set(state.unreadMessageCount, payload.id, payload.unreadMessageCount) 87 | } else { 88 | const currentCount = state.unreadMessageCount[payload.id] 89 | let count = 0 90 | if (currentCount !== undefined) { 91 | count = currentCount + 1 92 | } else { 93 | count += 1 94 | } 95 | Vue.set(state.unreadMessageCount, payload.id, count) 96 | } 97 | }, 98 | [TOGGLE_ADVANCED_VISIBLE](state: App, advancedVisible: boolean) { 99 | state.advancedVisible = advancedVisible 100 | }, 101 | [TOGGLE_WILL_MESSAGE_VISIBLE](state: App, willMessageVisible: boolean) { 102 | state.willMessageVisible = willMessageVisible 103 | }, 104 | [CHANGE_ALL_CONNECTIONS](state: App, allConnections: ConnectionModel[] | []) { 105 | state.allConnections = allConnections 106 | }, 107 | }, 108 | actions: { 109 | TOGGLE_THEME({ commit }: any, payload: App) { 110 | setSettings('settings.currentTheme', payload.currentTheme) 111 | commit(TOGGLE_THEME, payload.currentTheme) 112 | }, 113 | TOGGLE_LANG({ commit }: any, payload: App) { 114 | setSettings('settings.currentLang', payload.currentLang) 115 | commit(TOGGLE_LANG, payload.currentLang) 116 | }, 117 | TOGGLE_AUTO_CHECK({ commit }: any, payload: App) { 118 | setSettings('settings.autoCheck', payload.autoCheck) 119 | commit(TOGGLE_AUTO_CHECK, payload.autoCheck) 120 | }, 121 | SET_MAX_RECONNECT_TIMES({ commit }: any, payload: App) { 122 | setSettings('settings.maxReconnectTimes', payload.maxReconnectTimes) 123 | commit(SET_MAX_RECONNECT_TIMES, payload.maxReconnectTimes) 124 | }, 125 | CHANGE_ACTIVE_CONNECTION({ commit }: any, payload: App) { 126 | commit(CHANGE_ACTIVE_CONNECTION, payload) 127 | }, 128 | REMOVE_ACTIVE_CONNECTION({ commit }: any, { id }: { id: string }) { 129 | commit(REMOVE_ACTIVE_CONNECTION, id) 130 | }, 131 | CHANGE_SUBSCRIPTIONS({ commit }: any, payload: App) { 132 | commit(CHANGE_SUBSCRIPTIONS, payload) 133 | }, 134 | SHOW_CLIENT_INFO({ commit }: any, payload: App) { 135 | commit(SHOW_CLIENT_INFO, payload) 136 | }, 137 | SHOW_SUBSCRIPTIONS({ commit }: any, payload: App) { 138 | commit(SHOW_SUBSCRIPTIONS, payload) 139 | }, 140 | UNREAD_MESSAGE_COUNT_INCREMENT({ commit }: any, payload: App) { 141 | commit(UNREAD_MESSAGE_COUNT_INCREMENT, payload) 142 | }, 143 | TOGGLE_ADVANCED_VISIBLE({ commit }: any, payload: App) { 144 | commit(TOGGLE_ADVANCED_VISIBLE, payload.advancedVisible) 145 | }, 146 | TOGGLE_WILL_MESSAGE_VISIBLE({ commit }: any, payload: App) { 147 | commit(TOGGLE_WILL_MESSAGE_VISIBLE, payload.willMessageVisible) 148 | }, 149 | CHANGE_ALL_CONNECTIONS({ commit }: any, payload: App) { 150 | commit(CHANGE_ALL_CONNECTIONS, payload.allConnections) 151 | }, 152 | }, 153 | } 154 | 155 | export default app 156 | -------------------------------------------------------------------------------- /src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { TranslateResult } from 'vue-i18n' 3 | import { MqttClient } from 'mqtt' 4 | import { MessageModel, ConnectionModel } from '@/views/connections/types' 5 | 6 | declare global { 7 | type $TSFixed = any 8 | 9 | type Theme = 'light' | 'dark' | 'night' 10 | 11 | type Language = 'zh' | 'en' | 'ja' 12 | 13 | type Protocol = 'ws' | 'wss' 14 | 15 | type PayloadType = 'Plaintext' | 'Base64' | 'JSON' | 'Hex' 16 | 17 | type VueForm = Vue & { 18 | validate: (validate: (valid: boolean) => void) => void 19 | clearValidate: () => void 20 | resetFields: () => void 21 | } 22 | 23 | type EditorRef = Vue & { 24 | editorLayout: () => void 25 | } 26 | 27 | interface ActiveConnection { 28 | readonly id: string 29 | } 30 | 31 | interface Client extends ActiveConnection { 32 | client: MqttClient | {} 33 | messages: MessageModel[] 34 | } 35 | 36 | interface Message extends ActiveConnection { 37 | message: MessageModel 38 | } 39 | 40 | interface ClientInfo extends ActiveConnection { 41 | showClientInfo: boolean 42 | } 43 | 44 | interface Subscriptions extends ActiveConnection { 45 | subscriptions: SubscriptionModel[] 46 | } 47 | 48 | interface UnreadMessage extends ActiveConnection { 49 | unreadMessageCount?: 0 50 | } 51 | 52 | interface SubscriptionsVisible { 53 | showSubscriptions: boolean 54 | } 55 | 56 | type PluginFunction = (Vue: any, options?: T) => void 57 | 58 | interface PluginObject { 59 | install: PluginFunction 60 | [key: string]: any 61 | } 62 | 63 | interface App { 64 | currentTheme: Theme 65 | currentLang: Language 66 | autoCheck: boolean 67 | showSubscriptions: boolean 68 | maxReconnectTimes: number 69 | showClientInfo: { 70 | [id: string]: boolean 71 | } 72 | unreadMessageCount: { 73 | [id: string]: number 74 | } 75 | activeConnection: { 76 | [id: string]: { 77 | client: MqttClient | {} 78 | messages: MessageModel[] 79 | subscriptions?: SubscriptionModel[] 80 | } 81 | } 82 | willMessageVisible: boolean 83 | advancedVisible: boolean 84 | allConnections: ConnectionModel[] | [] 85 | } 86 | 87 | interface State { 88 | app: App 89 | } 90 | 91 | interface Routes { 92 | path: string 93 | component: any 94 | name: string 95 | redirect?: string 96 | children?: Routes[] 97 | } 98 | 99 | interface Options { 100 | value: any 101 | label: string | TranslateResult 102 | children?: Options[] 103 | disabled?: boolean 104 | } 105 | 106 | type QoSList = [0, 1, 2] 107 | 108 | interface SubscriptionModel { 109 | topic: string 110 | alias?: string 111 | qos: 0 | 1 | 2 112 | retain?: boolean 113 | color?: string 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/types/locale.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'element-ui/lib/transitions/collapse-transition' 2 | declare module 'element-ui/lib/locale' {} 3 | declare module 'element-ui/lib/locale/lang/en' {} 4 | declare module 'element-ui/lib/locale/lang/zh-CN' {} 5 | declare module 'element-ui/lib/locale/lang/ja' {} 6 | 7 | type i18nLocaleModel = ['connections', 'settings', 'common', 'about'] 8 | type SupportLangModel = ['zh', 'en', 'ja'] 9 | 10 | declare module '*.json' { 11 | const value: any 12 | export default value 13 | } 14 | -------------------------------------------------------------------------------- /src/types/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from 'vue' 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | interface Element extends VNode {} 7 | // tslint:disable no-empty-interface 8 | interface ElementClass extends Vue {} 9 | interface IntrinsicElements { 10 | [elem: string]: any 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/types/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'lodash-id' 2 | declare module 'jump.js' 3 | declare module 'uuid' 4 | declare module 'vue-click-outside' 5 | declare module '*.vue' { 6 | import Vue from 'vue' 7 | export default Vue 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/api/connection.ts: -------------------------------------------------------------------------------- 1 | import db from '@/database/index' 2 | import { ConnectionModel, MessageModel } from '@/views/connections/types' 3 | 4 | export const loadConnection = (id: string): ConnectionModel => { 5 | return db.find('connections', id) 6 | } 7 | 8 | export const loadConnections = (): ConnectionModel[] | [] => { 9 | return db.get('connections') 10 | } 11 | 12 | export const loadSuggestConnections = (): ConnectionModel[] | [] => { 13 | return db.get('suggestConnections') 14 | } 15 | 16 | export const createSuggestConnection = (data: ConnectionModel): ConnectionModel => { 17 | if (loadSuggestConnections().length > 9) { 18 | const deleteId = loadSuggestConnections()[0].id 19 | if (deleteId !== undefined) { 20 | deleteSuggestConnection(deleteId) 21 | } 22 | } 23 | return db.insert('suggestConnections', data) 24 | } 25 | 26 | export const deleteSuggestConnection = (id: string): ConnectionModel => { 27 | return db.remove('suggestConnections', id) 28 | } 29 | 30 | export const createConnection = (data: ConnectionModel): ConnectionModel => { 31 | createSuggestConnection(data) 32 | return db.insert('connections', data) 33 | } 34 | 35 | export const deleteConnection = (id: string): ConnectionModel => { 36 | return db.remove('connections', id) 37 | } 38 | 39 | export const updateConnection = (id: string, data: ConnectionModel): ConnectionModel => { 40 | return db.update('connections', id, data) 41 | } 42 | 43 | export const updateConnectionMessage = (id: string, message: MessageModel): ConnectionModel => { 44 | const connection: ConnectionModel = loadConnection(id) 45 | connection.messages.push(message) 46 | return db.update('connections', id, connection) 47 | } 48 | 49 | export default {} 50 | -------------------------------------------------------------------------------- /src/utils/api/setting.ts: -------------------------------------------------------------------------------- 1 | import db from '@/database/index' 2 | 3 | export const loadSettings = (): App => { 4 | return db.get('settings') 5 | } 6 | 7 | export const setSettings = (key: string, value: string | boolean | number): string | boolean | number => { 8 | return db.set(key, value) 9 | } 10 | 11 | export default {} 12 | -------------------------------------------------------------------------------- /src/utils/colors.ts: -------------------------------------------------------------------------------- 1 | export const defineColors = ['#34C388', '#6ECBEE', '#D08CF1', '#907AEF', '#EDB16E'] 2 | 3 | export const getRandomColor = (): string => { 4 | const letters = '0123456789ABCDEF' 5 | let color = '#' 6 | for (let i = 0; i < 6; i += 1) { 7 | color += letters[Math.floor(Math.random() * 16)] 8 | } 9 | return color 10 | } 11 | 12 | export default {} 13 | -------------------------------------------------------------------------------- /src/utils/convertPayload.ts: -------------------------------------------------------------------------------- 1 | interface CodeType { 2 | encode: (str: string) => string 3 | decode: (str: string) => string 4 | } 5 | 6 | const convertBase64 = (value: string, codeType: 'encode' | 'decode'): string => { 7 | const convertMap: CodeType = { 8 | encode(str: string): string { 9 | return window.btoa(str) 10 | }, 11 | decode(str: string): string { 12 | return window.atob(str) 13 | }, 14 | } 15 | return convertMap[codeType](value) 16 | } 17 | 18 | const convertHex = (value: string, codeType: 'encode' | 'decode'): string => { 19 | const convertMap: CodeType = { 20 | encode(str: string): string { 21 | return Buffer.from(str, 'utf-8').toString('hex') 22 | }, 23 | decode(str: string): string { 24 | return Buffer.from(str, 'hex').toString('utf-8') 25 | }, 26 | } 27 | return convertMap[codeType](value) 28 | } 29 | 30 | const convertJSON = (value: string): Promise => { 31 | return new Promise((resolve, reject) => { 32 | try { 33 | let $json = JSON.parse(value) 34 | $json = JSON.stringify($json, null, 2) 35 | return resolve($json) 36 | } catch (error) { 37 | return reject(error) 38 | } 39 | }) 40 | } 41 | 42 | const convertPayload = async (payload: string, currentType: PayloadType, fromType: PayloadType): Promise => { 43 | let $payload = payload 44 | switch (fromType) { 45 | case 'Base64': 46 | $payload = convertBase64(payload, 'decode') 47 | break 48 | case 'Hex': 49 | $payload = convertHex(payload, 'decode') 50 | break 51 | } 52 | if (currentType === 'Base64') { 53 | $payload = convertBase64($payload, 'encode') 54 | } 55 | if (currentType === 'JSON') { 56 | $payload = await convertJSON($payload) 57 | } 58 | if (currentType === 'Hex') { 59 | $payload = convertHex($payload, 'encode') 60 | } 61 | return $payload 62 | } 63 | 64 | export default convertPayload 65 | -------------------------------------------------------------------------------- /src/utils/deepMerge.ts: -------------------------------------------------------------------------------- 1 | interface MergeObjModel { 2 | [key: string]: any 3 | } 4 | 5 | const deepMerge = (target: MergeObjModel, source: MergeObjModel) => { 6 | for (const key of Object.keys(source)) { 7 | if (source[key] instanceof Object) { 8 | Object.assign(source[key], deepMerge(target[key], source[key])) 9 | } 10 | } 11 | Object.assign(target || {}, source) 12 | return target 13 | } 14 | 15 | export default deepMerge 16 | -------------------------------------------------------------------------------- /src/utils/element.ts: -------------------------------------------------------------------------------- 1 | import '@/assets/scss/element/element-variables.scss' 2 | import '@/assets/scss/element/element-reset.scss' 3 | import CollapseTransition from 'element-ui/lib/transitions/collapse-transition' 4 | 5 | import { 6 | // Pagination, 7 | Dialog, 8 | Autocomplete, 9 | Dropdown, 10 | DropdownMenu, 11 | DropdownItem, 12 | // Menu, 13 | // Submenu, 14 | // MenuItem, 15 | // MenuItemGroup, 16 | Input, 17 | InputNumber, 18 | Radio, 19 | RadioGroup, 20 | RadioButton, 21 | Checkbox, 22 | // CheckboxButton, 23 | // CheckboxGroup, 24 | Switch, 25 | Select, 26 | Option, 27 | // OptionGroup, 28 | Button, 29 | // ButtonGroup, 30 | // Table, 31 | // TableColumn, 32 | // DatePicker, 33 | // TimeSelect, 34 | // TimePicker, 35 | Popover, 36 | Tooltip, 37 | // Breadcrumb, 38 | // BreadcrumbItem, 39 | Form, 40 | FormItem, 41 | // Tabs, 42 | // TabPane, 43 | // Tag, 44 | // Tree, 45 | // Alert, 46 | // Slider, 47 | // Icon, 48 | Row, 49 | Col, 50 | // Upload, 51 | // Progress, 52 | // Badge, 53 | Card, 54 | // Rate, 55 | // Steps, 56 | // Step, 57 | // Carousel, 58 | // CarouselItem, 59 | // Collapse, 60 | // CollapseItem, 61 | Cascader, 62 | ColorPicker, 63 | // Transfer, 64 | // Container, 65 | // Header, 66 | // Aside, 67 | // Main, 68 | // Footer, 69 | Loading, 70 | MessageBox, 71 | Message, 72 | Notification, 73 | Divider, 74 | } from 'element-ui' 75 | 76 | export default (Vue: any) => { 77 | // Vue.use(Pagination) 78 | Vue.use(Dialog) 79 | Vue.use(Autocomplete) 80 | Vue.use(Dropdown) 81 | Vue.use(DropdownMenu) 82 | Vue.use(DropdownItem) 83 | // Vue.use(Menu) 84 | // Vue.use(Submenu) 85 | // Vue.use(MenuItem) 86 | // Vue.use(MenuItemGroup) 87 | Vue.use(Input) 88 | Vue.use(InputNumber) 89 | Vue.use(Radio) 90 | Vue.use(RadioGroup) 91 | Vue.use(RadioButton) 92 | Vue.use(Checkbox) 93 | // Vue.use(CheckboxButton) 94 | // Vue.use(CheckboxGroup) 95 | Vue.use(Switch) 96 | Vue.use(Select) 97 | Vue.use(Option) 98 | // Vue.use(OptionGroup) 99 | Vue.use(Button) 100 | // Vue.use(ButtonGroup) 101 | // Vue.use(Table) 102 | // Vue.use(TableColumn) 103 | // Vue.use(DatePicker) 104 | // Vue.use(TimeSelect) 105 | // Vue.use(TimePicker) 106 | Vue.use(Popover) 107 | Vue.use(Tooltip) 108 | // Vue.use(Breadcrumb) 109 | // Vue.use(BreadcrumbItem) 110 | Vue.use(Form) 111 | Vue.use(FormItem) 112 | // Vue.use(Tabs) 113 | // Vue.use(TabPane) 114 | // Vue.use(Tag) 115 | // Vue.use(Tree) 116 | // Vue.use(Alert) 117 | // Vue.use(Slider) 118 | // Vue.use(Icon) 119 | Vue.use(Row) 120 | Vue.use(Col) 121 | // Vue.use(Upload) 122 | // Vue.use(Progress) 123 | // Vue.use(Badge) 124 | Vue.use(Card) 125 | // Vue.use(Rate) 126 | // Vue.use(Steps) 127 | // Vue.use(Step) 128 | // Vue.use(Carousel) 129 | // Vue.use(CarouselItem) 130 | // Vue.use(Collapse) 131 | // Vue.use(CollapseItem) 132 | Vue.use(Cascader) 133 | Vue.use(ColorPicker) 134 | // Vue.use(Transfer) 135 | // Vue.use(Container) 136 | // Vue.use(Header) 137 | // Vue.use(Aside) 138 | // Vue.use(Main) 139 | // Vue.use(Footer) 140 | Vue.use(Divider) 141 | 142 | Vue.use(Loading.directive) 143 | 144 | // Vue.prototype.$loading = Loading.service 145 | // Vue.prototype.$msgbox = MessageBox 146 | // Vue.prototype.$alert = MessageBox.alert 147 | Vue.prototype.$confirm = MessageBox.confirm 148 | // Vue.prototype.$prompt = MessageBox.prompt 149 | Vue.prototype.$notify = Notification 150 | Vue.prototype.$message = Message 151 | 152 | Vue.component(CollapseTransition.name, CollapseTransition) 153 | } 154 | -------------------------------------------------------------------------------- /src/utils/getClientId.ts: -------------------------------------------------------------------------------- 1 | export default (): string => { 2 | return `mqttx_${Math.random().toString(16).substr(2, 8)}` as string 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/getFiles.ts: -------------------------------------------------------------------------------- 1 | import { Notification } from 'element-ui' 2 | import { SSLPath, SSLContent } from '@/views/connections/types' 3 | 4 | export const getSSLFile = (sslPath: SSLPath): SSLContent | undefined => { 5 | const { ca, cert, key } = sslPath 6 | try { 7 | const res: SSLContent = { 8 | ca: ca !== '' ? ca : undefined, 9 | cert: cert !== '' ? cert : undefined, 10 | key: key !== '' ? key : undefined, 11 | } 12 | return res 13 | } catch (error) { 14 | Notification({ 15 | title: error.toString(), 16 | message: '', 17 | type: 'error', 18 | }) 19 | } 20 | } 21 | 22 | export default {} 23 | -------------------------------------------------------------------------------- /src/utils/i18n.ts: -------------------------------------------------------------------------------- 1 | import VueI18n from 'vue-i18n' 2 | 3 | type FormatLang = { 4 | [key in Language]?: any 5 | } 6 | 7 | export const formati18n = (transItems: i18nLocaleModel, langs: SupportLangModel): VueI18n.LocaleMessages => { 8 | const formatLang: FormatLang = {} 9 | langs.forEach((lang) => { 10 | formatLang[lang] = { 11 | connections: {}, 12 | settings: {}, 13 | common: {}, 14 | about: {}, 15 | } 16 | }) 17 | transItems.forEach((item) => { 18 | const values = require(`@/lang/${item}`).default 19 | Object.keys(values).forEach((key: string) => { 20 | langs.forEach((lang) => { 21 | formatLang[lang][item][key] = values[key][lang] 22 | }) 23 | }) 24 | }) 25 | return formatLang 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/matchSearch.ts: -------------------------------------------------------------------------------- 1 | type MatchSearch = (data: any[], searchKey: string, searchValue: string) => Promise 2 | 3 | const matchSearch: MatchSearch = (data, searchKey, searchValue) => { 4 | return new Promise((resolve, reject) => { 5 | try { 6 | const filterData = data.filter(($) => { 7 | if ($[searchKey]) { 8 | const key: string = $[searchKey].toLowerCase().replace(/\s+/g, '') 9 | const value: string = searchValue 10 | .toLocaleLowerCase() 11 | .replace(/\s+/g, '') 12 | .replace(/[~#^$@%&!+*]/gi, (val) => `\\${val}`) 13 | return key.match(value) 14 | } else { 15 | return null 16 | } 17 | }) 18 | return resolve(filterData) 19 | } catch (error) { 20 | return reject(error) 21 | } 22 | }) 23 | } 24 | 25 | export default matchSearch 26 | -------------------------------------------------------------------------------- /src/utils/mqttUtils.ts: -------------------------------------------------------------------------------- 1 | import { IClientOptions } from 'mqtt' 2 | import time from '@/utils/time' 3 | import { getSSLFile } from '@/utils/getFiles' 4 | import { ConnectionModel, SSLContent, WillPropertiesModel } from '@/views/connections/types' 5 | 6 | const setMQTT5Properties = (option: IClientOptions['properties']): IClientOptions['properties'] | undefined => { 7 | if (option === undefined) { 8 | return undefined 9 | } 10 | const properties: IClientOptions['properties'] = {} 11 | if (option.sessionExpiryInterval || option.sessionExpiryInterval === 0) { 12 | properties.sessionExpiryInterval = option.sessionExpiryInterval 13 | } 14 | if (option.receiveMaximum || option.sessionExpiryInterval === 0) { 15 | properties.receiveMaximum = option.receiveMaximum 16 | } 17 | if (option.topicAliasMaximum || option.topicAliasMaximum === 0) { 18 | properties.topicAliasMaximum = option.topicAliasMaximum 19 | } 20 | return properties 21 | } 22 | 23 | export const getClientOptions = (record: ConnectionModel): IClientOptions => { 24 | const mqttVersionDict = { 25 | '3.1.1': 4, 26 | '5.0': 5, 27 | } 28 | const { 29 | clientId, 30 | username, 31 | password, 32 | keepalive, 33 | clean, 34 | connectTimeout, 35 | ssl, 36 | certType, 37 | mqttVersion, 38 | reconnect, 39 | will, 40 | rejectUnauthorized, 41 | } = record 42 | // reconnectPeriod = 0 disabled automatic reconnection in the client 43 | const reconnectPeriod = reconnect ? 4000 : 0 44 | const protocolVersion = mqttVersionDict[mqttVersion] 45 | const options: IClientOptions = { 46 | clientId, 47 | keepalive, 48 | clean, 49 | reconnectPeriod, 50 | protocolVersion, 51 | } 52 | options.connectTimeout = time.convertSecondsToMs(connectTimeout) 53 | // Auth 54 | if (username !== '') { 55 | options.username = username 56 | } 57 | if (password !== '') { 58 | options.password = password 59 | } 60 | // MQTT Version 61 | if (protocolVersion === 5) { 62 | const { sessionExpiryInterval, receiveMaximum, topicAliasMaximum } = record 63 | const properties = setMQTT5Properties({ 64 | sessionExpiryInterval, 65 | receiveMaximum, 66 | topicAliasMaximum, 67 | }) 68 | if (properties && Object.keys(properties).length > 0) { 69 | options.properties = properties 70 | } 71 | } 72 | // SSL 73 | if (ssl) { 74 | switch (certType) { 75 | case 'self': 76 | const sslRes: SSLContent | undefined = getSSLFile({ 77 | ca: record.ca, 78 | cert: record.cert, 79 | key: record.key, 80 | }) 81 | if (sslRes) { 82 | options.ca = sslRes.ca 83 | options.cert = sslRes.cert 84 | options.key = sslRes.key 85 | if (rejectUnauthorized === undefined) { 86 | options.rejectUnauthorized = false 87 | } else { 88 | options.rejectUnauthorized = rejectUnauthorized 89 | } 90 | } 91 | break 92 | default: 93 | options.rejectUnauthorized = false 94 | break 95 | } 96 | } 97 | // Will Message 98 | if (will) { 99 | const { lastWillTopic: topic, lastWillPayload: payload, lastWillQos: qos, lastWillRetain: retain } = will 100 | if (topic) { 101 | options.will = { topic, payload, qos, retain } 102 | if (protocolVersion === 5) { 103 | const { properties } = will 104 | const willProperties: WillPropertiesModel | undefined = {} 105 | if (properties !== undefined) { 106 | if (properties.willDelayInterval || properties.willDelayInterval === 0) { 107 | willProperties.willDelayInterval = properties.willDelayInterval 108 | } 109 | if (properties.messageExpiryInterval || properties.messageExpiryInterval === 0) { 110 | willProperties.messageExpiryInterval = properties.messageExpiryInterval 111 | } 112 | if (properties.contentType !== '') { 113 | willProperties.contentType = properties.contentType 114 | } 115 | if (properties.payloadFormatIndicator !== undefined) { 116 | willProperties.payloadFormatIndicator = properties.payloadFormatIndicator 117 | } 118 | } 119 | if (willProperties && Object.keys(willProperties).length > 0) { 120 | options.will.properties = willProperties 121 | } 122 | } 123 | } 124 | } 125 | return options 126 | } 127 | 128 | // Prevent old data from missing protocol field 129 | export const getMQTTProtocol = (data: ConnectionModel): Protocol => { 130 | const { protocol, ssl } = data 131 | if (!protocol) { 132 | return ssl ? 'wss' : 'ws' 133 | } 134 | return protocol 135 | } 136 | 137 | export default {} 138 | -------------------------------------------------------------------------------- /src/utils/time.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment' 2 | 3 | interface TimeModel { 4 | getNowDate: (format?: string) => string 5 | convertSecondsToMs: (seconds: number) => number 6 | } 7 | 8 | export const getNowDate = (format: string = 'YYYY-MM-DD HH:mm:ss'): string => moment().format(format) 9 | 10 | export const convertSecondsToMs = (seconds: number): number => { 11 | return seconds * 1000 12 | } 13 | 14 | const time: TimeModel = { 15 | getNowDate, 16 | convertSecondsToMs, 17 | } 18 | 19 | export default time 20 | -------------------------------------------------------------------------------- /src/utils/topicMatch.ts: -------------------------------------------------------------------------------- 1 | import { MessageModel } from '@/views/connections/types' 2 | 3 | export const matchTopicMethod = (filter: string, topic: string): boolean => { 4 | // Topic matching algorithm 5 | const filterArray: string[] = filter.split('/') 6 | const length: number = filterArray.length 7 | const topicArray: string[] = topic.split('/') 8 | for (let i = 0; i < length; i += 1) { 9 | const left: string = filterArray[i] 10 | const right: string = topicArray[i] 11 | if (left === '#') { 12 | return topicArray.length >= length - 1 13 | } 14 | if (left !== right && left !== '+') { 15 | return false 16 | } 17 | } 18 | return length === topicArray.length 19 | } 20 | 21 | const topicMatch = (data: MessageModel[], currentTopic: string): Promise => { 22 | return new Promise((resolve, reject) => { 23 | try { 24 | const filterData = data.filter((item) => matchTopicMethod(currentTopic, item.topic)) 25 | return resolve(filterData) 26 | } catch (error) { 27 | return reject(error) 28 | } 29 | }) 30 | } 31 | 32 | export default topicMatch 33 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 44 | 45 | 87 | -------------------------------------------------------------------------------- /src/views/about/index.vue: -------------------------------------------------------------------------------- 1 | 64 | 65 | 123 | 124 | 232 | -------------------------------------------------------------------------------- /src/views/connections/ConnectionForm.vue: -------------------------------------------------------------------------------- 1 | 434 | 435 | 693 | 694 | 769 | -------------------------------------------------------------------------------- /src/views/connections/ConnectionInfo.vue: -------------------------------------------------------------------------------- 1 | 76 | 77 | 153 | 154 | 189 | -------------------------------------------------------------------------------- /src/views/connections/ConnectionsList.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 111 | 112 | 195 | -------------------------------------------------------------------------------- /src/views/connections/index.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 142 | 143 | 172 | -------------------------------------------------------------------------------- /src/views/connections/types.ts: -------------------------------------------------------------------------------- 1 | import { MqttClient } from 'mqtt' 2 | 3 | type QoS = 0 | 1 | 2 4 | type searchCallBack = (data: ConnectionModel[]) => ConnectionModel[] 5 | type nameCallBack = (name: string) => string 6 | 7 | export interface SearchCallBack { 8 | callBack: searchCallBack 9 | } 10 | 11 | export interface NameCallBack { 12 | callBack: nameCallBack 13 | } 14 | 15 | export interface FormRule { 16 | field: string 17 | fullField: string 18 | type: string 19 | validator: () => void 20 | } 21 | 22 | export interface MessageModel { 23 | mid: string 24 | createAt: string 25 | out: boolean 26 | payload: string 27 | qos: QoS 28 | retain: boolean 29 | topic: string 30 | } 31 | 32 | export interface SSLPath { 33 | rejectUnauthorized?: boolean 34 | ca: string 35 | cert: string 36 | key: string 37 | } 38 | 39 | export interface WillPropertiesModel { 40 | willDelayInterval?: number 41 | payloadFormatIndicator?: boolean 42 | messageExpiryInterval?: number 43 | contentType?: string 44 | responseTopic?: string 45 | correlationData?: Buffer 46 | // tslint:disable-next-line:ban-types 47 | userProperties?: Object 48 | } 49 | 50 | export interface ConnectionModel extends SSLPath { 51 | readonly id?: string 52 | clientId: string 53 | name: string 54 | clean: boolean 55 | protocol?: Protocol 56 | host: string 57 | port: number 58 | keepalive: number 59 | connectTimeout: number 60 | reconnect: boolean 61 | username: string 62 | password: string 63 | path: string 64 | certType?: '' | 'server' | 'self' 65 | ssl: boolean 66 | mqttVersion: '3.1.1' | '5.0' 67 | unreadMessageCount: number 68 | messages: MessageModel[] 69 | subscriptions: SubscriptionModel[] 70 | client: 71 | | MqttClient 72 | | { 73 | connected: boolean 74 | } 75 | sessionExpiryInterval?: number 76 | receiveMaximum?: number 77 | topicAliasMaximum?: number 78 | requestResponseInformation?: boolean 79 | requestProblemInformation?: boolean 80 | will?: { 81 | lastWillTopic: string 82 | lastWillPayload: string 83 | lastWillQos: QoS 84 | lastWillRetain: boolean 85 | properties?: WillPropertiesModel 86 | } 87 | } 88 | 89 | export interface SSLContent { 90 | ca: string | string[] | Buffer | Buffer[] | undefined 91 | cert: string | string[] | Buffer | Buffer[] | undefined 92 | key: string | string[] | Buffer | Buffer[] | undefined 93 | } 94 | 95 | export interface ContextmenuModel { 96 | top: number 97 | left: number 98 | } 99 | -------------------------------------------------------------------------------- /src/views/settings/index.vue: -------------------------------------------------------------------------------- 1 | 67 | 68 | 117 | 118 | 187 | -------------------------------------------------------------------------------- /tests/e2e/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ["cypress"], 3 | env: { 4 | mocha: true, 5 | "cypress/globals": true 6 | }, 7 | rules: { 8 | strict: "off" 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /tests/e2e/plugins/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable arrow-body-style */ 2 | // https://docs.cypress.io/guides/guides/plugins-guide.html 3 | 4 | // if you need a custom webpack configuration you can uncomment the following import 5 | // and then use the `file:preprocessor` event 6 | // as explained in the cypress docs 7 | // https://docs.cypress.io/api/plugins/preprocessors-api.html#Examples 8 | 9 | // /* eslint-disable import/no-extraneous-dependencies, global-require */ 10 | // const webpack = require('@cypress/webpack-preprocessor') 11 | 12 | module.exports = (on, config) => { 13 | // on('file:preprocessor', webpack({ 14 | // webpackOptions: require('@vue/cli-service/webpack.config'), 15 | // watchOptions: {} 16 | // })) 17 | 18 | return Object.assign({}, config, { 19 | fixturesFolder: "tests/e2e/fixtures", 20 | integrationFolder: "tests/e2e/specs", 21 | screenshotsFolder: "tests/e2e/screenshots", 22 | videosFolder: "tests/e2e/videos", 23 | supportFile: "tests/e2e/support/index.js" 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /tests/e2e/specs/test.js: -------------------------------------------------------------------------------- 1 | // https://docs.cypress.io/api/introduction/api.html 2 | 3 | describe("My First Test", () => { 4 | it("Visits the app root url", () => { 5 | cy.visit("/"); 6 | cy.contains("h1", "Welcome to Your Vue.js + TypeScript App"); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /tests/e2e/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This is will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /tests/e2e/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import "./commands"; 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "strictFunctionTypes": false, 11 | "resolveJsonModule": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "sourceMap": true, 15 | "baseUrl": ".", 16 | "types": [ 17 | "webpack-env", 18 | "mocha", 19 | "chai" 20 | ], 21 | "paths": { 22 | "@/*": [ 23 | "src/*" 24 | ] 25 | }, 26 | "lib": [ 27 | "esnext", 28 | "dom", 29 | "dom.iterable", 30 | "scripthost" 31 | ] 32 | }, 33 | "include": [ 34 | "src/**/*.ts", 35 | "src/**/*.tsx", 36 | "src/**/*.vue", 37 | "tests/**/*.ts", 38 | "tests/**/*.tsx", 39 | "main/**/*.ts" 40 | ], 41 | "exclude": [ 42 | "node_modules" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin') 2 | 3 | module.exports = { 4 | configureWebpack: { 5 | plugins: [ 6 | new MonacoWebpackPlugin({ 7 | output: 'static/', 8 | languages: ['json'], 9 | features: [ 10 | '!accessibilityHelp', 11 | '!bracketMatching', 12 | 'caretOperations', 13 | 'clipboard', 14 | 'codeAction', 15 | 'codelens', 16 | 'colorDetector', 17 | '!comment', 18 | '!contextmenu', 19 | 'coreCommands', 20 | 'cursorUndo', 21 | '!dnd', 22 | '!find', 23 | '!folding', 24 | '!fontZoom', 25 | '!format', 26 | '!gotoError', 27 | '!gotoLine', 28 | '!gotoSymbol', 29 | '!hover', 30 | '!iPadShowKeyboard', 31 | '!inPlaceReplace', 32 | 'inspectTokens', 33 | 'linesOperations', 34 | '!links', 35 | '!multicursor', 36 | '!parameterHints', 37 | 'quickCommand', 38 | 'quickOutline', 39 | '!referenceSearch', 40 | '!rename', 41 | 'smartSelect', 42 | 'snippets', 43 | '!suggest', 44 | '!toggleHighContrast', 45 | 'toggleTabFocusMode', 46 | 'transpose', 47 | 'wordHighlighter', 48 | 'wordOperations', 49 | 'wordPartOperations', 50 | ], 51 | }), 52 | ], 53 | }, 54 | } 55 | --------------------------------------------------------------------------------