From 187e482feb0905b0f4ffcf9b25dd64ad6d19a525 Mon Sep 17 00:00:00 2001
From: Its-treason <39559178+Its-treason@users.noreply.github.com>
Date: Tue, 17 Oct 2023 00:07:51 +0200
Subject: [PATCH 1/2] feat(#596): Show real response in image preview
---
.../QueryResult/QueryResultPreview/index.js | 66 +++++++
.../ResponsePane/QueryResult/index.js | 180 +++++++-----------
.../src/components/ResponsePane/index.js | 1 +
.../RunnerResults/ResponsePane/index.js | 1 +
packages/bruno-app/src/utils/network/index.js | 2 +
.../bruno-electron/src/ipc/network/index.js | 9 +
.../src/ipc/network/prepare-request.js | 3 +-
7 files changed, 155 insertions(+), 107 deletions(-)
create mode 100644 packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultPreview/index.js
diff --git a/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultPreview/index.js b/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultPreview/index.js
new file mode 100644
index 0000000000..127d7e9c28
--- /dev/null
+++ b/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultPreview/index.js
@@ -0,0 +1,66 @@
+import CodeEditor from 'components/CodeEditor/index';
+import { get } from 'lodash';
+import { useDispatch, useSelector } from 'react-redux';
+import { useTheme } from 'providers/theme';
+
+const QueryResultPreview = ({
+ previewTab,
+ allowedPreviewModes,
+ data,
+ dataBuffer,
+ formattedData,
+ item,
+ contentType,
+ collection,
+ mode,
+ disableRunEventListener,
+ storedTheme
+}) => {
+ const preferences = useSelector((state) => state.app.preferences);
+ const dispatch = useDispatch();
+
+ // Fail safe, so we don't render anything with an invalid tab
+ if (!allowedPreviewModes.includes(previewTab)) {
+ return null;
+ }
+
+ const onRun = () => {
+ if (disableRunEventListener) {
+ return;
+ }
+ dispatch(sendRequest(item, collection.uid));
+ };
+
+ switch (previewTab) {
+ case 'preview-web': {
+ const webViewSrc = data.replace('
', ``);
+ return (
+
+ );
+ }
+ case 'preview-image': {
+ return ;
+ }
+ default:
+ case 'raw': {
+ console.log(mode, storedTheme);
+ return (
+
+ );
+ }
+ }
+};
+
+export default QueryResultPreview;
diff --git a/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js b/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js
index bb29abd3ab..5e25f7f80f 100644
--- a/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js
+++ b/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js
@@ -1,137 +1,105 @@
import React from 'react';
-import get from 'lodash/get';
-import CodeEditor from 'components/CodeEditor';
-import { useTheme } from 'providers/Theme';
-import { useDispatch, useSelector } from 'react-redux';
-import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import classnames from 'classnames';
import { getContentType, safeStringifyJSON, safeParseXML } from 'utils/common';
import { getCodeMirrorModeBasedOnContentType } from 'utils/common/codemirror';
+import QueryResultPreview from './QueryResultPreview';
import StyledWrapper from './StyledWrapper';
import { useState } from 'react';
import { useMemo } from 'react';
-
-const QueryResult = ({ item, collection, data, width, disableRunEventListener, headers, error }) => {
- const { storedTheme } = useTheme();
- const preferences = useSelector((state) => state.app.preferences);
- const [tab, setTab] = useState('preview');
- const dispatch = useDispatch();
- const contentType = getContentType(headers);
- const mode = getCodeMirrorModeBasedOnContentType(contentType);
-
- const formatResponse = (data, mode) => {
- if (!data) {
- return '';
+import { useEffect } from 'react';
+import { useTheme } from 'providers/Theme/index';
+
+const formatResponse = (data, mode) => {
+ if (!data) {
+ return '';
+ }
+
+ if (mode.includes('json')) {
+ return safeStringifyJSON(data, true);
+ }
+
+ if (mode.includes('xml')) {
+ let parsed = safeParseXML(data, { collapseContent: true });
+ if (typeof parsed === 'string') {
+ return parsed;
}
- if (mode.includes('json')) {
- return safeStringifyJSON(data, true);
- }
+ return safeStringifyJSON(parsed, true);
+ }
- if (mode.includes('xml')) {
- let parsed = safeParseXML(data, { collapseContent: true });
+ if (['text', 'html'].includes(mode) || typeof data === 'string') {
+ return data;
+ }
- if (typeof parsed === 'string') {
- return parsed;
- }
+ return safeStringifyJSON(data);
+};
- return safeStringifyJSON(parsed, true);
- }
+const QueryResult = ({ item, collection, data, width, disableRunEventListener, headers, error }) => {
+ const contentType = getContentType(headers);
+ const mode = getCodeMirrorModeBasedOnContentType(contentType);
+ const formattedData = formatResponse(data, mode);
+ const { storedTheme } = useTheme();
- if (['text', 'html'].includes(mode)) {
- if (typeof data === 'string') {
- return data;
- }
+ const allowedPreviewModes = useMemo(() => {
+ // Always show raw
+ const allowedPreviewModes = ['raw'];
- return safeStringifyJSON(data);
+ if (mode.includes('html') && typeof data === 'string') {
+ allowedPreviewModes.unshift('preview-web');
+ } else if (mode.includes('image')) {
+ allowedPreviewModes.unshift('preview-image');
}
- if (mode.includes('image')) {
- return item.requestSent.url;
- }
+ return allowedPreviewModes;
+ }, [mode, data, formattedData]);
- // final fallback
- if (typeof data === 'string') {
- return data;
+ const [previewTab, setPreviewTab] = useState(allowedPreviewModes[0]);
+ // Ensure the active Tab is always allowed
+ useEffect(() => {
+ if (!allowedPreviewModes.includes(previewTab)) {
+ setPreviewTab(allowedPreviewModes[0]);
}
+ }, [previewTab, allowedPreviewModes]);
- return safeStringifyJSON(data);
- };
-
- const value = formatResponse(data, mode);
-
- const onRun = () => {
- if (disableRunEventListener) {
- return;
- }
- dispatch(sendRequest(item, collection.uid));
- };
-
- const getTabClassname = (tabName) => {
- return classnames(`select-none ${tabName}`, {
- active: tabName === tab,
- 'cursor-pointer': tabName !== tab
- });
- };
-
- const getTabs = () => {
- if (!mode.includes('html')) {
+ const tabs = useMemo(() => {
+ if (allowedPreviewModes.length === 1) {
return null;
}
- return (
- <>
- setTab('raw')}>
- Raw
-
- setTab('preview')}>
- Preview
-
- >
- );
- };
-
- const activeResult = useMemo(() => {
- if (
- tab === 'preview' &&
- mode.includes('html') &&
- item.requestSent &&
- item.requestSent.url &&
- typeof data === 'string'
- ) {
- // Add the Base tag to the head so content loads properly. This also needs the correct CSP settings
- const webViewSrc = data.replace('', ``);
- return (
-
- );
- } else if (mode.includes('image')) {
- return ;
- }
-
- return (
-
- );
- }, [tab, collection, storedTheme, onRun, value, mode]);
+ return allowedPreviewModes.map((previewMode) => (
+ setPreviewTab(previewMode)}
+ >
+ {previewMode.replace(/-(.*)/, ' ')}
+
+ ));
+ }, [allowedPreviewModes, previewTab]);
return (
- {getTabs()}
+ {tabs}
- {error ? {error} : activeResult}
+ {error ? (
+ {error}
+ ) : (
+
+ )}
);
};
diff --git a/packages/bruno-app/src/components/ResponsePane/index.js b/packages/bruno-app/src/components/ResponsePane/index.js
index e1cfab2ca9..aea70de6f4 100644
--- a/packages/bruno-app/src/components/ResponsePane/index.js
+++ b/packages/bruno-app/src/components/ResponsePane/index.js
@@ -43,6 +43,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
data={response.data}
headers={response.headers}
error={response.error}
+ key={item.filename}
/>
);
}
diff --git a/packages/bruno-app/src/components/RunnerResults/ResponsePane/index.js b/packages/bruno-app/src/components/RunnerResults/ResponsePane/index.js
index 2c4f28b209..6526c74543 100644
--- a/packages/bruno-app/src/components/RunnerResults/ResponsePane/index.js
+++ b/packages/bruno-app/src/components/RunnerResults/ResponsePane/index.js
@@ -35,6 +35,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
disableRunEventListener={true}
data={responseReceived.data}
headers={responseReceived.headers}
+ key={item.filename}
/>
);
}
diff --git a/packages/bruno-app/src/utils/network/index.js b/packages/bruno-app/src/utils/network/index.js
index c54c3338ed..a0b0dfead7 100644
--- a/packages/bruno-app/src/utils/network/index.js
+++ b/packages/bruno-app/src/utils/network/index.js
@@ -8,6 +8,8 @@ export const sendNetworkRequest = async (item, collection, environment, collecti
resolve({
state: 'success',
data: response.data,
+ // Note that the Buffer is encoded as a base64 string, because Buffers / TypedArrays are not allowed in the redux store
+ dataBuffer: response.dataBuffer,
headers: Object.entries(response.headers),
size: getResponseSize(response),
status: response.status,
diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js
index adbf623e54..0addf48675 100644
--- a/packages/bruno-electron/src/ipc/network/index.js
+++ b/packages/bruno-electron/src/ipc/network/index.js
@@ -307,6 +307,14 @@ const registerNetworkIpc = (mainWindow) => {
/** @type {import('axios').AxiosResponse} */
const response = await axiosInstance(request);
+ const dataBuffer = Buffer.from(response.data);
+ // Overwrite the original data for backwards compatability
+ response.data = dataBuffer.toString('utf-8');
+ // Try to parse response to JSON, this can quitly fail
+ try {
+ response.data = JSON.parse(response.data);
+ } catch {}
+
// run post-response vars
const postResponseVars = get(request, 'vars.res', []);
if (postResponseVars?.length) {
@@ -424,6 +432,7 @@ const registerNetworkIpc = (mainWindow) => {
statusText: response.statusText,
headers: response.headers,
data: response.data,
+ dataBuffer: dataBuffer.toString('base64'),
duration: requestDuration
};
} catch (error) {
diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js
index 3beab80f7d..6c2d7d4b38 100644
--- a/packages/bruno-electron/src/ipc/network/prepare-request.js
+++ b/packages/bruno-electron/src/ipc/network/prepare-request.js
@@ -84,7 +84,8 @@ const prepareRequest = (request, collectionRoot) => {
let axiosRequest = {
method: request.method,
url: request.url,
- headers: headers
+ headers: headers,
+ responseType: 'arraybuffer'
};
axiosRequest = setAuthHeaders(axiosRequest, request, collectionRoot);
From b3ee0af22616bd26894129a6b021d89fa89e562f Mon Sep 17 00:00:00 2001
From: Its-treason <39559178+Its-treason@users.noreply.github.com>
Date: Tue, 17 Oct 2023 20:53:51 +0200
Subject: [PATCH 2/2] feat: Use real content size, fix runner, use the correct
content charset
---
.../QueryResult/QueryResultPreview/index.js | 3 +-
.../ResponsePane/QueryResult/index.js | 1 +
packages/bruno-app/src/utils/network/index.js | 6 +--
.../bruno-electron/src/ipc/network/index.js | 38 ++++++++++++++-----
4 files changed, 32 insertions(+), 16 deletions(-)
diff --git a/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultPreview/index.js b/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultPreview/index.js
index 127d7e9c28..e17fc3454c 100644
--- a/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultPreview/index.js
+++ b/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultPreview/index.js
@@ -43,11 +43,10 @@ const QueryResultPreview = ({
);
}
case 'preview-image': {
- return ;
+ return ;
}
default:
case 'raw': {
- console.log(mode, storedTheme);
return (
setPreviewTab(previewMode)}
+ key={previewMode}
>
{previewMode.replace(/-(.*)/, ' ')}
diff --git a/packages/bruno-app/src/utils/network/index.js b/packages/bruno-app/src/utils/network/index.js
index a0b0dfead7..ffd66743f9 100644
--- a/packages/bruno-app/src/utils/network/index.js
+++ b/packages/bruno-app/src/utils/network/index.js
@@ -11,7 +11,7 @@ export const sendNetworkRequest = async (item, collection, environment, collecti
// Note that the Buffer is encoded as a base64 string, because Buffers / TypedArrays are not allowed in the redux store
dataBuffer: response.dataBuffer,
headers: Object.entries(response.headers),
- size: getResponseSize(response),
+ size: response.size,
status: response.status,
statusText: response.statusText,
duration: response.duration
@@ -33,10 +33,6 @@ const sendHttpRequest = async (item, collection, environment, collectionVariable
});
};
-const getResponseSize = (response) => {
- return response.headers['content-length'] || Buffer.byteLength(safeStringifyJSON(response.data)) || 0;
-};
-
export const fetchGqlSchema = async (endpoint, environment, request, collection) => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js
index 0addf48675..f40d3ed0c8 100644
--- a/packages/bruno-electron/src/ipc/network/index.js
+++ b/packages/bruno-electron/src/ipc/network/index.js
@@ -174,6 +174,20 @@ const configureRequest = async (collectionUid, request, envVars, collectionVaria
return axiosInstance;
};
+const parseDataFromResponse = (response) => {
+ const dataBuffer = Buffer.from(response.data);
+ // Parse the charset from content type: https://stackoverflow.com/a/33192813
+ const charset = /charset=([^()<>@,;:\"/[\]?.=\s]*)/i.exec(response.headers['Content-Type'] || '');
+ // Overwrite the original data for backwards compatability
+ let data = dataBuffer.toString(charset || 'utf-8');
+ // Try to parse response to JSON, this can quitly fail
+ try {
+ data = JSON.parse(response.data);
+ } catch {}
+
+ return { data, dataBuffer };
+};
+
const registerNetworkIpc = (mainWindow) => {
// handler for sending http request
ipcMain.handle('send-http-request', async (event, item, collection, environment, collectionVariables) => {
@@ -307,13 +321,8 @@ const registerNetworkIpc = (mainWindow) => {
/** @type {import('axios').AxiosResponse} */
const response = await axiosInstance(request);
- const dataBuffer = Buffer.from(response.data);
- // Overwrite the original data for backwards compatability
- response.data = dataBuffer.toString('utf-8');
- // Try to parse response to JSON, this can quitly fail
- try {
- response.data = JSON.parse(response.data);
- } catch {}
+ const { data, dataBuffer } = parseDataFromResponse(response);
+ response.data = data;
// run post-response vars
const postResponseVars = get(request, 'vars.res', []);
@@ -433,6 +442,7 @@ const registerNetworkIpc = (mainWindow) => {
headers: response.headers,
data: response.data,
dataBuffer: dataBuffer.toString('base64'),
+ size: Buffer.byteLength(dataBuffer),
duration: requestDuration
};
} catch (error) {
@@ -448,6 +458,8 @@ const registerNetworkIpc = (mainWindow) => {
}
if (error?.response) {
+ const { data, dataBuffer } = parseDataFromResponse(error.response);
+ error.response.data = data;
// run assertions
const assertions = get(request, 'assertions');
if (assertions) {
@@ -513,6 +525,8 @@ const registerNetworkIpc = (mainWindow) => {
statusText: error.response.statusText,
headers: error.response.headers,
data: error.response.data,
+ dataBuffer: dataBuffer.toString('base64'),
+ size: Buffer.byteLength(dataBuffer),
duration: requestDuration ?? 0
};
}
@@ -729,6 +743,9 @@ const registerNetworkIpc = (mainWindow) => {
const response = await axiosInstance(request);
timeEnd = Date.now();
+ const { data, dataBuffer } = parseDataFromResponse(response);
+ response.data = data;
+
// run post-response vars
const postResponseVars = get(request, 'vars.res', []);
if (postResponseVars?.length) {
@@ -839,7 +856,7 @@ const registerNetworkIpc = (mainWindow) => {
statusText: response.statusText,
headers: Object.entries(response.headers),
duration: timeEnd - timeStart,
- size: response.headers['content-length'] || getSize(response.data),
+ size: Buffer.byteLength(dataBuffer),
data: response.data
}
});
@@ -852,12 +869,15 @@ const registerNetworkIpc = (mainWindow) => {
}
if (error?.response) {
+ const { data, dataBuffer } = parseDataFromResponse(error.response);
+ error.response.data = data;
+
responseReceived = {
status: error.response.status,
statusText: error.response.statusText,
headers: Object.entries(error.response.headers),
duration: duration,
- size: error.response.headers['content-length'] || getSize(error.response.data),
+ size: Buffer.byteLength(dataBuffer),
data: error.response.data
};