diff --git a/package-lock.json b/package-lock.json index fc2d827f6..21bb02a52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30268,4 +30268,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/bruno-app/package.json b/packages/bruno-app/package.json index de53b1ef7..232cd5cfb 100644 --- a/packages/bruno-app/package.json +++ b/packages/bruno-app/package.json @@ -39,6 +39,7 @@ "idb": "^7.0.0", "immer": "^9.0.15", "jsesc": "^3.0.2", + "jsonpath-plus": "^7.2.0", "jshint": "^2.13.6", "jsonlint": "^1.6.3", "know-your-http-well": "^0.5.0", 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..250610865 --- /dev/null +++ b/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultFilter/index.js @@ -0,0 +1,43 @@ +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 && } + + +
+ ); +}; + +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 64ab32f7e..9c3dcbc95 100644 --- a/packages/bruno-app/src/components/ResponsePane/QueryResult/StyledWrapper.js +++ b/packages/bruno-app/src/components/ResponsePane/QueryResult/StyledWrapper.js @@ -3,7 +3,7 @@ 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 1fr 2.25rem' : '1.25rem 1fr')}; /* This is a hack to force Codemirror to use all available space */ > div { @@ -40,6 +40,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 49f592aeb..2c02d42b0 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 === undefined) { 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']; @@ -81,8 +97,14 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven )); }, [allowedPreviewModes, previewTab]); + const queryFilterEnabled = useMemo(() => mode.includes('json'), [mode]); + return ( - +
{tabs}
@@ -98,19 +120,22 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven ) : null} ) : ( - + <> + + {queryFilterEnabled && } + )}
);