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