Mqtt.vue 12 KB

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