Merge pull request #913 from Oryss/feature/jsonpath-filtering

[Feature] Add JSONPath response filtering
This commit is contained in:
Anoop M D 2023-11-30 00:19:07 +05:30 committed by GitHub
commit 8e78c1b4e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 103 additions and 18 deletions

2
package-lock.json generated
View File

@ -30268,4 +30268,4 @@
}
}
}
}
}

View File

@ -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",

View File

@ -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 (
<div className={'response-filter relative'}>
<div className="absolute inset-y-0 left-0 pl-4 flex items-center">
<div className="text-gray-500 sm:text-sm" id="request-filter-icon">
<IconFilter size={16} strokeWidth={1.5} />
</div>
</div>
{tooltipText && <ReactTooltip anchorId={'request-filter-icon'} html={tooltipText} />}
<input
type="text"
name="response-filter"
id="response-filter"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
className="block w-full pl-10 py-1 sm:text-sm"
onChange={onChange}
/>
</div>
);
};
export default QueryResultFilter;

View File

@ -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;

View File

@ -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 (
<StyledWrapper className="w-full h-full" style={{ maxWidth: width }}>
<StyledWrapper
className="w-full h-full relative"
style={{ maxWidth: width }}
queryFilterEnabled={queryFilterEnabled}
>
<div className="flex justify-end gap-2 text-xs" role="tablist">
{tabs}
</div>
@ -98,19 +120,22 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
) : null}
</div>
) : (
<QueryResultPreview
previewTab={previewTab}
data={data}
dataBuffer={dataBuffer}
formattedData={formattedData}
item={item}
contentType={contentType}
mode={mode}
collection={collection}
allowedPreviewModes={allowedPreviewModes}
disableRunEventListener={disableRunEventListener}
storedTheme={storedTheme}
/>
<>
<QueryResultPreview
previewTab={previewTab}
data={data}
dataBuffer={dataBuffer}
formattedData={formattedData}
item={item}
contentType={contentType}
mode={mode}
collection={collection}
allowedPreviewModes={allowedPreviewModes}
disableRunEventListener={disableRunEventListener}
storedTheme={storedTheme}
/>
{queryFilterEnabled && <QueryResultFilter onChange={debouncedResultFilterOnChange} mode={mode} />}
</>
)}
</StyledWrapper>
);