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: {
|
||||
|
||||
25
pnpm-lock.yaml
generated
25
pnpm-lock.yaml
generated
@@ -152,6 +152,7 @@ importers:
|
||||
jest-serializer-vue: ^2.0.2
|
||||
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
|
||||
@@ -251,6 +252,7 @@ importers:
|
||||
io-ts: 2.2.16_fp-ts@2.11.10
|
||||
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
|
||||
@@ -9465,7 +9467,7 @@ packages:
|
||||
resolution: {integrity: sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=}
|
||||
|
||||
/escape-string-regexp/1.0.5:
|
||||
resolution: {integrity: sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=}
|
||||
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
|
||||
/escape-string-regexp/2.0.0:
|
||||
@@ -11111,7 +11113,7 @@ packages:
|
||||
dev: false
|
||||
|
||||
/hash-sum/1.0.2:
|
||||
resolution: {integrity: sha1-M7QHd3VMZDJXPBIMw4CLvRDUfwQ=}
|
||||
resolution: {integrity: sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==}
|
||||
dev: false
|
||||
|
||||
/hash-sum/2.0.0:
|
||||
@@ -11542,11 +11544,11 @@ packages:
|
||||
wrappy: 1.0.2
|
||||
|
||||
/inherits/2.0.1:
|
||||
resolution: {integrity: sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=}
|
||||
resolution: {integrity: sha512-8nWq2nLTAwd02jTqJExUYFSD/fKq6VH9Y/oG2accc/kdI0V98Bag8d5a4gi3XHz73rDWa2PvTtvcWYquKqSENA==}
|
||||
dev: false
|
||||
|
||||
/inherits/2.0.3:
|
||||
resolution: {integrity: sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=}
|
||||
resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==}
|
||||
dev: false
|
||||
|
||||
/inherits/2.0.4:
|
||||
@@ -12040,7 +12042,7 @@ packages:
|
||||
dev: true
|
||||
|
||||
/isarray/1.0.0:
|
||||
resolution: {integrity: sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=}
|
||||
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
||||
|
||||
/isarray/2.0.1:
|
||||
resolution: {integrity: sha512-c2cu3UxbI+b6kR3fy0nRnAhodsvR9dx7U5+znCOzdj6IfP3upFURTr0Xl5BlQZNKZjEtxrmVyfSdeE3O57smoQ==}
|
||||
@@ -12867,6 +12869,11 @@ packages:
|
||||
engines: {'0': node >= 0.2.0}
|
||||
dev: true
|
||||
|
||||
/jsonpath-plus/6.0.1:
|
||||
resolution: {integrity: sha512-EvGovdvau6FyLexFH2OeXfIITlgIbgZoAZe3usiySeaIDm5QS+A10DKNpaPBBqqRSZr2HN6HVNXxtwUAr2apEw==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
dev: false
|
||||
|
||||
/jsonwebtoken/8.5.1:
|
||||
resolution: {integrity: sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==}
|
||||
engines: {node: '>=4', npm: '>=1.4.28'}
|
||||
@@ -13543,14 +13550,14 @@ packages:
|
||||
fs-monkey: 1.0.3
|
||||
|
||||
/memory-fs/0.3.0:
|
||||
resolution: {integrity: sha1-e8xrYp46Q+hx1+Kaymrop/FcuyA=}
|
||||
resolution: {integrity: sha512-QTNXnl79X97kZ9jJk/meJrtDuvgvRakX5LU7HZW1L7MsXHuSTwoMIzN9tOLLH3Xfsj/gbsSqX/ovnsqz246zKQ==}
|
||||
dependencies:
|
||||
errno: 0.1.8
|
||||
readable-stream: 2.3.7
|
||||
dev: false
|
||||
|
||||
/memory-fs/0.4.1:
|
||||
resolution: {integrity: sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=}
|
||||
resolution: {integrity: sha512-cda4JKCxReDXFXRqOHPQscuIYg1PvxbE2S2GP45rnwfEK+vZaXC8C1OFvdHIbgw0DLzowXGVoxLaAmlgRy14GQ==}
|
||||
dependencies:
|
||||
errno: 0.1.8
|
||||
readable-stream: 2.3.7
|
||||
@@ -13948,7 +13955,7 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
|
||||
/ms/2.0.0:
|
||||
resolution: {integrity: sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=}
|
||||
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
||||
|
||||
/ms/2.1.1:
|
||||
resolution: {integrity: sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==}
|
||||
@@ -15840,7 +15847,7 @@ packages:
|
||||
dev: false
|
||||
|
||||
/prr/1.0.1:
|
||||
resolution: {integrity: sha1-0/wRS6BplaRexok/SEzrHXj19HY=}
|
||||
resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==}
|
||||
|
||||
/pseudomap/1.0.2:
|
||||
resolution: {integrity: sha1-8FKijacOYYkX7wqKw0wa5aaChrM=}
|
||||
|
||||
Reference in New Issue
Block a user