diff --git a/package-lock.json b/package-lock.json
index ade21b124..aa7cfa7cb 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 77ff96b67..c4bee9493 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 000000000..bc853ecad
--- /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 07f51b8bd..e6cb20987 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 b5508ba1b..5dbe6168d 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 && }
+ >
)}
);