feat(common): add status code and additional metadata to GraphQL responses (#4435)
Co-authored-by: Anwarul Islam <anwaarulislaam@gmail.com> Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
parent
caadfc8c55
commit
9751625414
4 changed files with 189 additions and 2 deletions
|
|
@ -111,6 +111,7 @@ declare module 'vue' {
|
|||
GraphqlRequestOptions: typeof import('./components/graphql/RequestOptions.vue')['default']
|
||||
GraphqlRequestTab: typeof import('./components/graphql/RequestTab.vue')['default']
|
||||
GraphqlResponse: typeof import('./components/graphql/Response.vue')['default']
|
||||
GraphqlResponseMeta: typeof import('./components/graphql/ResponseMeta.vue')['default']
|
||||
GraphqlSchemaDocumentation: typeof import('./components/graphql/SchemaDocumentation.vue')['default']
|
||||
GraphqlSchemaSearch: typeof import('./components/graphql/SchemaSearch.vue')['default']
|
||||
GraphqlSidebar: typeof import('./components/graphql/Sidebar.vue')['default']
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<div class="flex flex-1 flex-col overflow-auto whitespace-nowrap">
|
||||
<GraphqlResponseMeta :response="response" />
|
||||
<div
|
||||
v-if="
|
||||
response && response.length === 1 && response[0].type === 'response'
|
||||
|
|
@ -88,7 +89,6 @@
|
|||
>
|
||||
<GraphqlSubscriptionLog :log="response" />
|
||||
</div>
|
||||
<AppShortcutsPrompt v-else class="p-4" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,163 @@
|
|||
<template>
|
||||
<div
|
||||
class="sticky top-0 z-10 flex flex-shrink-0 items-center justify-center overflow-auto overflow-x-auto whitespace-nowrap bg-primary p-4"
|
||||
>
|
||||
<AppShortcutsPrompt v-if="response == null && !isEmbed" class="flex-1" />
|
||||
|
||||
<div v-if="response == null && isEmbed">
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('app.documentation')}`"
|
||||
to="https://docs.hoppscotch.io/documentation/features/graphql-api-testing"
|
||||
:icon="IconExternalLink"
|
||||
blank
|
||||
outline
|
||||
reverse
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else-if="response" class="flex flex-1 flex-col">
|
||||
<div v-if="isLoading" class="flex flex-col items-center justify-center">
|
||||
<HoppSmartSpinner class="my-4" />
|
||||
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
||||
</div>
|
||||
|
||||
<component
|
||||
:is="errorResponse?.error?.component"
|
||||
v-if="errorResponse?.error?.component"
|
||||
class="flex-1"
|
||||
/>
|
||||
|
||||
<HoppSmartPlaceholder
|
||||
v-if="errorResponse && !errorResponse.error.component"
|
||||
:src="`/images/states/${colorMode.value}/upload_error.svg`"
|
||||
:alt="errorResponse.error.message || t('error.network_fail')"
|
||||
:heading="errorResponse.error.message || t('error.network_fail')"
|
||||
:text="errorResponse.error.message || t('error.network_fail')"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="successResponse"
|
||||
class="flex items-center text-tiny font-semibold"
|
||||
>
|
||||
<div
|
||||
:class="statusCategory.className"
|
||||
class="inline-flex flex-1 space-x-4"
|
||||
>
|
||||
<span v-if="successResponse.document?.statusCode">
|
||||
<span class="text-secondary"> {{ t("response.status") }}: </span>
|
||||
{{ `${successResponse.document.statusCode}\xA0 • \xA0`
|
||||
}}{{
|
||||
getStatusCodeReasonPhrase(
|
||||
successResponse.document.statusCode,
|
||||
successResponse.document.statusText
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<span v-if="successResponse.document?.meta?.responseDuration">
|
||||
<span class="text-secondary"> {{ t("response.time") }}: </span>
|
||||
{{ `${successResponse.document.meta.responseDuration} ms` }}
|
||||
</span>
|
||||
<span
|
||||
v-if="successResponse.document?.meta?.responseSize"
|
||||
v-tippy="
|
||||
readableResponseSize
|
||||
? { theme: 'tooltip' }
|
||||
: { onShow: () => false }
|
||||
"
|
||||
:title="`${successResponse.document.meta.responseSize} B`"
|
||||
>
|
||||
<span class="text-secondary"> {{ t("response.size") }}: </span>
|
||||
{{
|
||||
readableResponseSize
|
||||
? readableResponseSize
|
||||
: `${successResponse.document.meta.responseSize} B`
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AppInspection
|
||||
v-if="!isLoading"
|
||||
:inspection-results="tabResults"
|
||||
:class="[
|
||||
response === null || errorResponse
|
||||
? 'absolute right-2 top-2'
|
||||
: '-m-2 ml-2',
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue"
|
||||
import findStatusGroup from "@helpers/findStatusGroup"
|
||||
import type { GQLResponseEvent } from "~/helpers/graphql/connection"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { getStatusCodeReasonPhrase } from "~/helpers/utils/statusCodes"
|
||||
import { useService } from "dioc/vue"
|
||||
import { InspectionService } from "~/services/inspection"
|
||||
import { GQLTabService } from "~/services/tab/graphql"
|
||||
import IconExternalLink from "~icons/lucide/external-link"
|
||||
|
||||
const t = useI18n()
|
||||
const colorMode = useColorMode()
|
||||
const tabs = useService(GQLTabService)
|
||||
|
||||
const props = defineProps<{
|
||||
response: GQLResponseEvent[] | null | undefined
|
||||
isEmbed?: boolean
|
||||
}>()
|
||||
|
||||
const isLoading = computed(() => {
|
||||
return props.response === null || props.response === undefined
|
||||
})
|
||||
|
||||
const successResponse = computed(() => {
|
||||
if (!props.response || props.response.length === 0) return null
|
||||
// For subscriptions with multiple responses, show the latest one
|
||||
const responses = props.response.filter((r) => r.type === "response")
|
||||
if (responses.length === 0) return null
|
||||
return responses[responses.length - 1]
|
||||
})
|
||||
|
||||
const errorResponse = computed(() => {
|
||||
if (!props.response || props.response.length === 0) return null
|
||||
const firstResponse = props.response[0]
|
||||
return firstResponse?.type === "error" ? firstResponse : null
|
||||
})
|
||||
|
||||
/**
|
||||
* Gives the response size in a human readable format
|
||||
* (changes unit from B to MB/KB depending on the size)
|
||||
* If no changes (error res state) or value can be made (size < 1KB ?),
|
||||
* it returns undefined
|
||||
*/
|
||||
const readableResponseSize = computed(() => {
|
||||
if (!successResponse.value?.document?.meta?.responseSize) return undefined
|
||||
|
||||
const size = successResponse.value.document.meta.responseSize
|
||||
|
||||
if (size >= 100000) return (size / 1000000).toFixed(2) + " MB"
|
||||
if (size >= 1000) return (size / 1000).toFixed(2) + " KB"
|
||||
|
||||
return undefined
|
||||
})
|
||||
|
||||
const statusCategory = computed(() => {
|
||||
if (!successResponse.value?.document?.statusCode) {
|
||||
return {
|
||||
name: "error",
|
||||
className: "text-red-500",
|
||||
}
|
||||
}
|
||||
return findStatusGroup(successResponse.value.document.statusCode)
|
||||
})
|
||||
|
||||
const inspectionService = useService(InspectionService)
|
||||
|
||||
const tabResults = inspectionService.getResultViewFor(
|
||||
tabs.currentTabID.value,
|
||||
(result) => result.locations.type === "response"
|
||||
)
|
||||
</script>
|
||||
|
|
@ -61,6 +61,15 @@ export type GQLResponseEvent =
|
|||
operationType: OperationType
|
||||
data: string
|
||||
rawQuery?: RunQueryOptions
|
||||
document?: {
|
||||
type: string
|
||||
statusCode: number
|
||||
statusText: string
|
||||
meta: {
|
||||
responseSize: number
|
||||
responseDuration: number
|
||||
}
|
||||
}
|
||||
}
|
||||
| {
|
||||
type: "error"
|
||||
|
|
@ -456,7 +465,21 @@ export const runGQLOperation = async (options: RunQueryOptions) => {
|
|||
throw new Error(parsedResponse.error.message)
|
||||
}
|
||||
|
||||
gqlMessageEvent.value = parsedResponse
|
||||
const timeStart = Date.now()
|
||||
const timeEnd = Date.now()
|
||||
|
||||
gqlMessageEvent.value = {
|
||||
...parsedResponse,
|
||||
document: {
|
||||
type: "success",
|
||||
statusCode: relayResponse.status,
|
||||
statusText: relayResponse.statusText,
|
||||
meta: {
|
||||
responseSize: relayResponse.body.body.byteLength,
|
||||
responseDuration: timeEnd - timeStart,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
addQueryToHistory(options, parsedResponse.data)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue