mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-09 06:55:03 +00:00
Merge branch 'main' into feature/always-indent-json-in-queryresult
This commit is contained in:
@@ -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;
|
||||
@@ -2,6 +2,11 @@ import CodeEditor from 'components/CodeEditor/index';
|
||||
import { get } from 'lodash';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { Document, Page } from 'react-pdf';
|
||||
import { useState } from 'react';
|
||||
import 'pdfjs-dist/build/pdf.worker';
|
||||
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
|
||||
import 'react-pdf/dist/esm/Page/TextLayer.css';
|
||||
|
||||
const QueryResultPreview = ({
|
||||
previewTab,
|
||||
@@ -19,6 +24,10 @@ const QueryResultPreview = ({
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [numPages, setNumPages] = useState(null);
|
||||
function onDocumentLoadSuccess({ numPages }) {
|
||||
setNumPages(numPages);
|
||||
}
|
||||
// Fail safe, so we don't render anything with an invalid tab
|
||||
if (!allowedPreviewModes.includes(previewTab)) {
|
||||
return null;
|
||||
@@ -45,6 +54,17 @@ const QueryResultPreview = ({
|
||||
case 'preview-image': {
|
||||
return <img src={`data:${contentType.replace(/\;(.*)/, '')};base64,${dataBuffer}`} className="mx-auto" />;
|
||||
}
|
||||
case 'preview-pdf': {
|
||||
return (
|
||||
<div className="preview-pdf" style={{ height: '100%', overflow: 'auto', maxHeight: 'calc(100vh - 220px)' }}>
|
||||
<Document file={`data:application/pdf;base64,${dataBuffer}`} onLoadSuccess={onDocumentLoadSuccess}>
|
||||
{Array.from(new Array(numPages), (el, index) => (
|
||||
<Page key={`page_${index + 1}`} pageNumber={index + 1} renderAnnotationLayer={false} />
|
||||
))}
|
||||
</Document>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
default:
|
||||
case 'raw': {
|
||||
return (
|
||||
|
||||
@@ -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 {
|
||||
@@ -18,6 +18,19 @@ const StyledWrapper = styled.div`
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.react-pdf__Page {
|
||||
margin-top: 10px;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
.react-pdf__Page__textContent {
|
||||
border: 1px solid darkgrey;
|
||||
box-shadow: 5px 5px 5px 1px #ccc;
|
||||
border-radius: 0px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.react-pdf__Page__canvas {
|
||||
margin: 0 auto;
|
||||
}
|
||||
div[role='tablist'] {
|
||||
.active {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
@@ -27,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;
|
||||
|
||||
@@ -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) => {
|
||||
if (!data) {
|
||||
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, data);
|
||||
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'];
|
||||
@@ -49,6 +65,8 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
|
||||
allowedPreviewModes.unshift('preview-web');
|
||||
} else if (mode.includes('image')) {
|
||||
allowedPreviewModes.unshift('preview-image');
|
||||
} else if (contentType.includes('pdf')) {
|
||||
allowedPreviewModes.unshift('preview-pdf');
|
||||
}
|
||||
|
||||
return allowedPreviewModes;
|
||||
@@ -79,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>
|
||||
@@ -96,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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user