mqtt.vue 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. <template>
  2. <AppPaneLayout layout-id="mqtt">
  3. <template #primary>
  4. <div
  5. class="sticky top-0 z-10 flex flex-shrink-0 p-4 overflow-x-auto space-x-2 bg-primary hide-scrollbar"
  6. >
  7. <div class="inline-flex flex-1 space-x-2">
  8. <input
  9. id="mqtt-url"
  10. v-model="url"
  11. type="url"
  12. autocomplete="off"
  13. spellcheck="false"
  14. class="w-full px-4 py-2 border rounded bg-primaryLight border-divider text-secondaryDark"
  15. :placeholder="$t('mqtt.url')"
  16. :disabled="
  17. connectionState === 'CONNECTED' ||
  18. connectionState === 'CONNECTING'
  19. "
  20. @keyup.enter="isUrlValid ? toggleConnection() : null"
  21. />
  22. <ButtonPrimary
  23. id="connect"
  24. :disabled="!isUrlValid"
  25. class="w-32"
  26. :label="
  27. connectionState === 'DISCONNECTED'
  28. ? t('action.connect')
  29. : t('action.disconnect')
  30. "
  31. :loading="connectionState === 'CONNECTING'"
  32. @click.native="toggleConnection"
  33. />
  34. </div>
  35. <div class="flex space-x-4">
  36. <input
  37. id="mqtt-username"
  38. v-model="username"
  39. type="text"
  40. spellcheck="false"
  41. class="input"
  42. :placeholder="$t('authorization.username')"
  43. />
  44. <input
  45. id="mqtt-password"
  46. v-model="password"
  47. type="password"
  48. spellcheck="false"
  49. class="input"
  50. :placeholder="$t('authorization.password')"
  51. />
  52. </div>
  53. </div>
  54. </template>
  55. <template #secondary>
  56. <RealtimeLog
  57. :title="$t('mqtt.log')"
  58. :log="log"
  59. @delete="clearLogEntries()"
  60. />
  61. </template>
  62. <template #sidebar>
  63. <div class="flex items-center justify-between p-4">
  64. <label for="pubTopic" class="font-semibold text-secondaryLight">
  65. {{ $t("mqtt.topic") }}
  66. </label>
  67. </div>
  68. <div class="flex px-4">
  69. <input
  70. id="pubTopic"
  71. v-model="pubTopic"
  72. class="input"
  73. :placeholder="$t('mqtt.topic_name')"
  74. type="text"
  75. autocomplete="off"
  76. spellcheck="false"
  77. />
  78. </div>
  79. <div class="flex items-center justify-between p-4">
  80. <label for="mqtt-message" class="font-semibold text-secondaryLight">
  81. {{ $t("mqtt.communication") }}
  82. </label>
  83. </div>
  84. <div class="flex px-4 space-x-2">
  85. <input
  86. id="mqtt-message"
  87. v-model="message"
  88. class="input"
  89. type="text"
  90. autocomplete="off"
  91. :placeholder="$t('mqtt.message')"
  92. spellcheck="false"
  93. />
  94. <ButtonPrimary
  95. id="publish"
  96. name="get"
  97. :disabled="!canPublish"
  98. :label="$t('mqtt.publish')"
  99. @click.native="publish"
  100. />
  101. </div>
  102. <div
  103. class="flex items-center justify-between p-4 mt-4 border-t border-dividerLight"
  104. >
  105. <label for="subTopic" class="font-semibold text-secondaryLight">
  106. {{ $t("mqtt.topic") }}
  107. </label>
  108. </div>
  109. <div class="flex px-4 space-x-2">
  110. <input
  111. id="subTopic"
  112. v-model="subTopic"
  113. type="text"
  114. autocomplete="off"
  115. :placeholder="$t('mqtt.topic_name')"
  116. spellcheck="false"
  117. class="input"
  118. />
  119. <ButtonPrimary
  120. id="subscribe"
  121. name="get"
  122. :disabled="!canSubscribe"
  123. :label="
  124. subscriptionState ? $t('mqtt.unsubscribe') : $t('mqtt.subscribe')
  125. "
  126. reverse
  127. @click.native="toggleSubscription"
  128. />
  129. </div>
  130. </template>
  131. </AppPaneLayout>
  132. </template>
  133. <script setup lang="ts">
  134. import {
  135. computed,
  136. onMounted,
  137. onUnmounted,
  138. ref,
  139. watch,
  140. } from "@nuxtjs/composition-api"
  141. import debounce from "lodash/debounce"
  142. import { MQTTConnection, MQTTError } from "~/helpers/realtime/MQTTConnection"
  143. import {
  144. useI18n,
  145. useNuxt,
  146. useReadonlyStream,
  147. useStream,
  148. useStreamSubscriber,
  149. useToast,
  150. } from "~/helpers/utils/composables"
  151. import {
  152. addMQTTLogLine,
  153. MQTTConn$,
  154. MQTTEndpoint$,
  155. MQTTLog$,
  156. setMQTTConn,
  157. setMQTTEndpoint,
  158. setMQTTLog,
  159. } from "~/newstore/MQTTSession"
  160. const t = useI18n()
  161. const nuxt = useNuxt()
  162. const toast = useToast()
  163. const { subscribeToStream } = useStreamSubscriber()
  164. const url = useStream(MQTTEndpoint$, "", setMQTTEndpoint)
  165. const log = useStream(MQTTLog$, [], setMQTTLog)
  166. const socket = useStream(MQTTConn$, new MQTTConnection(), setMQTTConn)
  167. const connectionState = useReadonlyStream(
  168. socket.value.connectionState$,
  169. "DISCONNECTED"
  170. )
  171. const subscriptionState = useReadonlyStream(
  172. socket.value.subscriptionState$,
  173. false
  174. )
  175. const isUrlValid = ref(true)
  176. const pubTopic = ref("")
  177. const subTopic = ref("")
  178. const message = ref("")
  179. const username = ref("")
  180. const password = ref("")
  181. let worker: Worker
  182. const canPublish = computed(
  183. () =>
  184. pubTopic.value !== "" &&
  185. message.value !== "" &&
  186. connectionState.value === "CONNECTED"
  187. )
  188. const canSubscribe = computed(
  189. () => subTopic.value !== "" && connectionState.value === "CONNECTED"
  190. )
  191. const workerResponseHandler = ({
  192. data,
  193. }: {
  194. data: { url: string; result: boolean }
  195. }) => {
  196. if (data.url === url.value) isUrlValid.value = data.result
  197. }
  198. onMounted(() => {
  199. worker = nuxt.value.$worker.createRejexWorker()
  200. worker.addEventListener("message", workerResponseHandler)
  201. subscribeToStream(socket.value.event$, (event) => {
  202. switch (event?.type) {
  203. case "CONNECTING":
  204. log.value = [
  205. {
  206. payload: `${t("state.connecting_to", { name: url.value })}`,
  207. source: "info",
  208. color: "var(--accent-color)",
  209. ts: undefined,
  210. },
  211. ]
  212. break
  213. case "CONNECTED":
  214. log.value = [
  215. {
  216. payload: `${t("state.connected_to", { name: url.value })}`,
  217. source: "info",
  218. color: "var(--accent-color)",
  219. ts: Date.now(),
  220. },
  221. ]
  222. toast.success(`${t("state.connected")}`)
  223. break
  224. case "MESSAGE_SENT":
  225. addMQTTLogLine({
  226. prefix: `${event.message.topic}`,
  227. payload: event.message.message,
  228. source: "client",
  229. ts: Date.now(),
  230. })
  231. break
  232. case "MESSAGE_RECEIVED":
  233. addMQTTLogLine({
  234. prefix: `${event.message.topic}`,
  235. payload: event.message.message,
  236. source: "server",
  237. ts: event.time,
  238. })
  239. break
  240. case "SUBSCRIBED":
  241. addMQTTLogLine({
  242. payload: subscriptionState.value
  243. ? `${t("state.subscribed_success", { topic: subTopic.value })}`
  244. : `${t("state.unsubscribed_success", { topic: subTopic.value })}`,
  245. source: "server",
  246. ts: event.time,
  247. })
  248. break
  249. case "SUBSCRIPTION_FAILED":
  250. addMQTTLogLine({
  251. payload: subscriptionState.value
  252. ? `${t("state.subscribed_failed", { topic: subTopic.value })}`
  253. : `${t("state.unsubscribed_failed", { topic: subTopic.value })}`,
  254. source: "server",
  255. ts: event.time,
  256. })
  257. break
  258. case "ERROR":
  259. addMQTTLogLine({
  260. payload: getI18nError(event.error),
  261. source: "info",
  262. color: "#ff5555",
  263. ts: event.time,
  264. })
  265. break
  266. case "DISCONNECTED":
  267. addMQTTLogLine({
  268. payload: t("state.disconnected_from", { name: url.value }).toString(),
  269. source: "info",
  270. color: "#ff5555",
  271. ts: event.time,
  272. })
  273. toast.error(`${t("state.disconnected")}`)
  274. break
  275. }
  276. })
  277. })
  278. const debouncer = debounce(function () {
  279. worker.postMessage({ type: "ws", url: url.value })
  280. }, 1000)
  281. watch(url, (newUrl) => {
  282. if (newUrl) debouncer()
  283. })
  284. onUnmounted(() => {
  285. worker.terminate()
  286. })
  287. // METHODS
  288. const toggleConnection = () => {
  289. // If it is connecting:
  290. if (connectionState.value === "DISCONNECTED") {
  291. return socket.value.connect(url.value, username.value, password.value)
  292. }
  293. // Otherwise, it's disconnecting.
  294. socket.value.disconnect()
  295. }
  296. const publish = () => {
  297. socket.value?.publish(pubTopic.value, message.value)
  298. }
  299. const toggleSubscription = () => {
  300. if (subscriptionState.value) {
  301. socket.value.unsubscribe(subTopic.value)
  302. } else {
  303. socket.value.subscribe(subTopic.value)
  304. }
  305. }
  306. const getI18nError = (error: MQTTError): string => {
  307. if (typeof error === "string") return error
  308. switch (error.type) {
  309. case "CONNECTION_NOT_ESTABLISHED":
  310. return t("state.connection_lost").toString()
  311. case "SUBSCRIPTION_FAILED":
  312. return t("state.mqtt_subscription_failed", {
  313. topic: error.topic,
  314. }).toString()
  315. case "PUBLISH_ERROR":
  316. return t("state.publish_error", { topic: error.topic }).toString()
  317. case "CONNECTION_LOST":
  318. return t("state.connection_lost").toString()
  319. case "CONNECTION_FAILED":
  320. return t("state.connection_failed").toString()
  321. default:
  322. return t("state.disconnected_from", { name: url.value }).toString()
  323. }
  324. }
  325. const clearLogEntries = () => {
  326. log.value = []
  327. }
  328. </script>