feat: filter json body response (#2404)
This commit is contained in:
13
packages/hoppscotch-app/assets/icons/filter.svg
Normal file
13
packages/hoppscotch-app/assets/icons/filter.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 283 B |
@@ -255,6 +255,7 @@
|
||||
--upper-mobile-raw-tertiary-sticky-fold: 8.188rem;
|
||||
--lower-primary-sticky-fold: 3rem;
|
||||
--lower-secondary-sticky-fold: 5rem;
|
||||
--lower-tertiary-sticky-fold: 7.05rem;
|
||||
--sidebar-primary-sticky-fold: 2rem;
|
||||
}
|
||||
|
||||
@@ -270,6 +271,7 @@
|
||||
--upper-mobile-raw-tertiary-sticky-fold: 8.938rem;
|
||||
--lower-primary-sticky-fold: 3.25rem;
|
||||
--lower-secondary-sticky-fold: 5.5rem;
|
||||
--lower-tertiary-sticky-fold: 7.8rem;
|
||||
--sidebar-primary-sticky-fold: 2.25rem;
|
||||
}
|
||||
|
||||
@@ -285,6 +287,7 @@
|
||||
--upper-mobile-raw-tertiary-sticky-fold: 9.688rem;
|
||||
--lower-primary-sticky-fold: 3.5rem;
|
||||
--lower-secondary-sticky-fold: 6rem;
|
||||
--lower-tertiary-sticky-fold: 8.55rem;
|
||||
--sidebar-primary-sticky-fold: 2.5rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
<template>
|
||||
<div class="flex flex-col flex-1">
|
||||
<div
|
||||
v-if="response.type === 'success' || response.type === 'fail'"
|
||||
class="flex flex-col flex-1"
|
||||
>
|
||||
<div
|
||||
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-lowerSecondaryStickyFold"
|
||||
>
|
||||
<label class="font-semibold text-secondaryLight">
|
||||
{{ t("response.body") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<div class="flex items-center">
|
||||
<ButtonSecondary
|
||||
v-if="response.body"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
@@ -15,6 +18,14 @@
|
||||
svg="wrap-text"
|
||||
@click.native.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-if="response.body"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.filter_response')"
|
||||
svg="filter"
|
||||
:class="{ '!text-accent': toggleFilter }"
|
||||
@click.native.prevent="toggleFilterState"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-if="response.body"
|
||||
ref="downloadResponse"
|
||||
@@ -33,7 +44,47 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="jsonResponse" class="flex flex-col flex-1"></div>
|
||||
<div
|
||||
v-if="toggleFilter"
|
||||
class="bg-primary flex sticky top-lowerTertiaryStickyFold z-10 border-b border-dividerLight"
|
||||
>
|
||||
<div
|
||||
class="bg-primaryLight border-divider text-secondaryDark inline-flex flex-1 items-center"
|
||||
>
|
||||
<span class="inline-flex flex-1 items-center px-4">
|
||||
<SmartIcon name="search" class="h-4 w-4 text-secondaryLight" />
|
||||
<input
|
||||
v-model="filterQueryText"
|
||||
v-focus
|
||||
class="input !border-0 !px-2"
|
||||
:placeholder="`${t('response.filter_response_body')}`"
|
||||
type="text"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
v-if="filterResponseError"
|
||||
class="px-2 py-1 text-tiny flex items-center justify-center text-accentContrast rounded"
|
||||
:class="{
|
||||
'bg-red-500':
|
||||
filterResponseError.type === 'JSON_PARSE_FAILED' ||
|
||||
filterResponseError.type === 'JSON_PATH_QUERY_ERROR',
|
||||
'bg-amber-500': filterResponseError.type === 'RESPONSE_EMPTY',
|
||||
}"
|
||||
>
|
||||
<SmartIcon name="info" class="svg-icons mr-1.5" />
|
||||
<span>{{ filterResponseError.error }}</span>
|
||||
</div>
|
||||
<ButtonSecondary
|
||||
v-if="response.body"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('app.wiki')"
|
||||
svg="help-circle"
|
||||
to="https://github.com/JSONPath-Plus/JSONPath"
|
||||
blank
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="jsonResponse" class="flex flex-col flex-1 h-auto h-full"></div>
|
||||
<div
|
||||
v-if="outlinePath"
|
||||
class="sticky bottom-0 z-10 flex px-2 overflow-auto border-t bg-primaryLight border-dividerLight flex-nowrap hide-scrollbar"
|
||||
@@ -142,8 +193,10 @@
|
||||
<script setup lang="ts">
|
||||
import * as LJSON from "lossless-json"
|
||||
import * as O from "fp-ts/Option"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import { computed, ref, reactive } from "@nuxtjs/composition-api"
|
||||
import { JSONPath } from "jsonpath-plus"
|
||||
import { useCodemirror } from "~/helpers/editor/codemirror"
|
||||
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
|
||||
import jsonParse, { JSONObjectMember, JSONValue } from "~/helpers/jsonParse"
|
||||
@@ -172,9 +225,51 @@ const { downloadIcon, downloadResponse } = useDownloadResponse(
|
||||
responseBodyText
|
||||
)
|
||||
|
||||
const jsonBodyText = computed(() =>
|
||||
const toggleFilter = ref(false)
|
||||
const filterQueryText = ref("")
|
||||
|
||||
type BodyParseError =
|
||||
| { type: "JSON_PARSE_FAILED" }
|
||||
| { type: "JSON_PATH_QUERY_FAILED"; error: Error }
|
||||
|
||||
const responseJsonObject = computed(() =>
|
||||
pipe(
|
||||
responseBodyText.value,
|
||||
E.tryCatchK(
|
||||
LJSON.parse,
|
||||
(): BodyParseError => ({ type: "JSON_PARSE_FAILED" })
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
const jsonResponseBodyText = computed(() => {
|
||||
if (filterQueryText.value.length > 0) {
|
||||
return pipe(
|
||||
responseJsonObject.value,
|
||||
E.chain((parsedJSON) =>
|
||||
E.tryCatch(
|
||||
() =>
|
||||
JSONPath({
|
||||
path: filterQueryText.value,
|
||||
json: parsedJSON,
|
||||
}) as undefined,
|
||||
(err): BodyParseError => ({
|
||||
type: "JSON_PATH_QUERY_FAILED",
|
||||
error: err as Error,
|
||||
})
|
||||
)
|
||||
),
|
||||
E.map(JSON.stringify)
|
||||
)
|
||||
} else {
|
||||
return E.right(responseBodyText.value)
|
||||
}
|
||||
})
|
||||
|
||||
const jsonBodyText = computed(() =>
|
||||
pipe(
|
||||
jsonResponseBodyText.value,
|
||||
E.getOrElse(() => responseBodyText.value),
|
||||
O.tryCatchK(LJSON.parse),
|
||||
O.map((val) => LJSON.stringify(val, undefined, 2)),
|
||||
O.getOrElse(() => responseBodyText.value)
|
||||
@@ -189,6 +284,32 @@ const ast = computed(() =>
|
||||
)
|
||||
)
|
||||
|
||||
const filterResponseError = computed(() =>
|
||||
pipe(
|
||||
jsonResponseBodyText.value,
|
||||
E.match(
|
||||
(e) => {
|
||||
switch (e.type) {
|
||||
case "JSON_PATH_QUERY_FAILED":
|
||||
return { type: "JSON_PATH_QUERY_ERROR", error: e.error.message }
|
||||
case "JSON_PARSE_FAILED":
|
||||
return {
|
||||
type: "JSON_PARSE_FAILED",
|
||||
error: t("error.json_parsing_failed").toString(),
|
||||
}
|
||||
}
|
||||
},
|
||||
(result) =>
|
||||
result === "[]"
|
||||
? {
|
||||
type: "RESPONSE_EMPTY",
|
||||
error: t("error.no_results_found").toString(),
|
||||
}
|
||||
: undefined
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
const outlineOptions = ref<any | null>(null)
|
||||
const jsonResponse = ref<any | null>(null)
|
||||
const linewrapEnabled = ref(true)
|
||||
@@ -227,6 +348,11 @@ const outlinePath = computed(() =>
|
||||
O.getOrElseW(() => null)
|
||||
)
|
||||
)
|
||||
|
||||
const toggleFilterState = () => {
|
||||
filterQueryText.value = ""
|
||||
toggleFilter.value = !toggleFilter.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"download_file": "Download file",
|
||||
"duplicate": "Duplicate",
|
||||
"edit": "Edit",
|
||||
"filter_response": "Filter response",
|
||||
"go_back": "Go back",
|
||||
"label": "Label",
|
||||
"learn_more": "Learn more",
|
||||
@@ -202,9 +203,11 @@
|
||||
"invalid_link": "Invalid link",
|
||||
"invalid_link_description": "The link you clicked is invalid or expired.",
|
||||
"json_prettify_invalid_body": "Couldn't prettify an invalid body, solve json syntax errors and try again",
|
||||
"json_parsing_failed": "Invalid JSON",
|
||||
"network_error": "There seems to be a network error. Please try again.",
|
||||
"network_fail": "Could not send request",
|
||||
"no_duration": "No duration",
|
||||
"no_results_found": "No matches found",
|
||||
"script_fail": "Could not execute pre-request script",
|
||||
"something_went_wrong": "Something went wrong",
|
||||
"test_script_fail": "Could not execute post-request script"
|
||||
@@ -379,6 +382,7 @@
|
||||
},
|
||||
"response": {
|
||||
"body": "Response Body",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "Headers",
|
||||
"html": "HTML",
|
||||
"image": "Image",
|
||||
|
||||
@@ -88,6 +88,7 @@
|
||||
"io-ts": "^2.2.16",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json-loader": "^0.5.7",
|
||||
"jsonpath-plus": "^6.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"lossless-json": "^1.0.5",
|
||||
"mustache": "^4.2.0",
|
||||
|
||||
6
packages/hoppscotch-app/types/jsonpath-plus.d.ts
vendored
Normal file
6
packages/hoppscotch-app/types/jsonpath-plus.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
import { JSONPathOptions } from "jsonpath-plus"
|
||||
|
||||
declare module "jsonpath-plus" {
|
||||
export type JSONPathType = (options: JSONPathOptions) => unknown
|
||||
export const JSONPath: JSONPathType
|
||||
}
|
||||
@@ -18,6 +18,7 @@ export default defineConfig({
|
||||
"var(--upper-mobile-raw-tertiary-sticky-fold)",
|
||||
lowerPrimaryStickyFold: "var(--lower-primary-sticky-fold)",
|
||||
lowerSecondaryStickyFold: "var(--lower-secondary-sticky-fold)",
|
||||
lowerTertiaryStickyFold: "var(--lower-tertiary-sticky-fold)",
|
||||
sidebarPrimaryStickyFold: "var(--sidebar-primary-sticky-fold)",
|
||||
},
|
||||
colors: {
|
||||
|
||||
Reference in New Issue
Block a user