mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-25 05:35:41 +00:00
Fix/response pane optimizations (#6395)
* refactor: update content type detection to use base64 decoding * fix: some styling issues and autofocus issues in input resolved * refactor: enhance ResponsePane and QueryResult components for improved response handling and size display * refactor: simplify size display logic in ResponseSize component * refactor: improve size formatting logic in ResponseSize component for better readability * refactor: enhance base64 decoding function to handle invalid input and improve error handling
This commit is contained in:
@@ -137,6 +137,7 @@ const CollectionProperties = ({ onClose }) => {
|
||||
value={searchText || ''}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
className="block textbox non-passphrase-input ml-auto font-normal"
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
|
||||
@@ -20,7 +20,9 @@ export default function XmlPreview({ data, defaultExpanded = true }) {
|
||||
// Check for parsing error
|
||||
if (parsedData && typeof parsedData === 'object' && parsedData.error) {
|
||||
return (
|
||||
<ErrorAlert title="Cannot preview as XML" message={parsedData.error} />
|
||||
<div className="px-2">
|
||||
<ErrorAlert title="Cannot preview as XML" message={parsedData.error} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -34,7 +36,9 @@ export default function XmlPreview({ data, defaultExpanded = true }) {
|
||||
|
||||
if (!isValidTreeData(parsedData)) {
|
||||
return (
|
||||
<ErrorAlert title="Cannot preview as XML" message="Data cannot be rendered as a tree. Expected a valid XML string." />
|
||||
<div className="px-2">
|
||||
<ErrorAlert title="Cannot preview as XML" message="Data cannot be rendered as a tree. Expected a valid XML string." />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,7 +54,9 @@ export default function XmlPreview({ data, defaultExpanded = true }) {
|
||||
} else if (keys.length === 0) {
|
||||
// Empty object with no children
|
||||
return (
|
||||
<ErrorAlert title="Cannot preview as XML" message="Cannot render XML tree. Root object is empty." />
|
||||
<div className="px-2">
|
||||
<ErrorAlert title="Cannot preview as XML" message="Cannot render XML tree. Root object is empty." />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,11 @@ import { debounce } from 'lodash';
|
||||
import { useTheme } from 'providers/Theme/index';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { formatResponse, getContentType } from 'utils/common';
|
||||
import { getEncoding } from 'utils/common/index';
|
||||
import { getDefaultResponseFormat } from 'utils/response';
|
||||
import { getDefaultResponseFormat, detectContentTypeFromBase64 } from 'utils/response';
|
||||
import LargeResponseWarning from '../LargeResponseWarning';
|
||||
import QueryResultFilter from './QueryResultFilter';
|
||||
import QueryResultPreview from './QueryResultPreview';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { detectContentTypeFromBuffer } from 'utils/response/index';
|
||||
|
||||
const PREVIEW_FORMAT_OPTIONS = [
|
||||
{
|
||||
@@ -46,15 +44,7 @@ const formatErrorMessage = (error) => {
|
||||
// Custom hook to determine the initial format and tab based on the data buffer and headers
|
||||
export const useInitialResponseFormat = (dataBuffer, headers) => {
|
||||
return useMemo(() => {
|
||||
let buffer = null;
|
||||
try {
|
||||
buffer = dataBuffer ? Buffer.from(dataBuffer, 'base64') : null;
|
||||
} catch (error) {
|
||||
console.error('Error converting dataBuffer to Buffer:', error);
|
||||
buffer = null;
|
||||
}
|
||||
|
||||
const detectedContentType = detectContentTypeFromBuffer(buffer);
|
||||
const detectedContentType = detectContentTypeFromBase64(dataBuffer);
|
||||
const contentType = getContentType(headers);
|
||||
|
||||
// Wait until both content types are available
|
||||
@@ -70,15 +60,7 @@ export const useInitialResponseFormat = (dataBuffer, headers) => {
|
||||
// Custom hook to determine preview format options based on content type
|
||||
export const useResponsePreviewFormatOptions = (dataBuffer, headers) => {
|
||||
return useMemo(() => {
|
||||
let buffer = null;
|
||||
try {
|
||||
buffer = dataBuffer ? Buffer.from(dataBuffer, 'base64') : null;
|
||||
} catch (error) {
|
||||
console.error('Error converting dataBuffer to Buffer:', error);
|
||||
buffer = null;
|
||||
}
|
||||
|
||||
const detectedContentType = detectContentTypeFromBuffer(buffer);
|
||||
const detectedContentType = detectContentTypeFromBase64(dataBuffer);
|
||||
const contentType = getContentType(headers);
|
||||
|
||||
const byteFormatTypes = ['image', 'video', 'audio', 'pdf', 'zip'];
|
||||
@@ -115,18 +97,9 @@ const QueryResult = ({
|
||||
selectedFormat, // one of the options in PREVIEW_FORMAT_OPTIONS
|
||||
selectedTab // 'editor' or 'preview'
|
||||
}) => {
|
||||
let buffer = null;
|
||||
try {
|
||||
buffer = Buffer.from(dataBuffer, 'base64'); // dataBuffer is already a base64 string, convert it to actual Buffer
|
||||
} catch (error) {
|
||||
console.error('Error converting dataBuffer to Buffer:', error);
|
||||
buffer = null;
|
||||
}
|
||||
const detectedContentType = detectContentTypeFromBuffer(buffer);
|
||||
const contentType = getContentType(headers);
|
||||
const [filter, setFilter] = useState(null);
|
||||
const [showLargeResponse, setShowLargeResponse] = useState(false);
|
||||
const responseEncoding = getEncoding(headers);
|
||||
const { displayedTheme } = useTheme();
|
||||
|
||||
const responseSize = useMemo(() => {
|
||||
@@ -135,19 +108,19 @@ const QueryResult = ({
|
||||
return response.size;
|
||||
}
|
||||
|
||||
if (!dataBuffer) return 0;
|
||||
|
||||
try {
|
||||
// dataBuffer is base64 encoded, so we need to calculate the actual size
|
||||
const buffer = Buffer.from(dataBuffer, 'base64');
|
||||
return buffer.length;
|
||||
} catch (error) {
|
||||
return 0;
|
||||
// Fallback: estimate from base64 length (base64 is ~4/3 of original size)
|
||||
if (dataBuffer && typeof dataBuffer === 'string') {
|
||||
return Math.floor(dataBuffer.length * 0.75);
|
||||
}
|
||||
return 0;
|
||||
}, [dataBuffer, item.response]);
|
||||
|
||||
const isLargeResponse = responseSize > 10 * 1024 * 1024; // 10 MB
|
||||
|
||||
const detectedContentType = useMemo(() => {
|
||||
return detectContentTypeFromBase64(dataBuffer);
|
||||
}, [dataBuffer, isLargeResponse]);
|
||||
|
||||
const formattedData = useMemo(
|
||||
() => {
|
||||
if (isLargeResponse && !showLargeResponse) {
|
||||
@@ -155,7 +128,7 @@ const QueryResult = ({
|
||||
}
|
||||
return formatResponse(data, dataBuffer, selectedFormat, filter);
|
||||
},
|
||||
[data, dataBuffer, responseEncoding, selectedFormat, filter, isLargeResponse, showLargeResponse]
|
||||
[data, dataBuffer, selectedFormat, filter, isLargeResponse, showLargeResponse]
|
||||
);
|
||||
|
||||
const debouncedResultFilterOnChange = debounce((e) => {
|
||||
|
||||
@@ -39,9 +39,11 @@ const ResponsePane = ({ item, collection }) => {
|
||||
const [selectedTab, setSelectedTab] = useState('editor');
|
||||
const rightContentRef = useRef(null);
|
||||
|
||||
// Initialize format and tab only once when data loads
|
||||
const { initialFormat, initialTab } = useInitialResponseFormat(item.response?.dataBuffer, item.response?.headers);
|
||||
const previewFormatOptions = useResponsePreviewFormatOptions(item.response?.dataBuffer, item.response?.headers);
|
||||
const response = item.response || {};
|
||||
|
||||
// Initialize format and tab only once when data loads.
|
||||
const { initialFormat, initialTab } = useInitialResponseFormat(response?.dataBuffer, response?.headers);
|
||||
const previewFormatOptions = useResponsePreviewFormatOptions(response?.dataBuffer, response?.headers);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialFormat !== null && initialTab !== null) {
|
||||
@@ -68,9 +70,6 @@ const ResponsePane = ({ item, collection }) => {
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const response = item.response || {};
|
||||
|
||||
const responseSize = useMemo(() => {
|
||||
if (typeof response.size === 'number') {
|
||||
return response.size;
|
||||
|
||||
@@ -34,6 +34,7 @@ const SearchInput = ({
|
||||
spellCheck="false"
|
||||
className="block w-full pl-7 py-2 rounded-md"
|
||||
value={searchText}
|
||||
autoFocus
|
||||
onChange={handleChange}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -13,6 +13,7 @@ const CollectionSearch = ({ searchText, setSearchText }) => {
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
autoFocus
|
||||
spellCheck="false"
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value.toLowerCase())}
|
||||
|
||||
@@ -170,6 +170,47 @@ export const isValidHtmlSnippet = (snippet) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Decode only the first N bytes from a Base64 string
|
||||
* Returns an empty buffer for invalid/missing input
|
||||
*/
|
||||
const decodeBase64Head = (base64, byteCount) => {
|
||||
// Validate input is a non-empty string
|
||||
if (!base64 || typeof base64 !== 'string') {
|
||||
return Buffer.alloc(0);
|
||||
}
|
||||
|
||||
try {
|
||||
// Safely remove data URL prefix (e.g., "data:image/png;base64,")
|
||||
const prefixMatch = base64.match(/^data:[^;]*;base64,/);
|
||||
const cleanedBase64 = prefixMatch ? base64.slice(prefixMatch[0].length) : base64;
|
||||
|
||||
// Return empty buffer if nothing left after stripping prefix
|
||||
if (!cleanedBase64) {
|
||||
return Buffer.alloc(0);
|
||||
}
|
||||
|
||||
// How many base64 chars needed to reconstruct "byteCount" bytes
|
||||
const neededChars = Math.ceil(byteCount / 3) * 4;
|
||||
|
||||
// Slice only required chars
|
||||
let slice = cleanedBase64.slice(0, neededChars);
|
||||
|
||||
// Sanitize: remove any non-base64 characters (whitespace, invalid chars)
|
||||
slice = slice.replace(/[^A-Za-z0-9+/=]/g, '');
|
||||
|
||||
// Pad to valid base64 length (must be multiple of 4)
|
||||
const padLength = (4 - (slice.length % 4)) % 4;
|
||||
slice = slice + '='.repeat(padLength);
|
||||
|
||||
// Decode and trim to requested bytes
|
||||
return Buffer.from(slice, 'base64').subarray(0, byteCount);
|
||||
} catch (error) {
|
||||
// On any decoding error, return an empty buffer
|
||||
return Buffer.alloc(0);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Detects content type from buffer by checking magic numbers (file signatures)
|
||||
* @param {Buffer} buffer - The data buffer to analyze
|
||||
@@ -258,3 +299,23 @@ export const detectContentTypeFromBuffer = (buffer) => {
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Main: detect from base64 string
|
||||
*/
|
||||
export const detectContentTypeFromBase64 = (base64) => {
|
||||
if (!base64) return null;
|
||||
|
||||
// 1. Decode first 12 bytes (magic numbers)
|
||||
const magicHead = decodeBase64Head(base64, 12);
|
||||
|
||||
const magicType = detectContentTypeFromBuffer(magicHead);
|
||||
if (magicType) return magicType;
|
||||
|
||||
// 2. If not binary → decode up to 512 bytes for text detection
|
||||
const textHead = decodeBase64Head(base64, 512);
|
||||
|
||||
if (isLikelyText(textHead)) return 'text/plain';
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user