feat: inspections (#3213)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
This commit is contained in:
112
packages/hoppscotch-common/src/components/app/Inspection.vue
Normal file
112
packages/hoppscotch-common/src/components/app/Inspection.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div v-if="inspectionResults && inspectionResults.length > 0">
|
||||
<tippy interactive trigger="click" theme="popover">
|
||||
<div class="flex justify-center items-center flex-1 flex-col">
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconAlertTriangle"
|
||||
:class="severityColor(getHighestSeverity.severity)"
|
||||
:title="t('inspections.description')"
|
||||
/>
|
||||
</div>
|
||||
<template #content="{ hide }">
|
||||
<div class="flex flex-col space-y-2 items-start flex-1">
|
||||
<div
|
||||
class="flex justify-between border rounded pl-2 border-divider bg-popover sticky top-0 self-stretch"
|
||||
>
|
||||
<span class="flex items-center flex-1">
|
||||
<icon-lucide-activity class="mr-2 svg-icons text-accent" />
|
||||
<span class="font-bold">
|
||||
{{ t("inspections.title") }}
|
||||
</span>
|
||||
</span>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/documentation/features/inspections"
|
||||
blank
|
||||
:title="t('app.wiki')"
|
||||
:icon="IconHelpCircle"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-for="(inspector, index) in inspectionResults"
|
||||
:key="index"
|
||||
class="flex self-stretch"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col flex-1 rounded border border-dashed border-dividerDark divide-y divide-dashed divide-dividerDark"
|
||||
>
|
||||
<span
|
||||
v-if="inspector.text.type === 'text'"
|
||||
class="flex-1 px-3 py-2"
|
||||
>
|
||||
{{ inspector.text.text }}
|
||||
<HoppSmartLink
|
||||
blank
|
||||
:to="inspector.doc.link"
|
||||
class="text-accent hover:text-accentDark transition"
|
||||
>
|
||||
{{ inspector.doc.text }}
|
||||
<icon-lucide-arrow-up-right class="svg-icons" />
|
||||
</HoppSmartLink>
|
||||
</span>
|
||||
<span v-if="inspector.action" class="flex p-2 space-x-2">
|
||||
<HoppButtonSecondary
|
||||
:label="inspector.action.text"
|
||||
outline
|
||||
filled
|
||||
@click="
|
||||
() => {
|
||||
inspector.action?.apply()
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { InspectorResult } from "~/services/inspection"
|
||||
import IconAlertTriangle from "~icons/lucide/alert-triangle"
|
||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||
import { computed } from "vue"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
inspectionResults: InspectorResult[] | undefined
|
||||
}>()
|
||||
|
||||
const getHighestSeverity = computed(() => {
|
||||
if (props.inspectionResults) {
|
||||
return props.inspectionResults.reduce(
|
||||
(prev, curr) => {
|
||||
return prev.severity > curr.severity ? prev : curr
|
||||
},
|
||||
{ severity: 0 }
|
||||
)
|
||||
} else {
|
||||
return { severity: 0 }
|
||||
}
|
||||
})
|
||||
|
||||
const severityColor = (severity: number) => {
|
||||
switch (severity) {
|
||||
case 1:
|
||||
return "!text-green-500 hover:!text-green-600"
|
||||
case 2:
|
||||
return "!text-yellow-500 hover:!text-yellow-600"
|
||||
case 3:
|
||||
return "!text-red-500 hover:!text-red-600"
|
||||
default:
|
||||
return "!text-gray-500 hover:!text-gray-600"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -79,16 +79,13 @@
|
||||
tabindex="-1"
|
||||
/>
|
||||
</span>
|
||||
<HoppSmartAutoComplete
|
||||
<SmartEnvInput
|
||||
v-model="header.key"
|
||||
:placeholder="`${t('count.header', { count: index + 1 })}`"
|
||||
:source="commonHeaders"
|
||||
:spellcheck="false"
|
||||
:value="header.key"
|
||||
autofocus
|
||||
styles=" bg-transparent flex flex-1
|
||||
py-1 px-4 truncate "
|
||||
class="flex-1 !flex"
|
||||
@input="
|
||||
:auto-complete-source="commonHeaders"
|
||||
:env-index="index"
|
||||
:inspection-results="getInspectorResult(headerKeyResults, index)"
|
||||
@change="
|
||||
updateHeader(index, {
|
||||
id: header.id,
|
||||
key: $event,
|
||||
@@ -100,6 +97,10 @@
|
||||
<SmartEnvInput
|
||||
v-model="header.value"
|
||||
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
||||
:inspection-results="
|
||||
getInspectorResult(headerValueResults, index)
|
||||
"
|
||||
:env-index="index"
|
||||
@change="
|
||||
updateHeader(index, {
|
||||
id: header.id,
|
||||
@@ -265,6 +266,9 @@ import {
|
||||
} from "~/helpers/utils/EffectiveURL"
|
||||
import { aggregateEnvs$, getAggregateEnvs } from "~/newstore/environments"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import { useService } from "dioc/vue"
|
||||
import { InspectionService, InspectorResult } from "~/services/inspection"
|
||||
import { currentTabID } from "~/helpers/rest/tab"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
@@ -502,4 +506,39 @@ const changeTab = (tab: ComputedHeader["source"]) => {
|
||||
if (tab === "auth") emit("change-tab", "authorization")
|
||||
else emit("change-tab", "bodyParams")
|
||||
}
|
||||
|
||||
const inspectionService = useService(InspectionService)
|
||||
|
||||
const allTabResults = inspectionService.tabs
|
||||
|
||||
const headerKeyResults = computed(() => {
|
||||
return (
|
||||
allTabResults.value
|
||||
.get(currentTabID.value)
|
||||
.filter(
|
||||
(result) =>
|
||||
result.locations.type === "header" &&
|
||||
result.locations.position === "key"
|
||||
) ?? []
|
||||
)
|
||||
})
|
||||
const headerValueResults = computed(() => {
|
||||
return (
|
||||
allTabResults.value
|
||||
.get(currentTabID.value)
|
||||
.filter(
|
||||
(result) =>
|
||||
result.locations.type === "header" &&
|
||||
result.locations.position === "value"
|
||||
) ?? []
|
||||
)
|
||||
})
|
||||
|
||||
const getInspectorResult = (results: InspectorResult[], index: number) => {
|
||||
return results.filter((result) => {
|
||||
if (result.locations.type === "url" || result.locations.type === "response")
|
||||
return
|
||||
return result.locations.index === index
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -82,6 +82,9 @@
|
||||
<SmartEnvInput
|
||||
v-model="param.key"
|
||||
:placeholder="`${t('count.parameter', { count: index + 1 })}`"
|
||||
:inspection-results="
|
||||
getInspectorResult(parameterKeyResults, index)
|
||||
"
|
||||
@change="
|
||||
updateParam(index, {
|
||||
id: param.id,
|
||||
@@ -94,6 +97,9 @@
|
||||
<SmartEnvInput
|
||||
v-model="param.value"
|
||||
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
||||
:inspection-results="
|
||||
getInspectorResult(parameterValueResults, index)
|
||||
"
|
||||
@change="
|
||||
updateParam(index, {
|
||||
id: param.id,
|
||||
@@ -173,7 +179,7 @@ import IconCheckCircle from "~icons/lucide/check-circle"
|
||||
import IconCircle from "~icons/lucide/circle"
|
||||
import IconTrash from "~icons/lucide/trash"
|
||||
import IconWrapText from "~icons/lucide/wrap-text"
|
||||
import { reactive, ref, watch } from "vue"
|
||||
import { computed, reactive, ref, watch } from "vue"
|
||||
import { flow, pipe } from "fp-ts/function"
|
||||
import * as O from "fp-ts/Option"
|
||||
import * as A from "fp-ts/Array"
|
||||
@@ -195,6 +201,9 @@ import { useToast } from "@composables/toast"
|
||||
import { throwError } from "@functional/error"
|
||||
import { objRemoveKey } from "@functional/object"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import { useService } from "dioc/vue"
|
||||
import { InspectionService, InspectorResult } from "~/services/inspection"
|
||||
import { currentTabID } from "~/helpers/rest/tab"
|
||||
|
||||
const colorMode = useColorMode()
|
||||
|
||||
@@ -398,4 +407,39 @@ const clearContent = () => {
|
||||
|
||||
bulkParams.value = ""
|
||||
}
|
||||
|
||||
const inspectionService = useService(InspectionService)
|
||||
|
||||
const allTabResults = inspectionService.tabs
|
||||
|
||||
const parameterKeyResults = computed(() => {
|
||||
return (
|
||||
allTabResults.value
|
||||
.get(currentTabID.value)
|
||||
.filter(
|
||||
(result) =>
|
||||
result.locations.type === "parameter" &&
|
||||
result.locations.position === "key"
|
||||
) ?? []
|
||||
)
|
||||
})
|
||||
const parameterValueResults = computed(() => {
|
||||
return (
|
||||
allTabResults.value
|
||||
.get(currentTabID.value)
|
||||
.filter(
|
||||
(result) =>
|
||||
result.locations.type === "parameter" &&
|
||||
result.locations.position === "value"
|
||||
) ?? []
|
||||
)
|
||||
})
|
||||
|
||||
const getInspectorResult = (results: InspectorResult[], index: number) => {
|
||||
return results.filter((result) => {
|
||||
if (result.locations.type === "url" || result.locations.type === "response")
|
||||
return
|
||||
return result.locations.index === index
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -53,9 +53,16 @@
|
||||
v-model="tab.document.request.endpoint"
|
||||
:placeholder="`${t('request.url')}`"
|
||||
:auto-complete-source="userHistories"
|
||||
:inspection-results="tabResults"
|
||||
@paste="onPasteUrl($event)"
|
||||
@enter="newSendRequest"
|
||||
/>
|
||||
>
|
||||
<template #empty>
|
||||
<span>
|
||||
{{ t("empty.history_suggestions") }}
|
||||
</span>
|
||||
</template>
|
||||
</SmartEnvInput>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex mt-2 sm:mt-0">
|
||||
@@ -259,12 +266,14 @@ import IconLink2 from "~icons/lucide/link-2"
|
||||
import IconRotateCCW from "~icons/lucide/rotate-ccw"
|
||||
import IconSave from "~icons/lucide/save"
|
||||
import IconShare2 from "~icons/lucide/share-2"
|
||||
import { HoppRESTTab } from "~/helpers/rest/tab"
|
||||
import { HoppRESTTab, currentTabID } from "~/helpers/rest/tab"
|
||||
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
||||
import { RESTHistoryEntry, restHistory$ } from "~/newstore/history"
|
||||
import { platform } from "~/platform"
|
||||
import { getCurrentStrategyID } from "~/helpers/network"
|
||||
import { HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { useService } from "dioc/vue"
|
||||
import { InspectionService } from "~/services/inspection"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
@@ -628,4 +637,12 @@ const isCustomMethod = computed(() => {
|
||||
})
|
||||
|
||||
const COLUMN_LAYOUT = useSetting("COLUMN_LAYOUT")
|
||||
|
||||
const inspectionService = useService(InspectionService)
|
||||
|
||||
const allTabResults = inspectionService.tabs
|
||||
|
||||
const tabResults = computed(() => {
|
||||
return allTabResults.value.get(currentTabID.value) ?? []
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="flex flex-col flex-1">
|
||||
<div class="flex flex-col flex-1 relative">
|
||||
<HttpResponseMeta :response="tab.response" />
|
||||
<LensesResponseBodyRenderer
|
||||
v-if="!loading && hasResponse"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="sticky top-0 z-10 flex items-start justify-center flex-shrink-0 p-4 overflow-auto overflow-x-auto bg-primary whitespace-nowrap"
|
||||
class="sticky top-0 z-10 flex items-center justify-center flex-shrink-0 p-4 overflow-auto overflow-x-auto bg-primary whitespace-nowrap"
|
||||
>
|
||||
<AppShortcutsPrompt v-if="response == null" class="flex-1" />
|
||||
<div v-else class="flex flex-col flex-1">
|
||||
@@ -70,6 +70,15 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AppInspection
|
||||
v-if="response?.type !== 'loading'"
|
||||
:inspection-results="tabResults"
|
||||
:class="[
|
||||
response === null || response?.type === 'network_fail'
|
||||
? 'absolute right-2 top-2'
|
||||
: 'ml-2 -m-2',
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -80,6 +89,9 @@ import type { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
|
||||
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 { currentTabID } from "~/helpers/rest/tab"
|
||||
|
||||
const t = useI18n()
|
||||
const colorMode = useColorMode()
|
||||
@@ -128,4 +140,16 @@ const statusCategory = computed(() => {
|
||||
}
|
||||
return findStatusGroup(props.response.statusCode)
|
||||
})
|
||||
|
||||
const inspectionService = useService(InspectionService)
|
||||
|
||||
const allTabResults = inspectionService.tabs
|
||||
|
||||
const tabResults = computed(() => {
|
||||
return (
|
||||
allTabResults.value
|
||||
.get(currentTabID.value)
|
||||
?.filter((result) => result.locations.type === "response") ?? []
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
@keydown="handleKeystroke"
|
||||
@focusin="showSuggestionPopover = true"
|
||||
></div>
|
||||
<AppInspection :inspection-results="inspectionResults" />
|
||||
</div>
|
||||
<ul
|
||||
v-if="showSuggestionPopover && autoCompleteSource"
|
||||
@@ -34,8 +35,11 @@
|
||||
</div>
|
||||
</li>
|
||||
<li v-if="suggestions.length === 0" class="pointer-events-none">
|
||||
<span class="truncate py-0.5">
|
||||
{{ t("empty.history_suggestions") }}
|
||||
<div v-if="slots.empty" class="truncate py-0.5">
|
||||
<slot name="empty"></slot>
|
||||
</div>
|
||||
<span v-else class="truncate py-0.5">
|
||||
{{ t("empty.suggestions") }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -43,7 +47,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch, nextTick, computed, Ref } from "vue"
|
||||
import { ref, onMounted, watch, nextTick, computed, Ref, useSlots } from "vue"
|
||||
import {
|
||||
EditorView,
|
||||
placeholder as placeholderExt,
|
||||
@@ -62,6 +66,7 @@ import { AggregateEnvironment, aggregateEnvs$ } from "~/newstore/environments"
|
||||
import { platform } from "~/platform"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
import { onClickOutside, useDebounceFn } from "@vueuse/core"
|
||||
import { InspectorResult } from "~/services/inspection"
|
||||
import { invokeAction } from "~/helpers/actions"
|
||||
|
||||
const props = withDefaults(
|
||||
@@ -75,6 +80,7 @@ const props = withDefaults(
|
||||
environmentHighlights?: boolean
|
||||
readonly?: boolean
|
||||
autoCompleteSource?: string[]
|
||||
inspectionResults?: InspectorResult[] | undefined
|
||||
}>(),
|
||||
{
|
||||
modelValue: "",
|
||||
@@ -85,6 +91,8 @@ const props = withDefaults(
|
||||
readonly: false,
|
||||
environmentHighlights: true,
|
||||
autoCompleteSource: undefined,
|
||||
inspectionResult: undefined,
|
||||
inspectionResults: undefined,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -98,6 +106,8 @@ const emit = defineEmits<{
|
||||
(e: "click", ev: any): void
|
||||
}>()
|
||||
|
||||
const slots = useSlots()
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const cachedValue = ref(props.modelValue)
|
||||
@@ -142,7 +152,9 @@ const suggestions = computed(() => {
|
||||
const updateModelValue = (value: string) => {
|
||||
emit("update:modelValue", value)
|
||||
emit("change", value)
|
||||
showSuggestionPopover.value = false
|
||||
nextTick(() => {
|
||||
showSuggestionPopover.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const handleKeystroke = (ev: KeyboardEvent) => {
|
||||
|
||||
Reference in New Issue
Block a user