Websocket.vue 13 KB

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