Merge branch 'main' of github.com:usebruno/bruno into feature/preview-response-html

This commit is contained in:
Its-treason
2023-10-02 14:34:54 +02:00
23 changed files with 503 additions and 65 deletions

View File

@@ -119,7 +119,7 @@ export default class CodeEditor extends React.Component {
render() {
return (
<StyledWrapper
className="h-full"
className="h-full w-full"
aria-label="Code Editor"
ref={(node) => {
this._node = node;

View File

@@ -13,13 +13,11 @@ const Placeholder = () => {
<div className="px-1 py-2">Send Request</div>
<div className="px-1 py-2">New Request</div>
<div className="px-1 py-2">Edit Environments</div>
<div className="px-1 py-2">Help</div>
</div>
<div className="flex flex-1 flex-col px-1">
<div className="px-1 py-2">Cmd + Enter</div>
<div className="px-1 py-2">Cmd + B</div>
<div className="px-1 py-2">Cmd + E</div>
<div className="px-1 py-2">Cmd + H</div>
</div>
</div>
</StyledWrapper>

View File

@@ -4,15 +4,56 @@ import { useTheme } from 'providers/Theme';
import { useDispatch } 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 StyledWrapper from './StyledWrapper';
import { useState } from 'react';
import { useMemo } from 'react';
const QueryResult = ({ item, collection, value, width, disableRunEventListener, mode }) => {
const QueryResult = ({ item, collection, data, width, disableRunEventListener, headers }) => {
const { storedTheme } = useTheme();
const [tab, setTab] = useState('raw');
const dispatch = useDispatch();
const contentType = getContentType(headers);
const mode = getCodeMirrorModeBasedOnContentType(contentType);
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;
}
return safeStringifyJSON(parsed, true);
}
if (['text', 'html'].includes(mode)) {
if (typeof data === 'string') {
return data;
}
return safeStringifyJSON(data);
}
// final fallback
if (typeof data === 'string') {
return data;
}
return safeStringifyJSON(data);
};
const value = formatResponse(data, mode);
const onRun = () => {
if (disableRunEventListener) {
@@ -32,7 +73,7 @@ const QueryResult = ({ item, collection, value, width, disableRunEventListener,
Raw
</div>
)];
if (mode.includes('text/html')) {
if (mode.includes('html')) {
tabs.push(
<div className={getTabClassname('preview')} role="tab" onClick={() => setTab('preview')}>
Preview
@@ -43,7 +84,7 @@ const QueryResult = ({ item, collection, value, width, disableRunEventListener,
const activeResult = useMemo(() => {
if (tab === 'preview') {
// Add the Base tag to the head so content loads proparly. This also needs the correct CSP settings
const webViewSrc = value.replace('<head>', `<head><base href="${item.requestSent.url}">`);
const webViewSrc = data.replace('<head>', `<head><base href="${item.requestSent.url}">`);
return (
<webview
src={`data:text/html; charset=utf-8,${encodeURIComponent(webViewSrc)}`}
@@ -58,7 +99,7 @@ const QueryResult = ({ item, collection, value, width, disableRunEventListener,
collection={collection}
theme={storedTheme}
onRun={onRun}
value={value || ''}
value={value}
mode={mode}
readOnly
/>

View File

@@ -2,7 +2,6 @@ import React from 'react';
import find from 'lodash/find';
import classnames from 'classnames';
import { useDispatch, useSelector } from 'react-redux';
import { getContentType, formatResponse } from 'utils/common';
import { updateResponsePaneTab } from 'providers/ReduxStore/slices/tabs';
import QueryResult from './QueryResult';
import Overlay from './Overlay';
@@ -41,8 +40,8 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
item={item}
collection={collection}
width={rightPaneWidth}
value={response.data ? formatResponse(response) : ''}
mode={getContentType(response.headers)}
data={response.data}
headers={response.headers}
/>
);
}

View File

@@ -33,7 +33,8 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
collection={collection}
width={rightPaneWidth}
disableRunEventListener={true}
value={responseReceived && responseReceived.data ? safeStringifyJSON(responseReceived.data, true) : ''}
data={responseReceived.data}
headers={responseReceived.headers}
/>
);
}

View File

@@ -0,0 +1,21 @@
import CodeEditor from 'components/CodeEditor/index';
import { HTTPSnippet } from 'httpsnippet';
import { useTheme } from 'providers/Theme/index';
import { buildHarRequest } from 'utils/codegenerator/har';
const CodeView = ({ language, item }) => {
const { storedTheme } = useTheme();
const { target, client, language: lang } = language;
let snippet = '';
try {
snippet = new HTTPSnippet(buildHarRequest(item.request)).convert(target, client);
} catch (e) {
console.error(e);
snippet = 'Error generating code snippet';
}
return <CodeEditor readOnly value={snippet} theme={storedTheme} mode={lang} />;
};
export default CodeView;

View File

@@ -0,0 +1,38 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
margin-inline: -1rem;
margin-block: -1.5rem;
background-color: ${(props) => props.theme.collection.environment.settings.bg};
.generate-code-sidebar {
background-color: ${(props) => props.theme.collection.environment.settings.sidebar.bg};
border-right: solid 1px ${(props) => props.theme.collection.environment.settings.sidebar.borderRight};
min-height: 400px;
}
.generate-code-item {
min-width: 150px;
display: block;
position: relative;
cursor: pointer;
padding: 8px 10px;
border-left: solid 2px transparent;
text-decoration: none;
&:hover {
text-decoration: none;
background-color: ${(props) => props.theme.collection.environment.settings.item.hoverBg};
}
}
.active {
background-color: ${(props) => props.theme.collection.environment.settings.item.active.bg} !important;
border-left: solid 2px ${(props) => props.theme.collection.environment.settings.item.border};
&:hover {
background-color: ${(props) => props.theme.collection.environment.settings.item.active.hoverBg} !important;
}
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,145 @@
import Modal from 'components/Modal/index';
import { useState } from 'react';
import CodeView from './CodeView';
import StyledWrapper from './StyledWrapper';
import { isValidUrl } from 'utils/url/index';
import get from 'lodash/get';
import handlebars from 'handlebars';
import { findEnvironmentInCollection } from 'utils/collections';
const interpolateUrl = ({ url, envVars, collectionVariables, processEnvVars }) => {
if (!url || !url.length || typeof url !== 'string') {
return str;
}
const template = handlebars.compile(url, { noEscape: true });
return template({
...envVars,
...collectionVariables,
process: {
env: {
...processEnvVars
}
}
});
};
const languages = [
{
name: 'HTTP',
target: 'http',
client: 'http1.1'
},
{
name: 'JavaScript-Fetch',
target: 'javascript',
client: 'fetch'
},
{
name: 'Javascript-jQuery',
target: 'javascript',
client: 'jquery'
},
{
name: 'Javascript-axios',
target: 'javascript',
client: 'axios'
},
{
name: 'Python-Python3',
target: 'python',
client: 'python3'
},
{
name: 'Python-Requests',
target: 'python',
client: 'requests'
},
{
name: 'PHP',
target: 'php',
client: 'curl'
},
{
name: 'Shell-curl',
target: 'shell',
client: 'curl'
},
{
name: 'Shell-httpie',
target: 'shell',
client: 'httpie'
}
];
const GenerateCodeItem = ({ collection, item, onClose }) => {
const url = get(item, 'request.url') || '';
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
let envVars = {};
if (environment) {
const vars = get(environment, 'variables', []);
envVars = vars.reduce((acc, curr) => {
acc[curr.name] = curr.value;
return acc;
}, {});
}
const interpolatedUrl = interpolateUrl({
url,
envVars,
collectionVariables: collection.collectionVariables,
processEnvVars: collection.processEnvVariables
});
const [selectedLanguage, setSelectedLanguage] = useState(languages[0]);
return (
<Modal size="lg" title="Generate Code" handleCancel={onClose} hideFooter={true}>
<StyledWrapper>
<div className="flex w-full">
<div>
<div className="generate-code-sidebar">
{languages &&
languages.length &&
languages.map((language) => (
<div
key={language.name}
className={
language.name === selectedLanguage.name ? 'generate-code-item active' : 'generate-code-item'
}
onClick={() => setSelectedLanguage(language)}
>
<span className="capitalize">{language.name}</span>
</div>
))}
</div>
</div>
<div className="flex-grow p-4">
{isValidUrl(interpolatedUrl) ? (
<CodeView
language={selectedLanguage}
item={{
...item,
request: {
...item.request,
url: interpolatedUrl
}
}}
/>
) : (
<div className="flex flex-col justify-center items-center w-full">
<div className="text-center">
<h1 className="text-2xl font-bold">Invalid URL: {interpolatedUrl}</h1>
<p className="text-gray-500">Please check the URL and try again</p>
</div>
</div>
)}
</div>
</div>
</StyledWrapper>
</Modal>
);
};
export default GenerateCodeItem;

View File

@@ -16,6 +16,7 @@ import RenameCollectionItem from './RenameCollectionItem';
import CloneCollectionItem from './CloneCollectionItem';
import DeleteCollectionItem from './DeleteCollectionItem';
import RunCollectionItem from './RunCollectionItem';
import GenerateCodeItem from './GenerateCodeItem';
import { isItemARequest, isItemAFolder, itemIsOpenedInTabs } from 'utils/tabs';
import { doesRequestMatchSearchText, doesFolderHaveItemsMatchSearchText } from 'utils/collections/search';
import { getDefaultRequestPaneTab } from 'utils/collections';
@@ -32,6 +33,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
const [renameItemModalOpen, setRenameItemModalOpen] = useState(false);
const [cloneItemModalOpen, setCloneItemModalOpen] = useState(false);
const [deleteItemModalOpen, setDeleteItemModalOpen] = useState(false);
const [generateCodeItemModalOpen, setGenerateCodeItemModalOpen] = useState(false);
const [newRequestModalOpen, setNewRequestModalOpen] = useState(false);
const [newFolderModalOpen, setNewFolderModalOpen] = useState(false);
const [runCollectionModalOpen, setRunCollectionModalOpen] = useState(false);
@@ -166,6 +168,9 @@ const CollectionItem = ({ item, collection, searchText }) => {
{runCollectionModalOpen && (
<RunCollectionItem collection={collection} item={item} onClose={() => setRunCollectionModalOpen(false)} />
)}
{generateCodeItemModalOpen && (
<GenerateCodeItem collection={collection} item={item} onClose={() => setGenerateCodeItemModalOpen(false)} />
)}
<div className={itemRowClassName} ref={(node) => drag(drop(node))}>
<div className="flex items-center h-full w-full">
{indents && indents.length
@@ -264,6 +269,18 @@ const CollectionItem = ({ item, collection, searchText }) => {
Clone
</div>
)}
{!isFolder && item.type === 'http-request' && (
<div
className="dropdown-item"
onClick={(e) => {
e.stopPropagation();
dropdownTippyRef.current.hide();
setGenerateCodeItemModalOpen(true);
}}
>
Generate Code
</div>
)}
<div
className="dropdown-item delete-item"
onClick={(e) => {

View File

@@ -1,6 +1,12 @@
import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { IconSearch, IconFolders, IconSortAZ } from '@tabler/icons';
import {
IconSearch,
IconFolders,
IconArrowsSort,
IconSortAscendingLetters,
IconSortDescendingLetters
} from '@tabler/icons';
import Collection from '../Collections/Collection';
import CreateCollection from '../CreateCollection';
import StyledWrapper from './StyledWrapper';
@@ -9,20 +15,47 @@ import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { sortCollections } from 'providers/ReduxStore/slices/collections/actions';
// todo: move this to a separate folder
// the coding convention is to keep all the components in a folder named after the component
const CollectionsBadge = () => {
const dispatch = useDispatch()
const dispatch = useDispatch();
const { collections } = useSelector((state) => state.collections);
const { collectionSortOrder } = useSelector((state) => state.collections);
const sortCollectionOrder = () => {
let order;
switch (collectionSortOrder) {
case 'default':
order = 'alphabetical';
break;
case 'alphabetical':
order = 'reverseAlphabetical';
break;
case 'reverseAlphabetical':
order = 'default';
break;
}
dispatch(sortCollections({ order }));
};
return (
<div className="items-center mt-2 relative">
<div className='collections-badge flex items-center justify-between px-2' >
<div className="collections-badge flex items-center justify-between px-2">
<div className="flex items-center py-1 select-none">
<span className="mr-2">
<IconFolders size={18} strokeWidth={1.5} />
</span>
<span>Collections</span>
</div>
<button onClick={() => dispatch(sortCollections())} >
<IconSortAZ size={18} strokeWidth={1.5} />
</button>
{collections.length >= 1 && (
<button onClick={() => sortCollectionOrder()}>
{collectionSortOrder == 'default' ? (
<IconArrowsSort size={18} strokeWidth={1.5} />
) : collectionSortOrder == 'alphabetical' ? (
<IconSortAscendingLetters size={18} strokeWidth={1.5} />
) : (
<IconSortDescendingLetters size={18} strokeWidth={1.5} />
)}
</button>
)}
</div>
</div>
);
@@ -71,12 +104,12 @@ const Collections = () => {
<div className="mt-4 flex flex-col overflow-y-auto absolute top-32 bottom-10 left-0 right-0">
{collections && collections.length
? collections.map((c) => {
return (
<DndProvider backend={HTML5Backend} key={c.uid}>
<Collection searchText={searchText} collection={c} key={c.uid} />
</DndProvider>
);
})
return (
<DndProvider backend={HTML5Backend} key={c.uid}>
<Collection searchText={searchText} collection={c} key={c.uid} />
</DndProvider>
);
})
: null}
</div>
</StyledWrapper>

View File

@@ -116,7 +116,7 @@ const Sidebar = () => {
</GitHubButton>
)}
</div>
<div className="flex flex-grow items-center justify-end text-xs mr-2">v0.16.5</div>
<div className="flex flex-grow items-center justify-end text-xs mr-2">v0.16.6</div>
</div>
</div>
</div>