Sidebar.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. <template>
  2. <SmartTabs
  3. v-model="selectedNavigationTab"
  4. styles="sticky bg-primary z-10 top-0"
  5. vertical
  6. render-inactive-tabs
  7. >
  8. <SmartTab :id="'history'" icon="clock" :label="`${t('tab.history')}`">
  9. <History
  10. ref="graphqlHistoryComponent"
  11. :page="'graphql'"
  12. @useHistory="handleUseHistory"
  13. />
  14. </SmartTab>
  15. <SmartTab
  16. :id="'collections'"
  17. icon="folder"
  18. :label="`${t('tab.collections')}`"
  19. >
  20. <CollectionsGraphql />
  21. </SmartTab>
  22. <SmartTab
  23. :id="'docs'"
  24. icon="book-open"
  25. :label="`${t('tab.documentation')}`"
  26. >
  27. <div
  28. v-if="
  29. queryFields.length === 0 &&
  30. mutationFields.length === 0 &&
  31. subscriptionFields.length === 0 &&
  32. graphqlTypes.length === 0
  33. "
  34. class="flex flex-col items-center justify-center p-4 text-secondaryLight"
  35. >
  36. <img
  37. :src="`/images/states/${$colorMode.value}/add_comment.svg`"
  38. loading="lazy"
  39. class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
  40. :alt="`${t('empty.documentation')}`"
  41. />
  42. <span class="mb-4 text-center">
  43. {{ t("empty.documentation") }}
  44. </span>
  45. </div>
  46. <div v-else>
  47. <div class="sticky top-0 z-10 flex bg-primary">
  48. <input
  49. v-model="graphqlFieldsFilterText"
  50. type="search"
  51. autocomplete="off"
  52. :placeholder="`${t('action.search')}`"
  53. class="flex flex-1 p-4 py-2 bg-transparent"
  54. />
  55. <div class="flex">
  56. <ButtonSecondary
  57. v-tippy="{ theme: 'tooltip' }"
  58. to="https://docs.hoppscotch.io/quickstart/graphql"
  59. blank
  60. :title="t('app.wiki')"
  61. svg="help-circle"
  62. />
  63. </div>
  64. </div>
  65. <SmartTabs
  66. v-model="selectedGqlTab"
  67. styles="border-t border-b border-dividerLight bg-primary sticky z-10 top-sidebarPrimaryStickyFold"
  68. render-inactive-tabs
  69. >
  70. <SmartTab
  71. v-if="queryFields.length > 0"
  72. :id="'queries'"
  73. :label="`${t('tab.queries')}`"
  74. class="divide-y divide-dividerLight"
  75. >
  76. <GraphqlField
  77. v-for="(field, index) in filteredQueryFields"
  78. :key="`field-${index}`"
  79. :gql-field="field"
  80. :jump-type-callback="handleJumpToType"
  81. class="p-4"
  82. />
  83. </SmartTab>
  84. <SmartTab
  85. v-if="mutationFields.length > 0"
  86. :id="'mutations'"
  87. :label="`${t('graphql.mutations')}`"
  88. class="divide-y divide-dividerLight"
  89. >
  90. <GraphqlField
  91. v-for="(field, index) in filteredMutationFields"
  92. :key="`field-${index}`"
  93. :gql-field="field"
  94. :jump-type-callback="handleJumpToType"
  95. class="p-4"
  96. />
  97. </SmartTab>
  98. <SmartTab
  99. v-if="subscriptionFields.length > 0"
  100. :id="'subscriptions'"
  101. :label="`${t('graphql.subscriptions')}`"
  102. class="divide-y divide-dividerLight"
  103. >
  104. <GraphqlField
  105. v-for="(field, index) in filteredSubscriptionFields"
  106. :key="`field-${index}`"
  107. :gql-field="field"
  108. :jump-type-callback="handleJumpToType"
  109. class="p-4"
  110. />
  111. </SmartTab>
  112. <SmartTab
  113. v-if="graphqlTypes.length > 0"
  114. :id="'types'"
  115. :label="`${t('tab.types')}`"
  116. class="divide-y divide-dividerLight"
  117. >
  118. <GraphqlType
  119. v-for="(type, index) in filteredGraphqlTypes"
  120. :key="`type-${index}`"
  121. :gql-type="type"
  122. :gql-types="graphqlTypes"
  123. :is-highlighted="isGqlTypeHighlighted(type)"
  124. :highlighted-fields="getGqlTypeHighlightedFields(type)"
  125. :jump-type-callback="handleJumpToType"
  126. />
  127. </SmartTab>
  128. </SmartTabs>
  129. </div>
  130. </SmartTab>
  131. <SmartTab :id="'schema'" icon="box" :label="`${t('tab.schema')}`">
  132. <div
  133. v-if="schemaString"
  134. class="sticky top-0 z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight"
  135. >
  136. <label class="font-semibold text-secondaryLight">
  137. {{ t("graphql.schema") }}
  138. </label>
  139. <div class="flex">
  140. <ButtonSecondary
  141. v-tippy="{ theme: 'tooltip' }"
  142. to="https://docs.hoppscotch.io/quickstart/graphql"
  143. blank
  144. :title="t('app.wiki')"
  145. svg="help-circle"
  146. />
  147. <ButtonSecondary
  148. v-tippy="{ theme: 'tooltip' }"
  149. :title="t('state.linewrap')"
  150. :class="{ '!text-accent': linewrapEnabled }"
  151. svg="wrap-text"
  152. @click.native.prevent="linewrapEnabled = !linewrapEnabled"
  153. />
  154. <ButtonSecondary
  155. ref="downloadSchema"
  156. v-tippy="{ theme: 'tooltip' }"
  157. :title="t('action.download_file')"
  158. :svg="downloadSchemaIcon"
  159. @click.native="downloadSchema"
  160. />
  161. <ButtonSecondary
  162. ref="copySchemaCode"
  163. v-tippy="{ theme: 'tooltip' }"
  164. :title="t('action.copy')"
  165. :svg="copySchemaIcon"
  166. @click.native="copySchema"
  167. />
  168. </div>
  169. </div>
  170. <div
  171. v-if="schemaString"
  172. ref="schemaEditor"
  173. class="flex flex-col flex-1"
  174. ></div>
  175. <div
  176. v-else
  177. class="flex flex-col items-center justify-center p-4 text-secondaryLight"
  178. >
  179. <img
  180. :src="`/images/states/${$colorMode.value}/blockchain.svg`"
  181. loading="lazy"
  182. class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
  183. :alt="`${t('empty.schema')}`"
  184. />
  185. <span class="mb-4 text-center">
  186. {{ t("empty.schema") }}
  187. </span>
  188. </div>
  189. </SmartTab>
  190. </SmartTabs>
  191. </template>
  192. <script setup lang="ts">
  193. import { computed, nextTick, reactive, ref } from "@nuxtjs/composition-api"
  194. import { GraphQLField, GraphQLType } from "graphql"
  195. import { map } from "rxjs/operators"
  196. import { GQLHeader } from "@hoppscotch/data"
  197. import { refAutoReset } from "@vueuse/core"
  198. import { useCodemirror } from "~/helpers/editor/codemirror"
  199. import { GQLConnection } from "~/helpers/GQLConnection"
  200. import { copyToClipboard } from "~/helpers/utils/clipboard"
  201. import {
  202. useReadonlyStream,
  203. useI18n,
  204. useToast,
  205. } from "~/helpers/utils/composables"
  206. import {
  207. setGQLAuth,
  208. setGQLHeaders,
  209. setGQLQuery,
  210. setGQLResponse,
  211. setGQLURL,
  212. setGQLVariables,
  213. } from "~/newstore/GQLSession"
  214. type NavigationTabs = "history" | "collection" | "docs" | "schema"
  215. type GqlTabs = "queries" | "mutations" | "subscriptions" | "types"
  216. const selectedNavigationTab = ref<NavigationTabs>("history")
  217. const selectedGqlTab = ref<GqlTabs>("queries")
  218. const t = useI18n()
  219. function isTextFoundInGraphqlFieldObject(
  220. text: string,
  221. field: GraphQLField<any, any>
  222. ) {
  223. const normalizedText = text.toLowerCase()
  224. const isFilterTextFoundInDescription = field.description
  225. ? field.description.toLowerCase().includes(normalizedText)
  226. : false
  227. const isFilterTextFoundInName = field.name
  228. .toLowerCase()
  229. .includes(normalizedText)
  230. return isFilterTextFoundInDescription || isFilterTextFoundInName
  231. }
  232. function getFilteredGraphqlFields(
  233. filterText: string,
  234. fields: GraphQLField<any, any>[]
  235. ) {
  236. if (!filterText) return fields
  237. return fields.filter((field) =>
  238. isTextFoundInGraphqlFieldObject(filterText, field)
  239. )
  240. }
  241. function getFilteredGraphqlTypes(filterText: string, types: GraphQLType[]) {
  242. if (!filterText) return types
  243. return types.filter((type) => {
  244. const isFilterTextMatching = isTextFoundInGraphqlFieldObject(
  245. filterText,
  246. type as any
  247. )
  248. if (isFilterTextMatching) {
  249. return true
  250. }
  251. const isFilterTextMatchingAtLeastOneField = Object.values(
  252. (type as any)._fields || {}
  253. ).some((field) => isTextFoundInGraphqlFieldObject(filterText, field as any))
  254. return isFilterTextMatchingAtLeastOneField
  255. })
  256. }
  257. function resolveRootType(type: GraphQLType) {
  258. let t: any = type
  259. while (t.ofType) t = t.ofType
  260. return t
  261. }
  262. type GQLHistoryEntry = {
  263. url: string
  264. headers: GQLHeader[]
  265. query: string
  266. response: string
  267. variables: string
  268. }
  269. const props = defineProps<{
  270. conn: GQLConnection
  271. }>()
  272. const toast = useToast()
  273. const queryFields = useReadonlyStream(
  274. props.conn.queryFields$.pipe(map((x) => x ?? [])),
  275. []
  276. )
  277. const mutationFields = useReadonlyStream(
  278. props.conn.mutationFields$.pipe(map((x) => x ?? [])),
  279. []
  280. )
  281. const subscriptionFields = useReadonlyStream(
  282. props.conn.subscriptionFields$.pipe(map((x) => x ?? [])),
  283. []
  284. )
  285. const graphqlTypes = useReadonlyStream(
  286. props.conn.graphqlTypes$.pipe(map((x) => x ?? [])),
  287. []
  288. )
  289. const downloadSchemaIcon = refAutoReset<"download" | "check">("download", 1000)
  290. const copySchemaIcon = refAutoReset<"copy" | "check">("copy", 1000)
  291. const graphqlFieldsFilterText = ref("")
  292. const filteredQueryFields = computed(() => {
  293. return getFilteredGraphqlFields(
  294. graphqlFieldsFilterText.value,
  295. queryFields.value as any
  296. )
  297. })
  298. const filteredMutationFields = computed(() => {
  299. return getFilteredGraphqlFields(
  300. graphqlFieldsFilterText.value,
  301. mutationFields.value as any
  302. )
  303. })
  304. const filteredSubscriptionFields = computed(() => {
  305. return getFilteredGraphqlFields(
  306. graphqlFieldsFilterText.value,
  307. subscriptionFields.value as any
  308. )
  309. })
  310. const filteredGraphqlTypes = computed(() => {
  311. return getFilteredGraphqlTypes(
  312. graphqlFieldsFilterText.value,
  313. graphqlTypes.value as any
  314. )
  315. })
  316. const isGqlTypeHighlighted = (gqlType: GraphQLType) => {
  317. if (!graphqlFieldsFilterText.value) return false
  318. return isTextFoundInGraphqlFieldObject(
  319. graphqlFieldsFilterText.value,
  320. gqlType as any
  321. )
  322. }
  323. const getGqlTypeHighlightedFields = (gqlType: GraphQLType) => {
  324. if (!graphqlFieldsFilterText.value) return []
  325. const fields = Object.values((gqlType as any)._fields || {})
  326. if (!fields || fields.length === 0) return []
  327. return fields.filter((field) =>
  328. isTextFoundInGraphqlFieldObject(graphqlFieldsFilterText.value, field as any)
  329. )
  330. }
  331. const handleJumpToType = async (type: GraphQLType) => {
  332. selectedGqlTab.value = "types"
  333. await nextTick()
  334. const rootTypeName = resolveRootType(type).name
  335. const target = document.getElementById(`type_${rootTypeName}`)
  336. if (target) {
  337. target.scrollIntoView({ block: "center", behavior: "smooth" })
  338. target.classList.add(
  339. "transition-all",
  340. "ring-inset",
  341. "ring-accentLight",
  342. "ring-4"
  343. )
  344. setTimeout(
  345. () =>
  346. target.classList.remove(
  347. "ring-inset",
  348. "ring-accentLight",
  349. "ring-4",
  350. "transition-all"
  351. ),
  352. 2000
  353. )
  354. }
  355. }
  356. const schemaString = useReadonlyStream(
  357. props.conn.schemaString$.pipe(map((x) => x ?? "")),
  358. ""
  359. )
  360. const schemaEditor = ref<any | null>(null)
  361. const linewrapEnabled = ref(true)
  362. useCodemirror(
  363. schemaEditor,
  364. schemaString,
  365. reactive({
  366. extendedEditorConfig: {
  367. mode: "graphql",
  368. readOnly: true,
  369. lineWrapping: linewrapEnabled,
  370. },
  371. linter: null,
  372. completer: null,
  373. environmentHighlights: false,
  374. })
  375. )
  376. const downloadSchema = () => {
  377. const dataToWrite = JSON.stringify(schemaString.value, null, 2)
  378. const file = new Blob([dataToWrite], { type: "application/graphql" })
  379. const a = document.createElement("a")
  380. const url = URL.createObjectURL(file)
  381. a.href = url
  382. a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.graphql`
  383. document.body.appendChild(a)
  384. a.click()
  385. downloadSchemaIcon.value = "check"
  386. toast.success(`${t("state.download_started")}`)
  387. setTimeout(() => {
  388. document.body.removeChild(a)
  389. URL.revokeObjectURL(url)
  390. }, 1000)
  391. }
  392. const copySchema = () => {
  393. if (!schemaString.value) return
  394. copyToClipboard(schemaString.value)
  395. copySchemaIcon.value = "check"
  396. }
  397. const handleUseHistory = (entry: GQLHistoryEntry) => {
  398. const url = entry.url
  399. const headers = entry.headers
  400. const gqlQueryString = entry.query
  401. const variableString = entry.variables
  402. const responseText = entry.response
  403. setGQLURL(url)
  404. setGQLHeaders(headers)
  405. setGQLQuery(gqlQueryString)
  406. setGQLVariables(variableString)
  407. setGQLResponse(responseText)
  408. setGQLAuth({
  409. authType: "none",
  410. authActive: true,
  411. })
  412. props.conn.reset()
  413. }
  414. </script>