diff --git a/contributing.md b/contributing.md index 6b70dc028..a538f1bd7 100644 --- a/contributing.md +++ b/contributing.md @@ -19,7 +19,7 @@ Libraries we use ### Dependencies -You would need [Node v14.x or the latest LTS version](https://nodejs.org/en/) and npm 8.x. We use npm workspaces in the project +You would need [Node v18.x or the latest LTS version](https://nodejs.org/en/) and npm 8.x. We use npm workspaces in the project ### Lets start coding diff --git a/docs/development.md b/docs/development.md index 21bf0cbab..77614d2f6 100644 --- a/docs/development.md +++ b/docs/development.md @@ -3,7 +3,8 @@ Bruno is being developed as a desktop app. You need to load the app by running the nextjs app in one terminal and then run the electron app in another terminal. ### Dependencies -* NodeJS v18 + +- NodeJS v18 ### Local Development @@ -15,7 +16,6 @@ nvm use npm i --legacy-peer-deps # build graphql docs -# note: you can for now ignore the error thrown while building the graphql docs npm run build:graphql-docs # build bruno query diff --git a/package.json b/package.json index 0c802c492..9aaaf23f2 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,5 @@ }, "overrides": { "rollup": "3.2.5" - }, - "dependencies": {} + } } diff --git a/packages/bruno-app/package.json b/packages/bruno-app/package.json index a0c3cfffa..625e9dc0d 100644 --- a/packages/bruno-app/package.json +++ b/packages/bruno-app/package.json @@ -30,8 +30,11 @@ "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", "lodash": "^4.17.21", "markdown-it": "^13.0.1", "mousetrap": "^1.6.5", diff --git a/packages/bruno-app/src/components/CodeEditor/index.js b/packages/bruno-app/src/components/CodeEditor/index.js index bcc13b5c2..96d5bb48a 100644 --- a/packages/bruno-app/src/components/CodeEditor/index.js +++ b/packages/bruno-app/src/components/CodeEditor/index.js @@ -119,7 +119,7 @@ export default class CodeEditor extends React.Component { render() { return ( { this._node = node; diff --git a/packages/bruno-app/src/components/Modal/StyledWrapper.js b/packages/bruno-app/src/components/Modal/StyledWrapper.js index f0cb79353..4ec5d4a25 100644 --- a/packages/bruno-app/src/components/Modal/StyledWrapper.js +++ b/packages/bruno-app/src/components/Modal/StyledWrapper.js @@ -1,6 +1,8 @@ import styled from 'styled-components'; const Wrapper = styled.div` + color: ${(props) => props.theme.text}; + &.modal--animate-out { animation: fade-out 0.5s forwards cubic-bezier(0.19, 1, 0.22, 1); diff --git a/packages/bruno-app/src/components/RequestPane/Assertions/AssertionRow/index.js b/packages/bruno-app/src/components/RequestPane/Assertions/AssertionRow/index.js index d4d6eeff8..61293b616 100644 --- a/packages/bruno-app/src/components/RequestPane/Assertions/AssertionRow/index.js +++ b/packages/bruno-app/src/components/RequestPane/Assertions/AssertionRow/index.js @@ -197,10 +197,11 @@ const AssertionRow = ({ handleAssertionChange(e, assertion, 'enabled')} /> - diff --git a/packages/bruno-app/src/components/RequestPane/Auth/AuthMode/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/Auth/AuthMode/StyledWrapper.js new file mode 100644 index 000000000..cdbdf8424 --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/AuthMode/StyledWrapper.js @@ -0,0 +1,28 @@ +import styled from 'styled-components'; + +const Wrapper = styled.div` + font-size: 0.8125rem; + + .auth-mode-selector { + background: transparent; + + .auth-mode-label { + color: ${(props) => props.theme.colors.text.yellow}; + } + + .dropdown-item { + padding: 0.2rem 0.6rem !important; + } + + .label-item { + padding: 0.2rem 0.6rem !important; + } + } + + .caret { + color: rgb(140, 140, 140); + fill: rgb(140 140 140); + } +`; + +export default Wrapper; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/AuthMode/index.js b/packages/bruno-app/src/components/RequestPane/Auth/AuthMode/index.js new file mode 100644 index 000000000..43c90e2ce --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/AuthMode/index.js @@ -0,0 +1,70 @@ +import React, { useRef, forwardRef } from 'react'; +import get from 'lodash/get'; +import { IconCaretDown } from '@tabler/icons'; +import Dropdown from 'components/Dropdown'; +import { useDispatch } from 'react-redux'; +import { updateRequestAuthMode } from 'providers/ReduxStore/slices/collections'; +import { humanizeRequestAuthMode } from 'utils/collections'; +import StyledWrapper from './StyledWrapper'; + +const AuthMode = ({ item, collection }) => { + const dispatch = useDispatch(); + const dropdownTippyRef = useRef(); + const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref); + const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode'); + + const Icon = forwardRef((props, ref) => { + return ( +
+ {humanizeRequestAuthMode(authMode)} +
+ ); + }); + + const onModeChange = (value) => { + dispatch( + updateRequestAuthMode({ + itemUid: item.uid, + collectionUid: collection.uid, + mode: value + }) + ); + }; + + return ( + +
+ } placement="bottom-end"> +
{ + dropdownTippyRef.current.hide(); + onModeChange('basic'); + }} + > + Basic Auth +
+
{ + dropdownTippyRef.current.hide(); + onModeChange('bearer'); + }} + > + Bearer Token +
+
{ + dropdownTippyRef.current.hide(); + onModeChange('none'); + }} + > + No Auth +
+
+
+
+ ); +}; +export default AuthMode; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/BasicAuth/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/Auth/BasicAuth/StyledWrapper.js new file mode 100644 index 000000000..c2bb5d207 --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/BasicAuth/StyledWrapper.js @@ -0,0 +1,16 @@ +import styled from 'styled-components'; + +const Wrapper = styled.div` + label { + font-size: 0.8125rem; + } + + .single-line-editor-wrapper { + padding: 0.15rem 0.4rem; + border-radius: 3px; + border: solid 1px ${(props) => props.theme.input.border}; + background-color: ${(props) => props.theme.input.bg}; + } +`; + +export default Wrapper; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/BasicAuth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/BasicAuth/index.js new file mode 100644 index 000000000..845dae273 --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/BasicAuth/index.js @@ -0,0 +1,76 @@ +import React from 'react'; +import get from 'lodash/get'; +import { useTheme } from 'providers/Theme'; +import { useDispatch } from 'react-redux'; +import SingleLineEditor from 'components/SingleLineEditor'; +import { updateAuth } from 'providers/ReduxStore/slices/collections'; +import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions'; +import StyledWrapper from './StyledWrapper'; + +const BasicAuth = ({ item, collection }) => { + const dispatch = useDispatch(); + const { storedTheme } = useTheme(); + + const basicAuth = item.draft ? get(item, 'draft.request.auth.basic', {}) : get(item, 'request.auth.basic', {}); + + const handleRun = () => dispatch(sendRequest(item, collection.uid)); + const handleSave = () => dispatch(saveRequest(item.uid, collection.uid)); + + const handleUsernameChange = (username) => { + dispatch( + updateAuth({ + mode: 'basic', + collectionUid: collection.uid, + itemUid: item.uid, + content: { + username: username, + password: basicAuth.password + } + }) + ); + }; + + const handlePasswordChange = (password) => { + dispatch( + updateAuth({ + mode: 'basic', + collectionUid: collection.uid, + itemUid: item.uid, + content: { + username: basicAuth.username, + password: password + } + }) + ); + }; + + return ( + + +
+ handleUsernameChange(val)} + onRun={handleRun} + collection={collection} + /> +
+ + +
+ handlePasswordChange(val)} + onRun={handleRun} + collection={collection} + /> +
+
+ ); +}; + +export default BasicAuth; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/BearerAuth/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/Auth/BearerAuth/StyledWrapper.js new file mode 100644 index 000000000..c2bb5d207 --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/BearerAuth/StyledWrapper.js @@ -0,0 +1,16 @@ +import styled from 'styled-components'; + +const Wrapper = styled.div` + label { + font-size: 0.8125rem; + } + + .single-line-editor-wrapper { + padding: 0.15rem 0.4rem; + border-radius: 3px; + border: solid 1px ${(props) => props.theme.input.border}; + background-color: ${(props) => props.theme.input.bg}; + } +`; + +export default Wrapper; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/BearerAuth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/BearerAuth/index.js new file mode 100644 index 000000000..d839d6206 --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/BearerAuth/index.js @@ -0,0 +1,51 @@ +import React from 'react'; +import get from 'lodash/get'; +import { useTheme } from 'providers/Theme'; +import { useDispatch } from 'react-redux'; +import SingleLineEditor from 'components/SingleLineEditor'; +import { updateAuth } from 'providers/ReduxStore/slices/collections'; +import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions'; +import StyledWrapper from './StyledWrapper'; + +const BearerAuth = ({ item, collection }) => { + const dispatch = useDispatch(); + const { storedTheme } = useTheme(); + + const bearerToken = item.draft + ? get(item, 'draft.request.auth.bearer.token') + : get(item, 'request.auth.bearer.token'); + + const handleRun = () => dispatch(sendRequest(item, collection.uid)); + const handleSave = () => dispatch(saveRequest(item.uid, collection.uid)); + + const handleTokenChange = (token) => { + dispatch( + updateAuth({ + mode: 'bearer', + collectionUid: collection.uid, + itemUid: item.uid, + content: { + token: token + } + }) + ); + }; + + return ( + + +
+ handleTokenChange(val)} + onRun={handleRun} + collection={collection} + /> +
+
+ ); +}; + +export default BearerAuth; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/Auth/StyledWrapper.js new file mode 100644 index 000000000..e49220854 --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/StyledWrapper.js @@ -0,0 +1,5 @@ +import styled from 'styled-components'; + +const Wrapper = styled.div``; + +export default Wrapper; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/index.js new file mode 100644 index 000000000..f07b80f95 --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/index.js @@ -0,0 +1,31 @@ +import React from 'react'; +import get from 'lodash/get'; +import AuthMode from './AuthMode'; +import BearerAuth from './BearerAuth'; +import BasicAuth from './BasicAuth'; +import StyledWrapper from './StyledWrapper'; + +const Auth = ({ item, collection }) => { + const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode'); + + const getAuthView = () => { + switch (authMode) { + case 'basic': { + return ; + } + case 'bearer': { + return ; + } + } + }; + + return ( + +
+ +
+ {getAuthView()} +
+ ); +}; +export default Auth; diff --git a/packages/bruno-app/src/components/RequestPane/FormUrlEncodedParams/index.js b/packages/bruno-app/src/components/RequestPane/FormUrlEncodedParams/index.js index 4f28cc579..3cf5842e8 100644 --- a/packages/bruno-app/src/components/RequestPane/FormUrlEncodedParams/index.js +++ b/packages/bruno-app/src/components/RequestPane/FormUrlEncodedParams/index.js @@ -116,10 +116,11 @@ const FormUrlEncodedParams = ({ item, collection }) => { handleParamChange(e, param, 'enabled')} /> - diff --git a/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js b/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js index caace776f..ec20514fe 100644 --- a/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js +++ b/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js @@ -7,6 +7,8 @@ import QueryParams from 'components/RequestPane/QueryParams'; import RequestHeaders from 'components/RequestPane/RequestHeaders'; import RequestBody from 'components/RequestPane/RequestBody'; import RequestBodyMode from 'components/RequestPane/RequestBody/RequestBodyMode'; +import Auth from 'components/RequestPane/Auth'; +import AuthMode from 'components/RequestPane/Auth/AuthMode'; import Vars from 'components/RequestPane/Vars'; import Assertions from 'components/RequestPane/Assertions'; import Script from 'components/RequestPane/Script'; @@ -38,6 +40,9 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => { case 'headers': { return ; } + case 'auth': { + return ; + } case 'vars': { return ; } @@ -83,6 +88,9 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
selectTab('headers')}> Headers
+
selectTab('auth')}> + Auth +
selectTab('vars')}> Vars
@@ -95,15 +103,15 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
selectTab('tests')}> Tests
- {/* Moved to post mvp */} - {/*
selectTab('auth')}>Auth
*/} {focusedTab.requestPaneTab === 'body' ? (
) : null} -
+
{getTabPanel(focusedTab.requestPaneTab)}
diff --git a/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js b/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js index 9cbfd67fd..f8bd7ebbd 100644 --- a/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js +++ b/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js @@ -116,10 +116,11 @@ const MultipartFormParams = ({ item, collection }) => { handleParamChange(e, param, 'enabled')} /> - diff --git a/packages/bruno-app/src/components/RequestPane/QueryParams/index.js b/packages/bruno-app/src/components/RequestPane/QueryParams/index.js index 0cb92f8e1..54e3ee0b3 100644 --- a/packages/bruno-app/src/components/RequestPane/QueryParams/index.js +++ b/packages/bruno-app/src/components/RequestPane/QueryParams/index.js @@ -115,10 +115,11 @@ const QueryParams = ({ item, collection }) => { handleParamChange(e, param, 'enabled')} /> - diff --git a/packages/bruno-app/src/components/RequestPane/RequestHeaders/index.js b/packages/bruno-app/src/components/RequestPane/RequestHeaders/index.js index 2cd5ccade..db8b20f5f 100644 --- a/packages/bruno-app/src/components/RequestPane/RequestHeaders/index.js +++ b/packages/bruno-app/src/components/RequestPane/RequestHeaders/index.js @@ -8,6 +8,8 @@ import { addRequestHeader, updateRequestHeader, deleteRequestHeader } from 'prov import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions'; import SingleLineEditor from 'components/SingleLineEditor'; import StyledWrapper from './StyledWrapper'; +import { headers as StandardHTTPHeaders } from 'know-your-http-well'; +const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header); const RequestHeaders = ({ item, collection }) => { const dispatch = useDispatch(); @@ -91,6 +93,7 @@ const RequestHeaders = ({ item, collection }) => { 'name' ) } + autocomplete={headerAutoCompleteList} onRun={handleRun} collection={collection} /> @@ -120,10 +123,11 @@ const RequestHeaders = ({ item, collection }) => { handleHeaderValueChange(e, header, 'enabled')} /> - diff --git a/packages/bruno-app/src/components/RequestPane/Vars/VarsTable/index.js b/packages/bruno-app/src/components/RequestPane/Vars/VarsTable/index.js index 7333acc62..6ee760978 100644 --- a/packages/bruno-app/src/components/RequestPane/Vars/VarsTable/index.js +++ b/packages/bruno-app/src/components/RequestPane/Vars/VarsTable/index.js @@ -128,10 +128,11 @@ const VarsTable = ({ item, collection, vars, varType }) => { handleVarChange(e, _var, 'enabled')} /> - diff --git a/packages/bruno-app/src/components/ResponsePane/Placeholder/index.js b/packages/bruno-app/src/components/ResponsePane/Placeholder/index.js index 0879ba520..2e7cd8621 100644 --- a/packages/bruno-app/src/components/ResponsePane/Placeholder/index.js +++ b/packages/bruno-app/src/components/ResponsePane/Placeholder/index.js @@ -13,13 +13,11 @@ const Placeholder = () => {
Send Request
New Request
Edit Environments
-
Help
Cmd + Enter
Cmd + B
Cmd + E
-
Cmd + H
diff --git a/packages/bruno-app/src/components/ResponsePane/QueryResult/StyledWrapper.js b/packages/bruno-app/src/components/ResponsePane/QueryResult/StyledWrapper.js index 9f7583222..5429ef440 100644 --- a/packages/bruno-app/src/components/ResponsePane/QueryResult/StyledWrapper.js +++ b/packages/bruno-app/src/components/ResponsePane/QueryResult/StyledWrapper.js @@ -1,9 +1,27 @@ import styled from 'styled-components'; const StyledWrapper = styled.div` + display: grid; + grid-template-columns: 100%; + grid-template-rows: 1.25rem calc(100% - 1.25rem); + + /* This is a hack to force Codemirror to use all available space */ + > div { + position: relative; + } + div.CodeMirror { - /* todo: find a better way */ - height: calc(100vh - 220px); + position: absolute; + top: 0; + bottom: 0; + height: 100%; + width: 100%; + } + + div[role='tablist'] { + .active { + color: ${(props) => props.theme.colors.text.yellow}; + } } `; diff --git a/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js b/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js index e6d6f94a8..5729e0b2d 100644 --- a/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js +++ b/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js @@ -3,12 +3,57 @@ import CodeEditor from 'components/CodeEditor'; 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('preview'); 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) { @@ -17,18 +62,52 @@ const QueryResult = ({ item, collection, value, width, disableRunEventListener, dispatch(sendRequest(item, collection.uid)); }; - return ( - -
- { + return classnames(`select-none ${tabName}`, { + active: tabName === tab, + 'cursor-pointer': tabName !== tab + }); + }; + + const getTabs = () => { + if (!mode.includes('html')) { + return null; + } + + return ( + <> +
setTab('raw')}> + Raw +
+
setTab('preview')}> + Preview +
+ + ); + }; + + const activeResult = useMemo(() => { + if (tab === 'preview' && mode.includes('html')) { + // Add the Base tag to the head so content loads properly. This also needs the correct CSP settings + const webViewSrc = data.replace('', ``); + return ( + + ); + } + + return ; + }, [tab, collection, storedTheme, onRun, value, mode]); + + return ( + +
+ {getTabs()}
+ {activeResult}
); }; diff --git a/packages/bruno-app/src/components/ResponsePane/ResponseSize/index.js b/packages/bruno-app/src/components/ResponsePane/ResponseSize/index.js index c3c2f1fe3..2500474cb 100644 --- a/packages/bruno-app/src/components/ResponsePane/ResponseSize/index.js +++ b/packages/bruno-app/src/components/ResponsePane/ResponseSize/index.js @@ -7,7 +7,7 @@ const ResponseSize = ({ size }) => { if (size > 1024) { // size is greater than 1kb let kb = Math.floor(size / 1024); - let decimal = ((size % 1024) / 1024).toFixed(2) * 100; + let decimal = Math.round(((size % 1024) / 1024).toFixed(2) * 100); sizeToDisplay = kb + '.' + decimal + 'KB'; } else { sizeToDisplay = size + 'B'; diff --git a/packages/bruno-app/src/components/ResponsePane/index.js b/packages/bruno-app/src/components/ResponsePane/index.js index 919922fd1..21c4ca16f 100644 --- a/packages/bruno-app/src/components/ResponsePane/index.js +++ b/packages/bruno-app/src/components/ResponsePane/index.js @@ -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} /> ); } @@ -93,10 +92,6 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => { }); }; - const isJson = (headers) => { - return getContentType(headers) === 'application/ld+json'; - }; - return (
@@ -120,7 +115,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
) : null}
-
{getTabPanel(focusedTab.responsePaneTab)}
+
{getTabPanel(focusedTab.responsePaneTab)}
); }; diff --git a/packages/bruno-app/src/components/RunnerResults/ResponsePane/index.js b/packages/bruno-app/src/components/RunnerResults/ResponsePane/index.js index ba87fcaf9..2c4f28b20 100644 --- a/packages/bruno-app/src/components/RunnerResults/ResponsePane/index.js +++ b/packages/bruno-app/src/components/RunnerResults/ResponsePane/index.js @@ -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} /> ); } diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/index.js new file mode 100644 index 000000000..79d636daf --- /dev/null +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/index.js @@ -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 ; +}; + +export default CodeView; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/StyledWrapper.js new file mode 100644 index 000000000..f1c1c33e4 --- /dev/null +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/StyledWrapper.js @@ -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; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js new file mode 100644 index 000000000..509a529bd --- /dev/null +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js @@ -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 ( + + +
+
+
+ {languages && + languages.length && + languages.map((language) => ( +
setSelectedLanguage(language)} + > + {language.name} +
+ ))} +
+
+
+ {isValidUrl(interpolatedUrl) ? ( + + ) : ( +
+
+

Invalid URL: {interpolatedUrl}

+

Please check the URL and try again

+
+
+ )} +
+
+
+
+ ); +}; + +export default GenerateCodeItem; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js index 3916cc2e6..a08b4fe1f 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js @@ -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); @@ -113,6 +115,10 @@ const CollectionItem = ({ item, collection, searchText }) => { } }; + const handleDoubleClick = (event) => { + setRenameItemModalOpen(true); + }; + let indents = range(item.depth); const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref); const isFolder = isItemAFolder(item); @@ -166,6 +172,9 @@ const CollectionItem = ({ item, collection, searchText }) => { {runCollectionModalOpen && ( setRunCollectionModalOpen(false)} /> )} + {generateCodeItemModalOpen && ( + setGenerateCodeItemModalOpen(false)} /> + )}
drag(drop(node))}>
{indents && indents.length @@ -173,6 +182,7 @@ const CollectionItem = ({ item, collection, searchText }) => { return (
{ : null}
{ Clone
)} + {!isFolder && item.type === 'http-request' && ( +
{ + e.stopPropagation(); + dropdownTippyRef.current.hide(); + setGenerateCodeItemModalOpen(true); + }} + > + Generate Code +
+ )}
{ diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js index b015df0aa..b150ade8c 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js @@ -16,7 +16,7 @@ import CollectionItem from './CollectionItem'; import RemoveCollection from './RemoveCollection'; import CollectionProperties from './CollectionProperties'; import { doesCollectionHaveItemsMatchingSearchText } from 'utils/collections/search'; -import { isItemAFolder, isItemARequest, transformCollectionToSaveToIdb } from 'utils/collections'; +import { isItemAFolder, isItemARequest, transformCollectionToSaveToExportAsFile } from 'utils/collections'; import exportCollection from 'utils/collections/export'; import RenameCollection from './RenameCollection'; @@ -69,7 +69,7 @@ const Collection = ({ collection, searchText }) => { const handleExportClick = () => { const collectionCopy = cloneDeep(collection); - exportCollection(transformCollectionToSaveToIdb(collectionCopy)); + exportCollection(transformCollectionToSaveToExportAsFile(collectionCopy)); }; const [{ isOver }, drop] = useDrop({ diff --git a/packages/bruno-app/src/components/Sidebar/Collections/index.js b/packages/bruno-app/src/components/Sidebar/Collections/index.js index 496f01456..af54350e4 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/index.js @@ -1,21 +1,61 @@ import React, { useState } from 'react'; -import { useSelector } from 'react-redux'; -import { IconSearch, IconFolders } from '@tabler/icons'; +import { useDispatch, useSelector } from 'react-redux'; +import { + IconSearch, + IconFolders, + IconArrowsSort, + IconSortAscendingLetters, + IconSortDescendingLetters +} from '@tabler/icons'; import Collection from '../Collections/Collection'; import CreateCollection from '../CreateCollection'; import StyledWrapper from './StyledWrapper'; import CreateOrOpenCollection from './CreateOrOpenCollection'; 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 { 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 (
-
- - - - Collections +
+
+ + + + Collections +
+ {collections.length >= 1 && ( + + )}
); diff --git a/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js b/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js index 8a65bedb2..bfe59ae86 100644 --- a/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js +++ b/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js @@ -27,8 +27,9 @@ const CreateCollection = ({ onClose }) => { collectionFolderName: Yup.string() .min(1, 'must be atleast 1 characters') .max(50, 'must be 50 characters or less') + .matches(/^[\w\-. ]+$/, 'Folder name contains invalid characters') .required('folder name is required'), - collectionLocation: Yup.string().required('location is required') + collectionLocation: Yup.string().min(1, 'location is required').required('location is required') }), onSubmit: (values) => { dispatch(createCollection(values.collectionName, values.collectionFolderName, values.collectionLocation)) @@ -43,7 +44,10 @@ const CreateCollection = ({ onClose }) => { const browse = () => { dispatch(browseDirectory()) .then((dirPath) => { - formik.setFieldValue('collectionLocation', dirPath); + // When the user closes the diolog without selecting anything dirPath will be false + if (typeof dirPath === 'string') { + formik.setFieldValue('collectionLocation', dirPath); + } }) .catch((error) => { formik.setFieldValue('collectionLocation', ''); @@ -63,9 +67,8 @@ const CreateCollection = ({ onClose }) => {
-
diff --git a/packages/bruno-app/src/components/Sidebar/index.js b/packages/bruno-app/src/components/Sidebar/index.js index 217b7e36f..8b8c7dbb0 100644 --- a/packages/bruno-app/src/components/Sidebar/index.js +++ b/packages/bruno-app/src/components/Sidebar/index.js @@ -116,7 +116,7 @@ const Sidebar = () => { )}
-
v0.16.3
+
v0.18.0
diff --git a/packages/bruno-app/src/components/SingleLineEditor/index.js b/packages/bruno-app/src/components/SingleLineEditor/index.js index 5c4ba12d7..bee59b2d9 100644 --- a/packages/bruno-app/src/components/SingleLineEditor/index.js +++ b/packages/bruno-app/src/components/SingleLineEditor/index.js @@ -9,6 +9,40 @@ const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODE if (!SERVER_RENDERED) { CodeMirror = require('codemirror'); + CodeMirror.registerHelper('hint', 'anyword', (editor, options) => { + const word = /[\w$-]+/; + const wordlist = (options && options.autocomplete) || []; + let cur = editor.getCursor(), + curLine = editor.getLine(cur.line); + let end = cur.ch, + start = end; + while (start && word.test(curLine.charAt(start - 1))) --start; + let curWord = start != end && curLine.slice(start, end); + + // Check if curWord is a valid string before proceeding + if (typeof curWord !== 'string' || curWord.length < 3) { + return null; // Abort the hint + } + + const list = (options && options.list) || []; + const re = new RegExp(word.source, 'g'); + for (let dir = -1; dir <= 1; dir += 2) { + let line = cur.line, + endLine = Math.min(Math.max(line + dir * 500, editor.firstLine()), editor.lastLine()) + dir; + for (; line != endLine; line += dir) { + let text = editor.getLine(line), + m; + while ((m = re.exec(text))) { + if (line == cur.line && curWord.length < 3) continue; + list.push(...wordlist.filter((el) => el.toLowerCase().startsWith(curWord.toLowerCase()))); + } + } + } + return { list: [...new Set(list)], from: CodeMirror.Pos(cur.line, start), to: CodeMirror.Pos(cur.line, end) }; + }); + CodeMirror.commands.autocomplete = (cm, hint, options) => { + cm.showHint({ hint, ...options }); + }; } class SingleLineEditor extends Component { @@ -32,6 +66,7 @@ class SingleLineEditor extends Component { variables: getAllVariables(this.props.collection) }, scrollbarStyle: null, + tabindex: 0, extraKeys: { Enter: () => { if (this.props.onRun) { @@ -70,9 +105,19 @@ class SingleLineEditor extends Component { }, 'Cmd-F': () => {}, 'Ctrl-F': () => {}, - Tab: () => {} + // Tabbing disabled to make tabindex work + Tab: false, + 'Shift-Tab': false } }); + if (this.props.autocomplete) { + this.editor.on('keyup', (cm, event) => { + if (!cm.state.completionActive /*Enables keyboard navigation in autocomplete list*/ && event.keyCode != 13) { + /*Enter - do not open autocomplete list just after item has been selected in it*/ + CodeMirror.commands.autocomplete(cm, CodeMirror.hint.anyword, { autocomplete: this.props.autocomplete }); + } + }); + } this.editor.setValue(this.props.value || ''); this.editor.on('change', this._onEdit); this.addOverlay(); diff --git a/packages/bruno-app/src/components/VariablesEditor/index.js b/packages/bruno-app/src/components/VariablesEditor/index.js index c8fc36fa1..735b9a543 100644 --- a/packages/bruno-app/src/components/VariablesEditor/index.js +++ b/packages/bruno-app/src/components/VariablesEditor/index.js @@ -87,7 +87,7 @@ const VariablesEditor = ({ collection }) => {
- Note: As of today, collection variables can only be set via the api -{' '} + Note: As of today, collection variables can only be set via the API -{' '} getVar() and setVar().
In the next release, we will add a UI to set and modify collection variables.
diff --git a/packages/bruno-app/src/components/Welcome/index.js b/packages/bruno-app/src/components/Welcome/index.js index 625f18abd..278516538 100644 --- a/packages/bruno-app/src/components/Welcome/index.js +++ b/packages/bruno-app/src/components/Welcome/index.js @@ -54,7 +54,7 @@ const Welcome = () => {
bruno
-
Opensource IDE for exploring and testing api's
+
Opensource IDE for exploring and testing APIs
Collections
diff --git a/packages/bruno-app/src/pages/ErrorBoundary/index.js b/packages/bruno-app/src/pages/ErrorBoundary/index.js new file mode 100644 index 000000000..3b45122ab --- /dev/null +++ b/packages/bruno-app/src/pages/ErrorBoundary/index.js @@ -0,0 +1,44 @@ +import React from 'react'; + +class ErrorBoundary extends React.Component { + constructor(props) { + super(props); + + this.state = { hasError: false }; + } + componentDidMount() { + // Add a global error event listener to capture client-side errors + window.onerror = (message, source, lineno, colno, error) => { + this.setState({ hasError: true, error }); + }; + } + componentDidCatch(error, errorInfo) { + console.log({ error, errorInfo }); + } + render() { + if (this.state.hasError) { + return ( +
+
+

Oops! Something went wrong

+

{this.state.error && this.state.error.toString()}

+ {this.state.error && this.state.error.stack && ( +
{this.state.error.stack}
+ )} + +
+
+ ); + } + return this.props.children; + } +} + +export default ErrorBoundary; diff --git a/packages/bruno-app/src/pages/_app.js b/packages/bruno-app/src/pages/_app.js index 382b95093..ab2692529 100644 --- a/packages/bruno-app/src/pages/_app.js +++ b/packages/bruno-app/src/pages/_app.js @@ -7,6 +7,7 @@ import { PreferencesProvider } from 'providers/Preferences'; import ReduxStore from 'providers/ReduxStore'; import ThemeProvider from 'providers/Theme/index'; +import ErrorBoundary from './ErrorBoundary'; import '../styles/app.scss'; import '../styles/globals.css'; @@ -41,23 +42,25 @@ function MyApp({ Component, pageProps }) { } return ( - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + ); } diff --git a/packages/bruno-app/src/providers/Hotkeys/index.js b/packages/bruno-app/src/providers/Hotkeys/index.js index a50e71dfb..522fa0d46 100644 --- a/packages/bruno-app/src/providers/Hotkeys/index.js +++ b/packages/bruno-app/src/providers/Hotkeys/index.js @@ -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 ( - {showBrunoSupportModal && setShowBrunoSupportModal(false)} />} {showSaveRequestModal && ( setShowSaveRequestModal(false)} /> )} diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js index cfede2a40..ccdd7fe1a 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -12,7 +12,6 @@ import { getItemsToResequence, moveCollectionItemToRootOfCollection, findCollectionByUid, - recursivelyGetAllItemUids, transformRequestToSaveToFilesystem, findParentItemInCollection, findEnvironmentInCollection, @@ -22,7 +21,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 { @@ -39,6 +38,7 @@ import { createCollection as _createCollection, renameCollection as _renameCollection, removeCollection as _removeCollection, + sortCollections as _sortCollections, collectionAddEnvFileEvent as _collectionAddEnvFileEvent } from './index'; @@ -145,6 +145,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); @@ -262,7 +267,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); }); }; @@ -346,7 +363,16 @@ 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; diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index 495989eb1..2cb1bdea5 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -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,6 +71,20 @@ export const collectionsSlice = createSlice({ removeCollection: (state, action) => { state.collections = filter(state.collections, (c) => c.uid !== action.payload.collectionUid); }, + 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; const collection = findCollectionByUid(state.collections, collectionUid); @@ -307,6 +322,31 @@ export const collectionsSlice = createSlice({ } } }, + updateAuth: (state, action) => { + const collection = findCollectionByUid(state.collections, action.payload.collectionUid); + + if (collection) { + const item = findItemInCollection(collection, action.payload.itemUid); + + if (item && isItemARequest(item)) { + if (!item.draft) { + item.draft = cloneDeep(item); + } + + item.draft.request.auth = item.draft.request.auth || {}; + switch (action.payload.mode) { + case 'bearer': + item.draft.request.auth.mode = 'bearer'; + item.draft.request.auth.bearer = action.payload.content; + break; + case 'basic': + item.draft.request.auth.mode = 'basic'; + item.draft.request.auth.basic = action.payload.content; + break; + } + } + } + }, addQueryParam: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); @@ -563,6 +603,20 @@ export const collectionsSlice = createSlice({ } } }, + updateRequestAuthMode: (state, action) => { + const collection = findCollectionByUid(state.collections, action.payload.collectionUid); + + if (collection && collection.items && collection.items.length) { + const item = findItemInCollection(collection, action.payload.itemUid); + + if (item && isItemARequest(item)) { + if (!item.draft) { + item.draft = cloneDeep(item); + } + item.draft.request.auth.mode = action.payload.mode; + } + } + }, updateRequestBodyMode: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); @@ -1141,6 +1195,7 @@ export const { brunoConfigUpdateEvent, renameCollection, removeCollection, + sortCollections, updateLastAction, collectionUnlinkEnvFileEvent, saveEnvironment, @@ -1158,6 +1213,7 @@ export const { collectionClicked, collectionFolderClicked, requestUrlChanged, + updateAuth, addQueryParam, updateQueryParam, deleteQueryParam, @@ -1170,6 +1226,7 @@ export const { addMultipartFormParam, updateMultipartFormParam, deleteMultipartFormParam, + updateRequestAuthMode, updateRequestBodyMode, updateRequestBody, updateRequestGraphqlQuery, diff --git a/packages/bruno-app/src/themes/dark.js b/packages/bruno-app/src/themes/dark.js index 122d16252..7a4ad64de 100644 --- a/packages/bruno-app/src/themes/dark.js +++ b/packages/bruno-app/src/themes/dark.js @@ -9,13 +9,20 @@ const darkTheme = { green: 'rgb(11 178 126)', danger: '#f06f57', muted: '#9d9d9d', - purple: '#cd56d6' + purple: '#cd56d6', + yellow: '#f59e0b' }, bg: { danger: '#d03544' } }, + input: { + bg: 'rgb(65, 65, 65)', + border: 'rgb(65, 65, 65)', + focusBorder: 'rgb(65, 65, 65)' + }, + variables: { bg: 'rgb(48, 48, 49)', diff --git a/packages/bruno-app/src/themes/light.js b/packages/bruno-app/src/themes/light.js index 846940cdb..b014c2f38 100644 --- a/packages/bruno-app/src/themes/light.js +++ b/packages/bruno-app/src/themes/light.js @@ -9,13 +9,20 @@ const lightTheme = { green: '#047857', danger: 'rgb(185, 28, 28)', muted: '#4b5563', - purple: '#8e44ad' + purple: '#8e44ad', + yellow: '#d97706' }, bg: { danger: '#dc3545' } }, + input: { + bg: 'white', + border: '#ccc', + focusBorder: '#8b8b8b' + }, + menubar: { bg: 'rgb(44, 44, 44)' }, diff --git a/packages/bruno-app/src/utils/codegenerator/har.js b/packages/bruno-app/src/utils/codegenerator/har.js new file mode 100644 index 000000000..b48fbc3c7 --- /dev/null +++ b/packages/bruno-app/src/utils/codegenerator/har.js @@ -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 + }; +}; diff --git a/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js b/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js index 50f314dac..d37e10bb6 100644 --- a/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js +++ b/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js @@ -66,8 +66,7 @@ if (!SERVER_RENDERED) { if (target.nodeName !== 'SPAN' || state.hoverTimeout !== undefined) { return; } - - if (target.className !== 'cm-variable-valid') { + if (!target.classList.contains('cm-variable-valid')) { return; } diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index 80fe41dd3..bf0fe6879 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -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); @@ -203,7 +207,7 @@ export const getItemsToResequence = (parent, collection) => { return itemsToResequence; }; -export const transformCollectionToSaveToIdb = (collection, options = {}) => { +export const transformCollectionToSaveToExportAsFile = (collection, options = {}) => { const copyHeaders = (headers) => { return map(headers, (header) => { return { @@ -281,6 +285,16 @@ export const transformCollectionToSaveToIdb = (collection, options = {}) => { formUrlEncoded: copyFormUrlEncodedParams(si.draft.request.body.formUrlEncoded), multipartForm: copyMultipartFormParams(si.draft.request.body.multipartForm) }, + auth: { + mode: get(si.draft.request, 'auth.mode', 'none'), + basic: { + username: get(si.draft.request, 'auth.basic.username', ''), + password: get(si.draft.request, 'auth.basic.password', '') + }, + bearer: { + token: get(si.draft.request, 'auth.bearer.token', '') + } + }, script: si.draft.request.script, vars: si.draft.request.vars, assertions: si.draft.request.assertions, @@ -303,6 +317,16 @@ export const transformCollectionToSaveToIdb = (collection, options = {}) => { formUrlEncoded: copyFormUrlEncodedParams(si.request.body.formUrlEncoded), multipartForm: copyMultipartFormParams(si.request.body.multipartForm) }, + auth: { + mode: get(si.request, 'auth.mode', 'none'), + basic: { + username: get(si.request, 'auth.basic.username', ''), + password: get(si.request, 'auth.basic.password', '') + }, + bearer: { + token: get(si.request, 'auth.bearer.token', '') + } + }, script: si.request.script, vars: si.request.vars, assertions: si.request.assertions, @@ -351,6 +375,7 @@ export const transformRequestToSaveToFilesystem = (item) => { url: _item.request.url, params: [], headers: [], + auth: _item.request.auth, body: _item.request.body, script: _item.request.script, vars: _item.request.vars, @@ -445,6 +470,22 @@ export const humanizeRequestBodyMode = (mode) => { return label; }; +export const humanizeRequestAuthMode = (mode) => { + let label = 'No Auth'; + switch (mode) { + case 'basic': { + label = 'Basic Auth'; + break; + } + case 'bearer': { + label = 'Bearer Token'; + break; + } + } + + return label; +}; + export const refreshUidsInItem = (item) => { item.uid = uuid(); diff --git a/packages/bruno-app/src/utils/common/codemirror.js b/packages/bruno-app/src/utils/common/codemirror.js index 59daee837..aa4ba0519 100644 --- a/packages/bruno-app/src/utils/common/codemirror.js +++ b/packages/bruno-app/src/utils/common/codemirror.js @@ -25,10 +25,11 @@ export const defineCodeMirrorBrunoVariablesMode = (variables, mode) => { stream.eat('}'); let found = pathFoundInVariables(word, variables); if (found) { - return 'variable-valid'; + return 'variable-valid random-' + (Math.random() + 1).toString(36).substring(9); } else { - return 'variable-invalid'; + return 'variable-invalid random-' + (Math.random() + 1).toString(36).substring(9); } + // Random classname added so adjacent variables are not rendered in the same SPAN by CodeMirror. } word += ch; } @@ -41,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'; + } +}; diff --git a/packages/bruno-app/src/utils/common/index.js b/packages/bruno-app/src/utils/common/index.js index 204cae6bf..992ec233e 100644 --- a/packages/bruno-app/src/utils/common/index.js +++ b/packages/bruno-app/src/utils/common/index.js @@ -51,6 +51,17 @@ export const safeStringifyJSON = (obj, indent = false) => { } }; +export const safeParseXML = (str, options) => { + if (!str || !str.length || typeof str !== 'string') { + return str; + } + try { + return xmlFormat(str, options); + } catch (e) { + return str; + } +}; + // Remove any characters that are not alphanumeric, spaces, hyphens, or underscores export const normalizeFileName = (name) => { if (!name) { @@ -76,18 +87,10 @@ export const getContentType = (headers) => { } else if (typeof contentType[0] == 'string' && /^[\w\-]+\/([\w\-]+\+)?xml/.test(contentType[0])) { return 'application/xml'; } + + 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; -}; diff --git a/packages/bruno-app/src/utils/common/platform.js b/packages/bruno-app/src/utils/common/platform.js index d144796e7..e49a66ec9 100644 --- a/packages/bruno-app/src/utils/common/platform.js +++ b/packages/bruno-app/src/utils/common/platform.js @@ -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'); +}; diff --git a/packages/bruno-app/src/utils/importers/insomnia-collection.js b/packages/bruno-app/src/utils/importers/insomnia-collection.js index b0fd7195e..b402e8903 100644 --- a/packages/bruno-app/src/utils/importers/insomnia-collection.js +++ b/packages/bruno-app/src/utils/importers/insomnia-collection.js @@ -30,10 +30,23 @@ const parseGraphQL = (text) => { } }; -const transformInsomniaRequestItem = (request) => { +const addSuffixToDuplicateName = (item, index, allItems) => { + // Check if the request name already exist and if so add a number suffix + const nameSuffix = allItems.reduce((nameSuffix, otherItem, otherIndex) => { + if (otherItem.name === item.name && otherIndex < index) { + nameSuffix++; + } + return nameSuffix; + }, 0); + return nameSuffix !== 0 ? `${item.name}_${nameSuffix}` : item.name; +}; + +const transformInsomniaRequestItem = (request, index, allRequests) => { + const name = addSuffixToDuplicateName(request, index, allRequests); + const brunoRequestItem = { uid: uuid(), - name: request.name, + name, type: 'http-request', request: { url: request.url, @@ -126,9 +139,7 @@ const parseInsomniaCollection = (data) => { try { const insomniaExport = JSON.parse(data); const insomniaResources = get(insomniaExport, 'resources', []); - const insomniaCollection = insomniaResources.find( - (resource) => resource._type === 'workspace' && resource.scope === 'collection' - ); + const insomniaCollection = insomniaResources.find((resource) => resource._type === 'workspace'); if (!insomniaCollection) { reject(new BrunoError('Collection not found inside Insomnia export')); @@ -145,14 +156,15 @@ const parseInsomniaCollection = (data) => { resources.filter((resource) => resource._type === 'request_group' && resource.parentId === parentId) || []; const requests = resources.filter((resource) => resource._type === 'request' && resource.parentId === parentId); - const folders = requestGroups.map((folder) => { + const folders = requestGroups.map((folder, index, allFolder) => { + const name = addSuffixToDuplicateName(folder, index, allFolder); const requests = resources.filter( (resource) => resource._type === 'request' && resource.parentId === folder._id ); return { uid: uuid(), - name: folder.name, + name, type: 'folder', items: createFolderStructure(resources, folder._id).concat(requests.map(transformInsomniaRequestItem)) }; diff --git a/packages/bruno-app/src/utils/url/index.js b/packages/bruno-app/src/utils/url/index.js index b28cc019e..7f5a8e825 100644 --- a/packages/bruno-app/src/utils/url/index.js +++ b/packages/bruno-app/src/utils/url/index.js @@ -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; + } +}; diff --git a/packages/bruno-cli/src/index.js b/packages/bruno-cli/src/index.js index d54c6861f..d9bc66550 100644 --- a/packages/bruno-cli/src/index.js +++ b/packages/bruno-cli/src/index.js @@ -20,7 +20,7 @@ const run = async () => { .commandDir('commands') .epilogue(CLI_EPILOGUE) .usage('Usage: $0 [options]') - .demandCommand(1, "Woof !! Let's play with some apis !!") + .demandCommand(1, "Woof !! Let's play with some APIs !!") .help('h') .alias('h', 'help'); }; diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index 5e4312b17..79435785e 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -187,7 +187,7 @@ const runSingleRequest = async function ( // run assertions let assertionResults = []; const assertions = get(bruJson, 'request.assertions'); - if (assertions && assertions.length) { + if (assertions) { const assertRuntime = new AssertRuntime(); assertionResults = assertRuntime.runAssertions( assertions, @@ -211,7 +211,7 @@ const runSingleRequest = async function ( // run tests let testResults = []; const testFile = get(bruJson, 'request.tests'); - if (testFile && testFile.length) { + if (typeof testFile === 'string') { const testRuntime = new TestRuntime(); const result = await testRuntime.runTests( testFile, diff --git a/packages/bruno-electron/package.json b/packages/bruno-electron/package.json index 6ff6e7f43..af0982c90 100644 --- a/packages/bruno-electron/package.json +++ b/packages/bruno-electron/package.json @@ -1,5 +1,5 @@ { - "version": "v0.16.3", + "version": "v0.18.0", "name": "bruno", "description": "Opensource API Client for Exploring and Testing APIs", "homepage": "https://www.usebruno.com", @@ -30,6 +30,7 @@ "fs-extra": "^10.1.0", "graphql": "^16.6.0", "handlebars": "^4.7.8", + "https-proxy-agent": "^7.0.2", "is-valid-path": "^0.1.1", "lodash": "^4.17.21", "mustache": "^4.2.0", diff --git a/packages/bruno-electron/src/about/256x256.png b/packages/bruno-electron/src/about/256x256.png new file mode 100644 index 000000000..d1263e9c5 Binary files /dev/null and b/packages/bruno-electron/src/about/256x256.png differ diff --git a/packages/bruno-electron/src/about/about.css b/packages/bruno-electron/src/about/about.css new file mode 100644 index 000000000..dd8b987d1 --- /dev/null +++ b/packages/bruno-electron/src/about/about.css @@ -0,0 +1,8 @@ +.versions { + -webkit-user-select: text; + user-select: text; +} +.title { + -webkit-user-select: text; + user-select: text; +} diff --git a/packages/bruno-electron/src/app/menu-template.js b/packages/bruno-electron/src/app/menu-template.js index 7e0911298..2efd93cde 100644 --- a/packages/bruno-electron/src/app/menu-template.js +++ b/packages/bruno-electron/src/app/menu-template.js @@ -51,7 +51,8 @@ const template = [ click: () => openAboutWindow({ product_name: 'Bruno', - icon_path: join(process.cwd(), '/resources/icons/png/256x256.png'), + icon_path: join(__dirname, '../about/256x256.png'), + css_path: join(__dirname, '../about/about.css'), homepage: 'https://www.usebruno.com/', package_json_dir: join(__dirname, '../..') }) diff --git a/packages/bruno-electron/src/bru/index.js b/packages/bruno-electron/src/bru/index.js index 45b10004f..a28c04a7b 100644 --- a/packages/bruno-electron/src/bru/index.js +++ b/packages/bruno-electron/src/bru/index.js @@ -61,6 +61,7 @@ const bruToJson = (bru) => { url: _.get(json, 'http.url'), params: _.get(json, 'query', []), headers: _.get(json, 'headers', []), + auth: _.get(json, 'auth', {}), body: _.get(json, 'body', {}), script: _.get(json, 'script', {}), vars: _.get(json, 'vars', {}), @@ -69,6 +70,7 @@ const bruToJson = (bru) => { } }; + transformedJson.request.auth.mode = _.get(json, 'http.auth', 'none'); transformedJson.request.body.mode = _.get(json, 'http.body', 'none'); return transformedJson; @@ -104,10 +106,12 @@ const jsonToBru = (json) => { http: { method: _.lowerCase(_.get(json, 'request.method')), url: _.get(json, 'request.url'), + auth: _.get(json, 'request.auth.mode', 'none'), body: _.get(json, 'request.body.mode', 'none') }, query: _.get(json, 'request.params', []), headers: _.get(json, 'request.headers', []), + auth: _.get(json, 'request.auth', {}), body: _.get(json, 'request.body', {}), script: _.get(json, 'request.script', {}), vars: { diff --git a/packages/bruno-electron/src/index.js b/packages/bruno-electron/src/index.js index 2a62dd969..fb7fc45b4 100644 --- a/packages/bruno-electron/src/index.js +++ b/packages/bruno-electron/src/index.js @@ -16,9 +16,7 @@ setContentSecurityPolicy(` default-src * 'unsafe-inline' 'unsafe-eval'; script-src * 'unsafe-inline' 'unsafe-eval'; connect-src * 'unsafe-inline'; - base-uri 'none'; form-action 'none'; - img-src 'self' data:image/svg+xml; `); const menu = Menu.buildFromTemplate(menuTemplate); @@ -35,7 +33,8 @@ app.on('ready', async () => { webPreferences: { nodeIntegration: true, contextIsolation: true, - preload: path.join(__dirname, 'preload.js') + preload: path.join(__dirname, 'preload.js'), + webviewTag: true } }); diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index d4cfa48b1..6c1bec01a 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -291,7 +291,7 @@ const registerNetworkIpc = (mainWindow) => { // run assertions const assertions = get(request, 'assertions'); - if (assertions && assertions.length) { + if (assertions) { const assertRuntime = new AssertRuntime(); const results = assertRuntime.runAssertions( assertions, @@ -313,7 +313,7 @@ const registerNetworkIpc = (mainWindow) => { // run tests const testFile = item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests'); - if (testFile && testFile.length) { + if (typeof testFile === 'string') { const testRuntime = new TestRuntime(); const testResults = await testRuntime.runTests( testFile, @@ -365,7 +365,7 @@ const registerNetworkIpc = (mainWindow) => { if (error && error.response) { // run assertions const assertions = get(request, 'assertions'); - if (assertions && assertions.length) { + if (assertions) { const assertRuntime = new AssertRuntime(); const results = assertRuntime.runAssertions( assertions, @@ -387,7 +387,7 @@ const registerNetworkIpc = (mainWindow) => { // run tests const testFile = item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests'); - if (testFile && testFile.length) { + if (typeof testFile === 'string') { const testRuntime = new TestRuntime(); const testResults = await testRuntime.runTests( testFile, @@ -702,7 +702,7 @@ const registerNetworkIpc = (mainWindow) => { // run assertions const assertions = get(item, 'request.assertions'); - if (assertions && assertions.length) { + if (assertions) { const assertRuntime = new AssertRuntime(); const results = assertRuntime.runAssertions( assertions, @@ -723,7 +723,7 @@ const registerNetworkIpc = (mainWindow) => { // run tests const testFile = item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests'); - if (testFile && testFile.length) { + if (typeof testFile === 'string') { const testRuntime = new TestRuntime(); const testResults = await testRuntime.runTests( testFile, @@ -781,7 +781,7 @@ const registerNetworkIpc = (mainWindow) => { // run assertions const assertions = get(item, 'request.assertions'); - if (assertions && assertions.length) { + if (assertions) { const assertRuntime = new AssertRuntime(); const results = assertRuntime.runAssertions( assertions, @@ -802,7 +802,7 @@ const registerNetworkIpc = (mainWindow) => { // run tests const testFile = item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests'); - if (testFile && testFile.length) { + if (typeof testFile === 'string') { const testRuntime = new TestRuntime(); const testResults = await testRuntime.runTests( testFile, diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index f07331c55..5a8512910 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -18,6 +18,20 @@ const prepareRequest = (request) => { headers: headers }; + // Authentication + if (request.auth) { + if (request.auth.mode === 'basic') { + axiosRequest.auth = { + username: get(request, 'auth.basic.username'), + password: get(request, 'auth.basic.password') + }; + } + + if (request.auth.mode === 'bearer') { + axiosRequest.headers['authorization'] = `Bearer ${get(request, 'auth.bearer.token')}`; + } + } + if (request.body.mode === 'json') { if (!contentTypeDefined) { axiosRequest.headers['content-type'] = 'application/json'; diff --git a/packages/bruno-graphql-docs/rollup.config.js b/packages/bruno-graphql-docs/rollup.config.js index dd2424c5f..d289e1df1 100644 --- a/packages/bruno-graphql-docs/rollup.config.js +++ b/packages/bruno-graphql-docs/rollup.config.js @@ -1,46 +1,48 @@ -const { nodeResolve } = require("@rollup/plugin-node-resolve"); -const commonjs = require("@rollup/plugin-commonjs"); -const typescript = require("@rollup/plugin-typescript"); -const dts = require("rollup-plugin-dts"); -const postcss = require("rollup-plugin-postcss"); -const { terser } = require("rollup-plugin-terser"); +const { nodeResolve } = require('@rollup/plugin-node-resolve'); +const commonjs = require('@rollup/plugin-commonjs'); +const typescript = require('@rollup/plugin-typescript'); +const dts = require('rollup-plugin-dts'); +const postcss = require('rollup-plugin-postcss'); +const { terser } = require('rollup-plugin-terser'); const peerDepsExternal = require('rollup-plugin-peer-deps-external'); -const packageJson = require("./package.json"); +const packageJson = require('./package.json'); module.exports = [ { - input: "src/index.ts", + input: 'src/index.ts', output: [ { file: packageJson.main, - format: "cjs", - sourcemap: true, + format: 'cjs', + sourcemap: true }, { file: packageJson.module, - format: "esm", - sourcemap: true, - }, + format: 'esm', + sourcemap: true + } ], plugins: [ postcss({ minimize: true, - extensions: ['.css'] + extensions: ['.css'], + extract: true }), peerDepsExternal(), nodeResolve({ extensions: ['.css'] }), commonjs(), - typescript({ tsconfig: "./tsconfig.json" }), + typescript({ tsconfig: './tsconfig.json' }), terser() ], - external: ["react", "react-dom", "index.css"] + external: ['react', 'react-dom', 'index.css'] }, { - input: "dist/esm/index.d.ts", - output: [{ file: "dist/index.d.ts", format: "esm" }], - plugins: [dts.default()], + input: 'dist/esm/index.d.ts', + external: [/\.css$/], + output: [{ file: 'dist/index.d.ts', format: 'esm' }], + plugins: [dts.default()] } -]; \ No newline at end of file +]; diff --git a/packages/bruno-graphql-docs/src/index.ts b/packages/bruno-graphql-docs/src/index.ts index e38befd3c..b8c6cd039 100644 --- a/packages/bruno-graphql-docs/src/index.ts +++ b/packages/bruno-graphql-docs/src/index.ts @@ -1,6 +1,5 @@ import { DocExplorer } from './components/DocExplorer'; -// Todo: Rollup throws error import './index.css'; export { DocExplorer }; diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js index 2241ca554..576c58c24 100644 --- a/packages/bruno-lang/v2/src/bruToJson.js +++ b/packages/bruno-lang/v2/src/bruToJson.js @@ -22,7 +22,8 @@ const { outdentString } = require('../../v1/src/utils'); * */ const grammar = ohm.grammar(`Bru { - BruFile = (meta | http | query | headers | bodies | varsandassert | script | tests | docs)* + BruFile = (meta | http | query | headers | auths | bodies | varsandassert | script | tests | docs)* + auths = authbasic | authbearer bodies = bodyjson | bodytext | bodyxml | bodygraphql | bodygraphqlvars | bodyforms | body bodyforms = bodyformurlencoded | bodymultipart @@ -75,6 +76,9 @@ const grammar = ohm.grammar(`Bru { varsres = "vars:post-response" dictionary assert = "assert" assertdictionary + authbasic = "auth:basic" dictionary + authbearer = "auth:bearer" dictionary + body = "body" st* "{" nl* textblock tagend bodyjson = "body:json" st* "{" nl* textblock tagend bodytext = "body:text" st* "{" nl* textblock tagend @@ -92,13 +96,21 @@ const grammar = ohm.grammar(`Bru { docs = "docs" st* "{" nl* textblock tagend }`); -const mapPairListToKeyValPairs = (pairList = []) => { +const mapPairListToKeyValPairs = (pairList = [], parseEnabled = true) => { if (!pairList.length) { return []; } return _.map(pairList[0], (pair) => { let name = _.keys(pair)[0]; let value = pair[name]; + + if (!parseEnabled) { + return { + name, + value + }; + } + let enabled = true; if (name && name.length && name.charAt(0) === '~') { name = name.slice(1); @@ -282,6 +294,33 @@ const sem = grammar.createSemantics().addAttribute('ast', { headers: mapPairListToKeyValPairs(dictionary.ast) }; }, + authbasic(_1, dictionary) { + const auth = mapPairListToKeyValPairs(dictionary.ast, false); + const usernameKey = _.find(auth, { name: 'username' }); + const passwordKey = _.find(auth, { name: 'password' }); + const username = usernameKey ? usernameKey.value : ''; + const password = passwordKey ? passwordKey.value : ''; + return { + auth: { + basic: { + username, + password + } + } + }; + }, + authbearer(_1, dictionary) { + const auth = mapPairListToKeyValPairs(dictionary.ast, false); + const tokenKey = _.find(auth, { name: 'token' }); + const token = tokenKey ? tokenKey.value : ''; + return { + auth: { + bearer: { + token + } + } + }; + }, bodyformurlencoded(_1, dictionary) { return { body: { diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js index 818d7c9cb..8ef44d7ad 100644 --- a/packages/bruno-lang/v2/src/jsonToBru.js +++ b/packages/bruno-lang/v2/src/jsonToBru.js @@ -13,7 +13,7 @@ const stripLastLine = (text) => { }; const jsonToBru = (json) => { - const { meta, http, query, headers, body, script, tests, vars, assertions, docs } = json; + const { meta, http, query, headers, auth, body, script, tests, vars, assertions, docs } = json; let bru = ''; @@ -34,6 +34,11 @@ const jsonToBru = (json) => { body: ${http.body}`; } + if (http.auth && http.auth.length) { + bru += ` + auth: ${http.auth}`; + } + bru += ` } @@ -82,6 +87,23 @@ const jsonToBru = (json) => { bru += '\n}\n\n'; } + if (auth && auth.basic) { + bru += `auth:basic { +${indentString(`username: ${auth.basic.username}`)} +${indentString(`password: ${auth.basic.password}`)} +} + +`; + } + + if (auth && auth.bearer) { + bru += `auth:bearer { +${indentString(`token: ${auth.bearer.token}`)} +} + +`; + } + if (body && body.json && body.json.length) { bru += `body:json { ${indentString(body.json)} diff --git a/packages/bruno-lang/v2/tests/fixtures/request.bru b/packages/bruno-lang/v2/tests/fixtures/request.bru index ae7318da5..c4ae4b058 100644 --- a/packages/bruno-lang/v2/tests/fixtures/request.bru +++ b/packages/bruno-lang/v2/tests/fixtures/request.bru @@ -7,6 +7,7 @@ meta { get { url: https://api.textlocal.in/send body: json + auth: bearer } query { @@ -21,6 +22,15 @@ headers { ~transaction-id: {{transactionId}} } +auth:basic { + username: john + password: secret +} + +auth:bearer { + token: 123 +} + body:json { { "hello": "world" diff --git a/packages/bruno-lang/v2/tests/fixtures/request.json b/packages/bruno-lang/v2/tests/fixtures/request.json index 867229de8..7a00f5bb3 100644 --- a/packages/bruno-lang/v2/tests/fixtures/request.json +++ b/packages/bruno-lang/v2/tests/fixtures/request.json @@ -7,7 +7,8 @@ "http": { "method": "get", "url": "https://api.textlocal.in/send", - "body": "json" + "body": "json", + "auth": "bearer" }, "query": [ { @@ -43,6 +44,15 @@ "enabled": false } ], + "auth": { + "basic": { + "username": "john", + "password": "secret" + }, + "bearer": { + "token": "123" + } + }, "body": { "json": "{\n \"hello\": \"world\"\n}", "text": "This is a text body", diff --git a/packages/bruno-lang/v2/tests/index.spec.js b/packages/bruno-lang/v2/tests/index.spec.js index ecc970670..e753ee962 100644 --- a/packages/bruno-lang/v2/tests/index.spec.js +++ b/packages/bruno-lang/v2/tests/index.spec.js @@ -14,7 +14,7 @@ describe('bruToJson', () => { }); describe('jsonToBru', () => { - it('should parse the bru file', () => { + it('should parse the json file', () => { const input = require('./fixtures/request.json'); const expected = fs.readFileSync(path.join(__dirname, 'fixtures', 'request.bru'), 'utf8'); const output = jsonToBru(input); diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js index 7076175b5..81cd2528b 100644 --- a/packages/bruno-schema/src/collections/index.js +++ b/packages/bruno-schema/src/collections/index.js @@ -69,6 +69,27 @@ const requestBodySchema = Yup.object({ .noUnknown(true) .strict(); +const authBasicSchema = Yup.object({ + username: Yup.string().nullable(), + password: Yup.string().nullable() +}) + .noUnknown(true) + .strict(); + +const authBearerSchema = Yup.object({ + token: Yup.string().nullable() +}) + .noUnknown(true) + .strict(); + +const authSchema = Yup.object({ + mode: Yup.string().oneOf(['none', 'basic', 'bearer']).required('mode is required'), + basic: authBasicSchema.nullable(), + bearer: authBearerSchema.nullable() +}) + .noUnknown(true) + .strict(); + // Right now, the request schema is very tightly coupled with http request // As we introduce more request types in the future, we will improve the definition to support // schema structure based on other request type @@ -77,6 +98,7 @@ const requestSchema = Yup.object({ method: requestMethodSchema, headers: Yup.array().of(keyValueSchema).required('headers are required'), params: Yup.array().of(keyValueSchema).required('params are required'), + auth: authSchema, body: requestBodySchema, script: Yup.object({ req: Yup.string().nullable(), diff --git a/readme.md b/readme.md index badd9430a..af60a1888 100644 --- a/readme.md +++ b/readme.md @@ -1,7 +1,7 @@
-### Bruno - Opensource IDE for exploring and testing api's. +### 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. +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)

### Run across multiple platforms 🖥️ + ![bruno](assets/images/run-anywhere.png)

### Collaborate via Git 👩‍💻🧑‍💻 + Or any version control system of your choice ![bruno](assets/images/version-control.png)

### 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 !!
### Stay in touch 🌐 + [Twitter](https://twitter.com/use_bruno)
[Website](https://www.usebruno.com)
[Discord](https://discord.com/invite/KgcZUncpjq) ### License 📄 + [MIT](license.md)