refactor: interceptor error display in graphql response (#3553)

This commit is contained in:
Nivedin
2023-11-17 17:03:53 +05:30
committed by GitHub
parent 50f475334e
commit a3aa9b68fc
6 changed files with 118 additions and 24 deletions

View File

@@ -5,7 +5,7 @@
// Read more: https://github.com/vuejs/core/pull/3399 // Read more: https://github.com/vuejs/core/pull/3399
export {} export {}
declare module "vue" { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
AppActionHandler: typeof import('./components/app/ActionHandler.vue')['default'] AppActionHandler: typeof import('./components/app/ActionHandler.vue')['default']
AppAnnouncement: typeof import('./components/app/Announcement.vue')['default'] AppAnnouncement: typeof import('./components/app/Announcement.vue')['default']
@@ -93,11 +93,13 @@ declare module "vue" {
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary'] HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary'] HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor'] HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']
HoppSmartAutoComplete: typeof import('@hoppscotch/ui')['HoppSmartAutoComplete']
HoppSmartCheckbox: typeof import('@hoppscotch/ui')['HoppSmartCheckbox'] HoppSmartCheckbox: typeof import('@hoppscotch/ui')['HoppSmartCheckbox']
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal'] HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
HoppSmartExpand: typeof import('@hoppscotch/ui')['HoppSmartExpand'] HoppSmartExpand: typeof import('@hoppscotch/ui')['HoppSmartExpand']
HoppSmartFileChip: typeof import('@hoppscotch/ui')['HoppSmartFileChip'] HoppSmartFileChip: typeof import('@hoppscotch/ui')['HoppSmartFileChip']
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput'] HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput']
HoppSmartIntersection: typeof import('@hoppscotch/ui')['HoppSmartIntersection']
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem'] HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink'] HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink']
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal'] HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
@@ -145,6 +147,7 @@ declare module "vue" {
IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default'] IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default']
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default'] IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
IconLucideArrowUpRight: typeof import('~icons/lucide/arrow-up-right')['default'] IconLucideArrowUpRight: typeof import('~icons/lucide/arrow-up-right')['default']
IconLucideBrush: typeof import('~icons/lucide/brush')['default']
IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default'] IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default']
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default'] IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
IconLucideGlobe: typeof import('~icons/lucide/globe')['default'] IconLucideGlobe: typeof import('~icons/lucide/globe')['default']
@@ -154,6 +157,7 @@ declare module "vue" {
IconLucideLayers: typeof import('~icons/lucide/layers')['default'] IconLucideLayers: typeof import('~icons/lucide/layers')['default']
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default'] IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
IconLucideMinus: typeof import('~icons/lucide/minus')['default'] IconLucideMinus: typeof import('~icons/lucide/minus')['default']
IconLucideRss: typeof import('~icons/lucide/rss')['default']
IconLucideSearch: typeof import('~icons/lucide/search')['default'] IconLucideSearch: typeof import('~icons/lucide/search')['default']
IconLucideUsers: typeof import('~icons/lucide/users')['default'] IconLucideUsers: typeof import('~icons/lucide/users')['default']
InterceptorsErrorPlaceholder: typeof import('./components/interceptors/ErrorPlaceholder.vue')['default'] InterceptorsErrorPlaceholder: typeof import('./components/interceptors/ErrorPlaceholder.vue')['default']
@@ -202,6 +206,7 @@ declare module "vue" {
SmartSlideOver: typeof import('./../../hoppscotch-ui/src/components/smart/SlideOver.vue')['default'] SmartSlideOver: typeof import('./../../hoppscotch-ui/src/components/smart/SlideOver.vue')['default']
SmartSpinner: typeof import('./../../hoppscotch-ui/src/components/smart/Spinner.vue')['default'] SmartSpinner: typeof import('./../../hoppscotch-ui/src/components/smart/Spinner.vue')['default']
SmartTab: typeof import('./../../hoppscotch-ui/src/components/smart/Tab.vue')['default'] SmartTab: typeof import('./../../hoppscotch-ui/src/components/smart/Tab.vue')['default']
SmartTable: typeof import('./../../hoppscotch-ui/src/components/smart/Table.vue')['default']
SmartTabs: typeof import('./../../hoppscotch-ui/src/components/smart/Tabs.vue')['default'] SmartTabs: typeof import('./../../hoppscotch-ui/src/components/smart/Tabs.vue')['default']
SmartToggle: typeof import('./../../hoppscotch-ui/src/components/smart/Toggle.vue')['default'] SmartToggle: typeof import('./../../hoppscotch-ui/src/components/smart/Toggle.vue')['default']
SmartTree: typeof import('./../../hoppscotch-ui/src/components/smart/Tree.vue')['default'] SmartTree: typeof import('./../../hoppscotch-ui/src/components/smart/Tree.vue')['default']

View File

@@ -63,6 +63,7 @@ import {
GQLResponseEvent, GQLResponseEvent,
runGQLOperation, runGQLOperation,
gqlMessageEvent, gqlMessageEvent,
connection,
} from "~/helpers/graphql/connection" } from "~/helpers/graphql/connection"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { InterceptorService } from "~/services/interceptor.service" import { InterceptorService } from "~/services/interceptor.service"
@@ -152,13 +153,7 @@ const runQuery = async (
toast.success(t("authorization.graphql_headers")) toast.success(t("authorization.graphql_headers"))
} }
} catch (e: any) { } catch (e: any) {
console.log(e)
// response.value = [`${e}`]
completePageProgress() completePageProgress()
toast.error(
`${t("error.something_went_wrong")}. ${t("error.check_console_details")}`,
{}
)
console.error(e) console.error(e)
} }
platform.analytics?.logEvent({ platform.analytics?.logEvent({
@@ -177,7 +172,10 @@ watch(
} }
try { try {
if (event?.operationType !== "subscription") { if (
event?.type === "response" &&
event?.operationType !== "subscription"
) {
// response.value = [event] // response.value = [event]
emit("update:response", [event]) emit("update:response", [event])
} else { } else {
@@ -192,6 +190,26 @@ watch(
{ deep: true } { deep: true }
) )
watch(
() => connection,
(newVal) => {
if (newVal.error && newVal.state === "DISCONNECTED") {
const response = [
{
type: "error",
error: {
message: newVal.error.message(t),
type: newVal.error.type,
component: newVal.error.component,
},
},
]
emit("update:response", response)
}
},
{ deep: true }
)
const hideRequestModal = () => { const hideRequestModal = () => {
showSaveRequestModal.value = false showSaveRequestModal.value = false
} }

View File

@@ -1,6 +1,11 @@
<template> <template>
<div class="flex flex-1 flex-col overflow-auto whitespace-nowrap"> <div class="flex flex-col flex-1 overflow-auto whitespace-nowrap">
<div v-if="response?.length === 1" class="flex flex-1 flex-col"> <div
v-if="
response && response.length === 1 && response[0].type === 'response'
"
class="flex flex-col flex-1"
>
<div <div
class="sticky top-0 z-10 flex flex-shrink-0 items-center justify-between overflow-x-auto border-b border-dividerLight bg-primary pl-4" class="sticky top-0 z-10 flex flex-shrink-0 items-center justify-between overflow-x-auto border-b border-dividerLight bg-primary pl-4"
> >
@@ -35,6 +40,13 @@
</div> </div>
<div ref="schemaEditor" class="flex flex-1 flex-col"></div> <div ref="schemaEditor" class="flex flex-1 flex-col"></div>
</div> </div>
<component
:is="response[0].error.component"
v-else-if="
response && response[0].type === 'error' && response[0].error.component
"
class="flex-1"
/>
<div <div
v-else-if="response && response?.length > 1" v-else-if="response && response?.length > 1"
class="flex flex-1 flex-col" class="flex flex-1 flex-col"
@@ -74,8 +86,16 @@ const props = withDefaults(
) )
const responseString = computed(() => { const responseString = computed(() => {
if (props.response?.length === 1) { const response = props.response
return JSON.stringify(JSON.parse(props.response[0].data), null, 2) if (response && response[0].type === "error") {
return ""
} else if (
response &&
response.length === 1 &&
response[0].type === "response" &&
response[0].data
) {
return JSON.stringify(JSON.parse(response[0].data), null, 2)
} }
return "" return ""
}) })

View File

@@ -49,7 +49,11 @@
v-for="(entry, index) in log" v-for="(entry, index) in log"
:key="`entry-${index}`" :key="`entry-${index}`"
:is-open="log.length - 1 === index" :is-open="log.length - 1 === index"
:entry="{ ts: entry.time, source: 'info', payload: entry.data }" :entry="{
ts: entry.type === 'response' ? entry.time : undefined,
source: 'info',
payload: entry.type === 'response' ? entry.data : '',
}"
/> />
</div> </div>
</div> </div>

View File

@@ -11,8 +11,9 @@ import {
getIntrospectionQuery, getIntrospectionQuery,
printSchema, printSchema,
} from "graphql" } from "graphql"
import { computed, reactive, ref } from "vue" import { Component, computed, reactive, ref } from "vue"
import { getService } from "~/modules/dioc" import { getService } from "~/modules/dioc"
import { getI18n } from "~/modules/i18n"
import { addGraphqlHistoryEntry, makeGQLHistoryEntry } from "~/newstore/history" import { addGraphqlHistoryEntry, makeGQLHistoryEntry } from "~/newstore/history"
@@ -32,13 +33,23 @@ type RunQueryOptions = {
operationType: OperationType operationType: OperationType
} }
export type GQLResponseEvent = { export type GQLResponseEvent =
time: number | {
operationName: string | undefined type: "response"
operationType: OperationType time: number
data: string operationName: string | undefined
rawQuery?: RunQueryOptions operationType: OperationType
} data: string
rawQuery?: RunQueryOptions
}
| {
type: "error"
error: {
type: string
message: string
component?: Component
}
}
export type ConnectionState = "CONNECTING" | "CONNECTED" | "DISCONNECTED" export type ConnectionState = "CONNECTING" | "CONNECTED" | "DISCONNECTED"
export type SubscriptionState = "SUBSCRIBING" | "SUBSCRIBED" | "UNSUBSCRIBED" export type SubscriptionState = "SUBSCRIBING" | "SUBSCRIBED" | "UNSUBSCRIBED"
@@ -61,6 +72,11 @@ type Connection = {
subscriptionState: Map<string, SubscriptionState> subscriptionState: Map<string, SubscriptionState>
socket: WebSocket | undefined socket: WebSocket | undefined
schema: GraphQLSchema | null schema: GraphQLSchema | null
error?: {
type: string
message: (t: ReturnType<typeof getI18n>) => string
component?: Component
} | null
} }
const tabs = getService(GQLTabService) const tabs = getService(GQLTabService)
@@ -71,6 +87,7 @@ export const connection = reactive<Connection>({
subscriptionState: new Map<string, SubscriptionState>(), subscriptionState: new Map<string, SubscriptionState>(),
socket: undefined, socket: undefined,
schema: null, schema: null,
error: null,
}) })
export const schema = computed(() => connection.schema) export const schema = computed(() => connection.schema)
@@ -202,7 +219,19 @@ const getSchema = async (url: string, headers: GQLHeader[]) => {
const res = await interceptorService.runRequest(reqOptions).response const res = await interceptorService.runRequest(reqOptions).response
if (E.isLeft(res)) { if (E.isLeft(res)) {
console.error(res.left) if (
res.left !== "cancellation" &&
res.left.error === "NO_PW_EXT_HOOK" &&
res.left.humanMessage
) {
connection.error = {
type: res.left.error,
message: (t: ReturnType<typeof getI18n>) =>
res.left.humanMessage.description(t),
component: res.left.component,
}
}
throw new Error(res.left.toString()) throw new Error(res.left.toString())
} }
@@ -218,6 +247,7 @@ const getSchema = async (url: string, headers: GQLHeader[]) => {
const schema = buildClientSchema(introspectResponse.data) const schema = buildClientSchema(introspectResponse.data)
connection.schema = schema connection.schema = schema
connection.error = null
} catch (e: any) { } catch (e: any) {
console.error(e) console.error(e)
disconnect() disconnect()
@@ -280,7 +310,18 @@ export const runGQLOperation = async (options: RunQueryOptions) => {
const result = await interceptorService.runRequest(reqOptions).response const result = await interceptorService.runRequest(reqOptions).response
if (E.isLeft(result)) { if (E.isLeft(result)) {
console.error(result.left) if (
result.left !== "cancellation" &&
result.left.error === "NO_PW_EXT_HOOK" &&
result.left.humanMessage
) {
connection.error = {
type: result.left.error,
message: (t: ReturnType<typeof getI18n>) =>
result.left.humanMessage.description(t),
component: result.left.component,
}
}
throw new Error(result.left.toString()) throw new Error(result.left.toString())
} }
@@ -292,6 +333,7 @@ export const runGQLOperation = async (options: RunQueryOptions) => {
.replace(/\0+$/, "") .replace(/\0+$/, "")
gqlMessageEvent.value = { gqlMessageEvent.value = {
type: "response",
time: Date.now(), time: Date.now(),
operationName: operationName ?? "query", operationName: operationName ?? "query",
data: responseText, data: responseText,
@@ -299,6 +341,10 @@ export const runGQLOperation = async (options: RunQueryOptions) => {
operationType, operationType,
} }
if (connection.state !== "CONNECTED") {
connection.state = "CONNECTED"
}
addQueryToHistory(options, responseText) addQueryToHistory(options, responseText)
return responseText return responseText
@@ -352,6 +398,7 @@ export const runSubscription = (
} }
case GQL.DATA: { case GQL.DATA: {
gqlMessageEvent.value = { gqlMessageEvent.value = {
type: "response",
time: Date.now(), time: Date.now(),
operationName, operationName,
data: JSON.stringify(data.payload), data: JSON.stringify(data.payload),

View File

@@ -209,7 +209,6 @@ export class ExtensionInterceptorService
req: AxiosRequestConfig req: AxiosRequestConfig
): RequestRunResult["response"] { ): RequestRunResult["response"] {
const extensionHook = window.__POSTWOMAN_EXTENSION_HOOK__ const extensionHook = window.__POSTWOMAN_EXTENSION_HOOK__
if (!extensionHook) { if (!extensionHook) {
return E.left(<InterceptorError>{ return E.left(<InterceptorError>{
// TODO: i18n this // TODO: i18n this
@@ -230,6 +229,7 @@ export class ExtensionInterceptorService
return E.right(result) return E.right(result)
} catch (e) { } catch (e) {
console.error(e)
// TODO: improve type checking // TODO: improve type checking
if ((e as any).response) { if ((e as any).response) {
return E.right((e as any).response) return E.right((e as any).response)