Sidebar.vue 12 KB

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