Websocket.vue 13 KB

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