diff --git a/packages/bruno-app/src/components/ResponsePane/LargeResponseWarning/StyledWrapper.js b/packages/bruno-app/src/components/ResponsePane/LargeResponseWarning/StyledWrapper.js new file mode 100644 index 000000000..0a5339df8 --- /dev/null +++ b/packages/bruno-app/src/components/ResponsePane/LargeResponseWarning/StyledWrapper.js @@ -0,0 +1,65 @@ +import styled from 'styled-components'; + +const StyledWrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; + + .warning-container { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 1.5rem; + margin-top: 10%; + text-align: center; + max-width: 480px; + } + + .warning-icon { + margin-bottom: 1rem; + color: ${(props) => props.theme.colors.text.yellow}; + } + + .warning-title { + font-weight: 600; + color: ${(props) => props.theme.text}; + margin-bottom: 1rem; + } + + .warning-description { + color: ${(props) => props.theme.colors.text.muted}; + + .size-highlight { + padding: 0.125rem 0.375rem; + border-radius: 4px; + font-size: 0.8rem; + } + + .current-size { + color: ${(props) => props.theme.colors.text.danger}; + background: ${(props) => props.theme.colors.text.danger}15; + } + + .supported-size { + color: ${(props) => props.theme.colors.text.yellow}; + background: ${(props) => props.theme.colors.text.yellow}15; + } + } + + .warning-actions { + display: flex; + gap: 0.75rem; + } + + button { + align-items: center; + display: flex; + gap: 0.5rem; + background: ${(props) => props.theme.button.secondary.bg}; + border-radius: 4px; + padding: 0.5rem 1rem; + cursor: pointer; + } +`; + +export default StyledWrapper; diff --git a/packages/bruno-app/src/components/ResponsePane/LargeResponseWarning/index.js b/packages/bruno-app/src/components/ResponsePane/LargeResponseWarning/index.js new file mode 100644 index 000000000..1686b4a38 --- /dev/null +++ b/packages/bruno-app/src/components/ResponsePane/LargeResponseWarning/index.js @@ -0,0 +1,92 @@ +import React from 'react'; +import { IconDownload, IconCopy, IconEye, IconAlertTriangle } from '@tabler/icons'; +import toast from 'react-hot-toast'; +import get from 'lodash/get'; +import StyledWrapper from './StyledWrapper'; +import { formatSize } from 'utils/common/index'; + +const LargeResponseWarning = ({ item, responseSize, onRevealResponse }) => { + const { ipcRenderer } = window; + const response = item.response || {}; + + const saveResponseToFile = () => { + return new Promise((resolve, reject) => { + ipcRenderer + .invoke('renderer:save-response-to-file', response, item.requestSent.url) + .then(() => { + toast.success('Response saved to file'); + resolve(); + }) + .catch((err) => { + toast.error(get(err, 'error.message') || 'Something went wrong!'); + reject(err); + }); + }); + }; + + const copyResponse = () => { + try { + const textToCopy = typeof response.data === 'string' + ? response.data + : JSON.stringify(response.data, null, 2); + + navigator.clipboard.writeText(textToCopy).then(() => { + toast.success('Response copied to clipboard'); + }).catch(() => { + toast.error('Failed to copy response'); + }); + } catch (error) { + toast.error('Failed to copy response'); + } + }; + + return ( + +
+
+ +
+
+
+ Large Response Warning +
+
+ Handling responses over {formatSize(10 * 1024 * 1024)} could degrade performance. +
+ Size of current response: {formatSize(responseSize)} +
+
+
+
+ + + +
+
+ ); +}; + +export default LargeResponseWarning; \ No newline at end of file diff --git a/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js b/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js index 1407e69ae..229a40072 100644 --- a/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js +++ b/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js @@ -11,6 +11,7 @@ import StyledWrapper from './StyledWrapper'; import { useState, useMemo, useEffect } from 'react'; import { useTheme } from 'providers/Theme/index'; import { getEncoding, uuid } from 'utils/common/index'; +import LargeResponseWarning from '../LargeResponseWarning'; const formatResponse = (data, dataBuffer, encoding, mode, filter) => { if (data === undefined || !dataBuffer || !mode) { @@ -77,6 +78,7 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven const contentType = getContentType(headers); const mode = getCodeMirrorModeBasedOnContentType(contentType, data); const [filter, setFilter] = useState(null); + const [showLargeResponse, setShowLargeResponse] = useState(false); const responseEncoding = getEncoding(headers); const formattedData = useMemo( () => formatResponse(data, dataBuffer, responseEncoding, mode, filter), @@ -84,6 +86,25 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven ); const { displayedTheme } = useTheme(); + const responseSize = useMemo(() => { + const response = item.response || {}; + if (typeof response.size === 'number') { + 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; + } + }, [dataBuffer, item.response]); + + const isLargeResponse = responseSize > 10 * 1024 * 1024; // 10 MB + const debouncedResultFilterOnChange = debounce((e) => { setFilter(e.target.value); }, 250); @@ -160,6 +181,12 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven ) : null} + ) : isLargeResponse && !showLargeResponse ? ( + setShowLargeResponse(true)} + /> ) : (
diff --git a/packages/bruno-app/src/themes/light.js b/packages/bruno-app/src/themes/light.js index e95b0e45e..55b1d0eaf 100644 --- a/packages/bruno-app/src/themes/light.js +++ b/packages/bruno-app/src/themes/light.js @@ -7,7 +7,7 @@ const lightTheme = { colors: { text: { green: '#047857', - danger: 'rgb(185, 28, 28)', + danger: '#B91C1C', muted: '#838383', purple: '#8e44ad', yellow: '#d97706' diff --git a/packages/bruno-app/src/utils/common/index.js b/packages/bruno-app/src/utils/common/index.js index 937e1a3b5..f839ba850 100644 --- a/packages/bruno-app/src/utils/common/index.js +++ b/packages/bruno-app/src/utils/common/index.js @@ -196,4 +196,23 @@ export const getEncoding = (headers) => { export const multiLineMsg = (...messages) => { return messages.filter(m => m !== undefined && m !== null && m !== '').join('\n'); +} + +export const formatSize = (bytes) => { + // Handle invalid inputs + if (isNaN(bytes) || typeof bytes !== 'number') { + return '0B'; + } + + if (bytes < 1024) { + return bytes + 'B'; + } + if (bytes < 1024 * 1024) { + return (bytes / 1024).toFixed(1) + 'KB'; + } + if (bytes < 1024 * 1024 * 1024) { + return (bytes / (1024 * 1024)).toFixed(1) + 'MB'; + } + + return (bytes / (1024 * 1024 * 1024)).toFixed(1) + 'GB'; } \ No newline at end of file diff --git a/packages/bruno-app/src/utils/common/index.spec.js b/packages/bruno-app/src/utils/common/index.spec.js index 81153674a..89eeebf2e 100644 --- a/packages/bruno-app/src/utils/common/index.spec.js +++ b/packages/bruno-app/src/utils/common/index.spec.js @@ -1,6 +1,6 @@ const { describe, it, expect } = require('@jest/globals'); -import { normalizeFileName, startsWith, humanizeDate, relativeDate, getContentType } from './index'; +import { normalizeFileName, startsWith, humanizeDate, relativeDate, getContentType, formatSize } from './index'; describe('common utils', () => { describe('normalizeFileName', () => { @@ -148,4 +148,40 @@ describe('common utils', () => { expect(getContentType(undefined)).toBe(''); }); }); + + describe('formatSize', () => { + it('should format bytes', () => { + expect(formatSize(0)).toBe('0B'); + expect(formatSize(1023)).toBe('1023B'); + }); + + it('should format kilobytes', () => { + expect(formatSize(1024)).toBe('1.0KB'); + expect(formatSize(1048575)).toBe('1024.0KB'); + }); + + it('should format megabytes', () => { + expect(formatSize(1048576)).toBe('1.0MB'); + expect(formatSize(1073741823)).toBe('1024.0MB'); + }); + + it('should format gigabytes', () => { + expect(formatSize(1073741824)).toBe('1.0GB'); + expect(formatSize(1099511627776)).toBe('1024.0GB'); + }); + + it('should format decimal values', () => { + expect(formatSize(1126.5)).toBe('1.1KB'); + expect(formatSize(1153433.6)).toBe('1.1MB'); + expect(formatSize(1153433600)).toBe('1.1GB'); + expect(formatSize(1024.1)).toBe('1.0KB'); + expect(formatSize(1048576.1)).toBe('1.0MB'); + }); + + it('should format invalid inputs', () => { + expect(formatSize(null)).toBe('0B'); + expect(formatSize(undefined)).toBe('0B'); + expect(formatSize(NaN)).toBe('0B'); + }); + }); });