feat: codemirror for graphql query, scheme and response

This commit is contained in:
liyasthomas
2021-09-10 16:12:04 +05:30
parent 3ef5a1e21a
commit 457b6b982c
4 changed files with 219 additions and 235 deletions

View File

@@ -55,20 +55,7 @@
/> />
</div> </div>
</div> </div>
<GraphqlQueryEditor <div ref="queryEditor" class="w-full block"></div>
ref="queryEditor"
v-model="gqlQueryString"
:on-run-g-q-l-query="runQuery"
:options="{
maxLines: Infinity,
minLines: 16,
autoScrollEditorIntoView: true,
showPrintMargin: false,
useWorker: false,
}"
styles="border-b border-dividerLight"
@update-query="updateQuery"
/>
</AppSection> </AppSection>
</SmartTab> </SmartTab>
@@ -284,6 +271,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref, useContext, watch } from "@nuxtjs/composition-api" import { onMounted, ref, useContext, watch } from "@nuxtjs/composition-api"
import clone from "lodash/clone" import clone from "lodash/clone"
import * as gql from "graphql"
import { copyToClipboard } from "~/helpers/utils/clipboard" import { copyToClipboard } from "~/helpers/utils/clipboard"
import { import {
useNuxt, useNuxt,
@@ -312,6 +300,9 @@ import { getCurrentStrategyID } from "~/helpers/network"
import { makeGQLRequest } from "~/helpers/types/HoppGQLRequest" import { makeGQLRequest } from "~/helpers/types/HoppGQLRequest"
import { useCodemirror } from "~/helpers/editor/codemirror" import { useCodemirror } from "~/helpers/editor/codemirror"
import "codemirror/mode/javascript/javascript" import "codemirror/mode/javascript/javascript"
import jsonLinter from "~/helpers/editor/linting/json"
import { createGQLQueryLinter } from "~/helpers/editor/linting/gqlQuery"
import queryCompleter from "~/helpers/editor/completion/gqlQuery"
const props = defineProps<{ const props = defineProps<{
conn: GQLConnection conn: GQLConnection
@@ -363,13 +354,22 @@ const variableEditor = ref<any | null>(null)
useCodemirror(variableEditor, variableString, { useCodemirror(variableEditor, variableString, {
extendedEditorConfig: { extendedEditorConfig: {
mode: "javascript", mode: "application/ld+json",
}, },
linter: null, linter: jsonLinter,
completer: null, completer: null,
}) })
const queryEditor = ref<any | null>(null) const queryEditor = ref<any | null>(null)
const schemaString = useReadonlyStream(props.conn.schema$, null)
useCodemirror(queryEditor, gqlQueryString, {
extendedEditorConfig: {
mode: "application/ld+json",
},
linter: createGQLQueryLinter(schemaString),
completer: queryCompleter(schemaString),
})
const copyQueryIcon = ref("copy") const copyQueryIcon = ref("copy")
const prettifyQueryIcon = ref("align-left") const prettifyQueryIcon = ref("align-left")
@@ -466,7 +466,13 @@ const hideRequestModal = () => {
} }
const prettifyQuery = () => { const prettifyQuery = () => {
queryEditor.value.prettifyQuery() try {
gqlQueryString.value = gql.print(gql.parse(gqlQueryString.value))
} catch (e) {
$toast.error(t("error.gql_prettify_invalid_query").toString(), {
icon: "error_outline",
})
}
prettifyQueryIcon.value = "check" prettifyQueryIcon.value = "check"
setTimeout(() => (prettifyQueryIcon.value = "align-left"), 1000) setTimeout(() => (prettifyQueryIcon.value = "align-left"), 1000)
} }
@@ -475,11 +481,6 @@ const saveRequest = () => {
showSaveRequestModal.value = true showSaveRequestModal.value = true
} }
// Why ?
const updateQuery = (updatedQuery: string) => {
gqlQueryString.value = updatedQuery
}
const copyVariables = () => { const copyVariables = () => {
copyToClipboard(variableString.value) copyToClipboard(variableString.value)
copyVariablesIcon.value = "check" copyVariablesIcon.value = "check"

View File

@@ -18,6 +18,13 @@
{{ $t("response.title") }} {{ $t("response.title") }}
</label> </label>
<div class="flex"> <div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="$t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }"
svg="corner-down-left"
@click.native.prevent="linewrapEnabled = !linewrapEnabled"
/>
<ButtonSecondary <ButtonSecondary
ref="downloadResponse" ref="downloadResponse"
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
@@ -34,21 +41,7 @@
/> />
</div> </div>
</div> </div>
<SmartAceEditor <div v-if="responseString" ref="schemaEditor" class="w-full block"></div>
v-if="responseString"
:value="responseString"
:lang="'json'"
:lint="false"
:options="{
maxLines: Infinity,
minLines: 16,
autoScrollEditorIntoView: true,
readOnly: true,
showPrintMargin: false,
useWorker: false,
}"
styles="border-b border-dividerLight"
/>
<div <div
v-else v-else
class=" class="
@@ -72,19 +65,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { PropType, ref, useContext } from "@nuxtjs/composition-api" import { reactive, ref, useContext } from "@nuxtjs/composition-api"
import { GQLConnection } from "~/helpers/GQLConnection" import { useCodemirror } from "~/helpers/editor/codemirror"
import { copyToClipboard } from "~/helpers/utils/clipboard" import { copyToClipboard } from "~/helpers/utils/clipboard"
import { useReadonlyStream } from "~/helpers/utils/composables" import { useReadonlyStream } from "~/helpers/utils/composables"
import { gqlResponse$ } from "~/newstore/GQLSession" import { gqlResponse$ } from "~/newstore/GQLSession"
defineProps({
conn: {
type: Object as PropType<GQLConnection>,
required: true,
},
})
const { const {
$toast, $toast,
app: { i18n }, app: { i18n },
@@ -93,6 +79,23 @@ const t = i18n.t.bind(i18n)
const responseString = useReadonlyStream(gqlResponse$, "") const responseString = useReadonlyStream(gqlResponse$, "")
const schemaEditor = ref<any | null>(null)
const linewrapEnabled = ref(true)
useCodemirror(
schemaEditor,
responseString,
reactive({
extendedEditorConfig: {
mode: "application/ld+json",
readOnly: true,
lineWrapping: linewrapEnabled,
},
linter: null,
completer: null,
})
)
const downloadResponseIcon = ref("download") const downloadResponseIcon = ref("download")
const copyResponseIcon = ref("copy") const copyResponseIcon = ref("copy")

View File

@@ -149,6 +149,13 @@
:title="$t('app.wiki')" :title="$t('app.wiki')"
svg="help-circle" svg="help-circle"
/> />
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="$t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }"
svg="corner-down-left"
@click.native.prevent="linewrapEnabled = !linewrapEnabled"
/>
<ButtonSecondary <ButtonSecondary
ref="downloadSchema" ref="downloadSchema"
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
@@ -165,20 +172,11 @@
/> />
</div> </div>
</div> </div>
<SmartAceEditor <div
v-if="schemaString" v-if="schemaString"
v-model="schemaString" ref="schemaEditor"
:lang="'graphqlschema'" class="w-full block"
:options="{ ></div>
maxLines: Infinity,
minLines: 16,
autoScrollEditorIntoView: true,
readOnly: true,
showPrintMargin: false,
useWorker: false,
}"
styles="border-b border-dividerLight"
/>
<div <div
v-else v-else
class=" class="
@@ -200,17 +198,17 @@
</aside> </aside>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { import {
computed, computed,
defineComponent,
nextTick, nextTick,
PropType, reactive,
ref, ref,
useContext, useContext,
} from "@nuxtjs/composition-api" } from "@nuxtjs/composition-api"
import { GraphQLField, GraphQLType } from "graphql" import { GraphQLField, GraphQLType } from "graphql"
import { map } from "rxjs/operators" import { map } from "rxjs/operators"
import { useCodemirror } from "~/helpers/editor/codemirror"
import { GQLConnection } from "~/helpers/GQLConnection" import { GQLConnection } from "~/helpers/GQLConnection"
import { GQLHeader } from "~/helpers/types/HoppGQLRequest" import { GQLHeader } from "~/helpers/types/HoppGQLRequest"
import { copyToClipboard } from "~/helpers/utils/clipboard" import { copyToClipboard } from "~/helpers/utils/clipboard"
@@ -285,186 +283,168 @@ type GQLHistoryEntry = {
variables: string variables: string
} }
export default defineComponent({ const props = defineProps<{
props: { conn: GQLConnection
conn: { }>()
type: Object as PropType<GQLConnection>,
required: true,
},
},
setup(props) {
const {
$toast,
app: { i18n },
} = useContext()
const t = i18n.t.bind(i18n)
const queryFields = useReadonlyStream( const {
props.conn.queryFields$.pipe(map((x) => x ?? [])), $toast,
[] app: { i18n },
) } = useContext()
const mutationFields = useReadonlyStream( const t = i18n.t.bind(i18n)
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("download") const queryFields = useReadonlyStream(
const copySchemaIcon = ref("copy") props.conn.queryFields$.pipe(map((x) => x ?? [])),
[]
)
const graphqlFieldsFilterText = ref("") const mutationFields = useReadonlyStream(
props.conn.mutationFields$.pipe(map((x) => x ?? [])),
[]
)
const gqlTabs = ref<any | null>(null) const subscriptionFields = useReadonlyStream(
const typesTab = ref<any | null>(null) props.conn.subscriptionFields$.pipe(map((x) => x ?? [])),
[]
)
const filteredQueryFields = computed(() => { const graphqlTypes = useReadonlyStream(
return getFilteredGraphqlFields( props.conn.graphqlTypes$.pipe(map((x) => x ?? [])),
graphqlFieldsFilterText.value, []
queryFields.value as any )
)
})
const filteredMutationFields = computed(() => { const downloadSchemaIcon = ref("download")
return getFilteredGraphqlFields( const copySchemaIcon = ref("copy")
graphqlFieldsFilterText.value,
mutationFields.value as any
)
})
const filteredSubscriptionFields = computed(() => { const graphqlFieldsFilterText = ref("")
return getFilteredGraphqlFields(
graphqlFieldsFilterText.value,
subscriptionFields.value as any
)
})
const filteredGraphqlTypes = computed(() => { const gqlTabs = ref<any | null>(null)
return getFilteredGraphqlTypes( const typesTab = ref<any | null>(null)
graphqlFieldsFilterText.value,
graphqlTypes.value as any
)
})
const isGqlTypeHighlighted = (gqlType: GraphQLType) => { const filteredQueryFields = computed(() => {
if (!graphqlFieldsFilterText.value) return false return getFilteredGraphqlFields(
graphqlFieldsFilterText.value,
return isTextFoundInGraphqlFieldObject( queryFields.value as any
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 = "check"
$toast.success(t("state.download_started").toString(), {
icon: "downloading",
})
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
downloadSchemaIcon.value = "download"
}, 1000)
}
const copySchema = () => {
if (!schemaString.value) return
copyToClipboard(schemaString.value)
copySchemaIcon.value = "check"
setTimeout(() => (copySchemaIcon.value = "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,
}
},
}) })
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 schemaEditor = ref<any | null>(null)
const linewrapEnabled = ref(true)
useCodemirror(
schemaEditor,
schemaString,
reactive({
extendedEditorConfig: {
mode: "application/ld+json",
readOnly: true,
lineWrapping: linewrapEnabled,
},
linter: null,
completer: null,
})
)
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 = "check"
$toast.success(t("state.download_started").toString(), {
icon: "downloading",
})
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
downloadSchemaIcon.value = "download"
}, 1000)
}
const copySchema = () => {
if (!schemaString.value) return
copyToClipboard(schemaString.value)
copySchemaIcon.value = "check"
setTimeout(() => (copySchemaIcon.value = "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()
}
</script> </script>

View File

@@ -24,7 +24,7 @@
> >
<Pane class="flex flex-1 hide-scrollbar !overflow-auto"> <Pane class="flex flex-1 hide-scrollbar !overflow-auto">
<main class="flex flex-1 w-full"> <main class="flex flex-1 w-full">
<nuxt class="flex flex-1" /> <nuxt class="flex overflow-y-auto flex-1" />
</main> </main>
</Pane> </Pane>
</Splitpanes> </Splitpanes>