Sidebar.vue 13 KB

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