forked from extern/bruno
Merge pull request #622 from Its-treason/feature/better-image-preview
feat(#593): Show real response in image preview
This commit is contained in:
commit
922c1ff253
@ -0,0 +1,65 @@
|
|||||||
|
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('<head>', `<head><base href="${item.requestSent?.url || ''}">`);
|
||||||
|
return (
|
||||||
|
<webview
|
||||||
|
src={`data:text/html; charset=utf-8,${encodeURIComponent(webViewSrc)}`}
|
||||||
|
webpreferences="disableDialogs=true, javascript=yes"
|
||||||
|
className="h-full bg-white"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case 'preview-image': {
|
||||||
|
return <img src={`data:${contentType.replace(/\;(.*)/, '')};base64,${dataBuffer}`} className="mx-auto" />;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
case 'raw': {
|
||||||
|
return (
|
||||||
|
<CodeEditor
|
||||||
|
collection={collection}
|
||||||
|
font={get(preferences, 'font.codeFont', 'default')}
|
||||||
|
theme={storedTheme}
|
||||||
|
onRun={onRun}
|
||||||
|
value={formattedData}
|
||||||
|
mode={mode}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QueryResultPreview;
|
@ -1,24 +1,14 @@
|
|||||||
import React from 'react';
|
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 classnames from 'classnames';
|
||||||
import { getContentType, safeStringifyJSON, safeParseXML } from 'utils/common';
|
import { getContentType, safeStringifyJSON, safeParseXML } from 'utils/common';
|
||||||
import { getCodeMirrorModeBasedOnContentType } from 'utils/common/codemirror';
|
import { getCodeMirrorModeBasedOnContentType } from 'utils/common/codemirror';
|
||||||
|
import QueryResultPreview from './QueryResultPreview';
|
||||||
|
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
import { useEffect } from 'react';
|
||||||
const QueryResult = ({ item, collection, data, width, disableRunEventListener, headers, error }) => {
|
import { useTheme } from 'providers/Theme/index';
|
||||||
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) => {
|
const formatResponse = (data, mode) => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
@ -31,7 +21,6 @@ const QueryResult = ({ item, collection, data, width, disableRunEventListener, h
|
|||||||
|
|
||||||
if (mode.includes('xml')) {
|
if (mode.includes('xml')) {
|
||||||
let parsed = safeParseXML(data, { collapseContent: true });
|
let parsed = safeParseXML(data, { collapseContent: true });
|
||||||
|
|
||||||
if (typeof parsed === 'string') {
|
if (typeof parsed === 'string') {
|
||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
@ -39,99 +28,79 @@ const QueryResult = ({ item, collection, data, width, disableRunEventListener, h
|
|||||||
return safeStringifyJSON(parsed, true);
|
return safeStringifyJSON(parsed, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['text', 'html'].includes(mode)) {
|
if (['text', 'html'].includes(mode) || typeof data === 'string') {
|
||||||
if (typeof data === 'string') {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
return safeStringifyJSON(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode.includes('image')) {
|
|
||||||
return item.requestSent.url;
|
|
||||||
}
|
|
||||||
|
|
||||||
// final fallback
|
|
||||||
if (typeof data === 'string') {
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
return safeStringifyJSON(data);
|
return safeStringifyJSON(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const value = formatResponse(data, mode);
|
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();
|
||||||
|
|
||||||
const onRun = () => {
|
const allowedPreviewModes = useMemo(() => {
|
||||||
if (disableRunEventListener) {
|
// Always show raw
|
||||||
return;
|
const allowedPreviewModes = ['raw'];
|
||||||
|
|
||||||
|
if (mode.includes('html') && typeof data === 'string') {
|
||||||
|
allowedPreviewModes.unshift('preview-web');
|
||||||
|
} else if (mode.includes('image')) {
|
||||||
|
allowedPreviewModes.unshift('preview-image');
|
||||||
}
|
}
|
||||||
dispatch(sendRequest(item, collection.uid));
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTabClassname = (tabName) => {
|
return allowedPreviewModes;
|
||||||
return classnames(`select-none ${tabName}`, {
|
}, [mode, data, formattedData]);
|
||||||
active: tabName === tab,
|
|
||||||
'cursor-pointer': tabName !== tab
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTabs = () => {
|
const [previewTab, setPreviewTab] = useState(allowedPreviewModes[0]);
|
||||||
if (!mode.includes('html')) {
|
// Ensure the active Tab is always allowed
|
||||||
|
useEffect(() => {
|
||||||
|
if (!allowedPreviewModes.includes(previewTab)) {
|
||||||
|
setPreviewTab(allowedPreviewModes[0]);
|
||||||
|
}
|
||||||
|
}, [previewTab, allowedPreviewModes]);
|
||||||
|
|
||||||
|
const tabs = useMemo(() => {
|
||||||
|
if (allowedPreviewModes.length === 1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return allowedPreviewModes.map((previewMode) => (
|
||||||
<>
|
<div
|
||||||
<div className={getTabClassname('raw')} role="tab" onClick={() => setTab('raw')}>
|
className={classnames('select-none capitalize', previewMode === previewTab ? 'active' : 'cursor-pointer')}
|
||||||
Raw
|
role="tab"
|
||||||
|
onClick={() => setPreviewTab(previewMode)}
|
||||||
|
key={previewMode}
|
||||||
|
>
|
||||||
|
{previewMode.replace(/-(.*)/, ' ')}
|
||||||
</div>
|
</div>
|
||||||
<div className={getTabClassname('preview')} role="tab" onClick={() => setTab('preview')}>
|
));
|
||||||
Preview
|
}, [allowedPreviewModes, previewTab]);
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
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('<head>', `<head><base href="${item.requestSent.url}">`);
|
|
||||||
return (
|
|
||||||
<webview
|
|
||||||
src={`data:text/html; charset=utf-8,${encodeURIComponent(webViewSrc)}`}
|
|
||||||
webpreferences="disableDialogs=true, javascript=yes"
|
|
||||||
className="h-full bg-white"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (mode.includes('image')) {
|
|
||||||
return <img src={item.requestSent.url} alt="image" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CodeEditor
|
|
||||||
collection={collection}
|
|
||||||
font={get(preferences, 'font.codeFont', 'default')}
|
|
||||||
theme={storedTheme}
|
|
||||||
onRun={onRun}
|
|
||||||
value={value}
|
|
||||||
mode={mode}
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}, [tab, collection, storedTheme, onRun, value, mode]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledWrapper className="w-full h-full" style={{ maxWidth: width }}>
|
<StyledWrapper className="w-full h-full" style={{ maxWidth: width }}>
|
||||||
<div className="flex justify-end gap-2 text-xs" role="tablist">
|
<div className="flex justify-end gap-2 text-xs" role="tablist">
|
||||||
{getTabs()}
|
{tabs}
|
||||||
</div>
|
</div>
|
||||||
{error ? <span className="text-red-500">{error}</span> : activeResult}
|
{error ? (
|
||||||
|
<span className="text-red-500">{error}</span>
|
||||||
|
) : (
|
||||||
|
<QueryResultPreview
|
||||||
|
previewTab={previewTab}
|
||||||
|
data={data}
|
||||||
|
dataBuffer={item.response.dataBuffer}
|
||||||
|
formattedData={formattedData}
|
||||||
|
item={item}
|
||||||
|
contentType={contentType}
|
||||||
|
mode={mode}
|
||||||
|
collection={collection}
|
||||||
|
allowedPreviewModes={allowedPreviewModes}
|
||||||
|
disableRunEventListener={disableRunEventListener}
|
||||||
|
storedTheme={storedTheme}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</StyledWrapper>
|
</StyledWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -43,6 +43,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
|||||||
data={response.data}
|
data={response.data}
|
||||||
headers={response.headers}
|
headers={response.headers}
|
||||||
error={response.error}
|
error={response.error}
|
||||||
|
key={item.filename}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -35,6 +35,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
|||||||
disableRunEventListener={true}
|
disableRunEventListener={true}
|
||||||
data={responseReceived.data}
|
data={responseReceived.data}
|
||||||
headers={responseReceived.headers}
|
headers={responseReceived.headers}
|
||||||
|
key={item.filename}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -76,14 +76,12 @@ const CreateCollection = ({ onClose }) => {
|
|||||||
name="collectionName"
|
name="collectionName"
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
className="block textbox mt-2 w-full"
|
className="block textbox mt-2 w-full"
|
||||||
onChange = {
|
onChange={(e) => {
|
||||||
(e) => {
|
|
||||||
formik.handleChange(e);
|
formik.handleChange(e);
|
||||||
if (formik.values.collectionName === formik.values.collectionFolderName) {
|
if (formik.values.collectionName === formik.values.collectionFolderName) {
|
||||||
formik.setFieldValue("collectionFolderName", e.target.value);
|
formik.setFieldValue('collectionFolderName', e.target.value);
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
autoCapitalize="off"
|
autoCapitalize="off"
|
||||||
|
@ -8,8 +8,10 @@ export const sendNetworkRequest = async (item, collection, environment, collecti
|
|||||||
resolve({
|
resolve({
|
||||||
state: 'success',
|
state: 'success',
|
||||||
data: response.data,
|
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),
|
headers: Object.entries(response.headers),
|
||||||
size: getResponseSize(response),
|
size: response.size,
|
||||||
status: response.status,
|
status: response.status,
|
||||||
statusText: response.statusText,
|
statusText: response.statusText,
|
||||||
duration: response.duration
|
duration: response.duration
|
||||||
@ -31,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) => {
|
export const fetchGqlSchema = async (endpoint, environment, request, collection) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const { ipcRenderer } = window;
|
const { ipcRenderer } = window;
|
||||||
|
@ -174,6 +174,20 @@ const configureRequest = async (collectionUid, request, envVars, collectionVaria
|
|||||||
return axiosInstance;
|
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) => {
|
const registerNetworkIpc = (mainWindow) => {
|
||||||
// handler for sending http request
|
// handler for sending http request
|
||||||
ipcMain.handle('send-http-request', async (event, item, collection, environment, collectionVariables) => {
|
ipcMain.handle('send-http-request', async (event, item, collection, environment, collectionVariables) => {
|
||||||
@ -336,6 +350,9 @@ const registerNetworkIpc = (mainWindow) => {
|
|||||||
|
|
||||||
// Continue with the rest of the request lifecycle - post response vars, script, assertions, tests
|
// Continue with the rest of the request lifecycle - post response vars, script, assertions, tests
|
||||||
|
|
||||||
|
const { data, dataBuffer } = parseDataFromResponse(response);
|
||||||
|
response.data = data;
|
||||||
|
|
||||||
// run post-response vars
|
// run post-response vars
|
||||||
const postResponseVars = get(request, 'vars.res', []);
|
const postResponseVars = get(request, 'vars.res', []);
|
||||||
if (postResponseVars?.length) {
|
if (postResponseVars?.length) {
|
||||||
@ -448,6 +465,8 @@ const registerNetworkIpc = (mainWindow) => {
|
|||||||
statusText: response.statusText,
|
statusText: response.statusText,
|
||||||
headers: response.headers,
|
headers: response.headers,
|
||||||
data: response.data,
|
data: response.data,
|
||||||
|
dataBuffer: dataBuffer.toString('base64'),
|
||||||
|
size: Buffer.byteLength(dataBuffer),
|
||||||
duration: responseTime ?? 0
|
duration: responseTime ?? 0
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -674,6 +693,9 @@ const registerNetworkIpc = (mainWindow) => {
|
|||||||
response = await axiosInstance(request);
|
response = await axiosInstance(request);
|
||||||
timeEnd = Date.now();
|
timeEnd = Date.now();
|
||||||
|
|
||||||
|
const { data, dataBuffer } = parseDataFromResponse(response);
|
||||||
|
response.data = data;
|
||||||
|
|
||||||
mainWindow.webContents.send('main:run-folder-event', {
|
mainWindow.webContents.send('main:run-folder-event', {
|
||||||
type: 'response-received',
|
type: 'response-received',
|
||||||
responseReceived: {
|
responseReceived: {
|
||||||
@ -681,20 +703,25 @@ const registerNetworkIpc = (mainWindow) => {
|
|||||||
statusText: response.statusText,
|
statusText: response.statusText,
|
||||||
headers: Object.entries(response.headers),
|
headers: Object.entries(response.headers),
|
||||||
duration: timeEnd - timeStart,
|
duration: timeEnd - timeStart,
|
||||||
size: response.headers['content-length'] || getSize(response.data),
|
dataBuffer: dataBuffer.toString('base64'),
|
||||||
|
size: Buffer.byteLength(dataBuffer),
|
||||||
data: response.data
|
data: response.data
|
||||||
},
|
},
|
||||||
...eventData
|
...eventData
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error?.response) {
|
if (error?.response) {
|
||||||
|
const { data, dataBuffer } = parseDataFromResponse(error.response);
|
||||||
|
error.response.data = data;
|
||||||
|
|
||||||
timeEnd = Date.now();
|
timeEnd = Date.now();
|
||||||
response = {
|
response = {
|
||||||
status: error.response.status,
|
status: error.response.status,
|
||||||
statusText: error.response.statusText,
|
statusText: error.response.statusText,
|
||||||
headers: Object.entries(error.response.headers),
|
headers: Object.entries(error.response.headers),
|
||||||
duration: timeEnd - timeStart,
|
duration: timeEnd - timeStart,
|
||||||
size: error.response.headers['content-length'] || getSize(error.response.data),
|
dataBuffer: dataBuffer.toString('base64'),
|
||||||
|
size: Buffer.byteLength(dataBuffer),
|
||||||
data: error.response.data
|
data: error.response.data
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -84,7 +84,8 @@ const prepareRequest = (request, collectionRoot) => {
|
|||||||
let axiosRequest = {
|
let axiosRequest = {
|
||||||
method: request.method,
|
method: request.method,
|
||||||
url: request.url,
|
url: request.url,
|
||||||
headers: headers
|
headers: headers,
|
||||||
|
responseType: 'arraybuffer'
|
||||||
};
|
};
|
||||||
|
|
||||||
axiosRequest = setAuthHeaders(axiosRequest, request, collectionRoot);
|
axiosRequest = setAuthHeaders(axiosRequest, request, collectionRoot);
|
||||||
|
Loading…
Reference in New Issue
Block a user