diff --git a/README-JP.md b/README-JP.md
index 5dab3ac3f..9a63d7ec3 100644
--- a/README-JP.md
+++ b/README-JP.md
@@ -41,7 +41,7 @@
## 利用方法
-詳細な利用方法については、[ブログ](https://www.emqx.io/blog/mqtt-x-guideline)または[ユーザーマニュアル](./docs/manual.md)を確認してください。
+詳細な利用方法については、[ブログ](https://qiita.com/emqx_japan/items/1ff6097fdfe273c5e22f)または[ユーザーマニュアル](./docs/manual-jp.md)を確認してください。
1. MQTT Brokerの準備。
diff --git a/assets/emqx_wx.jpeg b/assets/emqx_wx.jpeg
new file mode 100644
index 000000000..f1aa87efb
Binary files /dev/null and b/assets/emqx_wx.jpeg differ
diff --git a/assets/mqttx-preview.png b/assets/mqttx-preview.png
index 9ebc9d6dd..5a4f274b9 100644
Binary files a/assets/mqttx-preview.png and b/assets/mqttx-preview.png differ
diff --git a/assets/mqttx-script.png b/assets/mqttx-script.png
new file mode 100644
index 000000000..1e5cd752f
Binary files /dev/null and b/assets/mqttx-script.png differ
diff --git a/docs/manual-cn.md b/docs/manual-cn.md
index 5cf6b1e7c..c21965fe0 100644
--- a/docs/manual-cn.md
+++ b/docs/manual-cn.md
@@ -179,6 +179,24 @@ sudo snap install mqttx

+### 脚本
+
+在 v1.4.2 版本以后,MQTT X 新增了脚本编辑功能,在该功能中,用户可编写自定义脚本(JavaScript)对发送和接收到的 `Payload` 进行自定义转化,配合定时发送功能,可实现一些模拟数据上报的自动化测试功能。
+
+> 注意:该功能目前属于测试 Beta 阶段。
+
+点击左侧菜单栏中的 `脚本` 按钮,可进入到脚本编辑页面,在该页面中,用户可在最上方的代码编辑器中,编写 JavaScript 代码,全局只包含一个 `execute` API,用户需要编写一个脚本函数,该函数接收一个 `value` 参数,即为 `Payload`, 函数中便可对 `value` 进行自定义修改转化,最后将该函数作为参数传入到 `execute` 中即可执行自定义编写的函数。
+
+下方还包含了一个 `输入` 和 `输出` 框,可输入预想输入值,点击右边的 `测试` 按钮,便可在 `输出` 框中查看执行结果,输入的值的格式包含了 `JSON` 和 `Plaintext`,方便用户提前调试自定义编写的脚本功能。完成测试后,可点击最右上角的 `保存` 按钮,输入该脚本的名称后就可对该脚本进行保存。保存完成后就可以到连接页面进行使用了。保存完成的脚本还可进行编辑和删除。
+
+在连接页面中,点击右上角的下拉功能菜单,选择 `使用脚本`,在弹出窗中,选择你需要使用的预先保存好的脚本,然后选择应用类型,包含了,发送时,接收时和全部。选择完成后,根据数据类型选择发送或接收的数据格式,正常使用消息的收发,此时如果看到预期效果,便完成了一个完整的脚本使用的功能。如果用户需要取消脚本,可点击顶部状态栏中的红色的 `停止脚本` 按钮,便可停止使用脚本。
+
+该功能具有一定的扩展性和灵活性,需用户配合实际需求来进行使用。
+
+
+
+脚本使用实例可在 [/docs/script-example](https://github.com/emqx/MQTTX/tree/master/docs/script-example) 文件夹中查看,目前提供了两个内置脚本,时间戳转化和温湿度数据模拟。如果在您的使用中有更好的,更实用的脚本也可以提交您的代码到这里,方便让更多的人使用到。
+
### 其它
1. 连接操作
@@ -263,4 +281,8 @@ Linux: `vue-cli-service electron:build --linux`
| 方式 | 内容 |
| ---- | ---- |
| QQ 群(EMQ X 官方群3)| 937041105 |
-| EMQ X 官方公众号 |
|
+| EMQ X 官方公众号 |
|
+| EMQ X 微信群(扫码添加后邀请入群)|
|
+| 微博 | [@emqtt](https://weibo.com/emqtt) |
+| Twitter | [@emqtt](https://twitter.com/emqtt/) |
+| Slack | [EMQ X](https://slack-invite.emqx.io/) |
diff --git a/docs/manual.md b/docs/manual.md
index ba34d4ce4..609a5d6a2 100644
--- a/docs/manual.md
+++ b/docs/manual.md
@@ -179,6 +179,24 @@ In the settings page, you can choose to click the data backup and data recovery

+### Script
+
+After the v1.4.2 version, MQTT X has added a script editing function. In this function, users can write custom scripts (JavaScript) to perform custom conversions on sent and received `Payload`. With the timing sending function, realize the automated test function for the simulation data report.
+
+> Note: This feature is test feature in the beta stage
+
+Click the `script` button in the menu bar on the left to enter the script editing page. In this page, users can write JavaScript code in the code editor at the top. There is only one `execute` API globally, and the user needs write a script function that receives a `value` parameter, which is `Payload`, and the function can be customized to modify the `value`, and finally the function can be executed by passing it as a parameter to the `execute` custom-written functions.
+
+There is also an `input` and `output` box below. You can enter the expected input value. Click the `test` button on the right to view the execution result in the `output` box. The format of the input value includes `JSON` With `Plaintext`, it is convenient for users to debug custom-written script functions in advance. After completing the test, you can click the `Save` button in the upper right corner and enter the name of the script to save the script. After saving, you can go to the connection page for use. The saved script can also be edited and deleted.
+
+In the connection page, click the drop-down menu in the upper right corner, select `Use script`, in the pop-up window, select the pre-saved script you need to use, and then select the script to be applied, including: Published, Received, and All. After the selection is completed, select the data format to be sent or received according to the data type, and use the message sending and receiving normally. At this time, if the expected effect is seen, a script function is completed. If the user needs to cancel the script, you can click the red `Stop script` button in the top status bar to stop using the script.
+
+This function is scalable and flexible, and requires users to cooperate with actual needs to use it.
+
+
+
+The script usage examples can be viewed in the [/docs/script-example](https://github.com/emqx/MQTTX/tree/master/docs/script-example) folder. Currently, two built-in scripts are provided, timestamp conversion and temperature and humidity data simulation. If you have a better and more practical script in your use, you can submit your code here, so that more people can use it.
+
### Others
1. Connection operation
diff --git a/docs/script-example/tempAndHum.js b/docs/script-example/tempAndHum.js
new file mode 100644
index 000000000..2f845c581
--- /dev/null
+++ b/docs/script-example/tempAndHum.js
@@ -0,0 +1,21 @@
+/**
+ * Simulated temperature and humidity reporting
+ * @return Return a simulated temperature and humidity JSON data - { "temperature": 23, "humidity": 40 }
+ * @param value, MQTT Payload - {}
+ */
+
+function random(min, max) {
+ return Math.round(Math.random() * (max - min)) + min
+}
+
+function handlePayload(value) {
+ let _value = value
+ if (typeof value === 'string') {
+ _value = JSON.parse(value)
+ }
+ _value.temperature = random(10, 30)
+ _value.humidity = random(20, 40)
+ return JSON.stringify(_value, null, 2)
+}
+
+execute(handlePayload)
diff --git a/docs/script-example/timestamp.js b/docs/script-example/timestamp.js
new file mode 100644
index 000000000..c3508ecdb
--- /dev/null
+++ b/docs/script-example/timestamp.js
@@ -0,0 +1,18 @@
+/**
+ * Convert timestamp to normal time.
+ * @return Return the UTC time - { "time": "2020-12-17 14:18:07" }
+ * @param value, MQTT Payload - { "time": 1608185887 }
+ */
+
+function handleTimestamp(value) {
+ let _value = value
+ if (typeof value === 'string') {
+ _value = JSON.parse(value)
+ }
+ // East Eight District needs an additional 8 hours
+ const date = new Date(_value.time * 1000 + 8 * 3600 * 1000)
+ _value.time = date.toJSON().substr(0, 19).replace('T', ' ')
+ return JSON.stringify(_value, null, 2)
+}
+
+execute(handleTimestamp)
diff --git a/package.json b/package.json
index d89aa1452..7e38d8971 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "MQTTX",
- "version": "1.4.1",
+ "version": "1.4.2",
"description": "MQTT desktop client",
"author": "EMQ X Team",
"scripts": {
@@ -39,11 +39,10 @@
"vuex": "^3.0.1",
"vuex-class": "^0.3.2",
"xlsx": "^0.16.8",
- "xml-js": "^1.6.11"
+ "xml-js": "^1.6.11",
+ "vm2": "latest"
},
"devDependencies": {
- "@fullhuman/postcss-purgecss": "^2.1.0",
- "@fullhuman/vue-cli-plugin-purgecss": "~2.2.0",
"@types/chai": "^4.1.0",
"@types/chart.js": "^2.9.28",
"@types/fs-extra": "^8.0.0",
@@ -76,11 +75,6 @@
"vue-cli-plugin-electron-builder": "^1.4.6",
"vue-template-compiler": "^2.6.12"
},
- "postcss": {
- "plugins": {
- "autoprefixer": {}
- }
- },
"browserslist": [
"> 1%",
"last 2 versions"
diff --git a/postcss.config.js b/postcss.config.js
index ba414c28a..90d9fffcb 100644
--- a/postcss.config.js
+++ b/postcss.config.js
@@ -1,15 +1,5 @@
-const IN_PRODUCTION = process.env.NODE_ENV === 'production'
-
module.exports = {
- plugins: [
- IN_PRODUCTION && require('@fullhuman/postcss-purgecss')({
- content: [`./public/**/*.html`, `./src/**/*.vue`, `./src/**/*.scss`],
- defaultExtractor (content) {
- const contentWithoutStyleBlocks = content.replace(/")}catch(l){console&&console.log(l)}}!function(l){if(document.addEventListener)if(~["complete","loaded","interactive"].indexOf(document.readyState))setTimeout(l,0);else{var c=function(){document.removeEventListener("DOMContentLoaded",c,!1),l()};document.addEventListener("DOMContentLoaded",c,!1)}else document.attachEvent&&(h=l,i=s.document,o=!1,(e=function(){try{i.documentElement.doScroll("left")}catch(l){return void setTimeout(e,50)}t()})(),i.onreadystatechange=function(){"complete"==i.readyState&&(i.onreadystatechange=null,t())});function t(){o||(o=!0,h())}var h,i,o,e}(function(){var l,c,t,h,i,o;(l=document.createElement("div")).innerHTML=e,e=null,(c=l.getElementsByTagName("svg")[0])&&(c.setAttribute("aria-hidden","true"),c.style.position="absolute",c.style.width=0,c.style.height=0,c.style.overflow="hidden",t=c,(h=document.body).firstChild?(i=t,(o=h.firstChild).parentNode.insertBefore(i,o)):h.appendChild(t))})}(window);
\ No newline at end of file
+!(function (c) {
+ var l,
+ a,
+ t,
+ h,
+ i,
+ o,
+ s =
+ '',
+ e = (e = document.getElementsByTagName('script'))[e.length - 1].getAttribute('data-injectcss')
+ if (e && !c.__iconfont__svg__cssinject__) {
+ c.__iconfont__svg__cssinject__ = !0
+ try {
+ document.write(
+ '',
+ )
+ } catch (c) {
+ console && console.log(c)
+ }
+ }
+ function d() {
+ i || ((i = !0), t())
+ }
+ ;(l = function () {
+ var c, l, a, t
+ ;((t = document.createElement('div')).innerHTML = s),
+ (s = null),
+ (a = t.getElementsByTagName('svg')[0]) &&
+ (a.setAttribute('aria-hidden', 'true'),
+ (a.style.position = 'absolute'),
+ (a.style.width = 0),
+ (a.style.height = 0),
+ (a.style.overflow = 'hidden'),
+ (c = a),
+ (l = document.body).firstChild ? ((t = c), (a = l.firstChild).parentNode.insertBefore(t, a)) : l.appendChild(c))
+ }),
+ document.addEventListener
+ ? ~['complete', 'loaded', 'interactive'].indexOf(document.readyState)
+ ? setTimeout(l, 0)
+ : ((a = function () {
+ document.removeEventListener('DOMContentLoaded', a, !1), l()
+ }),
+ document.addEventListener('DOMContentLoaded', a, !1))
+ : document.attachEvent &&
+ ((t = l),
+ (h = c.document),
+ (i = !1),
+ (o = function () {
+ try {
+ h.documentElement.doScroll('left')
+ } catch (c) {
+ return void setTimeout(o, 50)
+ }
+ d()
+ })(),
+ (h.onreadystatechange = function () {
+ 'complete' == h.readyState && ((h.onreadystatechange = null), d())
+ }))
+})(window)
diff --git a/src/assets/font/iconfont.json b/src/assets/font/iconfont.json
index c19453306..720b18534 100644
--- a/src/assets/font/iconfont.json
+++ b/src/assets/font/iconfont.json
@@ -6,11 +6,67 @@
"description": "",
"glyphs": [
{
- "icon_id": "1663480",
+ "icon_id": "11348940",
+ "name": "coding",
+ "font_class": "coding",
+ "unicode": "e617",
+ "unicode_decimal": 58903
+ },
+ {
+ "icon_id": "16144037",
+ "name": "download",
+ "font_class": "download",
+ "unicode": "e657",
+ "unicode_decimal": 58967
+ },
+ {
+ "icon_id": "6720830",
+ "name": "microsoft",
+ "font_class": "microsoft",
+ "unicode": "e632",
+ "unicode_decimal": 58930
+ },
+ {
+ "icon_id": "3994864",
+ "name": "ubuntu",
+ "font_class": "ubuntu",
+ "unicode": "e71d",
+ "unicode_decimal": 59165
+ },
+ {
+ "icon_id": "11111717",
+ "name": "mac",
+ "font_class": "mac",
+ "unicode": "e698",
+ "unicode_decimal": 59032
+ },
+ {
+ "icon_id": "11305187",
+ "name": "linux",
+ "font_class": "linux",
+ "unicode": "e77d",
+ "unicode_decimal": 59261
+ },
+ {
+ "icon_id": "11983514",
+ "name": "windows",
+ "font_class": "windows",
+ "unicode": "ea06",
+ "unicode_decimal": 59910
+ },
+ {
+ "icon_id": "13091253",
+ "name": "arrow",
+ "font_class": "arrow",
+ "unicode": "e651",
+ "unicode_decimal": 58961
+ },
+ {
+ "icon_id": "8262530",
"name": "reddit",
- "font_class": "reddit",
- "unicode": "e7e4",
- "unicode_decimal": 59364
+ "font_class": "reddit1",
+ "unicode": "e621",
+ "unicode_decimal": 58913
},
{
"icon_id": "1878149",
@@ -19,6 +75,13 @@
"unicode": "e615",
"unicode_decimal": 58901
},
+ {
+ "icon_id": "1663480",
+ "name": "reddit",
+ "font_class": "reddit",
+ "unicode": "e7e4",
+ "unicode_decimal": 59364
+ },
{
"icon_id": "1937032",
"name": "weibo",
@@ -26,6 +89,20 @@
"unicode": "e73a",
"unicode_decimal": 59194
},
+ {
+ "icon_id": "9277879",
+ "name": "we-chat",
+ "font_class": "we-chat",
+ "unicode": "e70e",
+ "unicode_decimal": 59150
+ },
+ {
+ "icon_id": "12313365",
+ "name": "twitter",
+ "font_class": "ttww",
+ "unicode": "e6c7",
+ "unicode_decimal": 59079
+ },
{
"icon_id": "3876355",
"name": "slack",
@@ -33,6 +110,48 @@
"unicode": "e641",
"unicode_decimal": 58945
},
+ {
+ "icon_id": "12198496",
+ "name": "列表",
+ "font_class": "liebiao",
+ "unicode": "e64b",
+ "unicode_decimal": 58955
+ },
+ {
+ "icon_id": "12198497",
+ "name": "分组",
+ "font_class": "fenzu",
+ "unicode": "e64c",
+ "unicode_decimal": 58956
+ },
+ {
+ "icon_id": "12198498",
+ "name": "connection",
+ "font_class": "connection",
+ "unicode": "e64d",
+ "unicode_decimal": 58957
+ },
+ {
+ "icon_id": "12198499",
+ "name": "new",
+ "font_class": "new",
+ "unicode": "e64e",
+ "unicode_decimal": 58958
+ },
+ {
+ "icon_id": "12198500",
+ "name": "about",
+ "font_class": "about",
+ "unicode": "e64f",
+ "unicode_decimal": 58959
+ },
+ {
+ "icon_id": "12198501",
+ "name": "site",
+ "font_class": "site",
+ "unicode": "e650",
+ "unicode_decimal": 58960
+ },
{
"icon_id": "9047144",
"name": "折叠",
@@ -41,11 +160,18 @@
"unicode_decimal": 58880
},
{
- "icon_id": "9277879",
- "name": "we-chat",
- "font_class": "we-chat",
- "unicode": "e70e",
- "unicode_decimal": 59150
+ "icon_id": "9770925",
+ "name": "icon-plus",
+ "font_class": "plus",
+ "unicode": "e630",
+ "unicode_decimal": 58928
+ },
+ {
+ "icon_id": "9617831",
+ "name": "search",
+ "font_class": "search",
+ "unicode": "e631",
+ "unicode_decimal": 58929
},
{
"icon_id": "9408416",
@@ -55,39 +181,11 @@
"unicode_decimal": 58918
},
{
- "icon_id": "9504241",
- "name": "brokers",
- "font_class": "brokers",
- "unicode": "e625",
- "unicode_decimal": 58917
- },
- {
- "icon_id": "9504243",
- "name": "settings",
- "font_class": "settings",
- "unicode": "e627",
- "unicode_decimal": 58919
- },
- {
- "icon_id": "9507416",
- "name": "connections",
- "font_class": "connections",
- "unicode": "e628",
- "unicode_decimal": 58920
- },
- {
- "icon_id": "9552002",
- "name": "website",
- "font_class": "website",
- "unicode": "e629",
- "unicode_decimal": 58921
- },
- {
- "icon_id": "9552003",
- "name": "github",
- "font_class": "github",
- "unicode": "e62a",
- "unicode_decimal": 58922
+ "icon_id": "9559673",
+ "name": "send",
+ "font_class": "send",
+ "unicode": "e62f",
+ "unicode_decimal": 58927
},
{
"icon_id": "9558958",
@@ -118,74 +216,39 @@
"unicode_decimal": 58926
},
{
- "icon_id": "9559673",
- "name": "send",
- "font_class": "send",
- "unicode": "e62f",
- "unicode_decimal": 58927
- },
- {
- "icon_id": "9617831",
- "name": "search",
- "font_class": "search",
- "unicode": "e631",
- "unicode_decimal": 58929
- },
- {
- "icon_id": "9770925",
- "name": "icon-plus",
- "font_class": "plus",
- "unicode": "e630",
- "unicode_decimal": 58928
- },
- {
- "icon_id": "12198496",
- "name": "列表",
- "font_class": "liebiao",
- "unicode": "e64b",
- "unicode_decimal": 58955
- },
- {
- "icon_id": "12198497",
- "name": "分组",
- "font_class": "fenzu",
- "unicode": "e64c",
- "unicode_decimal": 58956
- },
- {
- "icon_id": "12198498",
- "name": "connection",
- "font_class": "connection",
- "unicode": "e64d",
- "unicode_decimal": 58957
+ "icon_id": "9552002",
+ "name": "website",
+ "font_class": "website",
+ "unicode": "e629",
+ "unicode_decimal": 58921
},
{
- "icon_id": "12198499",
- "name": "new",
- "font_class": "new",
- "unicode": "e64e",
- "unicode_decimal": 58958
+ "icon_id": "9552003",
+ "name": "github",
+ "font_class": "github",
+ "unicode": "e62a",
+ "unicode_decimal": 58922
},
{
- "icon_id": "12198500",
- "name": "about",
- "font_class": "about",
- "unicode": "e64f",
- "unicode_decimal": 58959
+ "icon_id": "9507416",
+ "name": "connections",
+ "font_class": "connections",
+ "unicode": "e628",
+ "unicode_decimal": 58920
},
{
- "icon_id": "12198501",
- "name": "site",
- "font_class": "site",
- "unicode": "e650",
- "unicode_decimal": 58960
+ "icon_id": "9504241",
+ "name": "brokers",
+ "font_class": "brokers",
+ "unicode": "e625",
+ "unicode_decimal": 58917
},
{
- "icon_id": "12313365",
- "name": "twitter",
- "font_class": "ttww",
- "unicode": "e6c7",
- "unicode_decimal": 59079
+ "icon_id": "9504243",
+ "name": "settings",
+ "font_class": "settings",
+ "unicode": "e627",
+ "unicode_decimal": 58919
}
]
}
diff --git a/src/assets/font/iconfont.svg b/src/assets/font/iconfont.svg
index 512053e4d..7c3b500b1 100644
--- a/src/assets/font/iconfont.svg
+++ b/src/assets/font/iconfont.svg
@@ -20,82 +20,109 @@ Created by iconfont
/>
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
diff --git a/src/assets/font/iconfont.ttf b/src/assets/font/iconfont.ttf
index 4c4157469..8272af0dd 100644
Binary files a/src/assets/font/iconfont.ttf and b/src/assets/font/iconfont.ttf differ
diff --git a/src/assets/font/iconfont.woff b/src/assets/font/iconfont.woff
index 6df688385..7bb257419 100644
Binary files a/src/assets/font/iconfont.woff and b/src/assets/font/iconfont.woff differ
diff --git a/src/assets/font/iconfont.woff2 b/src/assets/font/iconfont.woff2
index 689c7d5ec..0715a64e1 100644
Binary files a/src/assets/font/iconfont.woff2 and b/src/assets/font/iconfont.woff2 differ
diff --git a/src/assets/scss/mixins.scss b/src/assets/scss/mixins.scss
index f5802add0..9a66db134 100644
--- a/src/assets/scss/mixins.scss
+++ b/src/assets/scss/mixins.scss
@@ -55,3 +55,18 @@
}
}
}
+
+@mixin editor-lang-type {
+ .lang-type {
+ width: 100%;
+ height: 30px;
+ line-height: 30px;
+ padding: 0px 12px;
+ background: var(--color-bg-radio);
+ border: 1px solid var(--color-border-default);
+ border-top: none;
+ text-align: right;
+ border-bottom-left-radius: 4px;
+ border-bottom-right-radius: 4px;
+ }
+}
diff --git a/src/background.ts b/src/background.ts
index 9616b5cdb..3ebc9aa00 100644
--- a/src/background.ts
+++ b/src/background.ts
@@ -60,6 +60,9 @@ function handleIpcMessages() {
const { id, receivedMessage } = args[0]
updateConnectionMessage(id, { ...receivedMessage })
})
+ ipcMain.on('initEditor', (event: Electron.Event, ...args: any[]) => {
+ event.sender.send('initEditor')
+ })
}
function createWindow() {
diff --git a/src/components/Editor.vue b/src/components/Editor.vue
index f6e5737d9..40b0f9409 100644
--- a/src/components/Editor.vue
+++ b/src/components/Editor.vue
@@ -14,6 +14,8 @@ export default class Editor extends Vue {
@Prop({ required: true }) public id!: string
@Prop({ required: true }) public lang!: string
@Prop({ default: 14 }) public fontSize!: number
+ @Prop({ default: 'off' }) public lineNumbers!: 'off' | 'on'
+ @Prop({ default: 'none' }) public renderHighlight!: 'none' | 'line'
@Prop({ default: 'hidden' }) public scrollbarStatus: 'auto' | 'visible' | 'hidden' | undefined
@Prop({ default: false }) public disabled!: boolean
@@ -48,15 +50,15 @@ export default class Editor extends Vue {
}
}
- private initEditor(): void | boolean {
+ public initEditor(): void | boolean {
const defaultOptions: monaco.editor.IStandaloneEditorConstructionOptions = {
value: this.value,
language: this.lang,
readOnly: this.disabled,
fontSize: this.fontSize,
scrollBeyondLastLine: false,
- lineNumbers: 'off',
- renderLineHighlight: 'none',
+ lineNumbers: this.lineNumbers,
+ renderLineHighlight: this.renderHighlight,
matchBrackets: 'near',
folding: false,
theme: this.getTheme(),
@@ -94,6 +96,13 @@ export default class Editor extends Vue {
this.editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, () => {
this.$emit('enter-event', this.value)
})
+ // tslint:disable-next-line:no-bitwise
+ this.editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, () => {
+ this.$emit('enter-event', this.value)
+ })
+ this.editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_S, () => {
+ this.$emit('qucik-save', this.value)
+ })
// Update editor options
const model = this.editor.getModel()
if (model) {
@@ -106,7 +115,7 @@ export default class Editor extends Vue {
this.$emit('blur')
})
}
- private editorLayout() {
+ public editorLayout() {
if (this.editor) {
this.editor.layout()
}
diff --git a/src/components/ExportData.vue b/src/components/ExportData.vue
index fad1af56e..0a77c7cde 100644
--- a/src/components/ExportData.vue
+++ b/src/components/ExportData.vue
@@ -47,7 +47,7 @@ import { loadConnections } from '@/api/connection'
import MyDialog from './MyDialog.vue'
import { ConnectionModel } from '@/views/connections/types'
import XMLConvert from 'xml-js'
-const { parse: CSVConvert } = require('json2csv')
+import { parse as CSVConvert } from 'json2csv'
import ExcelConvert, { WorkBook } from 'xlsx'
type ExportFormat = 'JSON' | 'XML' | 'CSV' | 'Excel'
diff --git a/src/components/Leftbar.vue b/src/components/Leftbar.vue
index 9550e8daa..2577893f7 100644
--- a/src/components/Leftbar.vue
+++ b/src/components/Leftbar.vue
@@ -17,6 +17,11 @@
+
@@ -56,6 +61,9 @@ export default class Leftbar extends Vue {
get isAbout(): boolean {
return this.$route.path === '/about'
}
+ get isScript(): boolean {
+ return this.$route.path === '/script'
+ }
get isNewWindow(): boolean {
return this.$route.name === 'newWindow'
}
diff --git a/src/components/MessageList.vue b/src/components/MessageList.vue
index 4769ff514..cbe47ad77 100644
--- a/src/components/MessageList.vue
+++ b/src/components/MessageList.vue
@@ -1,6 +1,11 @@
-
-
+
+
+
@@ -40,6 +125,14 @@ export default class MessageList extends Vue {
overflow-y: scroll;
overflow-x: hidden;
padding: 0 16px;
- margin-top: 19px;
+
+ .loading-icon {
+ display: inline-block;
+ width: 100%;
+ margin-top: 6px;
+ text-align: center;
+ font-size: 14px;
+ color: var(--color-main-green);
+ }
}
diff --git a/src/components/MsgLeftItem.vue b/src/components/MsgLeftItem.vue
index 8d24ba4eb..968a1a1c2 100644
--- a/src/components/MsgLeftItem.vue
+++ b/src/components/MsgLeftItem.vue
@@ -41,8 +41,8 @@ export default class MsgLeftItem extends Vue {
}
private mounted() {
- const leftPayload = this.$refs.leftPayload as HTMLElement
- this.topicColorHeight = `${leftPayload.offsetHeight - 6}px`
+ const leftPayloadRef = this.$refs.leftPayload as HTMLElement
+ this.topicColorHeight = `${leftPayloadRef.offsetHeight - 6}px`
this.setCurrentTopicColor()
}
}
diff --git a/src/components/MsgPublish.vue b/src/components/MsgPublish.vue
index e25d73c80..4662a3d8d 100644
--- a/src/components/MsgPublish.vue
+++ b/src/components/MsgPublish.vue
@@ -116,6 +116,16 @@ export default class MsgPublish extends Vue {
}
}
+ private mounted() {
+ ipcRenderer.on('initEditor', () => {
+ const editorRef = this.$refs.payloadEditor as Editor
+ if (editorRef) {
+ editorRef.initEditor()
+ }
+ ipcRenderer.removeAllListeners('initEditor')
+ })
+ }
+
private send() {
this.msgRecord.mid = uuidv4()
this.$emit('handleSend', this.msgRecord, this.payloadType)
@@ -131,7 +141,7 @@ export default class MsgPublish extends Vue {
ipcRenderer.removeAllListeners('sendPayload')
}
private handleLayout() {
- const editorRef: EditorRef = this.$refs.payloadEditor as EditorRef
+ const editorRef = this.$refs.payloadEditor as Editor
editorRef.editorLayout()
}
diff --git a/src/components/MyDialog.vue b/src/components/MyDialog.vue
index bb9648825..3fdddd5bd 100644
--- a/src/components/MyDialog.vue
+++ b/src/components/MyDialog.vue
@@ -45,7 +45,7 @@ export default class MyDialog extends Vue {
private showDialog: boolean = this.visible
@Watch('visible')
- private onVisibleChanged(val: boolean, oldVal: boolean) {
+ private onVisibleChanged(val: boolean) {
this.showDialog = val
}
diff --git a/src/components/SubscriptionsList.vue b/src/components/SubscriptionsList.vue
index 234081825..fc05d1be5 100644
--- a/src/components/SubscriptionsList.vue
+++ b/src/components/SubscriptionsList.vue
@@ -102,7 +102,8 @@ import { updateConnection } from '@/api/connection'
import { defineColors, getRandomColor } from '@/utils/colors'
import LeftPanel from '@/components/LeftPanel.vue'
import MyDialog from '@/components/MyDialog.vue'
-import { ConnectionModel } from '../views/connections/types'
+import { ConnectionModel, SubscribeErrorReason } from '../views/connections/types'
+import VueI18n from 'vue-i18n'
@Component({
components: {
@@ -200,6 +201,28 @@ export default class SubscriptionsList extends Vue {
})
}
+ /**
+ * Get the error reason message corresponding to the enumeration.
+ * Check that errorReason not equal `SubscribeErrorReason.normal` before using.
+ * @return Return the message of failure subscribe
+ * @param errorReason - Type:enum, The reason cause the failed subscription
+ */
+ private getErrorReasonMsg(errorReason: SubscribeErrorReason): VueI18n.TranslateResult {
+ if (errorReason === SubscribeErrorReason.normal) return ''
+ switch (errorReason) {
+ case errorReason & SubscribeErrorReason.qosSubFailed: {
+ return this.$t('connections.qosSubFailed')
+ }
+ case errorReason & SubscribeErrorReason.qosSubSysFailed: {
+ return this.$t('connections.qosSubSysFailed')
+ }
+ case errorReason & SubscribeErrorReason.emptySubFailed: {
+ return this.$t('connections.emptySubFailed')
+ }
+ }
+ return this.$t('connections.unknowSubFailed')
+ }
+
public subscribe({ topic, qos }: SubscriptionModel, isAuto?: boolean) {
if (isAuto) {
this.subRecord.topic = topic
@@ -211,8 +234,19 @@ export default class SubscriptionsList extends Vue {
this.$message.error(error)
return false
}
- if (res.length < 1 || ![0, 1, 2].includes(res[0].qos)) {
- const errorMsg: string = `${topic} ${this.$t('connections.subFailed')}`
+
+ let errorReason: SubscribeErrorReason = SubscribeErrorReason.normal
+ if (res.length < 1) {
+ errorReason = SubscribeErrorReason.emptySubFailed
+ } else if (![0, 1, 2].includes(res[0].qos) && !topic.match('/^($SYS)//i')) {
+ errorReason = SubscribeErrorReason.qosSubSysFailed
+ } else if (![0, 1, 2].includes(res[0].qos)) {
+ errorReason = SubscribeErrorReason.qosSubFailed
+ }
+
+ if (errorReason !== SubscribeErrorReason.normal) {
+ const errorReasonMsg: VueI18n.TranslateResult = this.getErrorReasonMsg(errorReason)
+ const errorMsg: string = `${topic} ${this.$t('connections.subFailed')} ${errorReasonMsg}`
this.$message.error(errorMsg)
return false
}
diff --git a/src/components/UseScript.vue b/src/components/UseScript.vue
new file mode 100644
index 000000000..1b8a5d2c1
--- /dev/null
+++ b/src/components/UseScript.vue
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/database/index.ts b/src/database/index.ts
index b6c76b083..3c63a862b 100644
--- a/src/database/index.ts
+++ b/src/database/index.ts
@@ -5,7 +5,7 @@ import fs from 'fs-extra'
import LodashID from 'lodash-id'
import { app, remote } from 'electron'
-interface Schema {
+interface DBSchema {
windowSize: {
height: number
width: number
@@ -17,6 +17,7 @@ interface Schema {
}
connections: []
suggestConnections: []
+ scripts: []
}
const isRenderer: boolean = process.type === 'renderer'
@@ -35,9 +36,9 @@ if (!isRenderer) {
}
class DB {
- private db: Lowdb.LowdbSync
+ private db: Lowdb.LowdbSync
public constructor() {
- const adapter: Lowdb.AdapterSync = new FileSync(path.join(STORE_PATH, '/db.json'))
+ const adapter: Lowdb.AdapterSync = new FileSync(path.join(STORE_PATH, '/db.json'))
this.db = Lowdb(adapter)
// Use lodash-id must use insert methods
this.db._.mixin(LodashID)
@@ -69,6 +70,9 @@ class DB {
if (!this.db.has('suggestConnections').value()) {
this.db.set('suggestConnections', []).write()
}
+ if (!this.db.has('scripts').value()) {
+ this.db.set('scripts', []).write()
+ }
}
// read() is to keep the data of the main process and the rendering process up to date.
public read() {
diff --git a/src/lang/common.ts b/src/lang/common.ts
index 72887c4de..fe5aca004 100644
--- a/src/lang/common.ts
+++ b/src/lang/common.ts
@@ -25,7 +25,7 @@ export default {
ja: '戻る',
},
save: {
- zh: '保存',
+ zh: '保 存',
en: 'Save',
ja: '保存',
},
@@ -142,6 +142,11 @@ export default {
uptime: {
zh: '运行时间',
en: 'Uptime',
- ja: '運行時間',
+ ja: '稼働時間',
+ },
+ betaInfo: {
+ zh: '该功能属于 beta 测试阶段',
+ en: 'It is a test feature in the beta stage',
+ ja: 'この機能はベータテスト中です',
},
}
diff --git a/src/lang/connections.ts b/src/lang/connections.ts
index 399eb8a4e..28b97952a 100644
--- a/src/lang/connections.ts
+++ b/src/lang/connections.ts
@@ -84,6 +84,27 @@ export default {
en: 'Subscribe Failed',
ja: 'サブスクリプションが失敗しました',
},
+ qosSubSysFailed: {
+ zh: '拒绝了$SYS主题,错误的 QoS,MQTT Broker 拒绝了订阅。请检查 ACL 配置',
+ en: 'Rejected the $SYS topic,Unexpected QoS, MQTT Broker declined the subscription. Please check ACL configuration',
+ ja:
+ '$SYSトピックを拒否しま。した予期しないQoS、MQTT Brokerはサブスクリプションを拒否しました。ACL構成を確認してください',
+ },
+ qosSubFailed: {
+ zh: '错误的 QoS',
+ en: 'Unexpected QoS',
+ ja: '予期しないQoS',
+ },
+ emptySubFailed: {
+ zh: '订阅为空',
+ en: 'Subscription is empty',
+ ja: 'サブスクリプションは空です',
+ },
+ unknowSubFailed: {
+ zh: '未知的订阅错误',
+ en: 'Unknown subscription error',
+ ja: '不明なサブスクリプションエラー',
+ },
connected: {
zh: '已连接',
en: 'Connected',
@@ -279,6 +300,11 @@ export default {
en: 'Quick selection of created connection configurations',
ja: '作成された接続構成を早めに選択することができます',
},
+ clientIdWithTimeTip: {
+ zh: '附加时间戳到 ClientID 后,以防止重复的连接',
+ en: 'Append a timestamp to the ClientID to prevent duplicate connections',
+ ja: '重複接続を防ぐために、ClientIDにタイムスタンプを追加します',
+ },
publishMsg: {
zh: '发送消息',
en: 'Publish message',
@@ -385,51 +411,52 @@ export default {
timedMessage: {
zh: '定时消息',
en: 'Timed message',
- ja: '定時メッセージ',
+ ja: '自動送信',
},
msgFrequency: {
zh: '消息频率(秒)',
en: 'Message frequency(s)',
- ja: 'メッセージ頻度(秒)',
+ ja: '送信間隔(秒)',
},
clearIntervalBtn: {
zh: '清理定时器',
en: 'Clear timer',
- ja: 'タイマーをクリーンする',
+ ja: '自動送信を解除する',
},
setTimerSuccess: {
zh: '成功设置定时器,发送一条消息将触发定时消息',
en: 'Timer set successfully, sending a message will trigger timed messages',
- ja: 'タイマーを正しく設定した。メッセージ一通を送信すれば、定時メッセージを触発する',
+ ja: `自動送信を設定しました。
+ これから一つのメッセージを送信すると、設定した時間間隔で自動的に該当メッセージを送信できます`,
},
startTimedMessage: {
zh: '成功开启定时消息,消息频率(秒):',
en: 'Opened timed message successfully, frequency(s): ',
- ja: '定時メッセージを正しく設定した。メッセージ頻度(秒): ',
+ ja: 'メッセージの自動送信が始まりました。送信間隔(秒): ',
},
stopTimedMessage: {
zh: '已停止发送定时消息',
en: 'Stopped sending timed messages',
- ja: '定時メッセージを発送中止した',
+ ja: '自動送信設定を解除しました',
},
systemTopic: {
zh: '系统主题',
en: 'System topic',
- ja: 'システムテーマ',
+ ja: 'システムトピック',
},
bytesStatistics: {
zh: '收发流量统计',
en: 'Bytes statistics',
- ja: 'データ通信量の送受信を統計する',
+ ja: 'データ通信の利用状況',
},
bytesReceived: {
zh: '累计接收流量',
en: 'Accumulated received bytes',
- ja: 'データ通信量の受信を累算する',
+ ja: '累積受信バイト数',
},
bytesSent: {
zh: '累计发送流量',
en: 'Accumulated sent bytes',
- ja: 'データ通信量の送信を累算する',
+ ja: '累積送信バイト数',
},
}
diff --git a/src/lang/index.ts b/src/lang/index.ts
index e174bc7d3..3f136b2c6 100644
--- a/src/lang/index.ts
+++ b/src/lang/index.ts
@@ -7,7 +7,7 @@ import jaLocale from 'element-ui/lib/locale/lang/ja'
import { formati18n } from '@/utils/i18n'
const supportLang: SupportLangModel = ['zh', 'en', 'ja']
-const i18nModules: i18nLocaleModel = ['connections', 'settings', 'common', 'about']
+const i18nModules: i18nLocaleModel = ['connections', 'settings', 'common', 'about', 'script']
const { en, zh, ja }: VueI18n.LocaleMessages = formati18n(i18nModules, supportLang)
diff --git a/src/lang/script.ts b/src/lang/script.ts
new file mode 100644
index 000000000..2b018ed46
--- /dev/null
+++ b/src/lang/script.ts
@@ -0,0 +1,62 @@
+export default {
+ script: {
+ zh: '脚本',
+ en: 'Script',
+ ja: 'スクリプト',
+ },
+ scriptName: {
+ zh: '脚本名称',
+ en: 'Script name',
+ ja: 'スクリプト名',
+ },
+ test: {
+ zh: '测 试',
+ en: 'Test',
+ ja: 'テスト',
+ },
+ input: {
+ zh: '输入',
+ en: 'Input',
+ ja: 'インプット',
+ },
+ output: {
+ zh: '输出',
+ en: 'Output',
+ ja: 'アウトプット',
+ },
+ saveScript: {
+ zh: '保存脚本',
+ en: 'Save script',
+ ja: '保存',
+ },
+ useScript: {
+ zh: '使用脚本',
+ en: 'Use script',
+ ja: 'スクリプトを使う',
+ },
+ removeScript: {
+ zh: '停止脚本',
+ en: 'Stop script',
+ ja: '停止',
+ },
+ applyType: {
+ zh: '应用于',
+ en: 'Applied to',
+ ja: '適用対象',
+ },
+ scriptRequired: {
+ zh: '请选择脚本',
+ en: 'Script name is required',
+ ja: 'スクリプト名を選択してください',
+ },
+ startScript: {
+ zh: '脚本功能已开启',
+ en: 'Script is enabled',
+ ja: 'スクリプトが有効になっています',
+ },
+ stopScirpt: {
+ zh: '脚本功能已停止',
+ en: 'Script has stopped',
+ ja: 'スクリプトが停止しました',
+ },
+}
diff --git a/src/main/updateChecker.ts b/src/main/updateChecker.ts
index b68effa0b..d6efc22c6 100644
--- a/src/main/updateChecker.ts
+++ b/src/main/updateChecker.ts
@@ -1,7 +1,7 @@
import { dialog, shell } from 'electron'
import axios from 'axios'
-const version = 'v1.4.1'
+const version = 'v1.4.2'
const release = 'https://api.github.com/repos/emqx/MQTTX/releases/latest'
const downloadUrl = 'https://github.com/emqx/MQTTX/releases/latest'
diff --git a/src/router/routes.ts b/src/router/routes.ts
index 3a879b05e..9f8de022e 100644
--- a/src/router/routes.ts
+++ b/src/router/routes.ts
@@ -15,6 +15,7 @@ const routes: Routes[] = [
},
{ path: '/settings', name: 'Settings', component: () => import('../views/settings/index.vue') },
{ path: '/about', name: 'About', component: () => import('../views/about/index.vue') },
+ { path: '/script', name: 'Script', component: () => import('../views/script/index.vue') },
{ path: '/new_window/:id', name: 'newWindow', component: () => import('../views/window/Window.vue') },
],
},
diff --git a/src/store/getter.ts b/src/store/getter.ts
index ff80e1f00..14abe3a0a 100644
--- a/src/store/getter.ts
+++ b/src/store/getter.ts
@@ -10,6 +10,7 @@ const getters = {
willMessageVisible: (state: State) => state.app.willMessageVisible,
advancedVisible: (state: State) => state.app.advancedVisible,
allConnections: (state: State) => state.app.allConnections,
+ currentScript: (state: State) => state.app.currentScript,
}
export default getters
diff --git a/src/store/modules/app.ts b/src/store/modules/app.ts
index 7be003ce6..8aacfd928 100644
--- a/src/store/modules/app.ts
+++ b/src/store/modules/app.ts
@@ -1,6 +1,7 @@
import Vue from 'vue'
import { ConnectionModel } from '../../views/connections/types'
import { loadSettings, setSettings } from '@/api/setting'
+import { ScriptState } from '@/views/script/types'
const TOGGLE_THEME = 'TOGGLE_THEME'
const TOGGLE_LANG = 'TOGGLE_LANG'
@@ -15,6 +16,7 @@ const UNREAD_MESSAGE_COUNT_INCREMENT = 'UNREAD_MESSAGE_COUNT_INCREMENT'
const TOGGLE_WILL_MESSAGE_VISIBLE = 'TOGGLE_WILL_MESSAGE_VISIBLE'
const TOGGLE_ADVANCED_VISIBLE = 'TOGGLE_ADVANCED_VISIBLE'
const CHANGE_ALL_CONNECTIONS = 'CHANGE_ALL_CONNECTIONS'
+const SET_SCRIPT = 'SET_SCRIPT'
const stateRecord: App = loadSettings()
@@ -39,6 +41,7 @@ const app = {
advancedVisible: true,
willMessageVisible: true,
allConnections: [],
+ currentScript: null,
},
mutations: {
[TOGGLE_THEME](state: App, currentTheme: Theme) {
@@ -84,7 +87,13 @@ const app = {
if (payload.unreadMessageCount !== undefined) {
Vue.set(state.unreadMessageCount, payload.id, payload.unreadMessageCount)
} else {
- const count = (state.unreadMessageCount[payload.id] += 1)
+ const currentCount: number | undefined = state.unreadMessageCount[payload.id]
+ let count = 0
+ if (currentCount !== undefined) {
+ count = currentCount + 1
+ } else {
+ count += 1
+ }
Vue.set(state.unreadMessageCount, payload.id, count)
}
},
@@ -97,6 +106,9 @@ const app = {
[CHANGE_ALL_CONNECTIONS](state: App, allConnections: ConnectionModel[] | []) {
state.allConnections = allConnections
},
+ [SET_SCRIPT](state: App, currentScript: ScriptState) {
+ state.currentScript = currentScript
+ },
},
actions: {
TOGGLE_THEME({ commit }: any, payload: App) {
@@ -142,6 +154,9 @@ const app = {
CHANGE_ALL_CONNECTIONS({ commit }: any, payload: App) {
commit(CHANGE_ALL_CONNECTIONS, payload.allConnections)
},
+ SET_SCRIPT({ commit }: any, payload: App) {
+ commit(SET_SCRIPT, payload.currentScript)
+ },
},
}
diff --git a/src/types/global.d.ts b/src/types/global.d.ts
index 1fb3c93f7..a9b193b1c 100644
--- a/src/types/global.d.ts
+++ b/src/types/global.d.ts
@@ -2,6 +2,7 @@ import Vue from 'vue'
import { TranslateResult } from 'vue-i18n'
import { MqttClient } from 'mqtt'
import { MessageModel, ConnectionModel } from '@/views/connections/types'
+import { ScriptState } from '@/views/script/types'
declare global {
type $TSFixed = any
@@ -20,10 +21,6 @@ declare global {
resetFields: () => void
}
- type EditorRef = Vue & {
- editorLayout: () => void
- }
-
interface ActiveConnection {
readonly id: string
}
@@ -53,13 +50,6 @@ declare global {
showSubscriptions: boolean
}
- type PluginFunction = (Vue: any, options?: T) => void
-
- interface PluginObject {
- install: PluginFunction
- [key: string]: any
- }
-
interface App {
currentTheme: Theme
currentLang: Language
@@ -82,6 +72,7 @@ declare global {
willMessageVisible: boolean
advancedVisible: boolean
allConnections: ConnectionModel[] | []
+ currentScript: ScriptState | null
}
interface State {
diff --git a/src/types/locale.d.ts b/src/types/locale.d.ts
index 8e1c81a50..03a1e1300 100644
--- a/src/types/locale.d.ts
+++ b/src/types/locale.d.ts
@@ -4,7 +4,7 @@ declare module 'element-ui/lib/locale/lang/en' {}
declare module 'element-ui/lib/locale/lang/zh-CN' {}
declare module 'element-ui/lib/locale/lang/ja' {}
-type i18nLocaleModel = ['connections', 'settings', 'common', 'about']
+type i18nLocaleModel = ['connections', 'settings', 'common', 'about', 'script']
type SupportLangModel = ['zh', 'en', 'ja']
declare module '*.json' {
diff --git a/src/types/shims-vue.d.ts b/src/types/shims-vue.d.ts
index cc865a351..8dfa1cd91 100644
--- a/src/types/shims-vue.d.ts
+++ b/src/types/shims-vue.d.ts
@@ -2,6 +2,7 @@ declare module 'lodash-id'
declare module 'uuid'
declare module 'vue-click-outside'
declare module 'chart.js'
+declare module 'json2csv'
declare module '*.vue' {
import Vue from 'vue'
export default Vue
diff --git a/src/utils/i18n.ts b/src/utils/i18n.ts
index 8a64e9255..61049a331 100644
--- a/src/utils/i18n.ts
+++ b/src/utils/i18n.ts
@@ -12,6 +12,7 @@ export const formati18n = (transItems: i18nLocaleModel, langs: SupportLangModel)
settings: {},
common: {},
about: {},
+ script: {},
}
})
transItems.forEach((item) => {
diff --git a/src/utils/mqttUtils.ts b/src/utils/mqttUtils.ts
index 832a6ee8f..549cb46e1 100644
--- a/src/utils/mqttUtils.ts
+++ b/src/utils/mqttUtils.ts
@@ -38,6 +38,7 @@ export const getClientOptions = (record: ConnectionModel): IClientOptions => {
reconnect,
will,
rejectUnauthorized,
+ clientIdWithTime,
} = record
// reconnectPeriod = 0 disabled automatic reconnection in the client
const reconnectPeriod = reconnect ? 4000 : 0
@@ -50,6 +51,11 @@ export const getClientOptions = (record: ConnectionModel): IClientOptions => {
protocolVersion,
}
options.connectTimeout = time.convertSecondsToMs(connectTimeout)
+ // Append timestamp to MQTT client id
+ if (clientIdWithTime) {
+ const clickIconTime = Date.parse(new Date().toString())
+ options.clientId = `${options.clientId}_${clickIconTime}`
+ }
// Auth
if (username !== '') {
options.username = username
diff --git a/src/utils/sandbox.ts b/src/utils/sandbox.ts
new file mode 100644
index 000000000..5baa7a069
--- /dev/null
+++ b/src/utils/sandbox.ts
@@ -0,0 +1,37 @@
+import { VM, VMScript } from 'vm2'
+
+const executeScript = (input: string | number, script: string, type: PayloadType): string => {
+ let output = ''
+ try {
+ const vm = new VM({
+ timeout: 10000,
+ sandbox: {
+ execute(callback: (value: any) => any) {
+ let _inputValue = input
+ if (type === 'JSON' && typeof input === 'string') {
+ _inputValue = JSON.parse(input)
+ }
+ let _output = callback(_inputValue)
+ if (_output === undefined) {
+ _output = 'undefined'
+ } else if (_output === null) {
+ _output = 'null'
+ } else {
+ _output = _output.toString()
+ }
+ return _output
+ },
+ },
+ eval: false,
+ wasm: false,
+ })
+ const _script = new VMScript(script)
+ output = vm.run(_script)
+ return output
+ } catch (error) {
+ output = error.toString()
+ return output
+ }
+}
+
+export default { executeScript }
diff --git a/src/views/about/index.vue b/src/views/about/index.vue
index d7e0eaa00..5b9e2b47e 100644
--- a/src/views/about/index.vue
+++ b/src/views/about/index.vue
@@ -4,7 +4,7 @@
![mqttx]()
-
v1.4.1
+
v1.4.2
{{ $t('about.update') }}
diff --git a/src/views/connections/ConnectionForm.vue b/src/views/connections/ConnectionForm.vue
index 814da0bdb..db73d5d97 100644
--- a/src/views/connections/ConnectionForm.vue
+++ b/src/views/connections/ConnectionForm.vue
@@ -56,11 +56,30 @@
-
+
+
+
+
+
+
+
+
+
@@ -337,7 +356,7 @@
scrollbar-status="auto"
/>
-
+
JSON
Plaintext
@@ -492,6 +511,7 @@ export default class ConnectionCreate extends Vue {
sessionExpiryInterval: undefined,
receiveMaximum: undefined,
topicAliasMaximum: undefined,
+ clientIdWithTime: false,
}
@Watch('record', { immediate: true, deep: true })
@@ -503,6 +523,10 @@ export default class ConnectionCreate extends Vue {
}
}
+ get clientIdWithTime() {
+ return this.record.clientIdWithTime
+ }
+
get rules() {
return {
name: [
@@ -568,6 +592,11 @@ export default class ConnectionCreate extends Vue {
this.record.clientId = getClientId()
}
+ // Reverse the status of clientIdWithTime.
+ private reverseClientIDWithTime() {
+ this.record.clientIdWithTime = !this.record.clientIdWithTime
+ }
+
private getFilePath(key: 'ca' | 'cert' | 'key') {
remote.dialog.showOpenDialog(
{
@@ -685,15 +714,25 @@ export default class ConnectionCreate extends Vue {
.el-form {
padding-top: 80px;
padding-bottom: 40px;
+ // normal icon operation style
.icon-oper {
color: var(--color-text-default);
line-height: 43px;
transition: 0.2s color ease;
- &:hover,
- &:focus {
+ &:hover {
color: var(--color-main-green);
}
}
+ // icon style without fake class such as `:hover` style
+ .icon-oper-pure {
+ color: var(--color-text-default);
+ line-height: 43px;
+ transition: 0.2s color ease;
+ }
+ // icon active
+ .icon-oper-active {
+ color: var(--color-main-green);
+ }
.el-form-item__error {
top: 80%;
}
@@ -712,18 +751,7 @@ export default class ConnectionCreate extends Vue {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
- .payload-type {
- width: 100%;
- height: 30px;
- line-height: 30px;
- padding: 0px 12px;
- background: var(--color-bg-radio);
- border: 1px solid var(--color-border-default);
- border-top: none;
- text-align: right;
- border-bottom-left-radius: 4px;
- border-bottom-right-radius: 4px;
- }
+ @include editor-lang-type;
}
.info-header {
a.collapse-btn {
diff --git a/src/views/connections/ConnectionInfo.vue b/src/views/connections/ConnectionInfo.vue
index 22353b7ff..ed68611b3 100644
--- a/src/views/connections/ConnectionInfo.vue
+++ b/src/views/connections/ConnectionInfo.vue
@@ -4,33 +4,62 @@
-
+
-
-
+
+
+
+
+
+
+
+
-
+
-
+
-
+
- Clean Session
+ Clean Session
@@ -168,7 +226,32 @@ export default class ConnectionInfo extends Vue {
line-height: 35px;
height: 35px;
}
+ // normal icon
+ .icon-oper {
+ color: var(--color-text-tips);
+ line-height: 43px;
+ transition: 0.2s color ease;
+ }
+ // icon active
+ .icon-oper-active {
+ color: var(--color-main-green);
+ }
+ // icon disable
+ .icon-oper-disable {
+ color: var(--color-text-default);
+ }
+ // input disable
+ .is-disabled > input {
+ background: transparent;
+ }
}
+ // adjust the position of the clientID timestamp element
+ .icon-client-time {
+ position: absolute;
+ top: -2.65em;
+ left: 5em;
+ }
+
.el-checkbox {
margin-top: 42px;
}
diff --git a/src/views/connections/ConnectionsDetail.vue b/src/views/connections/ConnectionsDetail.vue
index bbcc28fd2..7645d72fb 100644
--- a/src/views/connections/ConnectionsDetail.vue
+++ b/src/views/connections/ConnectionsDetail.vue
@@ -42,17 +42,17 @@
v-if="sendTimeId"
placement="bottom"
:effect="theme !== 'light' ? 'light' : 'dark'"
- :open-delay="1000"
+ :open-delay="500"
:content="$t('connections.clearIntervalBtn')"
>
-
+
@@ -62,11 +62,22 @@
+
+
+
+
+
@@ -110,6 +121,9 @@
{{ $t('connections.bytesStatistics') }}
+
+ {{ $t('script.useScript') }}
+
{{ $t('connections.disconnect') }}
@@ -211,13 +225,15 @@
:record="record"
:top="showClientInfo ? bodyTop.open : bodyTop.close"
@onClickTopic="handleTopicClick"
- @deleteTopic="getMessages"
+ @deleteTopic="handleTopicDelete"
/>
@@ -233,6 +249,7 @@