Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/bruno-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"i18next": "24.1.2",
"idb": "^7.0.0",
"immer": "^9.0.15",
"jq-wasm": "1.1.0-jq-1.8.1",
"js-yaml": "^4.1.0",
"jsesc": "^3.0.2",
"jshint": "^2.13.6",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useRef } from 'react';
import { useState } from 'react';
import { Tooltip as ReactInfotip } from 'react-tooltip';

const QueryResultFilter = ({ filter, onChange, mode }) => {
const QueryResultFilter = ({ filter, onChange, mode, filterType, onFilterTypeChange, jqError }) => {
const inputRef = useRef(null);
const [isExpanded, toggleExpand] = useState(false);

Expand All @@ -19,35 +19,61 @@ const QueryResultFilter = ({ filter, onChange, mode }) => {
}
};

const handleFilterTypeChange = (type) => {
if (type === filterType) return;
onFilterTypeChange(type);
// Clear input when switching filter type
onChange({ target: { value: '' } });
if (inputRef?.current) {
inputRef.current.value = '';
}
};

const infotipText = useMemo(() => {
if (mode.includes('json')) {
return 'Filter with JSONPath';
return filterType === 'jq' ? 'Filter with jq' : 'Filter with JSONPath';
}

if (mode.includes('xml')) {
return 'Filter with XPath';
}

return null;
}, [mode]);
}, [mode, filterType]);

const placeholderText = useMemo(() => {
if (mode.includes('json')) {
return '$.store.books..author';
return filterType === 'jq' ? '.store.books[].author' : '$.store.books..author';
}

if (mode.includes('xml')) {
return '/store/books//author';
}

return null;
}, [mode]);
}, [mode, filterType]);

