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