Sidebar.vue 13 KB

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