feat: graphql rewrite to new state system
This commit is contained in:
486
components/graphql/ContentArea.vue
Normal file
486
components/graphql/ContentArea.vue
Normal file
@@ -0,0 +1,486 @@
|
||||
<template>
|
||||
<div>
|
||||
<SmartTabs styles="sticky top-upperPrimaryStickyFold z-10">
|
||||
<SmartTab :id="'query'" :label="$t('query')" :selected="true">
|
||||
<AppSection label="query">
|
||||
<div
|
||||
class="
|
||||
bg-primary
|
||||
border-b border-dividerLight
|
||||
flex flex-1
|
||||
top-upperSecondaryStickyFold
|
||||
pl-4
|
||||
z-10
|
||||
sticky
|
||||
items-center
|
||||
justify-between
|
||||
gqlRunQuery
|
||||
"
|
||||
>
|
||||
<label for="gqlQuery" class="font-semibold">
|
||||
{{ $t("query") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
:label="$t('run')"
|
||||
:shortcut="[getSpecialKey(), 'Enter']"
|
||||
icon="play_arrow"
|
||||
class="!text-accent"
|
||||
@click.native="runQuery()"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.copy')"
|
||||
:icon="copyQueryIcon"
|
||||
@click.native="copyQuery"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="`${$t(
|
||||
'prettify_query'
|
||||
)} <kbd>${getSpecialKey()}</kbd><kbd>P</kbd>`"
|
||||
:icon="prettifyQueryIcon"
|
||||
@click.native="prettifyQuery"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
ref="saveRequest"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('request.save')"
|
||||
icon="create_new_folder"
|
||||
@click.native="saveRequest"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<GraphqlQueryEditor
|
||||
ref="queryEditor"
|
||||
v-model="gqlQueryString"
|
||||
:on-run-g-q-l-query="runQuery"
|
||||
:options="{
|
||||
maxLines: Infinity,
|
||||
minLines: 16,
|
||||
fontSize: '12px',
|
||||
autoScrollEditorIntoView: true,
|
||||
showPrintMargin: false,
|
||||
useWorker: false,
|
||||
}"
|
||||
@update-query="updateQuery"
|
||||
/>
|
||||
</AppSection>
|
||||
</SmartTab>
|
||||
|
||||
<SmartTab :id="'variables'" :label="$t('variables')">
|
||||
<AppSection label="variables">
|
||||
<div
|
||||
class="
|
||||
bg-primary
|
||||
border-b border-dividerLight
|
||||
flex flex-1
|
||||
top-upperSecondaryStickyFold
|
||||
pl-4
|
||||
z-10
|
||||
sticky
|
||||
items-center
|
||||
justify-between
|
||||
"
|
||||
>
|
||||
<label class="font-semibold">
|
||||
{{ $t("variables") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.copy')"
|
||||
:icon="copyVariablesIcon"
|
||||
@click.native="copyVariables"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SmartAceEditor
|
||||
ref="variableEditor"
|
||||
v-model="variableString"
|
||||
:lang="'json'"
|
||||
:options="{
|
||||
maxLines: Infinity,
|
||||
minLines: 16,
|
||||
fontSize: '12px',
|
||||
autoScrollEditorIntoView: true,
|
||||
showPrintMargin: false,
|
||||
useWorker: false,
|
||||
}"
|
||||
/>
|
||||
</AppSection>
|
||||
</SmartTab>
|
||||
|
||||
<SmartTab :id="'headers'" :label="$t('headers')">
|
||||
<AppSection label="headers">
|
||||
<div
|
||||
class="
|
||||
bg-primary
|
||||
border-b border-dividerLight
|
||||
flex flex-1
|
||||
top-upperSecondaryStickyFold
|
||||
pl-4
|
||||
z-10
|
||||
sticky
|
||||
items-center
|
||||
justify-between
|
||||
"
|
||||
>
|
||||
<label class="font-semibold">
|
||||
{{ $t("headers") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('clear')"
|
||||
icon="clear_all"
|
||||
@click.native="headers = []"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('add.new')"
|
||||
icon="add"
|
||||
@click.native="addRequestHeader"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="(header, index) in headers"
|
||||
:key="`header-${index}`"
|
||||
class="
|
||||
divide-x divide-dividerLight
|
||||
border-b border-dividerLight
|
||||
flex
|
||||
"
|
||||
:class="{ 'border-t': index == 0 }"
|
||||
>
|
||||
<SmartAutoComplete
|
||||
:placeholder="$t('count.header', { count: index + 1 })"
|
||||
:source="commonHeaders"
|
||||
:spellcheck="false"
|
||||
:value="header.key"
|
||||
autofocus
|
||||
styles="
|
||||
bg-primaryLight
|
||||
flex
|
||||
font-semibold font-mono
|
||||
flex-1
|
||||
py-2
|
||||
px-4
|
||||
focus:outline-none
|
||||
"
|
||||
@input="
|
||||
updateGQLHeader(index, {
|
||||
key: $event,
|
||||
value: header.value,
|
||||
active: header.active,
|
||||
})
|
||||
"
|
||||
/>
|
||||
<input
|
||||
class="
|
||||
bg-primaryLight
|
||||
flex
|
||||
font-semibold font-mono
|
||||
w-full
|
||||
py-2
|
||||
px-4
|
||||
focus:outline-none
|
||||
"
|
||||
:placeholder="$t('count.value', { count: index + 1 })"
|
||||
:name="`value ${index}`"
|
||||
:value="header.value"
|
||||
autofocus
|
||||
@change="
|
||||
updateGQLHeader(index, {
|
||||
key: header.key,
|
||||
value: $event.target.value,
|
||||
active: header.active,
|
||||
})
|
||||
"
|
||||
/>
|
||||
<span>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="
|
||||
header.hasOwnProperty('active')
|
||||
? header.active
|
||||
? $t('action.turn_off')
|
||||
: $t('action.turn_on')
|
||||
: $t('action.turn_off')
|
||||
"
|
||||
:icon="
|
||||
header.hasOwnProperty('active')
|
||||
? header.active
|
||||
? 'check_box'
|
||||
: 'check_box_outline_blank'
|
||||
: 'check_box'
|
||||
"
|
||||
color="green"
|
||||
@click.native="
|
||||
updateGQLHeader(index, {
|
||||
key: header.key,
|
||||
value: header.value,
|
||||
active: !header.active,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('delete')"
|
||||
icon="delete"
|
||||
color="red"
|
||||
@click.native="removeRequestHeader(index)"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="headers.length === 0"
|
||||
class="
|
||||
flex flex-col
|
||||
text-secondaryLight
|
||||
p-4
|
||||
items-center
|
||||
justify-center
|
||||
"
|
||||
>
|
||||
<i class="opacity-75 pb-2 material-icons">post_add</i>
|
||||
<span class="text-center pb-4">
|
||||
{{ $t("empty.headers") }}
|
||||
</span>
|
||||
<ButtonSecondary
|
||||
:label="$t('add.new')"
|
||||
outline
|
||||
@click.native="addRequestHeader"
|
||||
/>
|
||||
</div>
|
||||
</AppSection>
|
||||
</SmartTab>
|
||||
</SmartTabs>
|
||||
|
||||
<CollectionsSaveRequest
|
||||
mode="graphql"
|
||||
:show="showSaveRequestModal"
|
||||
:editing-request="editRequest"
|
||||
@hide-modal="hideRequestModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
defineComponent,
|
||||
PropType,
|
||||
ref,
|
||||
useContext,
|
||||
watch,
|
||||
} from "@nuxtjs/composition-api"
|
||||
import clone from "lodash/clone"
|
||||
import { getPlatformSpecialKey } from "~/helpers/platformutils"
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
import {
|
||||
useNuxt,
|
||||
useReadonlyStream,
|
||||
useStream,
|
||||
} from "~/helpers/utils/composables"
|
||||
import {
|
||||
addGQLHeader,
|
||||
gqlHeaders$,
|
||||
gqlQuery$,
|
||||
gqlResponse$,
|
||||
gqlURL$,
|
||||
gqlVariables$,
|
||||
removeGQLHeader,
|
||||
setGQLHeaders,
|
||||
setGQLQuery,
|
||||
setGQLResponse,
|
||||
setGQLVariables,
|
||||
updateGQLHeader,
|
||||
} from "~/newstore/GQLSession"
|
||||
import { commonHeaders } from "~/helpers/headers"
|
||||
import { GQLConnection } from "~/helpers/GQLConnection"
|
||||
import { addGraphqlHistoryEntry } from "~/newstore/history"
|
||||
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
|
||||
import { getCurrentStrategyID } from "~/helpers/network"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
conn: {
|
||||
type: Object as PropType<GQLConnection>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const {
|
||||
$toast,
|
||||
app: { i18n },
|
||||
} = useContext()
|
||||
const t = i18n.t.bind(i18n)
|
||||
const nuxt = useNuxt()
|
||||
|
||||
const url = useReadonlyStream(gqlURL$, "")
|
||||
const gqlQueryString = useStream(gqlQuery$, "", setGQLQuery)
|
||||
const variableString = useStream(gqlVariables$, "", setGQLVariables)
|
||||
const headers = useStream(gqlHeaders$, [], setGQLHeaders)
|
||||
|
||||
const queryEditor = ref<any | null>(null)
|
||||
|
||||
const copyQueryIcon = ref("content_copy")
|
||||
const prettifyQueryIcon = ref("photo_filter")
|
||||
const copyVariablesIcon = ref("content_copy")
|
||||
|
||||
const editRequest = ref({})
|
||||
const showSaveRequestModal = ref(false)
|
||||
|
||||
const schema = useReadonlyStream(props.conn.schemaString$, "")
|
||||
watch(schema, () => {
|
||||
console.log(schema.value)
|
||||
})
|
||||
|
||||
const copyQuery = () => {
|
||||
copyToClipboard(gqlQueryString.value)
|
||||
copyQueryIcon.value = "done"
|
||||
setTimeout(() => (copyQueryIcon.value = "content_copy"), 1000)
|
||||
}
|
||||
|
||||
const response = useStream(gqlResponse$, "", setGQLResponse)
|
||||
|
||||
const runQuery = async () => {
|
||||
const startTime = Date.now()
|
||||
|
||||
nuxt.value.$loading.start()
|
||||
response.value = t("loading").toString()
|
||||
|
||||
try {
|
||||
const runURL = clone(url.value)
|
||||
const runHeaders = clone(headers.value)
|
||||
const runQuery = clone(gqlQueryString.value)
|
||||
const runVariables = clone(variableString.value)
|
||||
|
||||
const responseText = await props.conn.runQuery(
|
||||
runURL,
|
||||
runHeaders,
|
||||
runQuery,
|
||||
runVariables
|
||||
)
|
||||
const duration = Date.now() - startTime
|
||||
|
||||
nuxt.value.$loading.finish()
|
||||
|
||||
response.value = JSON.stringify(JSON.parse(responseText), null, 2)
|
||||
|
||||
const historyEntry = {
|
||||
url: runURL,
|
||||
query: runQuery,
|
||||
variables: runVariables,
|
||||
star: false,
|
||||
headers: runHeaders,
|
||||
response: response.value,
|
||||
date: new Date().toLocaleDateString(),
|
||||
time: new Date().toLocaleTimeString(),
|
||||
updatedOn: new Date(),
|
||||
duration,
|
||||
}
|
||||
|
||||
addGraphqlHistoryEntry(historyEntry)
|
||||
|
||||
$toast.success(t("finished_in", { duration }).toString(), {
|
||||
icon: "done",
|
||||
})
|
||||
} catch (error: any) {
|
||||
response.value = `${error}. ${t("check_console_details")}`
|
||||
nuxt.value.$loading.finish()
|
||||
|
||||
$toast.error(`${error} ${t("f12_details").toString()}`, {
|
||||
icon: "error",
|
||||
})
|
||||
console.log("Error", error)
|
||||
}
|
||||
|
||||
logHoppRequestRunToAnalytics({
|
||||
platform: "graphql-query",
|
||||
strategy: getCurrentStrategyID()
|
||||
})
|
||||
}
|
||||
|
||||
const hideRequestModal = () => {
|
||||
editRequest.value = {}
|
||||
showSaveRequestModal.value = false
|
||||
}
|
||||
|
||||
const prettifyQuery = () => {
|
||||
queryEditor.value.prettifyQuery()
|
||||
prettifyQueryIcon.value = "done"
|
||||
setTimeout(() => (prettifyQueryIcon.value = "photo_filter"), 1000)
|
||||
}
|
||||
|
||||
const saveRequest = () => {
|
||||
// TODO: Make the modal get the data from the session state
|
||||
editRequest.value = {
|
||||
url: url.value,
|
||||
query: gqlQueryString.value,
|
||||
headers: headers.value,
|
||||
variables: variableString.value,
|
||||
}
|
||||
|
||||
showSaveRequestModal.value = true
|
||||
}
|
||||
|
||||
// Why ?
|
||||
const updateQuery = (updatedQuery: string) => {
|
||||
gqlQueryString.value = updatedQuery
|
||||
}
|
||||
|
||||
const copyVariables = () => {
|
||||
copyToClipboard(variableString.value)
|
||||
copyVariablesIcon.value = "done"
|
||||
setTimeout(() => (copyVariablesIcon.value = "content_copy"), 1000)
|
||||
}
|
||||
|
||||
const addRequestHeader = () => {
|
||||
addGQLHeader({
|
||||
key: "",
|
||||
value: "",
|
||||
active: true,
|
||||
})
|
||||
}
|
||||
|
||||
const removeRequestHeader = (index: number) => {
|
||||
removeGQLHeader(index)
|
||||
}
|
||||
|
||||
return {
|
||||
gqlQueryString,
|
||||
variableString,
|
||||
headers,
|
||||
copyQueryIcon,
|
||||
prettifyQueryIcon,
|
||||
copyVariablesIcon,
|
||||
|
||||
queryEditor,
|
||||
|
||||
editRequest,
|
||||
showSaveRequestModal,
|
||||
hideRequestModal,
|
||||
|
||||
schema,
|
||||
|
||||
copyQuery,
|
||||
runQuery,
|
||||
prettifyQuery,
|
||||
saveRequest,
|
||||
updateQuery,
|
||||
copyVariables,
|
||||
addRequestHeader,
|
||||
removeRequestHeader,
|
||||
|
||||
getSpecialKey: getPlatformSpecialKey,
|
||||
|
||||
commonHeaders,
|
||||
updateGQLHeader,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
186
components/graphql/ResponseSection.vue
Normal file
186
components/graphql/ResponseSection.vue
Normal file
@@ -0,0 +1,186 @@
|
||||
<template>
|
||||
<AppSection ref="response" label="response">
|
||||
<div
|
||||
v-if="responseString"
|
||||
class="
|
||||
bg-primary
|
||||
border-b border-dividerLight
|
||||
flex flex-1
|
||||
pl-4
|
||||
top-0
|
||||
z-10
|
||||
sticky
|
||||
items-center
|
||||
justify-between
|
||||
"
|
||||
>
|
||||
<label class="font-semibold" for="responseField">
|
||||
{{ $t("response") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
ref="downloadResponse"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('download_file')"
|
||||
:icon="downloadResponseIcon"
|
||||
@click.native="downloadResponse"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
ref="copyResponseButton"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.copy')"
|
||||
:icon="copyResponseIcon"
|
||||
@click.native="copyResponse"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SmartAceEditor
|
||||
v-if="responseString"
|
||||
:value="responseString"
|
||||
:lang="'json'"
|
||||
:lint="false"
|
||||
:options="{
|
||||
maxLines: Infinity,
|
||||
minLines: 16,
|
||||
fontSize: '12px',
|
||||
autoScrollEditorIntoView: true,
|
||||
readOnly: true,
|
||||
showPrintMargin: false,
|
||||
useWorker: false,
|
||||
}"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="
|
||||
flex flex-col flex-1
|
||||
text-secondaryLight
|
||||
p-4
|
||||
items-center
|
||||
justify-center
|
||||
"
|
||||
>
|
||||
<div class="flex space-x-2 pb-8">
|
||||
<div class="flex flex-col space-y-4 items-end">
|
||||
<span class="flex flex-1 items-center">
|
||||
{{ $t("shortcut.send_request") }}
|
||||
</span>
|
||||
<span class="flex flex-1 items-center">
|
||||
{{ $t("shortcut.general.show_all") }}
|
||||
</span>
|
||||
<span class="flex flex-1 items-center">
|
||||
{{ $t("shortcut.general.command_menu") }}
|
||||
</span>
|
||||
<span class="flex flex-1 items-center">
|
||||
{{ $t("shortcut.general.help_menu") }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-4">
|
||||
<div class="flex">
|
||||
<span class="shortcut-key">{{ getSpecialKey() }}</span>
|
||||
<span class="shortcut-key">G</span>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<span class="shortcut-key">{{ getSpecialKey() }}</span>
|
||||
<span class="shortcut-key">K</span>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<span class="shortcut-key">/</span>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<span class="shortcut-key">?</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ButtonSecondary
|
||||
:label="$t('documentation')"
|
||||
to="https://docs.hoppscotch.io"
|
||||
blank
|
||||
outline
|
||||
/>
|
||||
</div>
|
||||
</AppSection>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
defineComponent,
|
||||
PropType,
|
||||
ref,
|
||||
useContext,
|
||||
} from "@nuxtjs/composition-api"
|
||||
import { GQLConnection } from "~/helpers/GQLConnection"
|
||||
import { getPlatformSpecialKey } from "~/helpers/platformutils"
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
import { useReadonlyStream } from "~/helpers/utils/composables"
|
||||
import { gqlResponse$ } from "~/newstore/GQLSession"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
conn: {
|
||||
type: Object as PropType<GQLConnection>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const {
|
||||
$toast,
|
||||
app: { i18n },
|
||||
} = useContext()
|
||||
const t = i18n.t.bind(i18n)
|
||||
|
||||
const responseString = useReadonlyStream(gqlResponse$, "")
|
||||
|
||||
const downloadResponseIcon = ref("save_alt")
|
||||
const copyResponseIcon = ref("content_copy")
|
||||
|
||||
const copyResponse = () => {
|
||||
copyToClipboard(responseString.value!)
|
||||
copyResponseIcon.value = "done"
|
||||
setTimeout(() => (copyResponseIcon.value = "content_copy"), 1000)
|
||||
}
|
||||
|
||||
const downloadResponse = () => {
|
||||
const dataToWrite = responseString.value
|
||||
const file = new Blob([dataToWrite!], { type: "application/json" })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
downloadResponseIcon.value = "done"
|
||||
$toast.success(t("download_started").toString(), {
|
||||
icon: "done",
|
||||
})
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
downloadResponseIcon.value = "save_alt"
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
return {
|
||||
responseString,
|
||||
|
||||
downloadResponseIcon,
|
||||
copyResponseIcon,
|
||||
|
||||
downloadResponse,
|
||||
copyResponse,
|
||||
|
||||
getSpecialKey: getPlatformSpecialKey,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.shortcut-key {
|
||||
@apply bg-dividerLight;
|
||||
@apply rounded;
|
||||
@apply ml-2;
|
||||
@apply py-1;
|
||||
@apply px-2;
|
||||
@apply inline-flex;
|
||||
}
|
||||
</style>
|
||||
475
components/graphql/Sidebar.vue
Normal file
475
components/graphql/Sidebar.vue
Normal file
@@ -0,0 +1,475 @@
|
||||
<template>
|
||||
<aside>
|
||||
<SmartTabs styles="sticky z-10 top-0">
|
||||
<SmartTab :id="'docs'" :label="`Docs`" :selected="true">
|
||||
<AppSection label="docs">
|
||||
<div
|
||||
class="
|
||||
bg-primaryLight
|
||||
flex flex-col
|
||||
top-sidebarPrimaryStickyFold
|
||||
z-10
|
||||
sticky
|
||||
"
|
||||
>
|
||||
<div class="search-wrapper">
|
||||
<input
|
||||
v-model="graphqlFieldsFilterText"
|
||||
type="search"
|
||||
:placeholder="$t('search')"
|
||||
class="
|
||||
bg-primaryLight
|
||||
flex
|
||||
font-semibold font-mono
|
||||
w-full
|
||||
py-2
|
||||
pr-2
|
||||
pl-9
|
||||
truncate
|
||||
focus:outline-none
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SmartTabs
|
||||
ref="gqlTabs"
|
||||
styles="
|
||||
border-t border-dividerLight sticky z-8 top-sidebarSecondaryStickyFold"
|
||||
>
|
||||
<div class="gqlTabs">
|
||||
<SmartTab
|
||||
v-if="queryFields.length > 0"
|
||||
:id="'queries'"
|
||||
:label="$t('queries')"
|
||||
:selected="true"
|
||||
class="divide-y divide-dividerLight"
|
||||
>
|
||||
<GraphqlField
|
||||
v-for="(field, index) in filteredQueryFields"
|
||||
:key="`field-${index}`"
|
||||
:gql-field="field"
|
||||
:jump-type-callback="handleJumpToType"
|
||||
class="p-4"
|
||||
/>
|
||||
</SmartTab>
|
||||
<SmartTab
|
||||
v-if="mutationFields.length > 0"
|
||||
:id="'mutations'"
|
||||
:label="$t('mutations')"
|
||||
class="divide-y divide-dividerLight"
|
||||
>
|
||||
<GraphqlField
|
||||
v-for="(field, index) in filteredMutationFields"
|
||||
:key="`field-${index}`"
|
||||
:gql-field="field"
|
||||
:jump-type-callback="handleJumpToType"
|
||||
class="p-4"
|
||||
/>
|
||||
</SmartTab>
|
||||
<SmartTab
|
||||
v-if="subscriptionFields.length > 0"
|
||||
:id="'subscriptions'"
|
||||
:label="$t('subscriptions')"
|
||||
class="divide-y divide-dividerLight"
|
||||
>
|
||||
<GraphqlField
|
||||
v-for="(field, index) in filteredSubscriptionFields"
|
||||
:key="`field-${index}`"
|
||||
:gql-field="field"
|
||||
:jump-type-callback="handleJumpToType"
|
||||
class="p-4"
|
||||
/>
|
||||
</SmartTab>
|
||||
<SmartTab
|
||||
v-if="graphqlTypes.length > 0"
|
||||
:id="'types'"
|
||||
ref="typesTab"
|
||||
:label="$t('types')"
|
||||
class="divide-y divide-dividerLight"
|
||||
>
|
||||
<GraphqlType
|
||||
v-for="(type, index) in filteredGraphqlTypes"
|
||||
:key="`type-${index}`"
|
||||
:gql-type="type"
|
||||
:gql-types="graphqlTypes"
|
||||
:is-highlighted="isGqlTypeHighlighted(type)"
|
||||
:highlighted-fields="getGqlTypeHighlightedFields(type)"
|
||||
:jump-type-callback="handleJumpToType"
|
||||
/>
|
||||
</SmartTab>
|
||||
</div>
|
||||
</SmartTabs>
|
||||
<div
|
||||
v-if="
|
||||
queryFields.length === 0 &&
|
||||
mutationFields.length === 0 &&
|
||||
subscriptionFields.length === 0 &&
|
||||
graphqlTypes.length === 0
|
||||
"
|
||||
class="
|
||||
flex flex-col
|
||||
text-secondaryLight
|
||||
p-4
|
||||
items-center
|
||||
justify-center
|
||||
"
|
||||
>
|
||||
<i class="opacity-75 pb-2 material-icons">link</i>
|
||||
<span class="text-center">
|
||||
{{ $t("empty.schema") }}
|
||||
</span>
|
||||
</div>
|
||||
</AppSection>
|
||||
</SmartTab>
|
||||
|
||||
<SmartTab :id="'history'" :label="$t('history')">
|
||||
<History
|
||||
ref="graphqlHistoryComponent"
|
||||
:page="'graphql'"
|
||||
@useHistory="handleUseHistory"
|
||||
/>
|
||||
</SmartTab>
|
||||
|
||||
<SmartTab :id="'collections'" :label="$t('collections')">
|
||||
<CollectionsGraphql />
|
||||
</SmartTab>
|
||||
|
||||
<SmartTab :id="'schema'" :label="`Schema`">
|
||||
<AppSection ref="schema" label="schema">
|
||||
<div
|
||||
v-if="schemaString"
|
||||
class="
|
||||
bg-primary
|
||||
border-b border-dividerLight
|
||||
flex flex-1
|
||||
top-sidebarPrimaryStickyFold
|
||||
pl-4
|
||||
z-10
|
||||
sticky
|
||||
items-center
|
||||
justify-between
|
||||
"
|
||||
>
|
||||
<label class="font-semibold">
|
||||
{{ $t("schema") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
ref="downloadSchema"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('download_file')"
|
||||
:icon="downloadSchemaIcon"
|
||||
@click.native="downloadSchema"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
ref="copySchemaCode"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.copy')"
|
||||
:icon="copySchemaIcon"
|
||||
@click.native="copySchema"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SmartAceEditor
|
||||
v-if="schemaString"
|
||||
v-model="schemaString"
|
||||
:lang="'graphqlschema'"
|
||||
:options="{
|
||||
maxLines: Infinity,
|
||||
minLines: 16,
|
||||
fontSize: '12px',
|
||||
autoScrollEditorIntoView: true,
|
||||
readOnly: true,
|
||||
showPrintMargin: false,
|
||||
useWorker: false,
|
||||
}"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="
|
||||
flex flex-col
|
||||
text-secondaryLight
|
||||
p-4
|
||||
items-center
|
||||
justify-center
|
||||
"
|
||||
>
|
||||
<i class="opacity-75 pb-2 material-icons">link</i>
|
||||
<span class="text-center">
|
||||
{{ $t("empty.schema") }}
|
||||
</span>
|
||||
</div>
|
||||
</AppSection>
|
||||
</SmartTab>
|
||||
</SmartTabs>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
computed,
|
||||
defineComponent,
|
||||
nextTick,
|
||||
PropType,
|
||||
ref,
|
||||
useContext,
|
||||
} from "@nuxtjs/composition-api"
|
||||
import { GraphQLField, GraphQLType } from 'graphql'
|
||||
import { map } from 'rxjs/operators'
|
||||
import { GQLConnection } from '~/helpers/GQLConnection'
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
import { useReadonlyStream } from '~/helpers/utils/composables'
|
||||
import {
|
||||
GQLHeader,
|
||||
setGQLHeaders,
|
||||
setGQLQuery,
|
||||
setGQLResponse,
|
||||
setGQLURL,
|
||||
setGQLVariables,
|
||||
} from "~/newstore/GQLSession"
|
||||
|
||||
function isTextFoundInGraphqlFieldObject(
|
||||
text: string,
|
||||
field: GraphQLField<any, any>
|
||||
) {
|
||||
const normalizedText = text.toLowerCase()
|
||||
|
||||
const isFilterTextFoundInDescription = field.description
|
||||
? field.description.toLowerCase().includes(normalizedText)
|
||||
: false
|
||||
const isFilterTextFoundInName = field.name
|
||||
.toLowerCase()
|
||||
.includes(normalizedText)
|
||||
|
||||
return isFilterTextFoundInDescription || isFilterTextFoundInName
|
||||
}
|
||||
|
||||
function getFilteredGraphqlFields(
|
||||
filterText: string,
|
||||
fields: GraphQLField<any, any>[]
|
||||
) {
|
||||
if (!filterText) return fields
|
||||
|
||||
return fields.filter((field) =>
|
||||
isTextFoundInGraphqlFieldObject(filterText, field)
|
||||
)
|
||||
}
|
||||
|
||||
function getFilteredGraphqlTypes(filterText: string, types: GraphQLType[]) {
|
||||
if (!filterText) return types
|
||||
|
||||
return types.filter((type) => {
|
||||
const isFilterTextMatching = isTextFoundInGraphqlFieldObject(
|
||||
filterText,
|
||||
type as any
|
||||
)
|
||||
|
||||
if (isFilterTextMatching) {
|
||||
return true
|
||||
}
|
||||
|
||||
const isFilterTextMatchingAtLeastOneField = Object.values(
|
||||
(type as any)._fields || {}
|
||||
).some((field) => isTextFoundInGraphqlFieldObject(filterText, field as any))
|
||||
|
||||
return isFilterTextMatchingAtLeastOneField
|
||||
})
|
||||
}
|
||||
|
||||
function resolveRootType(type: GraphQLType) {
|
||||
let t: any = type
|
||||
while (t.ofType) t = t.ofType
|
||||
return t
|
||||
}
|
||||
|
||||
type GQLHistoryEntry = {
|
||||
url: string
|
||||
headers: GQLHeader[]
|
||||
query: string
|
||||
response: string
|
||||
variables: string
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
conn: {
|
||||
type: Object as PropType<GQLConnection>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const {
|
||||
$toast,
|
||||
app: { i18n },
|
||||
} = useContext()
|
||||
const t = i18n.t.bind(i18n)
|
||||
|
||||
const queryFields = useReadonlyStream(
|
||||
props.conn.queryFields$.pipe(map((x) => x ?? [])),
|
||||
[]
|
||||
)
|
||||
const mutationFields = useReadonlyStream(
|
||||
props.conn.mutationFields$.pipe(map((x) => x ?? [])),
|
||||
[]
|
||||
)
|
||||
const subscriptionFields = useReadonlyStream(
|
||||
props.conn.subscriptionFields$.pipe(map((x) => x ?? [])),
|
||||
[]
|
||||
)
|
||||
const graphqlTypes = useReadonlyStream(
|
||||
props.conn.graphqlTypes$.pipe(map((x) => x ?? [])),
|
||||
[]
|
||||
)
|
||||
|
||||
const downloadSchemaIcon = ref("save_alt")
|
||||
const copySchemaIcon = ref("content_copy")
|
||||
|
||||
const graphqlFieldsFilterText = ref("")
|
||||
|
||||
const gqlTabs = ref<any | null>(null)
|
||||
const typesTab = ref<any | null>(null)
|
||||
|
||||
const filteredQueryFields = computed(() => {
|
||||
return getFilteredGraphqlFields(
|
||||
graphqlFieldsFilterText.value,
|
||||
queryFields.value as any
|
||||
)
|
||||
})
|
||||
|
||||
const filteredMutationFields = computed(() => {
|
||||
return getFilteredGraphqlFields(
|
||||
graphqlFieldsFilterText.value,
|
||||
mutationFields.value as any
|
||||
)
|
||||
})
|
||||
|
||||
const filteredSubscriptionFields = computed(() => {
|
||||
return getFilteredGraphqlFields(
|
||||
graphqlFieldsFilterText.value,
|
||||
subscriptionFields.value as any
|
||||
)
|
||||
})
|
||||
|
||||
const filteredGraphqlTypes = computed(() => {
|
||||
return getFilteredGraphqlTypes(
|
||||
graphqlFieldsFilterText.value,
|
||||
graphqlTypes.value as any
|
||||
)
|
||||
})
|
||||
|
||||
const isGqlTypeHighlighted = (gqlType: GraphQLType) => {
|
||||
if (!graphqlFieldsFilterText.value) return false
|
||||
|
||||
return isTextFoundInGraphqlFieldObject(
|
||||
graphqlFieldsFilterText.value,
|
||||
gqlType as any
|
||||
)
|
||||
}
|
||||
|
||||
const getGqlTypeHighlightedFields = (gqlType: GraphQLType) => {
|
||||
if (!graphqlFieldsFilterText.value) return []
|
||||
|
||||
const fields = Object.values((gqlType as any)._fields || {})
|
||||
if (!fields || fields.length === 0) return []
|
||||
|
||||
return fields.filter((field) =>
|
||||
isTextFoundInGraphqlFieldObject(
|
||||
graphqlFieldsFilterText.value,
|
||||
field as any
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const handleJumpToType = async (type: GraphQLType) => {
|
||||
gqlTabs.value.selectTab(typesTab.value)
|
||||
await nextTick()
|
||||
|
||||
const rootTypeName = resolveRootType(type).name
|
||||
|
||||
const target = document.getElementById(`type_${rootTypeName}`)
|
||||
if (target) {
|
||||
gqlTabs.value.$el
|
||||
.querySelector(".gqlTabs")
|
||||
.scrollTo({ top: target.offsetTop, behavior: "smooth" })
|
||||
}
|
||||
}
|
||||
const schemaString = useReadonlyStream(
|
||||
props.conn.schemaString$.pipe(map((x) => x ?? "")),
|
||||
""
|
||||
)
|
||||
|
||||
const downloadSchema = () => {
|
||||
const dataToWrite = JSON.stringify(schemaString.value, null, 2)
|
||||
const file = new Blob([dataToWrite], { type: "application/graphql" })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
a.download = `${
|
||||
url.split("/").pop()!.split("#")[0].split("?")[0]
|
||||
}.graphql`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
downloadSchemaIcon.value = "done"
|
||||
$toast.success(t("download_started").toString(), {
|
||||
icon: "done",
|
||||
})
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
downloadSchemaIcon.value = "save_alt"
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const copySchema = () => {
|
||||
if (!schemaString.value) return
|
||||
|
||||
copyToClipboard(schemaString.value)
|
||||
copySchemaIcon.value = "done"
|
||||
setTimeout(() => (copySchemaIcon.value = "content_copy"), 1000)
|
||||
}
|
||||
|
||||
const handleUseHistory = (entry: GQLHistoryEntry) => {
|
||||
const url = entry.url
|
||||
const headers = entry.headers
|
||||
const gqlQueryString = entry.query
|
||||
const variableString = entry.variables
|
||||
const responseText = entry.response
|
||||
|
||||
setGQLURL(url)
|
||||
setGQLHeaders(headers)
|
||||
setGQLQuery(gqlQueryString)
|
||||
setGQLVariables(variableString)
|
||||
setGQLResponse(responseText)
|
||||
props.conn.reset()
|
||||
}
|
||||
|
||||
return {
|
||||
queryFields,
|
||||
mutationFields,
|
||||
subscriptionFields,
|
||||
graphqlTypes,
|
||||
schemaString,
|
||||
|
||||
graphqlFieldsFilterText,
|
||||
|
||||
filteredQueryFields,
|
||||
filteredMutationFields,
|
||||
filteredSubscriptionFields,
|
||||
filteredGraphqlTypes,
|
||||
|
||||
isGqlTypeHighlighted,
|
||||
getGqlTypeHighlightedFields,
|
||||
|
||||
gqlTabs,
|
||||
typesTab,
|
||||
handleJumpToType,
|
||||
|
||||
downloadSchema,
|
||||
downloadSchemaIcon,
|
||||
copySchemaIcon,
|
||||
copySchema,
|
||||
handleUseHistory,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
79
components/graphql/URLBar.vue
Normal file
79
components/graphql/URLBar.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<div class="bg-primary flex p-4 top-0 z-10 sticky">
|
||||
<div class="flex-1 inline-flex">
|
||||
<input
|
||||
id="url"
|
||||
v-model="url"
|
||||
v-focus
|
||||
type="url"
|
||||
spellcheck="false"
|
||||
class="
|
||||
bg-primaryLight
|
||||
border border-divider
|
||||
rounded-l
|
||||
font-semibold font-mono
|
||||
text-secondaryDark
|
||||
w-full
|
||||
py-2
|
||||
px-4
|
||||
transition
|
||||
truncate
|
||||
focus:(border-accent
|
||||
outline-none)
|
||||
"
|
||||
:placeholder="$t('url')"
|
||||
@keyup.enter="onConnectClick"
|
||||
/>
|
||||
<ButtonPrimary
|
||||
id="get"
|
||||
name="get"
|
||||
:label="!connected ? $t('connect') : $t('disconnect')"
|
||||
class="rounded-l-none w-28"
|
||||
@click.native="onConnectClick"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from "@nuxtjs/composition-api"
|
||||
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
|
||||
import { GQLConnection } from "~/helpers/GQLConnection"
|
||||
import { getCurrentStrategyID } from "~/helpers/network"
|
||||
import { useReadonlyStream, useStream } from "~/helpers/utils/composables"
|
||||
import { gqlHeaders$, gqlURL$, setGQLURL } from "~/newstore/GQLSession"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
conn: {
|
||||
type: Object as PropType<GQLConnection>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const connected = useReadonlyStream(props.conn.connected$, false)
|
||||
const headers = useReadonlyStream(gqlHeaders$, [])
|
||||
|
||||
const url = useStream(gqlURL$, "", setGQLURL)
|
||||
|
||||
const onConnectClick = () => {
|
||||
if (!connected.value) {
|
||||
props.conn.connect(url.value, headers.value as any)
|
||||
|
||||
logHoppRequestRunToAnalytics({
|
||||
platform: "graphql-schema",
|
||||
strategy: getCurrentStrategyID(),
|
||||
})
|
||||
} else {
|
||||
props.conn.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
url,
|
||||
connected,
|
||||
onConnectClick,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
215
helpers/GQLConnection.ts
Normal file
215
helpers/GQLConnection.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
import { BehaviorSubject } from "rxjs"
|
||||
import {
|
||||
getIntrospectionQuery,
|
||||
buildClientSchema,
|
||||
GraphQLSchema,
|
||||
printSchema,
|
||||
GraphQLObjectType,
|
||||
GraphQLInputObjectType,
|
||||
GraphQLEnumType,
|
||||
GraphQLInterfaceType,
|
||||
} from "graphql"
|
||||
import { distinctUntilChanged, map } from "rxjs/operators"
|
||||
import { sendNetworkRequest } from "./network"
|
||||
import { GQLHeader } from "~/newstore/GQLSession"
|
||||
|
||||
const GQL_SCHEMA_POLL_INTERVAL = 7000
|
||||
|
||||
/**
|
||||
GQLConnection deals with all the operations (like polling, schema extraction) that runs
|
||||
when a connection is made to a GraphQL server.
|
||||
*/
|
||||
export class GQLConnection {
|
||||
public isLoading$ = new BehaviorSubject<boolean>(false)
|
||||
public connected$ = new BehaviorSubject<boolean>(false)
|
||||
public schema$ = new BehaviorSubject<GraphQLSchema | null>(null)
|
||||
|
||||
public schemaString$ = this.schema$.pipe(
|
||||
distinctUntilChanged(),
|
||||
map((schema) => {
|
||||
if (!schema) return null
|
||||
|
||||
return printSchema(schema, {
|
||||
commentDescriptions: true,
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
public queryFields$ = this.schema$.pipe(
|
||||
distinctUntilChanged(),
|
||||
map((schema) => {
|
||||
if (!schema) return null
|
||||
|
||||
const fields = schema.getQueryType()?.getFields()
|
||||
if (!fields) return null
|
||||
|
||||
return Object.values(fields)
|
||||
})
|
||||
)
|
||||
|
||||
public mutationFields$ = this.schema$.pipe(
|
||||
distinctUntilChanged(),
|
||||
map((schema) => {
|
||||
if (!schema) return null
|
||||
|
||||
const fields = schema.getMutationType()?.getFields()
|
||||
if (!fields) return null
|
||||
|
||||
return Object.values(fields)
|
||||
})
|
||||
)
|
||||
|
||||
public subscriptionFields$ = this.schema$.pipe(
|
||||
distinctUntilChanged(),
|
||||
map((schema) => {
|
||||
if (!schema) return null
|
||||
|
||||
const fields = schema.getSubscriptionType()?.getFields()
|
||||
if (!fields) return null
|
||||
|
||||
return Object.values(fields)
|
||||
})
|
||||
)
|
||||
|
||||
public graphqlTypes$ = this.schema$.pipe(
|
||||
distinctUntilChanged(),
|
||||
map((schema) => {
|
||||
if (!schema) return null
|
||||
|
||||
const typeMap = schema.getTypeMap()
|
||||
|
||||
const queryTypeName = schema.getQueryType()?.name ?? ""
|
||||
const mutationTypeName = schema.getMutationType()?.name ?? ""
|
||||
const subscriptionTypeName = schema.getSubscriptionType()?.name ?? ""
|
||||
|
||||
return Object.values(typeMap).filter((type) => {
|
||||
return (
|
||||
!type.name.startsWith("__") &&
|
||||
![queryTypeName, mutationTypeName, subscriptionTypeName].includes(
|
||||
type.name
|
||||
) &&
|
||||
(type instanceof GraphQLObjectType ||
|
||||
type instanceof GraphQLInputObjectType ||
|
||||
type instanceof GraphQLEnumType ||
|
||||
type instanceof GraphQLInterfaceType)
|
||||
)
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
private timeoutSubscription: any
|
||||
|
||||
public connect(url: string, headers: GQLHeader[]) {
|
||||
if (this.connected$.value) {
|
||||
throw new Error(
|
||||
"A connection is already running. Close it before starting another."
|
||||
)
|
||||
}
|
||||
|
||||
// Polling
|
||||
this.connected$.next(true)
|
||||
|
||||
const poll = async () => {
|
||||
await this.getSchema(url, headers)
|
||||
this.timeoutSubscription = setTimeout(() => {
|
||||
poll()
|
||||
}, GQL_SCHEMA_POLL_INTERVAL)
|
||||
}
|
||||
poll()
|
||||
}
|
||||
|
||||
public disconnect() {
|
||||
if (!this.connected$.value) {
|
||||
throw new Error("No connections are running to be disconnected")
|
||||
}
|
||||
|
||||
clearTimeout(this.timeoutSubscription)
|
||||
this.connected$.next(false)
|
||||
}
|
||||
|
||||
public reset() {
|
||||
if (this.connected$.value) this.disconnect()
|
||||
|
||||
this.isLoading$.next(false)
|
||||
this.connected$.next(false)
|
||||
this.schema$.next(null)
|
||||
}
|
||||
|
||||
private async getSchema(url: string, headers: GQLHeader[]) {
|
||||
try {
|
||||
this.isLoading$.next(true)
|
||||
|
||||
const introspectionQuery = JSON.stringify({
|
||||
query: getIntrospectionQuery(),
|
||||
})
|
||||
|
||||
const finalHeaders: Record<string, string> = {}
|
||||
headers
|
||||
.filter((x) => x.active && x.key !== "")
|
||||
.forEach((x) => (finalHeaders[x.key] = x.value))
|
||||
|
||||
const reqOptions = {
|
||||
method: "post",
|
||||
url,
|
||||
headers: {
|
||||
...finalHeaders,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
data: introspectionQuery,
|
||||
}
|
||||
|
||||
const data = await sendNetworkRequest(reqOptions)
|
||||
|
||||
// HACK : Temporary trailing null character issue from the extension fix
|
||||
const response = new TextDecoder("utf-8")
|
||||
.decode(data.data)
|
||||
.replace(/\0+$/, "")
|
||||
|
||||
const introspectResponse = JSON.parse(response)
|
||||
|
||||
const schema = buildClientSchema(introspectResponse.data)
|
||||
|
||||
this.schema$.next(schema)
|
||||
|
||||
this.isLoading$.next(false)
|
||||
} catch (error: any) {
|
||||
this.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
public async runQuery(
|
||||
url: string,
|
||||
headers: GQLHeader[],
|
||||
query: string,
|
||||
variables: string
|
||||
) {
|
||||
const finalHeaders: Record<string, string> = {}
|
||||
headers
|
||||
.filter((item) => item.active && item.key !== "")
|
||||
.forEach(({ key, value }) => (finalHeaders[key] = value))
|
||||
|
||||
const parsedVariables = JSON.parse(variables || "{}")
|
||||
|
||||
const reqOptions = {
|
||||
method: "post",
|
||||
url,
|
||||
headers: {
|
||||
...headers,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
data: JSON.stringify({
|
||||
query,
|
||||
variables: parsedVariables,
|
||||
}),
|
||||
}
|
||||
|
||||
const res = await sendNetworkRequest(reqOptions)
|
||||
|
||||
// HACK: Temporary trailing null character issue from the extension fix
|
||||
const responseText = new TextDecoder("utf-8")
|
||||
.decode(res.data)
|
||||
.replace(/\0+$/, "")
|
||||
|
||||
return responseText
|
||||
}
|
||||
}
|
||||
212
newstore/GQLSession.ts
Normal file
212
newstore/GQLSession.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
import { distinctUntilChanged, pluck } from "rxjs/operators"
|
||||
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
|
||||
|
||||
export type GQLHeader = {
|
||||
key: string
|
||||
value: string
|
||||
active: boolean
|
||||
}
|
||||
|
||||
type GQLSession = {
|
||||
url: string
|
||||
connected: boolean
|
||||
headers: GQLHeader[]
|
||||
schema: string
|
||||
query: string
|
||||
variables: string
|
||||
response: string
|
||||
}
|
||||
|
||||
const defaultGQLSession: GQLSession = {
|
||||
url: "https://rickandmortyapi.com/graphql",
|
||||
connected: false,
|
||||
headers: [],
|
||||
schema: "",
|
||||
query: `query GetCharacter($id: ID!) {
|
||||
character(id: $id) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: `{ "id": "1" }`,
|
||||
response: "",
|
||||
}
|
||||
|
||||
const dispatchers = defineDispatchers({
|
||||
setURL(_: GQLSession, { newURL }: { newURL: string }) {
|
||||
return {
|
||||
url: newURL,
|
||||
}
|
||||
},
|
||||
setConnected(_: GQLSession, { newStatus }: { newStatus: boolean }) {
|
||||
return {
|
||||
connected: newStatus,
|
||||
}
|
||||
},
|
||||
setHeaders(_, { headers }: { headers: GQLHeader[] }) {
|
||||
return {
|
||||
headers,
|
||||
}
|
||||
},
|
||||
addHeader(curr: GQLSession, { header }: { header: GQLHeader }) {
|
||||
return {
|
||||
headers: [...curr.headers, header],
|
||||
}
|
||||
},
|
||||
removeHeader(curr: GQLSession, { headerIndex }: { headerIndex: number }) {
|
||||
return {
|
||||
headers: curr.headers.filter((_x, i) => i !== headerIndex),
|
||||
}
|
||||
},
|
||||
updateHeader(
|
||||
curr: GQLSession,
|
||||
{
|
||||
headerIndex,
|
||||
updatedHeader,
|
||||
}: { headerIndex: number; updatedHeader: GQLHeader }
|
||||
) {
|
||||
return {
|
||||
headers: curr.headers.map((x, i) =>
|
||||
i === headerIndex ? updatedHeader : x
|
||||
),
|
||||
}
|
||||
},
|
||||
setQuery(_: GQLSession, { newQuery }: { newQuery: string }) {
|
||||
return {
|
||||
query: newQuery,
|
||||
}
|
||||
},
|
||||
setVariables(_: GQLSession, { newVariables }: { newVariables: string }) {
|
||||
return {
|
||||
variables: newVariables,
|
||||
}
|
||||
},
|
||||
setResponse(_: GQLSession, { newResponse }: { newResponse: string }) {
|
||||
return {
|
||||
response: newResponse,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const gqlSessionStore = new DispatchingStore(
|
||||
defaultGQLSession,
|
||||
dispatchers
|
||||
)
|
||||
|
||||
export function setGQLURL(newURL: string) {
|
||||
gqlSessionStore.dispatch({
|
||||
dispatcher: "setURL",
|
||||
payload: {
|
||||
newURL,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setGQLConnected(newStatus: boolean) {
|
||||
gqlSessionStore.dispatch({
|
||||
dispatcher: "setConnected",
|
||||
payload: {
|
||||
newStatus,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setGQLHeaders(headers: GQLHeader[]) {
|
||||
gqlSessionStore.dispatch({
|
||||
dispatcher: "setHeaders",
|
||||
payload: {
|
||||
headers,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function addGQLHeader(header: GQLHeader) {
|
||||
gqlSessionStore.dispatch({
|
||||
dispatcher: "addHeader",
|
||||
payload: {
|
||||
header,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function updateGQLHeader(headerIndex: number, updatedHeader: GQLHeader) {
|
||||
gqlSessionStore.dispatch({
|
||||
dispatcher: "updateHeader",
|
||||
payload: {
|
||||
headerIndex,
|
||||
updatedHeader,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function removeGQLHeader(headerIndex: number) {
|
||||
gqlSessionStore.dispatch({
|
||||
dispatcher: "removeHeader",
|
||||
payload: {
|
||||
headerIndex,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function clearGQLHeaders() {
|
||||
gqlSessionStore.dispatch({
|
||||
dispatcher: "setHeaders",
|
||||
payload: {
|
||||
headers: [],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setGQLQuery(newQuery: string) {
|
||||
gqlSessionStore.dispatch({
|
||||
dispatcher: "setQuery",
|
||||
payload: {
|
||||
newQuery,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setGQLVariables(newVariables: string) {
|
||||
gqlSessionStore.dispatch({
|
||||
dispatcher: "setVariables",
|
||||
payload: {
|
||||
newVariables,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setGQLResponse(newResponse: string) {
|
||||
gqlSessionStore.dispatch({
|
||||
dispatcher: "setResponse",
|
||||
payload: {
|
||||
newResponse,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const gqlURL$ = gqlSessionStore.subject$.pipe(
|
||||
pluck("url"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
export const gqlConnected$ = gqlSessionStore.subject$.pipe(
|
||||
pluck("connected"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
export const gqlQuery$ = gqlSessionStore.subject$.pipe(
|
||||
pluck("query"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
export const gqlVariables$ = gqlSessionStore.subject$.pipe(
|
||||
pluck("variables"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
export const gqlHeaders$ = gqlSessionStore.subject$.pipe(
|
||||
pluck("headers"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const gqlResponse$ = gqlSessionStore.subject$.pipe(
|
||||
pluck("response"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
1295
pages/graphql.vue
1295
pages/graphql.vue
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user