feat: filter json body response (#2404)

This commit is contained in:
Nivedin
2022-06-10 18:12:40 +05:30
committed by GitHub
parent 04a9c4dc52
commit 6b1ca1dce1
8 changed files with 174 additions and 13 deletions

View 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

View File

@@ -255,6 +255,7 @@
--upper-mobile-raw-tertiary-sticky-fold: 8.188rem; --upper-mobile-raw-tertiary-sticky-fold: 8.188rem;
--lower-primary-sticky-fold: 3rem; --lower-primary-sticky-fold: 3rem;
--lower-secondary-sticky-fold: 5rem; --lower-secondary-sticky-fold: 5rem;
--lower-tertiary-sticky-fold: 7.05rem;
--sidebar-primary-sticky-fold: 2rem; --sidebar-primary-sticky-fold: 2rem;
} }
@@ -270,6 +271,7 @@
--upper-mobile-raw-tertiary-sticky-fold: 8.938rem; --upper-mobile-raw-tertiary-sticky-fold: 8.938rem;
--lower-primary-sticky-fold: 3.25rem; --lower-primary-sticky-fold: 3.25rem;
--lower-secondary-sticky-fold: 5.5rem; --lower-secondary-sticky-fold: 5.5rem;
--lower-tertiary-sticky-fold: 7.8rem;
--sidebar-primary-sticky-fold: 2.25rem; --sidebar-primary-sticky-fold: 2.25rem;
} }
@@ -285,6 +287,7 @@
--upper-mobile-raw-tertiary-sticky-fold: 9.688rem; --upper-mobile-raw-tertiary-sticky-fold: 9.688rem;
--lower-primary-sticky-fold: 3.5rem; --lower-primary-sticky-fold: 3.5rem;
--lower-secondary-sticky-fold: 6rem; --lower-secondary-sticky-fold: 6rem;
--lower-tertiary-sticky-fold: 8.55rem;
--sidebar-primary-sticky-fold: 2.5rem; --sidebar-primary-sticky-fold: 2.5rem;
} }

View File

@@ -1,12 +1,15 @@
<template> <template>
<div class="flex flex-col flex-1"> <div
v-if="response.type === 'success' || response.type === 'fail'"
class="flex flex-col flex-1"
>
<div <div
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-lowerSecondaryStickyFold" 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"> <label class="font-semibold text-secondaryLight">
{{ t("response.body") }} {{ t("response.body") }}
</label> </label>
<div class="flex"> <div class="flex items-center">
<ButtonSecondary <ButtonSecondary
v-if="response.body" v-if="response.body"
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
@@ -15,6 +18,14 @@
svg="wrap-text" svg="wrap-text"
@click.native.prevent="linewrapEnabled = !linewrapEnabled" @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 <ButtonSecondary
v-if="response.body" v-if="response.body"
ref="downloadResponse" ref="downloadResponse"
@@ -33,7 +44,47 @@
/> />
</div> </div>
</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 <div
v-if="outlinePath" v-if="outlinePath"
class="sticky bottom-0 z-10 flex px-2 overflow-auto border-t bg-primaryLight border-dividerLight flex-nowrap hide-scrollbar" 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"> <script setup lang="ts">
import * as LJSON from "lossless-json" import * as LJSON from "lossless-json"
import * as O from "fp-ts/Option" import * as O from "fp-ts/Option"
import * as E from "fp-ts/Either"
import { pipe } from "fp-ts/function" import { pipe } from "fp-ts/function"
import { computed, ref, reactive } from "@nuxtjs/composition-api" import { computed, ref, reactive } from "@nuxtjs/composition-api"
import { JSONPath } from "jsonpath-plus"
import { useCodemirror } from "~/helpers/editor/codemirror" import { useCodemirror } from "~/helpers/editor/codemirror"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse" import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
import jsonParse, { JSONObjectMember, JSONValue } from "~/helpers/jsonParse" import jsonParse, { JSONObjectMember, JSONValue } from "~/helpers/jsonParse"
@@ -172,9 +225,51 @@ const { downloadIcon, downloadResponse } = useDownloadResponse(
responseBodyText 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( pipe(
responseBodyText.value, 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.tryCatchK(LJSON.parse),
O.map((val) => LJSON.stringify(val, undefined, 2)), O.map((val) => LJSON.stringify(val, undefined, 2)),
O.getOrElse(() => responseBodyText.value) 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 outlineOptions = ref<any | null>(null)
const jsonResponse = ref<any | null>(null) const jsonResponse = ref<any | null>(null)
const linewrapEnabled = ref(true) const linewrapEnabled = ref(true)
@@ -227,6 +348,11 @@ const outlinePath = computed(() =>
O.getOrElseW(() => null) O.getOrElseW(() => null)
) )
) )
const toggleFilterState = () => {
filterQueryText.value = ""
toggleFilter.value = !toggleFilter.value
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -14,6 +14,7 @@
"download_file": "Download file", "download_file": "Download file",
"duplicate": "Duplicate", "duplicate": "Duplicate",
"edit": "Edit", "edit": "Edit",
"filter_response": "Filter response",
"go_back": "Go back", "go_back": "Go back",
"label": "Label", "label": "Label",
"learn_more": "Learn more", "learn_more": "Learn more",
@@ -202,9 +203,11 @@
"invalid_link": "Invalid link", "invalid_link": "Invalid link",
"invalid_link_description": "The link you clicked is invalid or expired.", "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_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_error": "There seems to be a network error. Please try again.",
"network_fail": "Could not send request", "network_fail": "Could not send request",
"no_duration": "No duration", "no_duration": "No duration",
"no_results_found": "No matches found",
"script_fail": "Could not execute pre-request script", "script_fail": "Could not execute pre-request script",
"something_went_wrong": "Something went wrong", "something_went_wrong": "Something went wrong",
"test_script_fail": "Could not execute post-request script" "test_script_fail": "Could not execute post-request script"
@@ -379,6 +382,7 @@
}, },
"response": { "response": {
"body": "Response Body", "body": "Response Body",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Headers", "headers": "Headers",
"html": "HTML", "html": "HTML",
"image": "Image", "image": "Image",

View File

@@ -88,6 +88,7 @@
"io-ts": "^2.2.16", "io-ts": "^2.2.16",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"json-loader": "^0.5.7", "json-loader": "^0.5.7",
"jsonpath-plus": "^6.0.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lossless-json": "^1.0.5", "lossless-json": "^1.0.5",
"mustache": "^4.2.0", "mustache": "^4.2.0",

View File

@@ -0,0 +1,6 @@
import { JSONPathOptions } from "jsonpath-plus"
declare module "jsonpath-plus" {
export type JSONPathType = (options: JSONPathOptions) => unknown
export const JSONPath: JSONPathType
}

View File

@@ -18,6 +18,7 @@ export default defineConfig({
"var(--upper-mobile-raw-tertiary-sticky-fold)", "var(--upper-mobile-raw-tertiary-sticky-fold)",
lowerPrimaryStickyFold: "var(--lower-primary-sticky-fold)", lowerPrimaryStickyFold: "var(--lower-primary-sticky-fold)",
lowerSecondaryStickyFold: "var(--lower-secondary-sticky-fold)", lowerSecondaryStickyFold: "var(--lower-secondary-sticky-fold)",
lowerTertiaryStickyFold: "var(--lower-tertiary-sticky-fold)",
sidebarPrimaryStickyFold: "var(--sidebar-primary-sticky-fold)", sidebarPrimaryStickyFold: "var(--sidebar-primary-sticky-fold)",
}, },
colors: { colors: {

25
pnpm-lock.yaml generated
View File

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