return (
<div
className="response-filter absolute bottom-2 w-full justify-end right-0 flex flex-row items-center gap-2 py-4 px-2 pointer-events-none"
>
{infotipText && !isExpanded && <ReactInfotip anchorId="request-filter-icon" html={infotipText} />}
{isExpanded && mode.includes('json') && (
<div className="filter-type-toggle flex items-center pointer-events-auto">
<button
className={`toggle-btn ${filterType === 'jsonpath' ? 'active' : ''}`}
onClick={() => handleFilterTypeChange('jsonpath')}
>
JSONPath
</button>
<button
className={`toggle-btn ${filterType === 'jq' ? 'active' : ''}`}
onClick={() => handleFilterTypeChange('jq')}
>
jq
</button>
</div>
)}
<input
ref={inputRef}
type="text"
Expand All @@ -58,14 +84,17 @@ const QueryResultFilter = ({ filter, onChange, mode }) => {
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
className={`block ml-14 p-2 py-1 transition-all duration-200 ease-in-out border border-gray-300 rounded-md ${
className={`block ${isExpanded && mode.includes('json') ? 'ml-0' : 'ml-14'} p-2 py-1 transition-all duration-200 ease-in-out border border-gray-300 rounded-md ${
isExpanded ? 'w-full opacity-100 pointer-events-auto' : 'w-[0] opacity-0'
}`}
onChange={onChange}
/>
<div className="text-gray-500 cursor-pointer pointer-events-auto" id="request-filter-icon" onClick={handleFilterClick}>
{isExpanded ? <IconX size={20} strokeWidth={1.5} /> : <IconFilter size={20} strokeWidth={1.5} />}
</div>
{isExpanded && jqError && (
<div className="jq-error pointer-events-auto">{jqError}</div>
)}
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,55 @@ const StyledWrapper = styled.div`
outline: none;
}
}

.jq-error {
position: absolute;
bottom: 100%;
right: 0;
margin-bottom: 4px;
padding: 2px 8px;
font-size: 11px;
color: ${(props) => props.theme.colors.text.danger};
background-color: ${(props) => props.theme.background.base};
border: solid 1px ${(props) => props.theme.colors.text.danger};
border-radius: ${(props) => props.theme.border.radius.sm};
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}

.filter-type-toggle {
flex-shrink: 0;

.toggle-btn {
font-size: 11px;
padding: 2px 8px;
border: solid 1px ${(props) => props.theme.border.border2};
background-color: ${(props) => props.theme.background.mantle};
color: ${(props) => props.theme.colors.text.muted};
cursor: pointer;
white-space: nowrap;

&:first-child {
border-radius: 4px 0 0 4px;
}

&:last-child {
border-radius: 0 4px 4px 0;
margin-left: -1px;
}

&.active {
background-color: ${(props) => props.theme.background.base};
color: ${(props) => props.theme.colors.text.yellow};
border-color: ${(props) => props.theme.colors.text.yellow};
font-weight: 600;
position: relative;
z-index: 1;
}
}
}
}
`;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { debounce } from 'lodash';
import { useTheme } from 'providers/Theme/index';
import React, { useMemo, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { formatResponse, getContentType } from 'utils/common';
import { runJqFilter } from 'utils/common/jq-service';
import { getDefaultResponseFormat, detectContentTypeFromBase64 } from 'utils/response';
import LargeResponseWarning from '../LargeResponseWarning';
import QueryResultFilter from './QueryResultFilter';
Expand Down Expand Up @@ -102,6 +103,9 @@ const QueryResult = ({
}) => {
const contentType = getContentType(headers);
const [filter, setFilter] = useState(null);
const [filterType, setFilterType] = useState('jsonpath');
const [jqResult, setJqResult] = useState(null);
const [jqError, setJqError] = useState(null);
const [showLargeResponse, setShowLargeResponse] = useState(false);
const { displayedTheme } = useTheme();

Expand All @@ -124,14 +128,46 @@ const QueryResult = ({
return detectContentTypeFromBase64(dataBuffer);
}, [dataBuffer, isLargeResponse]);

// Run jq filter asynchronously when filterType is 'jq'
useEffect(() => {
if (filterType !== 'jq' || !filter) {
setJqResult(null);
setJqError(null);
return;
}
let cancelled = false;
setJqResult(null);
setJqError(null);
runJqFilter(data, filter)
.then((result) => {
if (!cancelled) {
setJqResult(result);
setJqError(null);
}
})
.catch((err) => {
if (!cancelled) {
setJqResult(null);
setJqError(err.message);
}
});
return () => { cancelled = true; };
}, [data, filter, filterType]);

const formattedData = useMemo(
() => {
if (isLargeResponse && !showLargeResponse) {
return '';
}
return formatResponse(data, dataBuffer, selectedFormat, filter);
// For jq mode with an active filter, use the async result
if (filterType === 'jq' && filter) {
return jqResult ?? formatResponse(data, dataBuffer, selectedFormat, null);
}
// For JSONPath mode, pass filter to formatResponse as before
const jsonPathFilter = filterType === 'jsonpath' ? filter : null;
return formatResponse(data, dataBuffer, selectedFormat, jsonPathFilter);
},
[data, dataBuffer, selectedFormat, filter, isLargeResponse, showLargeResponse]
[data, dataBuffer, selectedFormat, filter, filterType, jqResult, isLargeResponse, showLargeResponse]
);

const debouncedResultFilterOnChange = debounce((e) => {
Expand Down Expand Up @@ -213,7 +249,14 @@ const QueryResult = ({
/>
</div>
{queryFilterEnabled && (
<QueryResultFilter filter={filter} onChange={debouncedResultFilterOnChange} mode={codeMirrorMode} />
<QueryResultFilter
filter={filter}
onChange={debouncedResultFilterOnChange}
mode={codeMirrorMode}
filterType={filterType}
onFilterTypeChange={setFilterType}
jqError={jqError}
/>
)}
</div>
</div>
Expand Down
14 changes: 14 additions & 0 deletions packages/bruno-app/src/utils/common/jq-service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as jq from 'jq-wasm';

export async function runJqFilter(data, filter) {
try {
const result = await jq.raw(data, filter);
if (result.exitCode !== 0) {
throw new Error(result.stderr || 'jq error');
}
return result.stdout;
} catch (e) {
console.warn('Could not apply jq filter:', e.message);
throw e;
}
}
2 changes: 1 addition & 1 deletion packages/bruno-electron/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ const contentSecurityPolicy = [
'connect-src \'self\' https://*.posthog.com',
'font-src \'self\' https: data:;',
'frame-src data:',
'script-src \'self\' data:',
'script-src \'self\' data: \'wasm-unsafe-eval\'',
// this has been commented out to make oauth2 work
// "form-action 'none'",
// we make an exception and allow http for images so that
Expand Down
Loading