Websocket.vue 14 KB

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