Mqtt.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. <template>
  2. <Splitpanes
  3. class="smart-splitter"
  4. :rtl="SIDEBAR_ON_LEFT && windowInnerWidth.x.value >= 768"
  5. :class="{
  6. '!flex-row-reverse': SIDEBAR_ON_LEFT && windowInnerWidth.x.value >= 768,
  7. }"
  8. :horizontal="!(windowInnerWidth.x.value >= 768)"
  9. >
  10. <Pane size="75" min-size="65" class="hide-scrollbar !overflow-auto">
  11. <Splitpanes class="smart-splitter" :horizontal="COLUMN_LAYOUT">
  12. <Pane
  13. :size="COLUMN_LAYOUT ? 45 : 50"
  14. class="hide-scrollbar !overflow-auto"
  15. >
  16. <AppSection label="request">
  17. <div
  18. class="bg-primary flex flex-col space-y-4 p-4 top-0 z-10 sticky"
  19. >
  20. <div class="space-x-2 flex-1 inline-flex">
  21. <input
  22. id="mqtt-url"
  23. v-model="url"
  24. type="url"
  25. autocomplete="off"
  26. spellcheck="false"
  27. class="bg-primaryLight border border-divider rounded text-secondaryDark w-full py-2 px-4 hover:border-dividerDark focus-visible:bg-transparent focus-visible:border-dividerDark"
  28. :placeholder="$t('mqtt.url')"
  29. :disabled="connectionState"
  30. @keyup.enter="validUrl ? toggleConnection() : null"
  31. />
  32. <ButtonPrimary
  33. id="connect"
  34. :disabled="!validUrl"
  35. class="w-32"
  36. :label="
  37. connectionState
  38. ? $t('action.disconnect')
  39. : $t('action.connect')
  40. "
  41. :loading="connectingState"
  42. @click.native="toggleConnection"
  43. />
  44. </div>
  45. <div class="flex space-x-4">
  46. <input
  47. id="mqtt-username"
  48. v-model="username"
  49. type="text"
  50. spellcheck="false"
  51. class="input"
  52. :placeholder="$t('authorization.username')"
  53. />
  54. <input
  55. id="mqtt-password"
  56. v-model="password"
  57. type="password"
  58. spellcheck="false"
  59. class="input"
  60. :placeholder="$t('authorization.password')"
  61. />
  62. </div>
  63. </div>
  64. </AppSection>
  65. </Pane>
  66. <Pane
  67. :size="COLUMN_LAYOUT ? 65 : 50"
  68. class="hide-scrollbar !overflow-auto"
  69. >
  70. <AppSection label="response">
  71. <RealtimeLog :title="$t('mqtt.log')" :log="log" />
  72. </AppSection>
  73. </Pane>
  74. </Splitpanes>
  75. </Pane>
  76. <Pane
  77. v-if="SIDEBAR"
  78. size="25"
  79. min-size="20"
  80. class="hide-scrollbar !overflow-auto"
  81. >
  82. <AppSection label="messages">
  83. <div class="flex flex-col flex-1 p-4 inline-flex">
  84. <label for="pub_topic" class="font-semibold text-secondaryLight">
  85. {{ $t("mqtt.topic") }}
  86. </label>
  87. </div>
  88. <div class="flex px-4">
  89. <input
  90. id="pub_topic"
  91. v-model="pub_topic"
  92. class="input"
  93. :placeholder="$t('mqtt.topic_name')"
  94. type="text"
  95. autocomplete="off"
  96. spellcheck="false"
  97. />
  98. </div>
  99. <div class="flex flex-1 p-4 items-center justify-between">
  100. <label for="mqtt-message" class="font-semibold text-secondaryLight">
  101. {{ $t("mqtt.communication") }}
  102. </label>
  103. </div>
  104. <div class="flex space-x-2 px-4">
  105. <input
  106. id="mqtt-message"
  107. v-model="msg"
  108. class="input"
  109. type="text"
  110. autocomplete="off"
  111. :placeholder="$t('mqtt.message')"
  112. spellcheck="false"
  113. />
  114. <ButtonPrimary
  115. id="publish"
  116. name="get"
  117. :disabled="!canpublish"
  118. :label="$t('mqtt.publish')"
  119. @click.native="publish"
  120. />
  121. </div>
  122. <div
  123. class="border-t border-dividerLight flex flex-col flex-1 mt-4 p-4 inline-flex"
  124. >
  125. <label for="sub_topic" class="font-semibold text-secondaryLight">
  126. {{ $t("mqtt.topic") }}
  127. </label>
  128. </div>
  129. <div class="flex space-x-2 px-4">
  130. <input
  131. id="sub_topic"
  132. v-model="sub_topic"
  133. type="text"
  134. autocomplete="off"
  135. :placeholder="$t('mqtt.topic_name')"
  136. spellcheck="false"
  137. class="input"
  138. />
  139. <ButtonPrimary
  140. id="subscribe"
  141. name="get"
  142. :disabled="!cansubscribe"
  143. :label="
  144. subscriptionState ? $t('mqtt.unsubscribe') : $t('mqtt.subscribe')
  145. "
  146. reverse
  147. @click.native="toggleSubscription"
  148. />
  149. </div>
  150. </AppSection>
  151. </Pane>
  152. </Splitpanes>
  153. </template>
  154. <script>
  155. import { defineComponent } from "@nuxtjs/composition-api"
  156. import { Splitpanes, Pane } from "splitpanes"
  157. import "splitpanes/dist/splitpanes.css"
  158. import Paho from "paho-mqtt"
  159. import debounce from "lodash/debounce"
  160. import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
  161. import { useSetting } from "~/newstore/settings"
  162. import useWindowSize from "~/helpers/utils/useWindowSize"
  163. import {
  164. MQTTEndpoint$,
  165. setMQTTEndpoint,
  166. MQTTConnectingState$,
  167. MQTTConnectionState$,
  168. setMQTTConnectingState,
  169. setMQTTConnectionState,
  170. MQTTSubscriptionState$,
  171. setMQTTSubscriptionState,
  172. MQTTSocket$,
  173. setMQTTSocket,
  174. MQTTLog$,
  175. setMQTTLog,
  176. addMQTTLogLine,
  177. } from "~/newstore/MQTTSession"
  178. import { useStream } from "~/helpers/utils/composables"
  179. export default defineComponent({
  180. components: { Splitpanes, Pane },
  181. setup() {
  182. return {
  183. windowInnerWidth: useWindowSize(),
  184. SIDEBAR: useSetting("SIDEBAR"),
  185. COLUMN_LAYOUT: useSetting("COLUMN_LAYOUT"),
  186. SIDEBAR_ON_LEFT: useSetting("SIDEBAR_ON_LEFT"),
  187. url: useStream(MQTTEndpoint$, "", setMQTTEndpoint),
  188. connectionState: useStream(
  189. MQTTConnectionState$,
  190. false,
  191. setMQTTConnectionState
  192. ),
  193. connectingState: useStream(
  194. MQTTConnectingState$,
  195. false,
  196. setMQTTConnectingState
  197. ),
  198. subscriptionState: useStream(
  199. MQTTSubscriptionState$,
  200. false,
  201. setMQTTSubscriptionState
  202. ),
  203. log: useStream(MQTTLog$, null, setMQTTLog),
  204. client: useStream(MQTTSocket$, null, setMQTTSocket),
  205. }
  206. },
  207. data() {
  208. return {
  209. isUrlValid: true,
  210. pub_topic: "",
  211. sub_topic: "",
  212. msg: "",
  213. manualDisconnect: false,
  214. username: "",
  215. password: "",
  216. }
  217. },
  218. computed: {
  219. validUrl() {
  220. return this.isUrlValid
  221. },
  222. canpublish() {
  223. return this.pub_topic !== "" && this.msg !== "" && this.connectionState
  224. },
  225. cansubscribe() {
  226. return this.sub_topic !== "" && this.connectionState
  227. },
  228. },
  229. watch: {
  230. url() {
  231. this.debouncer()
  232. },
  233. },
  234. created() {
  235. if (process.browser) {
  236. this.worker = this.$worker.createRejexWorker()
  237. this.worker.addEventListener("message", this.workerResponseHandler)
  238. }
  239. },
  240. destroyed() {
  241. this.worker.terminate()
  242. },
  243. methods: {
  244. debouncer: debounce(function () {
  245. this.worker.postMessage({ type: "ws", url: this.url })
  246. }, 1000),
  247. workerResponseHandler({ data }) {
  248. if (data.url === this.url) this.isUrlValid = data.result
  249. },
  250. connect() {
  251. this.connectingState = true
  252. this.log = [
  253. {
  254. payload: this.$t("state.connecting_to", { name: this.url }),
  255. source: "info",
  256. color: "var(--accent-color)",
  257. ts: new Date().toLocaleTimeString(),
  258. },
  259. ]
  260. const parseUrl = new URL(this.url)
  261. this.client = new Paho.Client(
  262. `${parseUrl.hostname}${
  263. parseUrl.pathname !== "/" ? parseUrl.pathname : ""
  264. }`,
  265. parseUrl.port !== "" ? Number(parseUrl.port) : 8081,
  266. "hoppscotch"
  267. )
  268. const connectOptions = {
  269. onSuccess: this.onConnectionSuccess,
  270. onFailure: this.onConnectionFailure,
  271. useSSL: parseUrl.protocol !== "ws:",
  272. }
  273. if (this.username !== "") {
  274. connectOptions.userName = this.username
  275. }
  276. if (this.password !== "") {
  277. connectOptions.password = this.password
  278. }
  279. this.client.connect(connectOptions)
  280. this.client.onConnectionLost = this.onConnectionLost
  281. this.client.onMessageArrived = this.onMessageArrived
  282. logHoppRequestRunToAnalytics({
  283. platform: "mqtt",
  284. })
  285. },
  286. onConnectionFailure() {
  287. this.connectingState = false
  288. this.connectionState = false
  289. addMQTTLogLine({
  290. payload: this.$t("error.something_went_wrong"),
  291. source: "info",
  292. color: "#ff5555",
  293. ts: new Date().toLocaleTimeString(),
  294. })
  295. },
  296. onConnectionSuccess() {
  297. this.connectingState = false
  298. this.connectionState = true
  299. addMQTTLogLine({
  300. payload: this.$t("state.connected_to", { name: this.url }),
  301. source: "info",
  302. color: "var(--accent-color)",
  303. ts: new Date().toLocaleTimeString(),
  304. })
  305. this.$toast.success(this.$t("state.connected"))
  306. },
  307. onMessageArrived({ payloadString, destinationName }) {
  308. addMQTTLogLine({
  309. payload: `Message: ${payloadString} arrived on topic: ${destinationName}`,
  310. source: "info",
  311. color: "var(--accent-color)",
  312. ts: new Date().toLocaleTimeString(),
  313. })
  314. },
  315. toggleConnection() {
  316. if (this.connectionState) {
  317. this.disconnect()
  318. } else {
  319. this.connect()
  320. }
  321. },
  322. disconnect() {
  323. this.manualDisconnect = true
  324. this.client.disconnect()
  325. addMQTTLogLine({
  326. payload: this.$t("state.disconnected_from", { name: this.url }),
  327. source: "info",
  328. color: "#ff5555",
  329. ts: new Date().toLocaleTimeString(),
  330. })
  331. },
  332. onConnectionLost() {
  333. this.connectingState = false
  334. this.connectionState = false
  335. if (this.manualDisconnect) {
  336. this.$toast.error(this.$t("state.disconnected"))
  337. } else {
  338. this.$toast.error(this.$t("error.something_went_wrong"))
  339. }
  340. this.manualDisconnect = false
  341. this.subscriptionState = false
  342. },
  343. publish() {
  344. try {
  345. this.client.publish(this.pub_topic, this.msg, 0, false)
  346. addMQTTLogLine({
  347. payload: `Published message: ${this.msg} to topic: ${this.pub_topic}`,
  348. ts: new Date().toLocaleTimeString(),
  349. source: "info",
  350. color: "var(--accent-color)",
  351. })
  352. } catch (e) {
  353. addMQTTLogLine({
  354. payload:
  355. this.$t("error.something_went_wrong") +
  356. `while publishing msg: ${this.msg} to topic: ${this.pub_topic}`,
  357. source: "info",
  358. color: "#ff5555",
  359. ts: new Date().toLocaleTimeString(),
  360. })
  361. }
  362. },
  363. toggleSubscription() {
  364. if (this.subscriptionState) {
  365. this.unsubscribe()
  366. } else {
  367. this.subscribe()
  368. }
  369. },
  370. subscribe() {
  371. try {
  372. this.client.subscribe(this.sub_topic, {
  373. onSuccess: this.usubSuccess,
  374. onFailure: this.usubFailure,
  375. })
  376. } catch (e) {
  377. addMQTTLogLine({
  378. payload:
  379. this.$t("error.something_went_wrong") +
  380. `while subscribing to topic: ${this.sub_topic}`,
  381. source: "info",
  382. color: "#ff5555",
  383. ts: new Date().toLocaleTimeString(),
  384. })
  385. }
  386. },
  387. usubSuccess() {
  388. this.subscriptionState = !this.subscriptionState
  389. addMQTTLogLine({
  390. payload:
  391. `Successfully ` +
  392. (this.subscriptionState ? "subscribed" : "unsubscribed") +
  393. ` to topic: ${this.sub_topic}`,
  394. source: "info",
  395. color: "var(--accent-color)",
  396. ts: new Date().toLocaleTimeString(),
  397. })
  398. },
  399. usubFailure() {
  400. addMQTTLogLine({
  401. payload:
  402. `Failed to ` +
  403. (this.subscriptionState ? "unsubscribe" : "subscribe") +
  404. ` to topic: ${this.sub_topic}`,
  405. source: "info",
  406. color: "#ff5555",
  407. ts: new Date().toLocaleTimeString(),
  408. })
  409. },
  410. unsubscribe() {
  411. this.client.unsubscribe(this.sub_topic, {
  412. onSuccess: this.usubSuccess,
  413. onFailure: this.usubFailure,
  414. })
  415. },
  416. },
  417. })
  418. </script>