From 04aa921099a5bad9f14d65597d2c6f6667e20b0e Mon Sep 17 00:00:00 2001 From: Boris Baskovec Date: Tue, 7 Nov 2023 12:36:57 +0100 Subject: [PATCH 1/2] Add jsonpath response filtering --- package-lock.json | 17 +++++- packages/bruno-app/package.json | 1 + .../QueryResult/QueryResultFilter/index.js | 45 +++++++++++++++ .../ResponsePane/QueryResult/StyledWrapper.js | 19 ++++++- .../ResponsePane/QueryResult/index.js | 57 +++++++++++++------ 5 files changed, 121 insertions(+), 18 deletions(-) create mode 100644 packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultFilter/index.js diff --git a/package-lock.json b/package-lock.json index ade21b12..aa7cfa7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10903,6 +10903,14 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonpath-plus": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-7.2.0.tgz", + "integrity": "sha512-zBfiUPM5nD0YZSBT/o/fbCUlCcepMIdP0CJZxM1+KgA4f2T206f6VAg9e7mX35+KlMaIc5qXW34f3BnwJ3w+RA==", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/jsprim": { "version": "1.4.2", "dev": true, @@ -16653,6 +16661,7 @@ "httpsnippet": "^3.0.1", "idb": "^7.0.0", "immer": "^9.0.15", + "jsonpath-plus": "^7.2.0", "know-your-http-well": "^0.5.0", "lodash": "^4.17.21", "markdown-it": "^13.0.2", @@ -16820,7 +16829,7 @@ }, "packages/bruno-electron": { "name": "bruno", - "version": "v1.0.1", + "version": "v1.1.0", "dependencies": { "@aws-sdk/credential-providers": "^3.425.0", "@usebruno/js": "0.9.1", @@ -20760,6 +20769,7 @@ "httpsnippet": "^3.0.1", "idb": "^7.0.0", "immer": "^9.0.15", + "jsonpath-plus": "^7.2.0", "know-your-http-well": "^0.5.0", "lodash": "^4.17.21", "markdown-it": "^13.0.2", @@ -25003,6 +25013,11 @@ "universalify": "^2.0.0" } }, + "jsonpath-plus": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-7.2.0.tgz", + "integrity": "sha512-zBfiUPM5nD0YZSBT/o/fbCUlCcepMIdP0CJZxM1+KgA4f2T206f6VAg9e7mX35+KlMaIc5qXW34f3BnwJ3w+RA==" + }, "jsprim": { "version": "1.4.2", "dev": true, diff --git a/packages/bruno-app/package.json b/packages/bruno-app/package.json index 77ff96b6..c4bee949 100644 --- a/packages/bruno-app/package.json +++ b/packages/bruno-app/package.json @@ -36,6 +36,7 @@ "httpsnippet": "^3.0.1", "idb": "^7.0.0", "immer": "^9.0.15", + "jsonpath-plus": "^7.2.0", "know-your-http-well": "^0.5.0", "lodash": "^4.17.21", "markdown-it": "^13.0.2", diff --git a/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultFilter/index.js b/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultFilter/index.js new file mode 100644 index 00000000..bc853eca --- /dev/null +++ b/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultFilter/index.js @@ -0,0 +1,45 @@ +import { IconFilter } from '@tabler/icons'; +import React, { useMemo } from 'react'; +import { Tooltip as ReactTooltip } from 'react-tooltip'; + +const QueryResultFilter = ({ onChange, mode }) => { + const tooltipText = useMemo(() => { + if (mode.includes('json')) { + return 'Filter with JSONPath'; + } + + if (mode.includes('xml')) { + return 'Filter with XPath'; + } + + return null; + }, [mode]); + + return ( +
+
+
+ +
+
+ + {tooltipText && typeof tooltipText === 'string' && ( + + )} + + +
+ ); +}; + +export default QueryResultFilter; diff --git a/packages/bruno-app/src/components/ResponsePane/QueryResult/StyledWrapper.js b/packages/bruno-app/src/components/ResponsePane/QueryResult/StyledWrapper.js index 07f51b8b..e6cb2098 100644 --- a/packages/bruno-app/src/components/ResponsePane/QueryResult/StyledWrapper.js +++ b/packages/bruno-app/src/components/ResponsePane/QueryResult/StyledWrapper.js @@ -3,7 +3,8 @@ import styled from 'styled-components'; const StyledWrapper = styled.div` display: grid; grid-template-columns: 100%; - grid-template-rows: 1.25rem calc(100% - 1.25rem); + grid-template-rows: ${(props) => + props.queryFilterEnabled ? '1.25rem calc(100% - 3.5rem)' : '1.25rem calc(100% - 1.25rem)'}; /* This is a hack to force Codemirror to use all available space */ > div { @@ -27,6 +28,22 @@ const StyledWrapper = styled.div` .muted { color: ${(props) => props.theme.colors.text.muted}; } + + .response-filter { + position: absolute; + bottom: 0; + width: 100%; + + input { + border: ${(props) => props.theme.sidebar.search.border}; + border-radius: 2px; + background-color: ${(props) => props.theme.sidebar.search.bg}; + + &:focus { + outline: none; + } + } + } `; export default StyledWrapper; diff --git a/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js b/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js index b5508ba1..5dbe6168 100644 --- a/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js +++ b/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js @@ -1,3 +1,6 @@ +import { debounce } from 'lodash'; +import QueryResultFilter from './QueryResultFilter'; +import { JSONPath } from 'jsonpath-plus'; import React from 'react'; import classnames from 'classnames'; import { getContentType, safeStringifyJSON, safeParseXML } from 'utils/common'; @@ -10,12 +13,20 @@ import { useMemo } from 'react'; import { useEffect } from 'react'; import { useTheme } from 'providers/Theme/index'; -const formatResponse = (data, mode) => { +const formatResponse = (data, mode, filter) => { if (!data) { return ''; } if (mode.includes('json')) { + if (filter) { + try { + data = JSONPath({ path: filter, json: data }); + } catch (e) { + console.warn('Could not filter with JSONPath.', e.message); + } + } + return safeStringifyJSON(data, true); } @@ -38,9 +49,14 @@ const formatResponse = (data, mode) => { const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEventListener, headers, error }) => { const contentType = getContentType(headers); const mode = getCodeMirrorModeBasedOnContentType(contentType); - const formattedData = formatResponse(data, mode); + const [filter, setFilter] = useState(null); + const formattedData = formatResponse(data, mode, filter); const { storedTheme } = useTheme(); + const debouncedResultFilterOnChange = debounce((e) => { + setFilter(e.target.value); + }, 250); + const allowedPreviewModes = useMemo(() => { // Always show raw const allowedPreviewModes = ['raw']; @@ -79,8 +95,14 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven )); }, [allowedPreviewModes, previewTab]); + const queryFilterEnabled = useMemo(() => mode.includes('json'), [mode]); + return ( - +
{tabs}
@@ -96,19 +118,22 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven ) : null} ) : ( - + <> + + {queryFilterEnabled && } + )}
); From bab12d7894011a66b91cce6106b4e6465f534eee Mon Sep 17 00:00:00 2001 From: Boris Baskovec Date: Wed, 8 Nov 2023 22:45:45 +0100 Subject: [PATCH 2/2] Changes from review --- .../ResponsePane/QueryResult/QueryResultFilter/index.js | 4 +--- .../src/components/ResponsePane/QueryResult/StyledWrapper.js | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultFilter/index.js b/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultFilter/index.js index bc853eca..25061086 100644 --- a/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultFilter/index.js +++ b/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultFilter/index.js @@ -23,9 +23,7 @@ const QueryResultFilter = ({ onChange, mode }) => { - {tooltipText && typeof tooltipText === 'string' && ( - - )} + {tooltipText && } - props.queryFilterEnabled ? '1.25rem calc(100% - 3.5rem)' : '1.25rem calc(100% - 1.25rem)'}; + grid-template-rows: ${(props) => (props.queryFilterEnabled ? '1.25rem 1fr 2.25rem' : '1.25rem 1fr')}; /* This is a hack to force Codemirror to use all available space */ > div {