mirror of
https://github.com/usebruno/bruno.git
synced 2024-11-23 00:13:24 +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",
|
"graphiql": "^1.5.9",
|
||||||
"graphql": "^16.6.0",
|
"graphql": "^16.6.0",
|
||||||
"graphql-request": "^3.7.0",
|
"graphql-request": "^3.7.0",
|
||||||
|
"handlebars": "^4.7.8",
|
||||||
|
"httpsnippet": "^3.0.1",
|
||||||
"idb": "^7.0.0",
|
"idb": "^7.0.0",
|
||||||
"immer": "^9.0.15",
|
"immer": "^9.0.15",
|
||||||
"know-your-http-well": "^0.5.0",
|
"know-your-http-well": "^0.5.0",
|
||||||
|
@ -119,7 +119,7 @@ export default class CodeEditor extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<StyledWrapper
|
<StyledWrapper
|
||||||
className="h-full"
|
className="h-full w-full"
|
||||||
aria-label="Code Editor"
|
aria-label="Code Editor"
|
||||||
ref={(node) => {
|
ref={(node) => {
|
||||||
this._node = 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">Send Request</div>
|
||||||
<div className="px-1 py-2">New 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">Edit Environments</div>
|
||||||
<div className="px-1 py-2">Help</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-1 flex-col px-1">
|
<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 + Enter</div>
|
||||||
<div className="px-1 py-2">Cmd + B</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 + E</div>
|
||||||
<div className="px-1 py-2">Cmd + H</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</StyledWrapper>
|
</StyledWrapper>
|
||||||
|
@ -4,15 +4,56 @@ import { useTheme } from 'providers/Theme';
|
|||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
import { getContentType, safeStringifyJSON, safeParseXML } from 'utils/common';
|
||||||
|
import { getCodeMirrorModeBasedOnContentType } from 'utils/common/codemirror';
|
||||||
|
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useMemo } 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 { storedTheme } = useTheme();
|
||||||
const [tab, setTab] = useState('raw');
|
const [tab, setTab] = useState('raw');
|
||||||
const dispatch = useDispatch();
|
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 = () => {
|
const onRun = () => {
|
||||||
if (disableRunEventListener) {
|
if (disableRunEventListener) {
|
||||||
@ -32,7 +73,7 @@ const QueryResult = ({ item, collection, value, width, disableRunEventListener,
|
|||||||
Raw
|
Raw
|
||||||
</div>
|
</div>
|
||||||
)];
|
)];
|
||||||
if (mode.includes('text/html')) {
|
if (mode.includes('html')) {
|
||||||
tabs.push(
|
tabs.push(
|
||||||
<div className={getTabClassname('preview')} role="tab" onClick={() => setTab('preview')}>
|
<div className={getTabClassname('preview')} role="tab" onClick={() => setTab('preview')}>
|
||||||
Preview
|
Preview
|
||||||
@ -43,7 +84,7 @@ const QueryResult = ({ item, collection, value, width, disableRunEventListener,
|
|||||||
const activeResult = useMemo(() => {
|
const activeResult = useMemo(() => {
|
||||||
if (tab === 'preview') {
|
if (tab === 'preview') {
|
||||||
// Add the Base tag to the head so content loads proparly. This also needs the correct CSP settings
|
// 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 (
|
return (
|
||||||
<webview
|
<webview
|
||||||
src={`data:text/html; charset=utf-8,${encodeURIComponent(webViewSrc)}`}
|
src={`data:text/html; charset=utf-8,${encodeURIComponent(webViewSrc)}`}
|
||||||
@ -58,7 +99,7 @@ const QueryResult = ({ item, collection, value, width, disableRunEventListener,
|
|||||||
collection={collection}
|
collection={collection}
|
||||||
theme={storedTheme}
|
theme={storedTheme}
|
||||||
onRun={onRun}
|
onRun={onRun}
|
||||||
value={value || ''}
|
value={value}
|
||||||
mode={mode}
|
mode={mode}
|
||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
|
@ -2,7 +2,6 @@ import React from 'react';
|
|||||||
import find from 'lodash/find';
|
import find from 'lodash/find';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { getContentType, formatResponse } from 'utils/common';
|
|
||||||
import { updateResponsePaneTab } from 'providers/ReduxStore/slices/tabs';
|
import { updateResponsePaneTab } from 'providers/ReduxStore/slices/tabs';
|
||||||
import QueryResult from './QueryResult';
|
import QueryResult from './QueryResult';
|
||||||
import Overlay from './Overlay';
|
import Overlay from './Overlay';
|
||||||
@ -41,8 +40,8 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
|||||||
item={item}
|
item={item}
|
||||||
collection={collection}
|
collection={collection}
|
||||||
width={rightPaneWidth}
|
width={rightPaneWidth}
|
||||||
value={response.data ? formatResponse(response) : ''}
|
data={response.data}
|
||||||
mode={getContentType(response.headers)}
|
headers={response.headers}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,8 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
|||||||
collection={collection}
|
collection={collection}
|
||||||
width={rightPaneWidth}
|
width={rightPaneWidth}
|
||||||
disableRunEventListener={true}
|
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 CloneCollectionItem from './CloneCollectionItem';
|
||||||
import DeleteCollectionItem from './DeleteCollectionItem';
|
import DeleteCollectionItem from './DeleteCollectionItem';
|
||||||
import RunCollectionItem from './RunCollectionItem';
|
import RunCollectionItem from './RunCollectionItem';
|
||||||
|
import GenerateCodeItem from './GenerateCodeItem';
|
||||||
import { isItemARequest, isItemAFolder, itemIsOpenedInTabs } from 'utils/tabs';
|
import { isItemARequest, isItemAFolder, itemIsOpenedInTabs } from 'utils/tabs';
|
||||||
import { doesRequestMatchSearchText, doesFolderHaveItemsMatchSearchText } from 'utils/collections/search';
|
import { doesRequestMatchSearchText, doesFolderHaveItemsMatchSearchText } from 'utils/collections/search';
|
||||||
import { getDefaultRequestPaneTab } from 'utils/collections';
|
import { getDefaultRequestPaneTab } from 'utils/collections';
|
||||||
@ -32,6 +33,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
|||||||
const [renameItemModalOpen, setRenameItemModalOpen] = useState(false);
|
const [renameItemModalOpen, setRenameItemModalOpen] = useState(false);
|
||||||
const [cloneItemModalOpen, setCloneItemModalOpen] = useState(false);
|
const [cloneItemModalOpen, setCloneItemModalOpen] = useState(false);
|
||||||
const [deleteItemModalOpen, setDeleteItemModalOpen] = useState(false);
|
const [deleteItemModalOpen, setDeleteItemModalOpen] = useState(false);
|
||||||
|
const [generateCodeItemModalOpen, setGenerateCodeItemModalOpen] = useState(false);
|
||||||
const [newRequestModalOpen, setNewRequestModalOpen] = useState(false);
|
const [newRequestModalOpen, setNewRequestModalOpen] = useState(false);
|
||||||
const [newFolderModalOpen, setNewFolderModalOpen] = useState(false);
|
const [newFolderModalOpen, setNewFolderModalOpen] = useState(false);
|
||||||
const [runCollectionModalOpen, setRunCollectionModalOpen] = useState(false);
|
const [runCollectionModalOpen, setRunCollectionModalOpen] = useState(false);
|
||||||
@ -166,6 +168,9 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
|||||||
{runCollectionModalOpen && (
|
{runCollectionModalOpen && (
|
||||||
<RunCollectionItem collection={collection} item={item} onClose={() => setRunCollectionModalOpen(false)} />
|
<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={itemRowClassName} ref={(node) => drag(drop(node))}>
|
||||||
<div className="flex items-center h-full w-full">
|
<div className="flex items-center h-full w-full">
|
||||||
{indents && indents.length
|
{indents && indents.length
|
||||||
@ -264,6 +269,18 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
|||||||
Clone
|
Clone
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{!isFolder && item.type === 'http-request' && (
|
||||||
|
<div
|
||||||
|
className="dropdown-item"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
dropdownTippyRef.current.hide();
|
||||||
|
setGenerateCodeItemModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Generate Code
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
className="dropdown-item delete-item"
|
className="dropdown-item delete-item"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
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 Collection from '../Collections/Collection';
|
||||||
import CreateCollection from '../CreateCollection';
|
import CreateCollection from '../CreateCollection';
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
@ -9,20 +15,47 @@ import { DndProvider } from 'react-dnd';
|
|||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
import { sortCollections } from 'providers/ReduxStore/slices/collections/actions';
|
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 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 (
|
return (
|
||||||
<div className="items-center mt-2 relative">
|
<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">
|
<div className="flex items-center py-1 select-none">
|
||||||
<span className="mr-2">
|
<span className="mr-2">
|
||||||
<IconFolders size={18} strokeWidth={1.5} />
|
<IconFolders size={18} strokeWidth={1.5} />
|
||||||
</span>
|
</span>
|
||||||
<span>Collections</span>
|
<span>Collections</span>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={() => dispatch(sortCollections())} >
|
{collections.length >= 1 && (
|
||||||
<IconSortAZ size={18} strokeWidth={1.5} />
|
<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>
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -116,7 +116,7 @@ const Sidebar = () => {
|
|||||||
</GitHubButton>
|
</GitHubButton>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -7,7 +7,6 @@ import SaveRequest from 'components/RequestPane/SaveRequest';
|
|||||||
import EnvironmentSettings from 'components/Environments/EnvironmentSettings';
|
import EnvironmentSettings from 'components/Environments/EnvironmentSettings';
|
||||||
import NetworkError from 'components/ResponsePane/NetworkError';
|
import NetworkError from 'components/ResponsePane/NetworkError';
|
||||||
import NewRequest from 'components/Sidebar/NewRequest';
|
import NewRequest from 'components/Sidebar/NewRequest';
|
||||||
import BrunoSupport from 'components/BrunoSupport';
|
|
||||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||||
import { findCollectionByUid, findItemInCollection } from 'utils/collections';
|
import { findCollectionByUid, findItemInCollection } from 'utils/collections';
|
||||||
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
|
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
|
||||||
@ -22,7 +21,6 @@ export const HotkeysProvider = (props) => {
|
|||||||
const [showSaveRequestModal, setShowSaveRequestModal] = useState(false);
|
const [showSaveRequestModal, setShowSaveRequestModal] = useState(false);
|
||||||
const [showEnvSettingsModal, setShowEnvSettingsModal] = useState(false);
|
const [showEnvSettingsModal, setShowEnvSettingsModal] = useState(false);
|
||||||
const [showNewRequestModal, setShowNewRequestModal] = useState(false);
|
const [showNewRequestModal, setShowNewRequestModal] = useState(false);
|
||||||
const [showBrunoSupportModal, setShowBrunoSupportModal] = useState(false);
|
|
||||||
|
|
||||||
const getCurrentCollectionItems = () => {
|
const getCurrentCollectionItems = () => {
|
||||||
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
|
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||||
@ -133,18 +131,6 @@ export const HotkeysProvider = (props) => {
|
|||||||
};
|
};
|
||||||
}, [activeTabUid, tabs, collections, setShowNewRequestModal]);
|
}, [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
|
// close tab hotkey
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Mousetrap.bind(['command+w', 'ctrl+w'], (e) => {
|
Mousetrap.bind(['command+w', 'ctrl+w'], (e) => {
|
||||||
@ -164,7 +150,6 @@ export const HotkeysProvider = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<HotkeysContext.Provider {...props} value="hotkey">
|
<HotkeysContext.Provider {...props} value="hotkey">
|
||||||
{showBrunoSupportModal && <BrunoSupport onClose={() => setShowBrunoSupportModal(false)} />}
|
|
||||||
{showSaveRequestModal && (
|
{showSaveRequestModal && (
|
||||||
<SaveRequest items={getCurrentCollectionItems()} onClose={() => setShowSaveRequestModal(false)} />
|
<SaveRequest items={getCurrentCollectionItems()} onClose={() => setShowSaveRequestModal(false)} />
|
||||||
)}
|
)}
|
||||||
|
@ -22,7 +22,7 @@ import {
|
|||||||
} from 'utils/collections';
|
} from 'utils/collections';
|
||||||
import { collectionSchema, itemSchema, environmentSchema, environmentsSchema } from '@usebruno/schema';
|
import { collectionSchema, itemSchema, environmentSchema, environmentsSchema } from '@usebruno/schema';
|
||||||
import { waitForNextTick } from 'utils/common';
|
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 { sendNetworkRequest, cancelNetworkRequest } from 'utils/network';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -34,12 +34,12 @@ import {
|
|||||||
renameItem as _renameItem,
|
renameItem as _renameItem,
|
||||||
cloneItem as _cloneItem,
|
cloneItem as _cloneItem,
|
||||||
deleteItem as _deleteItem,
|
deleteItem as _deleteItem,
|
||||||
sortCollections as _sortCollections,
|
|
||||||
saveRequest as _saveRequest,
|
saveRequest as _saveRequest,
|
||||||
selectEnvironment as _selectEnvironment,
|
selectEnvironment as _selectEnvironment,
|
||||||
createCollection as _createCollection,
|
createCollection as _createCollection,
|
||||||
renameCollection as _renameCollection,
|
renameCollection as _renameCollection,
|
||||||
removeCollection as _removeCollection,
|
removeCollection as _removeCollection,
|
||||||
|
sortCollections as _sortCollections,
|
||||||
collectionAddEnvFileEvent as _collectionAddEnvFileEvent
|
collectionAddEnvFileEvent as _collectionAddEnvFileEvent
|
||||||
} from './index';
|
} from './index';
|
||||||
|
|
||||||
@ -146,6 +146,11 @@ export const cancelRequest = (cancelTokenUid, item, collection) => (dispatch) =>
|
|||||||
.catch((err) => console.log(err));
|
.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) => {
|
export const runCollectionFolder = (collectionUid, folderUid, recursive) => (dispatch, getState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||||
@ -263,7 +268,19 @@ export const renameItem = (newName, itemUid, collectionUid) => (dispatch, getSta
|
|||||||
}
|
}
|
||||||
const { ipcRenderer } = window;
|
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
|
ipcRenderer
|
||||||
.invoke('renderer:delete-item', item.pathname, item.type)
|
.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));
|
.catch((error) => reject(error));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sortCollections = () => (dispatch) => {
|
|
||||||
dispatch(_sortCollections())
|
|
||||||
}
|
|
||||||
export const moveItem = (collectionUid, draggedItemUid, targetItemUid) => (dispatch, getState) => {
|
export const moveItem = (collectionUid, draggedItemUid, targetItemUid) => (dispatch, getState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||||
|
@ -28,7 +28,8 @@ import { getSubdirectoriesFromRoot, getDirectoryName } from 'utils/common/platfo
|
|||||||
const PATH_SEPARATOR = path.sep;
|
const PATH_SEPARATOR = path.sep;
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
collections: []
|
collections: [],
|
||||||
|
collectionSortOrder: 'default'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const collectionsSlice = createSlice({
|
export const collectionsSlice = createSlice({
|
||||||
@ -38,12 +39,12 @@ export const collectionsSlice = createSlice({
|
|||||||
createCollection: (state, action) => {
|
createCollection: (state, action) => {
|
||||||
const collectionUids = map(state.collections, (c) => c.uid);
|
const collectionUids = map(state.collections, (c) => c.uid);
|
||||||
const collection = action.payload;
|
const collection = action.payload;
|
||||||
|
|
||||||
// last action is used to track the last action performed on the collection
|
// last action is used to track the last action performed on the collection
|
||||||
// this is optional
|
// this is optional
|
||||||
// this is used in scenarios where we want to know the last action performed on the collection
|
// 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
|
// and take some extra action based on that
|
||||||
// for example, when a env is created, we want to auto select it the env modal
|
// for example, when a env is created, we want to auto select it the env modal
|
||||||
|
collection.importedAt = new Date().getTime();
|
||||||
collection.lastAction = null;
|
collection.lastAction = null;
|
||||||
|
|
||||||
collapseCollection(collection);
|
collapseCollection(collection);
|
||||||
@ -70,8 +71,19 @@ export const collectionsSlice = createSlice({
|
|||||||
removeCollection: (state, action) => {
|
removeCollection: (state, action) => {
|
||||||
state.collections = filter(state.collections, (c) => c.uid !== action.payload.collectionUid);
|
state.collections = filter(state.collections, (c) => c.uid !== action.payload.collectionUid);
|
||||||
},
|
},
|
||||||
sortCollections: (state) => {
|
sortCollections: (state, action) => {
|
||||||
state.collections = state.collections.sort((a, b) => a.name.localeCompare(b.name))
|
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) => {
|
updateLastAction: (state, action) => {
|
||||||
const { collectionUid, lastAction } = action.payload;
|
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);
|
let draggedItemParent = findParentItemInCollection(collection, draggedItem.uid);
|
||||||
|
|
||||||
if (draggedItemParent) {
|
if (draggedItemParent) {
|
||||||
|
draggedItemParent.items = sortBy(draggedItemParent.items, (item) => item.seq);
|
||||||
draggedItemParent.items = filter(draggedItemParent.items, (i) => i.uid !== draggedItem.uid);
|
draggedItemParent.items = filter(draggedItemParent.items, (i) => i.uid !== draggedItem.uid);
|
||||||
draggedItem.pathname = path.join(draggedItemParent.pathname, draggedItem.filename);
|
draggedItem.pathname = path.join(draggedItemParent.pathname, draggedItem.filename);
|
||||||
} else {
|
} else {
|
||||||
|
collection.items = sortBy(collection.items, (item) => item.seq);
|
||||||
collection.items = filter(collection.items, (i) => i.uid !== draggedItem.uid);
|
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);
|
let targetItemParent = findParentItemInCollection(collection, targetItem.uid);
|
||||||
|
|
||||||
if (targetItemParent) {
|
if (targetItemParent) {
|
||||||
|
targetItemParent.items = sortBy(targetItemParent.items, (item) => item.seq);
|
||||||
let targetItemIndex = findIndex(targetItemParent.items, (i) => i.uid === targetItem.uid);
|
let targetItemIndex = findIndex(targetItemParent.items, (i) => i.uid === targetItem.uid);
|
||||||
targetItemParent.items.splice(targetItemIndex + 1, 0, draggedItem);
|
targetItemParent.items.splice(targetItemIndex + 1, 0, draggedItem);
|
||||||
draggedItem.pathname = path.join(targetItemParent.pathname, draggedItem.filename);
|
draggedItem.pathname = path.join(targetItemParent.pathname, draggedItem.filename);
|
||||||
} else {
|
} else {
|
||||||
|
collection.items = sortBy(collection.items, (item) => item.seq);
|
||||||
let targetItemIndex = findIndex(collection.items, (i) => i.uid === targetItem.uid);
|
let targetItemIndex = findIndex(collection.items, (i) => i.uid === targetItem.uid);
|
||||||
collection.items.splice(targetItemIndex + 1, 0, draggedItem);
|
collection.items.splice(targetItemIndex + 1, 0, draggedItem);
|
||||||
draggedItem.pathname = path.join(collection.pathname, draggedItem.filename);
|
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);
|
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
|
// Remove any characters that are not alphanumeric, spaces, hyphens, or underscores
|
||||||
export const normalizeFileName = (name) => {
|
export const normalizeFileName = (name) => {
|
||||||
if (!name) {
|
if (!name) {
|
||||||
@ -80,16 +91,6 @@ export const getContentType = (headers) => {
|
|||||||
return contentType[0];
|
return contentType[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
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 trim from 'lodash/trim';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import slash from './slash';
|
import slash from './slash';
|
||||||
|
import platform from 'platform';
|
||||||
|
|
||||||
export const isElectron = () => {
|
export const isElectron = () => {
|
||||||
if (!window) {
|
if (!window) {
|
||||||
@ -33,3 +34,10 @@ export const getDirectoryName = (pathname) => {
|
|||||||
|
|
||||||
return path.dirname(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)];
|
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",
|
"name": "bruno",
|
||||||
"description": "Opensource API Client for Exploring and Testing APIs",
|
"description": "Opensource API Client for Exploring and Testing APIs",
|
||||||
"homepage": "https://www.usebruno.com",
|
"homepage": "https://www.usebruno.com",
|
||||||
|
10
readme.md
10
readme.md
@ -10,36 +10,42 @@
|
|||||||
[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
|
[![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)
|
[![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 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.
|
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.
|
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 />
|
![bruno](assets/images/landing-2.png) <br /><br />
|
||||||
|
|
||||||
### Run across multiple platforms 🖥️
|
### Run across multiple platforms 🖥️
|
||||||
|
|
||||||
![bruno](assets/images/run-anywhere.png) <br /><br />
|
![bruno](assets/images/run-anywhere.png) <br /><br />
|
||||||
|
|
||||||
### Collaborate via Git 👩💻🧑💻
|
### Collaborate via Git 👩💻🧑💻
|
||||||
|
|
||||||
Or any version control system of your choice
|
Or any version control system of your choice
|
||||||
|
|
||||||
![bruno](assets/images/version-control.png) <br /><br />
|
![bruno](assets/images/version-control.png) <br /><br />
|
||||||
|
|
||||||
### Website 📄
|
### Website 📄
|
||||||
|
|
||||||
Please visit [here](https://www.usebruno.com) to checkout our website and download the app
|
Please visit [here](https://www.usebruno.com) to checkout our website and download the app
|
||||||
|
|
||||||
### Documentation 📄
|
### Documentation 📄
|
||||||
|
|
||||||
Please visit [here](https://docs.usebruno.com) for documentation
|
Please visit [here](https://docs.usebruno.com) for documentation
|
||||||
|
|
||||||
### Contribute 👩💻🧑💻
|
### Contribute 👩💻🧑💻
|
||||||
|
|
||||||
I am happy that you are looking to improve bruno. Please checkout the [contributing guide](contributing.md)
|
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.
|
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 !!
|
Woof! If you like project, hit that ⭐ button !!
|
||||||
|
|
||||||
### Authors
|
### Authors
|
||||||
@ -51,9 +57,11 @@ Woof! If you like project, hit that ⭐ button !!
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
### Stay in touch 🌐
|
### Stay in touch 🌐
|
||||||
|
|
||||||
[Twitter](https://twitter.com/use_bruno) <br />
|
[Twitter](https://twitter.com/use_bruno) <br />
|
||||||
[Website](https://www.usebruno.com) <br />
|
[Website](https://www.usebruno.com) <br />
|
||||||
[Discord](https://discord.com/invite/KgcZUncpjq)
|
[Discord](https://discord.com/invite/KgcZUncpjq)
|
||||||
|
|
||||||
### License 📄
|
### License 📄
|
||||||
|
|
||||||
[MIT](license.md)
|
[MIT](license.md)
|
||||||
|
Loading…
Reference in New Issue
Block a user