Mqtt.vue 12 KB

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