Websocket.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. <template>
  2. <AppPaneLayout>
  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="websocket-url"
  10. v-model="url"
  11. class="w-full px-4 py-2 border rounded bg-primaryLight border-divider text-secondaryDark"
  12. type="url"
  13. autocomplete="off"
  14. spellcheck="false"
  15. :class="{ error: !urlValid }"
  16. :placeholder="$t('websocket.url')"
  17. :disabled="connectionState"
  18. @keyup.enter="urlValid ? toggleConnection() : null"
  19. />
  20. <ButtonPrimary
  21. id="connect"
  22. :disabled="!urlValid"
  23. class="w-32"
  24. name="connect"
  25. :label="
  26. !connectionState ? $t('action.connect') : $t('action.disconnect')
  27. "
  28. :loading="connectingState"
  29. @click.native="toggleConnection"
  30. />
  31. </div>
  32. </div>
  33. <div
  34. class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperPrimaryStickyFold"
  35. >
  36. <label class="font-semibold text-secondaryLight">
  37. {{ $t("websocket.protocols") }}
  38. </label>
  39. <div class="flex">
  40. <ButtonSecondary
  41. v-tippy="{ theme: 'tooltip' }"
  42. :title="$t('action.clear_all')"
  43. svg="trash-2"
  44. @click.native="clearContent"
  45. />
  46. <ButtonSecondary
  47. v-tippy="{ theme: 'tooltip' }"
  48. :title="$t('add.new')"
  49. svg="plus"
  50. @click.native="addProtocol"
  51. />
  52. </div>
  53. </div>
  54. <draggable
  55. v-model="protocols"
  56. animation="250"
  57. handle=".draggable-handle"
  58. draggable=".draggable-content"
  59. ghost-class="cursor-move"
  60. chosen-class="bg-primaryLight"
  61. drag-class="cursor-grabbing"
  62. >
  63. <div
  64. v-for="(protocol, index) of protocols"
  65. :key="`protocol-${index}`"
  66. class="flex border-b divide-x divide-dividerLight border-dividerLight draggable-content group"
  67. >
  68. <span>
  69. <ButtonSecondary
  70. svg="grip-vertical"
  71. class="cursor-auto text-primary hover:text-primary"
  72. :class="{
  73. 'draggable-handle group-hover:text-secondaryLight !cursor-grab':
  74. index !== protocols?.length - 1,
  75. }"
  76. tabindex="-1"
  77. />
  78. </span>
  79. <input
  80. v-model="protocol.value"
  81. class="flex flex-1 px-4 py-2 bg-transparent"
  82. :placeholder="$t('count.protocol', { count: index + 1 })"
  83. name="message"
  84. type="text"
  85. autocomplete="off"
  86. @change="
  87. updateProtocol(index, {
  88. value: $event.target.value,
  89. active: protocol.active,
  90. })
  91. "
  92. />
  93. <span>
  94. <ButtonSecondary
  95. v-tippy="{ theme: 'tooltip' }"
  96. :title="
  97. protocol.hasOwnProperty('active')
  98. ? protocol.active
  99. ? $t('action.turn_off')
  100. : $t('action.turn_on')
  101. : $t('action.turn_off')
  102. "
  103. :svg="
  104. protocol.hasOwnProperty('active')
  105. ? protocol.active
  106. ? 'check-circle'
  107. : 'circle'
  108. : 'check-circle'
  109. "
  110. color="green"
  111. @click.native="
  112. updateProtocol(index, {
  113. value: protocol.value,
  114. active: !protocol.active,
  115. })
  116. "
  117. />
  118. </span>
  119. <span>
  120. <ButtonSecondary
  121. v-tippy="{ theme: 'tooltip' }"
  122. :title="$t('action.remove')"
  123. svg="trash"
  124. color="red"
  125. @click.native="deleteProtocol({ index })"
  126. />
  127. </span>
  128. </div>
  129. </draggable>
  130. <div
  131. v-if="protocols.length === 0"
  132. class="flex flex-col items-center justify-center p-4 text-secondaryLight"
  133. >
  134. <img
  135. :src="`/images/states/${$colorMode.value}/add_category.svg`"
  136. loading="lazy"
  137. class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
  138. :alt="$t('empty.protocols')"
  139. />
  140. <span class="mb-4 text-center">
  141. {{ $t("empty.protocols") }}
  142. </span>
  143. </div>
  144. </template>
  145. <template #secondary>
  146. <RealtimeLog :title="$t('websocket.log')" :log="log" />
  147. </template>
  148. <template #sidebar>
  149. <div class="flex items-center justify-between p-4">
  150. <label
  151. for="websocket-message"
  152. class="font-semibold text-secondaryLight"
  153. >
  154. {{ $t("websocket.communication") }}
  155. </label>
  156. </div>
  157. <div class="flex px-4 space-x-2">
  158. <input
  159. id="websocket-message"
  160. v-model="communication.input"
  161. name="message"
  162. type="text"
  163. autocomplete="off"
  164. :disabled="!connectionState"
  165. :placeholder="$t('websocket.message')"
  166. class="input"
  167. @keyup.enter="connectionState ? sendMessage() : null"
  168. @keyup.up="connectionState ? walkHistory('up') : null"
  169. @keyup.down="connectionState ? walkHistory('down') : null"
  170. />
  171. <ButtonPrimary
  172. id="send"
  173. name="send"
  174. :disabled="!connectionState"
  175. :label="$t('action.send')"
  176. @click.native="sendMessage"
  177. />
  178. </div>
  179. </template>
  180. </AppPaneLayout>
  181. </template>
  182. <script>
  183. import { defineComponent } from "@nuxtjs/composition-api"
  184. import debounce from "lodash/debounce"
  185. import draggable from "vuedraggable"
  186. import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
  187. import {
  188. setWSEndpoint,
  189. WSEndpoint$,
  190. WSProtocols$,
  191. setWSProtocols,
  192. addWSProtocol,
  193. deleteWSProtocol,
  194. updateWSProtocol,
  195. deleteAllWSProtocols,
  196. WSSocket$,
  197. setWSSocket,
  198. setWSConnectionState,
  199. setWSConnectingState,
  200. WSConnectionState$,
  201. WSConnectingState$,
  202. addWSLogLine,
  203. WSLog$,
  204. setWSLog,
  205. } from "~/newstore/WebSocketSession"
  206. import { useStream } from "~/helpers/utils/composables"
  207. export default defineComponent({
  208. components: {
  209. draggable,
  210. },
  211. setup() {
  212. return {
  213. url: useStream(WSEndpoint$, "", setWSEndpoint),
  214. protocols: useStream(WSProtocols$, [], setWSProtocols),
  215. connectionState: useStream(
  216. WSConnectionState$,
  217. false,
  218. setWSConnectionState
  219. ),
  220. connectingState: useStream(
  221. WSConnectingState$,
  222. false,
  223. setWSConnectingState
  224. ),
  225. socket: useStream(WSSocket$, null, setWSSocket),
  226. log: useStream(WSLog$, [], setWSLog),
  227. }
  228. },
  229. data() {
  230. return {
  231. isUrlValid: true,
  232. communication: {
  233. input: "",
  234. },
  235. currentIndex: -1, // index of the message log array to put in input box
  236. activeProtocols: [],
  237. }
  238. },
  239. computed: {
  240. urlValid() {
  241. return this.isUrlValid
  242. },
  243. },
  244. watch: {
  245. url() {
  246. this.debouncer()
  247. },
  248. protocols: {
  249. handler(newVal) {
  250. this.activeProtocols = newVal
  251. .filter((item) =>
  252. Object.prototype.hasOwnProperty.call(item, "active")
  253. ? item.active === true
  254. : true
  255. )
  256. .map(({ value }) => value)
  257. },
  258. deep: true,
  259. },
  260. },
  261. created() {
  262. if (process.browser) {
  263. this.worker = this.$worker.createRejexWorker()
  264. this.worker.addEventListener("message", this.workerResponseHandler)
  265. }
  266. },
  267. destroyed() {
  268. this.worker.terminate()
  269. },
  270. methods: {
  271. clearContent() {
  272. deleteAllWSProtocols()
  273. },
  274. debouncer: debounce(function () {
  275. this.worker.postMessage({ type: "ws", url: this.url })
  276. }, 1000),
  277. workerResponseHandler({ data }) {
  278. if (data.url === this.url) this.isUrlValid = data.result
  279. },
  280. toggleConnection() {
  281. // If it is connecting:
  282. if (!this.connectionState) return this.connect()
  283. // Otherwise, it's disconnecting.
  284. else return this.disconnect()
  285. },
  286. connect() {
  287. this.log = [
  288. {
  289. payload: this.$t("state.connecting_to", { name: this.url }),
  290. source: "info",
  291. color: "var(--accent-color)",
  292. },
  293. ]
  294. try {
  295. this.connectingState = true
  296. this.socket = new WebSocket(this.url, this.activeProtocols)
  297. this.socket.onopen = () => {
  298. this.connectingState = false
  299. this.connectionState = true
  300. this.log = [
  301. {
  302. payload: this.$t("state.connected_to", { name: this.url }),
  303. source: "info",
  304. color: "var(--accent-color)",
  305. ts: new Date().toLocaleTimeString(),
  306. },
  307. ]
  308. this.$toast.success(this.$t("state.connected"))
  309. }
  310. this.socket.onerror = () => {
  311. this.handleError()
  312. }
  313. this.socket.onclose = () => {
  314. this.connectionState = false
  315. addWSLogLine({
  316. payload: this.$t("state.disconnected_from", { name: this.url }),
  317. source: "info",
  318. color: "#ff5555",
  319. ts: new Date().toLocaleTimeString(),
  320. })
  321. this.$toast.error(this.$t("state.disconnected"))
  322. }
  323. this.socket.onmessage = ({ data }) => {
  324. addWSLogLine({
  325. payload: data,
  326. source: "server",
  327. ts: new Date().toLocaleTimeString(),
  328. })
  329. }
  330. } catch (e) {
  331. this.handleError(e)
  332. this.$toast.error(this.$t("error.something_went_wrong"))
  333. }
  334. logHoppRequestRunToAnalytics({
  335. platform: "wss",
  336. })
  337. },
  338. disconnect() {
  339. if (this.socket) {
  340. this.socket.close()
  341. this.connectionState = false
  342. this.connectingState = false
  343. }
  344. },
  345. handleError(error) {
  346. this.disconnect()
  347. this.connectionState = false
  348. addWSLogLine({
  349. payload: this.$t("error.something_went_wrong"),
  350. source: "info",
  351. color: "#ff5555",
  352. ts: new Date().toLocaleTimeString(),
  353. })
  354. if (error !== null)
  355. addWSLogLine({
  356. payload: error,
  357. source: "info",
  358. color: "#ff5555",
  359. ts: new Date().toLocaleTimeString(),
  360. })
  361. },
  362. sendMessage() {
  363. const message = this.communication.input
  364. this.socket.send(message)
  365. addWSLogLine({
  366. payload: message,
  367. source: "client",
  368. ts: new Date().toLocaleTimeString(),
  369. })
  370. this.communication.input = ""
  371. },
  372. walkHistory(direction) {
  373. const clientMessages = this.log.filter(
  374. ({ source }) => source === "client"
  375. )
  376. const length = clientMessages.length
  377. switch (direction) {
  378. case "up":
  379. if (length > 0 && this.currentIndex !== 0) {
  380. // does nothing if message log is empty or the currentIndex is 0 when up arrow is pressed
  381. if (this.currentIndex === -1) {
  382. this.currentIndex = length - 1
  383. this.communication.input =
  384. clientMessages[this.currentIndex].payload
  385. } else if (this.currentIndex === 0) {
  386. this.communication.input = clientMessages[0].payload
  387. } else if (this.currentIndex > 0) {
  388. this.currentIndex = this.currentIndex - 1
  389. this.communication.input =
  390. clientMessages[this.currentIndex].payload
  391. }
  392. }
  393. break
  394. case "down":
  395. if (length > 0 && this.currentIndex > -1) {
  396. if (this.currentIndex === length - 1) {
  397. this.currentIndex = -1
  398. this.communication.input = ""
  399. } else if (this.currentIndex < length - 1) {
  400. this.currentIndex = this.currentIndex + 1
  401. this.communication.input =
  402. clientMessages[this.currentIndex].payload
  403. }
  404. }
  405. break
  406. }
  407. },
  408. addProtocol() {
  409. addWSProtocol({ value: "", active: true })
  410. },
  411. deleteProtocol({ index }) {
  412. const oldProtocols = this.protocols.slice()
  413. deleteWSProtocol(index)
  414. this.$toast.success(this.$t("state.deleted"), {
  415. action: {
  416. text: this.$t("action.undo"),
  417. duration: 4000,
  418. onClick: (_, toastObject) => {
  419. this.protocols = oldProtocols
  420. toastObject.remove()
  421. },
  422. },
  423. })
  424. },
  425. updateProtocol(index, updated) {
  426. updateWSProtocol(index, updated)
  427. },
  428. },
  429. })
  430. </script>