feat: inspections (#3213)

Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
This commit is contained in:
Nivedin
2023-08-18 01:37:21 +05:30
committed by GitHub
parent b55970cc7a
commit f21ed30e10
20 changed files with 1406 additions and 19 deletions

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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"

View File

@@ -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>

View File

@@ -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) => {