Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(#593): Show real response in image preview #622

Merged
merged 3 commits into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion contributing_ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,4 @@ Bruno построен с использованием NextJs и React. Мы т
- feature/[название функции]: Эта ветка должна содержать изменения для конкретной функции
- Пример: feature/dark-mode
- bugfix/[название ошибки]: Эта ветка должна содержать только исправления для конкретной ошибки
- Пример bugfix/bug-1
- Пример bugfix/bug-1
Original file line number Diff line number Diff line change
@@ -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;
181 changes: 75 additions & 106 deletions packages/bruno-app/src/components/ResponsePane/QueryResult/index.js
Original file line number Diff line number Diff line change
@@ -1,137 +1,106 @@
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 (
<>
<div className={getTabClassname('raw')} role="tab" onClick={() => setTab('raw')}>
Raw
</div>
<div className={getTabClassname('preview')} role="tab" onClick={() => setTab('preview')}>
Preview
</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 allowedPreviewModes.map((previewMode) => (
<div
className={classnames('select-none capitalize', previewMode === previewTab ? 'active' : 'cursor-pointer')}
role="tab"
onClick={() => setPreviewTab(previewMode)}
key={previewMode}
>
{previewMode.replace(/-(.*)/, ' ')}
</div>
));
}, [allowedPreviewModes, previewTab]);

return (
<StyledWrapper className="w-full h-full" style={{ maxWidth: width }}>
<div className="flex justify-end gap-2 text-xs" role="tablist">
{getTabs()}
{tabs}
</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>
);
};
Expand Down
1 change: 1 addition & 0 deletions packages/bruno-app/src/components/ResponsePane/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
data={response.data}
headers={response.headers}
error={response.error}
key={item.filename}
/>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
disableRunEventListener={true}
data={responseReceived.data}
headers={responseReceived.headers}
key={item.filename}
/>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,12 @@ const CreateCollection = ({ onClose }) => {
name="collectionName"
ref={inputRef}
className="block textbox mt-2 w-full"
onChange = {
(e) => {
formik.handleChange(e);
if (formik.values.collectionName === formik.values.collectionFolderName) {
formik.setFieldValue("collectionFolderName", e.target.value);
}
}
}
onChange={(e) => {
formik.handleChange(e);
if (formik.values.collectionName === formik.values.collectionFolderName) {
formik.setFieldValue('collectionFolderName', e.target.value);
}
}}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
Expand Down
8 changes: 3 additions & 5 deletions packages/bruno-app/src/utils/network/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ 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),
size: response.size,
status: response.status,
statusText: response.statusText,
duration: response.duration
Expand All @@ -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) => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
Expand Down
Loading