Sidebar.vue 13 KB

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