mirror of
https://github.com/usebruno/bruno.git
synced 2024-12-23 23:29:47 +01:00
Merge branch 'main' of github.com:usebruno/bruno into feature/preview-response-html
This commit is contained in:
commit
dce11d1bd5
@ -30,6 +30,8 @@
|
||||
"graphiql": "^1.5.9",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-request": "^3.7.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"httpsnippet": "^3.0.1",
|
||||
"idb": "^7.0.0",
|
||||
"immer": "^9.0.15",
|
||||
"know-your-http-well": "^0.5.0",
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
/>
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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) => {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -7,7 +7,6 @@ import SaveRequest from 'components/RequestPane/SaveRequest';
|
||||
import EnvironmentSettings from 'components/Environments/EnvironmentSettings';
|
||||
import NetworkError from 'components/ResponsePane/NetworkError';
|
||||
import NewRequest from 'components/Sidebar/NewRequest';
|
||||
import BrunoSupport from 'components/BrunoSupport';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { findCollectionByUid, findItemInCollection } from 'utils/collections';
|
||||
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
@ -22,7 +21,6 @@ export const HotkeysProvider = (props) => {
|
||||
const [showSaveRequestModal, setShowSaveRequestModal] = useState(false);
|
||||
const [showEnvSettingsModal, setShowEnvSettingsModal] = useState(false);
|
||||
const [showNewRequestModal, setShowNewRequestModal] = useState(false);
|
||||
const [showBrunoSupportModal, setShowBrunoSupportModal] = useState(false);
|
||||
|
||||
const getCurrentCollectionItems = () => {
|
||||
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
@ -133,18 +131,6 @@ export const HotkeysProvider = (props) => {
|
||||
};
|
||||
}, [activeTabUid, tabs, collections, setShowNewRequestModal]);
|
||||
|
||||
// help (ctrl/cmd + h)
|
||||
useEffect(() => {
|
||||
Mousetrap.bind(['command+h', 'ctrl+h'], (e) => {
|
||||
setShowBrunoSupportModal(true);
|
||||
return false; // this stops the event bubbling
|
||||
});
|
||||
|
||||
return () => {
|
||||
Mousetrap.unbind(['command+h', 'ctrl+h']);
|
||||
};
|
||||
}, [setShowNewRequestModal]);
|
||||
|
||||
// close tab hotkey
|
||||
useEffect(() => {
|
||||
Mousetrap.bind(['command+w', 'ctrl+w'], (e) => {
|
||||
@ -164,7 +150,6 @@ export const HotkeysProvider = (props) => {
|
||||
|
||||
return (
|
||||
<HotkeysContext.Provider {...props} value="hotkey">
|
||||
{showBrunoSupportModal && <BrunoSupport onClose={() => setShowBrunoSupportModal(false)} />}
|
||||
{showSaveRequestModal && (
|
||||
<SaveRequest items={getCurrentCollectionItems()} onClose={() => setShowSaveRequestModal(false)} />
|
||||
)}
|
||||
|
@ -22,7 +22,7 @@ import {
|
||||
} from 'utils/collections';
|
||||
import { collectionSchema, itemSchema, environmentSchema, environmentsSchema } from '@usebruno/schema';
|
||||
import { waitForNextTick } from 'utils/common';
|
||||
import { getDirectoryName } from 'utils/common/platform';
|
||||
import { getDirectoryName, isWindowsOS } from 'utils/common/platform';
|
||||
import { sendNetworkRequest, cancelNetworkRequest } from 'utils/network';
|
||||
|
||||
import {
|
||||
@ -34,12 +34,12 @@ import {
|
||||
renameItem as _renameItem,
|
||||
cloneItem as _cloneItem,
|
||||
deleteItem as _deleteItem,
|
||||
sortCollections as _sortCollections,
|
||||
saveRequest as _saveRequest,
|
||||
selectEnvironment as _selectEnvironment,
|
||||
createCollection as _createCollection,
|
||||
renameCollection as _renameCollection,
|
||||
removeCollection as _removeCollection,
|
||||
sortCollections as _sortCollections,
|
||||
collectionAddEnvFileEvent as _collectionAddEnvFileEvent
|
||||
} from './index';
|
||||
|
||||
@ -146,6 +146,11 @@ export const cancelRequest = (cancelTokenUid, item, collection) => (dispatch) =>
|
||||
.catch((err) => console.log(err));
|
||||
};
|
||||
|
||||
// todo: this can be directly put inside the collections/index.js file
|
||||
// the coding convention is to put only actions that need ipc in this file
|
||||
export const sortCollections = (order) => (dispatch) => {
|
||||
dispatch(_sortCollections(order));
|
||||
};
|
||||
export const runCollectionFolder = (collectionUid, folderUid, recursive) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
@ -263,7 +268,19 @@ export const renameItem = (newName, itemUid, collectionUid) => (dispatch, getSta
|
||||
}
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
ipcRenderer.invoke('renderer:rename-item', item.pathname, newPathname, newName).then(resolve).catch(reject);
|
||||
ipcRenderer
|
||||
.invoke('renderer:rename-item', item.pathname, newPathname, newName)
|
||||
.then(() => {
|
||||
// In case of Mac and Linux, we get the unlinkDir and addDir IPC events from electron which takes care of updating the state
|
||||
// But in windows we don't get those events, so we need to update the state manually
|
||||
// This looks like an issue in our watcher library chokidar
|
||||
// GH: https://github.com/usebruno/bruno/issues/251
|
||||
if (isWindowsOS()) {
|
||||
dispatch(_renameItem({ newName, itemUid, collectionUid }));
|
||||
}
|
||||
resolve();
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
@ -347,16 +364,22 @@ export const deleteItem = (itemUid, collectionUid) => (dispatch, getState) => {
|
||||
|
||||
ipcRenderer
|
||||
.invoke('renderer:delete-item', item.pathname, item.type)
|
||||
.then(() => resolve())
|
||||
.then(() => {
|
||||
// In case of Mac and Linux, we get the unlinkDir IPC event from electron which takes care of updating the state
|
||||
// But in windows we don't get those events, so we need to update the state manually
|
||||
// This looks like an issue in our watcher library chokidar
|
||||
// GH: https://github.com/usebruno/bruno/issues/265
|
||||
if (isWindowsOS()) {
|
||||
dispatch(_deleteItem({ itemUid, collectionUid }));
|
||||
}
|
||||
resolve();
|
||||
})
|
||||
.catch((error) => reject(error));
|
||||
}
|
||||
return;
|
||||
});
|
||||
};
|
||||
|
||||
export const sortCollections = () => (dispatch) => {
|
||||
dispatch(_sortCollections())
|
||||
}
|
||||
export const moveItem = (collectionUid, draggedItemUid, targetItemUid) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
|
@ -28,7 +28,8 @@ import { getSubdirectoriesFromRoot, getDirectoryName } from 'utils/common/platfo
|
||||
const PATH_SEPARATOR = path.sep;
|
||||
|
||||
const initialState = {
|
||||
collections: []
|
||||
collections: [],
|
||||
collectionSortOrder: 'default'
|
||||
};
|
||||
|
||||
export const collectionsSlice = createSlice({
|
||||
@ -38,12 +39,12 @@ export const collectionsSlice = createSlice({
|
||||
createCollection: (state, action) => {
|
||||
const collectionUids = map(state.collections, (c) => c.uid);
|
||||
const collection = action.payload;
|
||||
|
||||
// last action is used to track the last action performed on the collection
|
||||
// this is optional
|
||||
// this is used in scenarios where we want to know the last action performed on the collection
|
||||
// and take some extra action based on that
|
||||
// for example, when a env is created, we want to auto select it the env modal
|
||||
collection.importedAt = new Date().getTime();
|
||||
collection.lastAction = null;
|
||||
|
||||
collapseCollection(collection);
|
||||
@ -70,8 +71,19 @@ export const collectionsSlice = createSlice({
|
||||
removeCollection: (state, action) => {
|
||||
state.collections = filter(state.collections, (c) => c.uid !== action.payload.collectionUid);
|
||||
},
|
||||
sortCollections: (state) => {
|
||||
state.collections = state.collections.sort((a, b) => a.name.localeCompare(b.name))
|
||||
sortCollections: (state, action) => {
|
||||
state.collectionSortOrder = action.payload.order;
|
||||
switch (action.payload.order) {
|
||||
case 'default':
|
||||
state.collections = state.collections.sort((a, b) => a.importedAt - b.importedAt);
|
||||
break;
|
||||
case 'alphabetical':
|
||||
state.collections = state.collections.sort((a, b) => a.name.localeCompare(b.name));
|
||||
break;
|
||||
case 'reverseAlphabetical':
|
||||
state.collections = state.collections.sort((a, b) => b.name.localeCompare(a.name));
|
||||
break;
|
||||
}
|
||||
},
|
||||
updateLastAction: (state, action) => {
|
||||
const { collectionUid, lastAction } = action.payload;
|
||||
|
71
packages/bruno-app/src/utils/codegenerator/har.js
Normal file
71
packages/bruno-app/src/utils/codegenerator/har.js
Normal file
@ -0,0 +1,71 @@
|
||||
const createContentType = (mode) => {
|
||||
switch (mode) {
|
||||
case 'json':
|
||||
return 'application/json';
|
||||
case 'xml':
|
||||
return 'application/xml';
|
||||
case 'formUrlEncoded':
|
||||
return 'application/x-www-form-urlencoded';
|
||||
case 'multipartForm':
|
||||
return 'multipart/form-data';
|
||||
default:
|
||||
return 'application/json';
|
||||
}
|
||||
};
|
||||
|
||||
const createHeaders = (headers, mode) => {
|
||||
const contentType = createContentType(mode);
|
||||
const headersArray = headers
|
||||
.filter((header) => header.enabled)
|
||||
.map((header) => {
|
||||
return {
|
||||
name: header.name,
|
||||
value: header.value
|
||||
};
|
||||
});
|
||||
const headerNames = headersArray.map((header) => header.name);
|
||||
if (!headerNames.includes('Content-Type')) {
|
||||
return [...headersArray, { name: 'Content-Type', value: contentType }];
|
||||
}
|
||||
return headersArray;
|
||||
};
|
||||
|
||||
const createQuery = (queryParams = []) => {
|
||||
return queryParams.map((param) => {
|
||||
return {
|
||||
name: param.name,
|
||||
value: param.value
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const createPostData = (body) => {
|
||||
const contentType = createContentType(body.mode);
|
||||
if (body.mode === 'formUrlEncoded' || body.mode === 'multipartForm') {
|
||||
return {
|
||||
mimeType: contentType,
|
||||
params: body[body.mode]
|
||||
.filter((param) => param.enabled)
|
||||
.map((param) => ({ name: param.name, value: param.value }))
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
mimeType: contentType,
|
||||
text: body[body.mode]
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const buildHarRequest = (request) => {
|
||||
return {
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
httpVersion: 'HTTP/1.1',
|
||||
cookies: [],
|
||||
headers: createHeaders(request.headers, request.body.mode),
|
||||
queryString: createQuery(request.params),
|
||||
postData: createPostData(request.body),
|
||||
headersSize: 0,
|
||||
bodySize: 0
|
||||
};
|
||||
};
|
@ -129,9 +129,11 @@ export const moveCollectionItem = (collection, draggedItem, targetItem) => {
|
||||
let draggedItemParent = findParentItemInCollection(collection, draggedItem.uid);
|
||||
|
||||
if (draggedItemParent) {
|
||||
draggedItemParent.items = sortBy(draggedItemParent.items, (item) => item.seq);
|
||||
draggedItemParent.items = filter(draggedItemParent.items, (i) => i.uid !== draggedItem.uid);
|
||||
draggedItem.pathname = path.join(draggedItemParent.pathname, draggedItem.filename);
|
||||
} else {
|
||||
collection.items = sortBy(collection.items, (item) => item.seq);
|
||||
collection.items = filter(collection.items, (i) => i.uid !== draggedItem.uid);
|
||||
}
|
||||
|
||||
@ -143,10 +145,12 @@ export const moveCollectionItem = (collection, draggedItem, targetItem) => {
|
||||
let targetItemParent = findParentItemInCollection(collection, targetItem.uid);
|
||||
|
||||
if (targetItemParent) {
|
||||
targetItemParent.items = sortBy(targetItemParent.items, (item) => item.seq);
|
||||
let targetItemIndex = findIndex(targetItemParent.items, (i) => i.uid === targetItem.uid);
|
||||
targetItemParent.items.splice(targetItemIndex + 1, 0, draggedItem);
|
||||
draggedItem.pathname = path.join(targetItemParent.pathname, draggedItem.filename);
|
||||
} else {
|
||||
collection.items = sortBy(collection.items, (item) => item.seq);
|
||||
let targetItemIndex = findIndex(collection.items, (i) => i.uid === targetItem.uid);
|
||||
collection.items.splice(targetItemIndex + 1, 0, draggedItem);
|
||||
draggedItem.pathname = path.join(collection.pathname, draggedItem.filename);
|
||||
|
@ -42,3 +42,25 @@ export const defineCodeMirrorBrunoVariablesMode = (variables, mode) => {
|
||||
return CodeMirror.overlayMode(CodeMirror.getMode(config, parserConfig.backdrop || mode), variablesOverlay);
|
||||
});
|
||||
};
|
||||
|
||||
export const getCodeMirrorModeBasedOnContentType = (contentType) => {
|
||||
if (!contentType || typeof contentType !== 'string') {
|
||||
return 'application/text';
|
||||
}
|
||||
|
||||
if (contentType.includes('json')) {
|
||||
return 'application/ld+json';
|
||||
} else if (contentType.includes('xml')) {
|
||||
return 'application/xml';
|
||||
} else if (contentType.includes('html')) {
|
||||
return 'application/html';
|
||||
} else if (contentType.includes('text')) {
|
||||
return 'application/text';
|
||||
} else if (contentType.includes('application/edn')) {
|
||||
return 'application/xml';
|
||||
} else if (mimeType.includes('yaml')) {
|
||||
return 'application/yaml';
|
||||
} else {
|
||||
return 'application/text';
|
||||
}
|
||||
};
|
||||
|
@ -51,6 +51,17 @@ export const safeStringifyJSON = (obj, indent = false) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const safeParseXML = (str) => {
|
||||
if (!str || !str.length || typeof str !== 'string') {
|
||||
return str;
|
||||
}
|
||||
try {
|
||||
return xmlFormat(str);
|
||||
} catch (e) {
|
||||
return str;
|
||||
}
|
||||
};
|
||||
|
||||
// Remove any characters that are not alphanumeric, spaces, hyphens, or underscores
|
||||
export const normalizeFileName = (name) => {
|
||||
if (!name) {
|
||||
@ -80,16 +91,6 @@ export const getContentType = (headers) => {
|
||||
return contentType[0];
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
export const formatResponse = (response) => {
|
||||
let type = getContentType(response.headers);
|
||||
if (type.includes('json')) {
|
||||
return safeStringifyJSON(response.data, true);
|
||||
}
|
||||
if (type.includes('xml')) {
|
||||
return xmlFormat(response.data, { collapseContent: true });
|
||||
}
|
||||
return response.data;
|
||||
};
|
||||
|
@ -1,6 +1,7 @@
|
||||
import trim from 'lodash/trim';
|
||||
import path from 'path';
|
||||
import slash from './slash';
|
||||
import platform from 'platform';
|
||||
|
||||
export const isElectron = () => {
|
||||
if (!window) {
|
||||
@ -33,3 +34,10 @@ export const getDirectoryName = (pathname) => {
|
||||
|
||||
return path.dirname(pathname);
|
||||
};
|
||||
|
||||
export const isWindowsOS = () => {
|
||||
const os = platform.os;
|
||||
const osFamily = os.family.toLowerCase();
|
||||
|
||||
return osFamily.includes('windows');
|
||||
};
|
||||
|
@ -53,3 +53,12 @@ export const splitOnFirst = (str, char) => {
|
||||
|
||||
return [str.slice(0, index), str.slice(index + 1)];
|
||||
};
|
||||
|
||||
export const isValidUrl = (url) => {
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "v0.16.5",
|
||||
"version": "v0.16.6",
|
||||
"name": "bruno",
|
||||
"description": "Opensource API Client for Exploring and Testing APIs",
|
||||
"homepage": "https://www.usebruno.com",
|
||||
|
14
readme.md
14
readme.md
@ -1,7 +1,7 @@
|
||||
<br />
|
||||
<img src="assets/images/logo-transparent.png" width="80"/>
|
||||
|
||||
### Bruno - Opensource IDE for exploring and testing APIs.
|
||||
### Bruno - Opensource IDE for exploring and testing APIs.
|
||||
|
||||
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[![CI](https://github.com/usebruno/bruno/actions/workflows/unit-tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/workflows/unit-tests.yml)
|
||||
@ -10,36 +10,42 @@
|
||||
[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
|
||||
[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)
|
||||
|
||||
|
||||
Bruno is a new and innovative API client, aimed at revolutionizing the status quo represented by Postman and similar tools out there.
|
||||
|
||||
Bruno stores your collections directly in a folder on your filesystem. We use a plain text markup language, Bru, to save information about API requests.
|
||||
|
||||
You can use git or any version control of your choice to collaborate over your API collections.
|
||||
|
||||
Bruno is offline-only. There are no plans to add cloud-sync to Bruno, ever. We value your data privacy and believe it should stay on your device. Read our long-term vision [here](https://github.com/usebruno/bruno/discussions/269)
|
||||
|
||||
![bruno](assets/images/landing-2.png) <br /><br />
|
||||
|
||||
### Run across multiple platforms 🖥️
|
||||
|
||||
![bruno](assets/images/run-anywhere.png) <br /><br />
|
||||
|
||||
### Collaborate via Git 👩💻🧑💻
|
||||
|
||||
Or any version control system of your choice
|
||||
|
||||
![bruno](assets/images/version-control.png) <br /><br />
|
||||
|
||||
### Website 📄
|
||||
|
||||
Please visit [here](https://www.usebruno.com) to checkout our website and download the app
|
||||
|
||||
### Documentation 📄
|
||||
|
||||
Please visit [here](https://docs.usebruno.com) for documentation
|
||||
|
||||
### Contribute 👩💻🧑💻
|
||||
|
||||
I am happy that you are looking to improve bruno. Please checkout the [contributing guide](contributing.md)
|
||||
|
||||
Even if you are not able to make contributions via code, please don't hesitate to file bugs and feature requests that needs to be implemented to solve your use case.
|
||||
|
||||
### Support ❤️
|
||||
### Support ❤️
|
||||
|
||||
Woof! If you like project, hit that ⭐ button !!
|
||||
|
||||
### Authors
|
||||
@ -51,9 +57,11 @@ Woof! If you like project, hit that ⭐ button !!
|
||||
</div>
|
||||
|
||||
### Stay in touch 🌐
|
||||
|
||||
[Twitter](https://twitter.com/use_bruno) <br />
|
||||
[Website](https://www.usebruno.com) <br />
|
||||
[Discord](https://discord.com/invite/KgcZUncpjq)
|
||||
|
||||
### License 📄
|
||||
|
||||
[MIT](license.md)
|
||||
|
Loading…
Reference in New Issue
Block a user