Skip to content
Open
Show file tree
Hide file tree
Changes from all 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" title={jqError}>{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