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
export {}
declare module "vue" {
declare module 'vue' {
export interface GlobalComponents {
AppActionHandler: typeof import('./components/app/ActionHandler.vue')['default']
AppAnnouncement: typeof import('./components/app/Announcement.vue')['default']
@@ -93,11 +93,13 @@ declare module "vue" {
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']
HoppSmartAutoComplete: typeof import('@hoppscotch/ui')['HoppSmartAutoComplete']
HoppSmartCheckbox: typeof import('@hoppscotch/ui')['HoppSmartCheckbox']
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
HoppSmartExpand: typeof import('@hoppscotch/ui')['HoppSmartExpand']
HoppSmartFileChip: typeof import('@hoppscotch/ui')['HoppSmartFileChip']
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput']
HoppSmartIntersection: typeof import('@hoppscotch/ui')['HoppSmartIntersection']
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink']
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
@@ -145,6 +147,7 @@ declare module "vue" {
IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default']
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['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']
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
IconLucideGlobe: typeof import('~icons/lucide/globe')['default']
@@ -154,6 +157,7 @@ declare module "vue" {
IconLucideLayers: typeof import('~icons/lucide/layers')['default']
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
IconLucideRss: typeof import('~icons/lucide/rss')['default']
IconLucideSearch: typeof import('~icons/lucide/search')['default']
IconLucideUsers: typeof import('~icons/lucide/users')['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']
SmartSpinner: typeof import('./../../hoppscotch-ui/src/components/smart/Spinner.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']
SmartToggle: typeof import('./../../hoppscotch-ui/src/components/smart/Toggle.vue')['default']
SmartTree: typeof import('./../../hoppscotch-ui/src/components/smart/Tree.vue')['default']

View File

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

View File

@@ -1,6 +1,11 @@
<template>
<div class="flex flex-1 flex-col overflow-auto whitespace-nowrap">
<div v-if="response?.length === 1" class="flex flex-1 flex-col">
<div class="flex flex-col flex-1 overflow-auto whitespace-nowrap">
<div
v-if="
response && response.length === 1 && response[0].type === 'response'
"
class="flex flex-col flex-1"
>
<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"
>
@@ -35,6 +40,13 @@
</div>
<div ref="schemaEditor" class="flex flex-1 flex-col"></div>
</div>
<component
:is="response[0].error.component"
v-else-if="
response && response[0].type === 'error' && response[0].error.component
"
class="flex-1"
/>
<div
v-else-if="response && response?.length > 1"
class="flex flex-1 flex-col"
@@ -74,8 +86,16 @@ const props = withDefaults(
)
const responseString = computed(() => {
if (props.response?.length === 1) {
return JSON.stringify(JSON.parse(props.response[0].data), null, 2)
const response = props.response
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 ""
})

View File

@@ -49,7 +49,11 @@
v-for="(entry, index) in log"
:key="`entry-${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>

View File

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

View File

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