Websocket.vue 14 KB

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