├── .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 |
69 | We're sorry but mqttx doesn't work properly without JavaScript enabled. Please enable it to continue.
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
2 |
3 |
11 |
12 |
13 |
14 |
35 |
36 |
63 |
--------------------------------------------------------------------------------
/src/components/Editor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
142 |
143 |
153 |
--------------------------------------------------------------------------------
/src/components/EmptyPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {{ btnTitle }}
9 |
10 |
11 |
12 |
13 |
14 |
15 |
29 |
30 |
55 |
--------------------------------------------------------------------------------
/src/components/Ipc.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
51 |
--------------------------------------------------------------------------------
/src/components/LeftPanel.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
16 |
17 |
62 |
--------------------------------------------------------------------------------
/src/components/Leftbar.vue:
--------------------------------------------------------------------------------
1 |
2 |
34 |
35 |
36 |
69 |
70 |
135 |
--------------------------------------------------------------------------------
/src/components/MsgLeftItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Topic: {{ topic }}
7 | QoS: {{ qos }}
8 |
9 |
{{ payload }}
10 |
11 |
{{ createAt }}
12 |
13 |
14 |
15 |
50 |
51 |
72 |
--------------------------------------------------------------------------------
/src/components/MsgPublish.vue:
--------------------------------------------------------------------------------
1 |
2 |
49 |
50 |
51 |
136 |
137 |
193 |
--------------------------------------------------------------------------------
/src/components/MsgRightItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Topic: {{ topic }}
6 | QoS: {{ qos }}
7 |
8 |
{{ payload }}
9 |
10 |
{{ createAt }}
11 |
12 |
13 |
14 |
25 |
26 |
40 |
--------------------------------------------------------------------------------
/src/components/MyDialog.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
25 |
26 |
27 |
28 |
74 |
75 |
111 |
--------------------------------------------------------------------------------
/src/components/ResizeHeight.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
38 |
39 |
51 |
--------------------------------------------------------------------------------
/src/components/SubscriptionsList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
21 |
51 |
52 |
53 |
54 |
55 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
44 |
45 |
87 |
--------------------------------------------------------------------------------
/src/views/about/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ $t('about.about') }}
4 |
5 |
6 |
7 |
8 |
9 |
35 |
60 |
61 |
62 |
63 |
64 |
65 |
123 |
124 |
232 |
--------------------------------------------------------------------------------
/src/views/connections/ConnectionForm.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
{{ oper === 'create' ? $t('common.new') : $t('common.edit') }}
11 |
12 |
17 |
18 |
19 |
20 |
431 |
432 |
433 |
434 |
435 |
693 |
694 |
769 |
--------------------------------------------------------------------------------
/src/views/connections/ConnectionInfo.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | Clean Session
34 |
35 |
36 |
46 | {{ $t('connections.connectBtn') }}
47 |
48 |
58 | {{ $t('connections.disconnectedBtn') }}
59 |
60 |
69 | {{ $t('common.cancel') }}
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
153 |
154 |
189 |
--------------------------------------------------------------------------------
/src/views/connections/ConnectionsList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ $t('common.noData') }}
4 |
5 |
12 |
13 |
21 |
22 |
29 | {{ item.name }}@{{ item.host }}:{{ item.port }}
30 |
31 |
32 |
33 | SSL
34 |
35 |
36 |
37 | {{ unreadMessageCount[item.id] }}
38 |
39 |
40 |
41 |
42 |
45 |
46 |
47 |
48 |
49 |
111 |
112 |
195 |
--------------------------------------------------------------------------------
/src/views/connections/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{ $t('connections.connections') }}
5 |
6 |
7 |
8 |
9 |
15 |
16 |
17 |
24 |
25 |
26 |
27 |
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 |
2 |
3 |
{{ $t('settings.settings') }}
4 |
5 |
{{ $t('settings.general') }}
6 |
7 |
8 |
9 |
10 |
11 | {{ $t('settings.language') }}
12 |
13 |
14 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | {{ $t('settings.maxReconnectTimes') }}
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
{{ $t('settings.appearance') }}
43 |
44 |
45 |
46 |
47 |
48 | {{ $t('settings.theme') }}
49 |
50 |
51 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
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 |
--------------------------------------------------------------------------------