mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-05 10:28:32 +00:00
Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d000625c39 | ||
|
|
4bdbfb5c0c | ||
|
|
4fbd2f0bdb | ||
|
|
5ae3f0e75a | ||
|
|
b80269b68f | ||
|
|
67de396927 | ||
|
|
65b80cfd06 | ||
|
|
caa0a22e74 | ||
|
|
c7f0335d96 | ||
|
|
22fab7f599 | ||
|
|
c0d214f2bc | ||
|
|
9efbd7377a | ||
|
|
b63cf46734 | ||
|
|
73ac969d35 | ||
|
|
dc21206fc0 | ||
|
|
8f58235e17 | ||
|
|
fd6b3630a5 | ||
|
|
25e57d2578 | ||
|
|
cef6f85845 | ||
|
|
5f0e6f13eb | ||
|
|
0b9554c8cc | ||
|
|
8b76ecede3 | ||
|
|
44d70ca02a | ||
|
|
4d55b50250 | ||
|
|
6320a80cbe | ||
|
|
d1c34bd379 | ||
|
|
71ffe1f8d4 | ||
|
|
04ccb2f6ee | ||
|
|
99ddd8021c | ||
|
|
ee8e162f3d | ||
|
|
b1a140a4e0 | ||
|
|
e4407f3981 | ||
|
|
74e75a7da2 | ||
|
|
4aff61b665 | ||
|
|
e31c552dee | ||
|
|
753a576c3c | ||
|
|
3ad4eda861 | ||
|
|
d3e57d0ea6 | ||
|
|
77750ecc0b | ||
|
|
a1783c46ed | ||
|
|
017d2235b8 | ||
|
|
df120787ca | ||
|
|
b872fdfe6d | ||
|
|
f0b7bf3430 | ||
|
|
4169bb7ea4 | ||
|
|
74b1527513 | ||
|
|
3b8909e301 | ||
|
|
1bedfc2046 | ||
|
|
8de6b72ab9 | ||
|
|
9d84906f57 | ||
|
|
eceb114d6c |
13
.github/workflows/tests.yml
vendored
13
.github/workflows/tests.yml
vendored
@@ -25,8 +25,14 @@ jobs:
|
||||
run: |
|
||||
npm run build --workspace=packages/bruno-common
|
||||
npm run build --workspace=packages/bruno-query
|
||||
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
|
||||
|
||||
# tests
|
||||
- name: Test Package bruno-js
|
||||
run: npm run test --workspace=packages/bruno-js
|
||||
- name: Test Package bruno-cli
|
||||
run: npm run test --workspace=packages/bruno-cli
|
||||
|
||||
# test
|
||||
- name: Test Package bruno-query
|
||||
run: npm run test --workspace=packages/bruno-query
|
||||
- name: Test Package bruno-lang
|
||||
@@ -35,12 +41,8 @@ jobs:
|
||||
run: npm run test --workspace=packages/bruno-schema
|
||||
- name: Test Package bruno-app
|
||||
run: npm run test --workspace=packages/bruno-app
|
||||
- name: Test Package bruno-js
|
||||
run: npm run test --workspace=packages/bruno-js
|
||||
- name: Test Package bruno-common
|
||||
run: npm run test --workspace=packages/bruno-common
|
||||
- name: Test Package bruno-cli
|
||||
run: npm run test --workspace=packages/bruno-cli
|
||||
- name: Test Package bruno-electron
|
||||
run: npm run test --workspace=packages/bruno-electron
|
||||
|
||||
@@ -62,6 +64,7 @@ jobs:
|
||||
run: |
|
||||
npm run build --workspace=packages/bruno-query
|
||||
npm run build --workspace=packages/bruno-common
|
||||
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
|
||||
@@ -37,7 +37,7 @@ Libraries we use
|
||||
|
||||
### Dependencies
|
||||
|
||||
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
|
||||
You would need [Node v20.x or the latest LTS version](https://nodejs.org/en/) and npm 8.x. We use npm workspaces in the project
|
||||
|
||||
## Development
|
||||
|
||||
@@ -57,6 +57,9 @@ npm run build:graphql-docs
|
||||
npm run build:bruno-query
|
||||
npm run build:bruno-common
|
||||
|
||||
# bundle js sandbox libraries
|
||||
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
|
||||
|
||||
# run next app (terminal 1)
|
||||
npm run dev:web
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ Bruno 基于 NextJs 和 React 构建。我们使用 Electron 来封装桌面版
|
||||
|
||||
### 依赖项
|
||||
|
||||
您需要 [Node v18.x 或最新的 LTS 版本](https://nodejs.org/en/) 和 npm 8.x。我们在这个项目中也使用 npm 工作区(_npm workspaces_)。
|
||||
您需要 [Node v20.x 或最新的 LTS 版本](https://nodejs.org/en/) 和 npm 8.x。我们在这个项目中也使用 npm 工作区(_npm workspaces_)。
|
||||
|
||||
## 开发
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ Bibliotheken die wir benutzen
|
||||
|
||||
### Abhängigkeiten
|
||||
|
||||
Du benötigst [Node v18.x oder die neuste LTS Version](https://nodejs.org/en/) und npm 8.x. Wir benutzen npm workspaces in dem Projekt.
|
||||
Du benötigst [Node v20.x oder die neuste LTS Version](https://nodejs.org/en/) und npm 8.x. Wir benutzen npm workspaces in dem Projekt.
|
||||
|
||||
### Lass uns coden
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ Librerías que utilizamos:
|
||||
|
||||
### Dependencias
|
||||
|
||||
Necesitarás [Node v18.x o la última versión LTS](https://nodejs.org/es) y npm 8.x. Ten en cuenta que utilizamos espacios de trabajo de npm en el proyecto.
|
||||
Necesitarás [Node v20.x o la última versión LTS](https://nodejs.org/es) y npm 8.x. Ten en cuenta que utilizamos espacios de trabajo de npm en el proyecto.
|
||||
|
||||
## Desarrollo
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ Les librairies que nous utilisons :
|
||||
|
||||
### Dépendances
|
||||
|
||||
Vous aurez besoin de [Node v18.x ou la dernière version LTS](https://nodejs.org/en/) et npm 8.x. Nous utilisons aussi les espaces de travail npm (_npm workspaces_) dans ce projet.
|
||||
Vous aurez besoin de [Node v20.x ou la dernière version LTS](https://nodejs.org/en/) et npm 8.x. Nous utilisons aussi les espaces de travail npm (_npm workspaces_) dans ce projet.
|
||||
|
||||
## Développement
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ Libraries जिनका हम उपयोग करते हैं
|
||||
|
||||
### निर्भरताएँ
|
||||
|
||||
आपको [Node v18.x या नवीनतम LTS संस्करण](https://nodejs.org/en/) और npm 8.x की आवश्यकता होगी। हम प्रोजेक्ट में npm वर्कस्पेस का उपयोग करते हैं
|
||||
आपको [Node v20.x या नवीनतम LTS संस्करण](https://nodejs.org/en/) और npm 8.x की आवश्यकता होगी। हम प्रोजेक्ट में npm वर्कस्पेस का उपयोग करते हैं
|
||||
|
||||
## डेवलपमेंट
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ Le librerie che utilizziamo sono:
|
||||
|
||||
### Dependences
|
||||
|
||||
Hai bisogno di [Node v18.x o dell'ultima versione LTS](https://nodejs.org/en/) di npm 8.x. Utilizziamo gli spazi di lavoro npm (_npm workspaces_) in questo progetto.
|
||||
Hai bisogno di [Node v20.x o dell'ultima versione LTS](https://nodejs.org/en/) di npm 8.x. Utilizziamo gli spazi di lavoro npm (_npm workspaces_) in questo progetto.
|
||||
|
||||
### Iniziamo a codificare
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ Bruno は Next.js と React で作られています。デスクトップアプ
|
||||
|
||||
### 依存関係
|
||||
|
||||
[Node v18.x もしくは最新の LTS バージョン](https://nodejs.org/en/)と npm 8.x が必要です。プロジェクトに npm ワークスペースを使用しています。
|
||||
[Node v20.x もしくは最新の LTS バージョン](https://nodejs.org/en/)と npm 8.x が必要です。プロジェクトに npm ワークスペースを使用しています。
|
||||
|
||||
## 開発
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ Bruno는 Next.js와 React로 구축되었습니다. 또한, (로컬 컬렉션을
|
||||
|
||||
### 의존성
|
||||
|
||||
[Node v18.x 혹은 최신 LTS version](https://nodejs.org/en/)과 npm 8.x 버전이 필요합니다. 우리는 이 프로젝트에서 npm workspaces를 사용합니다.
|
||||
[Node v20.x 혹은 최신 LTS version](https://nodejs.org/en/)과 npm 8.x 버전이 필요합니다. 우리는 이 프로젝트에서 npm workspaces를 사용합니다.
|
||||
|
||||
## 개발
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ Biblioteki, których używamy
|
||||
|
||||
### Zależności
|
||||
|
||||
Będziesz potrzebować [Node v18.x lub najnowszej wersji LTS](https://nodejs.org/en/) oraz npm 8.x. W projekcie używamy npm workspaces
|
||||
Będziesz potrzebować [Node v20.x lub najnowszej wersji LTS](https://nodejs.org/en/) oraz npm 8.x. W projekcie używamy npm workspaces
|
||||
|
||||
## Rozwój
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ Bibliotecas que utilizamos:
|
||||
|
||||
### Dependências
|
||||
|
||||
Você precisará do [Node v18.x (ou da versão LTS mais recente)](https://nodejs.org/en/) e do npm na versão 8.x. Nós utilizamos npm workspaces no projeto.
|
||||
Você precisará do [Node v20.x (ou da versão LTS mais recente)](https://nodejs.org/en/) e do npm na versão 8.x. Nós utilizamos npm workspaces no projeto.
|
||||
|
||||
## Desenvolvimento
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ Bibliotecile pe care le folosim
|
||||
|
||||
### Dependențele
|
||||
|
||||
Veți avea nevoie de [Node v18.x sau cea mai recentă versiune LTS](https://nodejs.org/en/) și npm 8.x. Noi folosim spații de lucru npm în proiect
|
||||
Veți avea nevoie de [Node v20.x sau cea mai recentă versiune LTS](https://nodejs.org/en/) și npm 8.x. Noi folosim spații de lucru npm în proiect
|
||||
|
||||
## Dezvoltarea
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ Bruno построен с использованием Next.js и React. Мы т
|
||||
|
||||
### Зависимости
|
||||
|
||||
Вам потребуется [Node v18.x или последняя версия LTS](https://nodejs.org/en/) и npm 8.x. В проекте мы используем рабочие пространства npm
|
||||
Вам потребуется [Node v20.x или последняя версия LTS](https://nodejs.org/en/) и npm 8.x. В проекте мы используем рабочие пространства npm
|
||||
|
||||
### Приступим к коду
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ Kullandığımız kütüphaneler
|
||||
|
||||
### Bağımlılıklar
|
||||
|
||||
[Node v18.x veya en son LTS sürümüne](https://nodejs.org/en/) ve npm 8.x'e ihtiyacınız olacaktır. Projede npm çalışma alanlarını kullanıyoruz
|
||||
[Node v20.x veya en son LTS sürümüne](https://nodejs.org/en/) ve npm 8.x'e ihtiyacınız olacaktır. Projede npm çalışma alanlarını kullanıyoruz
|
||||
|
||||
## Gelişim
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ Bruno побудований на Next.js та React. Також для деск
|
||||
|
||||
### Залежності
|
||||
|
||||
Вам знадобиться [Node v18.x або остання LTS версія](https://nodejs.org/en/) та npm 8.x. Ми використовуєм npm workspaces в цьому проекті
|
||||
Вам знадобиться [Node v20.x або остання LTS версія](https://nodejs.org/en/) та npm 8.x. Ми використовуєм npm workspaces в цьому проекті
|
||||
|
||||
### Починаєм писати код
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ Bruno 使用 Next.js 和 React 構建。我們使用 Electron 來封裝及發佈
|
||||
|
||||
### 依賴關係
|
||||
|
||||
您需要使用 [Node v18.x 或最新的 LTS 版本](https://nodejs.org/en/) 和 npm 8.x。我們在這個專案中使用 npm 工作區(_npm workspaces_)。
|
||||
您需要使用 [Node v20.x 或最新的 LTS 版本](https://nodejs.org/en/) 和 npm 8.x。我們在這個專案中使用 npm 工作區(_npm workspaces_)。
|
||||
|
||||
## 開發
|
||||
|
||||
|
||||
22892
package-lock.json
generated
22892
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -20,14 +20,17 @@
|
||||
"@jest/globals": "^29.2.0",
|
||||
"@playwright/test": "^1.27.1",
|
||||
"@types/jest": "^29.5.11",
|
||||
"concurrently": "^8.2.2",
|
||||
"fs-extra": "^11.1.1",
|
||||
"husky": "^8.0.3",
|
||||
"jest": "^29.2.0",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"randomstring": "^1.2.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"ts-jest": "^29.0.5"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "concurrently --kill-others \"npm run dev:web\" \"npm run dev:electron\"",
|
||||
"dev:web": "npm run dev --workspace=packages/bruno-app",
|
||||
"build:web": "npm run build --workspace=packages/bruno-app",
|
||||
"prettier:web": "npm run prettier --workspace=packages/bruno-app",
|
||||
@@ -47,9 +50,8 @@
|
||||
"test:prettier:web": "npm run test:prettier --workspace=packages/bruno-app",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
|
||||
"overrides": {
|
||||
"rollup": "3.2.5"
|
||||
"rollup":"3.29.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"json-bigint": "^1.0.0",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"prettier": "prettier --write \"./src/**/*.{js,jsx,json,ts,tsx}\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/inter": "^5.0.15",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
||||
"@fortawesome/react-fontawesome": "^0.1.16",
|
||||
@@ -35,18 +36,19 @@
|
||||
"graphiql": "^1.5.9",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-request": "^3.7.0",
|
||||
"httpsnippet": "^3.0.1",
|
||||
"httpsnippet": "^3.0.6",
|
||||
"idb": "^7.0.0",
|
||||
"immer": "^9.0.15",
|
||||
"jsesc": "^3.0.2",
|
||||
"jshint": "^2.13.6",
|
||||
"json5": "^2.2.3",
|
||||
"jsonc-parser": "^3.2.1",
|
||||
"jsonlint": "^1.6.3",
|
||||
"jsonpath-plus": "^7.2.0",
|
||||
"jsonlint": "^1.6.3",
|
||||
"know-your-http-well": "^0.5.0",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-it": "^13.0.2",
|
||||
"markdown-it-replace-link": "^1.2.0",
|
||||
"mousetrap": "^1.6.5",
|
||||
"nanoid": "3.3.4",
|
||||
"next": "12.3.3",
|
||||
|
||||
@@ -14,6 +14,24 @@ const StyledWrapper = styled.div`
|
||||
background: #d2d7db;
|
||||
}
|
||||
|
||||
.CodeMirror-dialog {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
#search-results-count {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: calc(100% + 1px);
|
||||
right: 0;
|
||||
border-width: 0 0 1px 1px;
|
||||
border-style: solid;
|
||||
border-color: ${(props) => props.theme.codemirror.border};
|
||||
padding: 0.1em 0.8em;
|
||||
background-color: ${(props) => props.theme.codemirror.bg};
|
||||
color: rgb(102, 102, 102);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
textarea.cm-editor {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -66,7 +66,9 @@ if (!SERVER_RENDERED) {
|
||||
'bru.getVar(key)',
|
||||
'bru.setVar(key,value)',
|
||||
'bru.deleteVar(key)',
|
||||
'bru.setNextRequest(requestName)'
|
||||
'bru.setNextRequest(requestName)',
|
||||
'bru.getRequestVar(key)',
|
||||
'bru.sleep(ms)'
|
||||
];
|
||||
CodeMirror.registerHelper('hint', 'brunoJS', (editor, options) => {
|
||||
const cursor = editor.getCursor();
|
||||
@@ -109,6 +111,7 @@ export default class CodeEditor extends React.Component {
|
||||
// unnecessary updates during the update lifecycle.
|
||||
this.cachedValue = props.value || '';
|
||||
this.variables = {};
|
||||
this.searchResultsCountElementId = 'search-results-count';
|
||||
|
||||
this.lintOptions = {
|
||||
esversion: 11,
|
||||
@@ -155,8 +158,16 @@ export default class CodeEditor extends React.Component {
|
||||
this.props.onSave();
|
||||
}
|
||||
},
|
||||
'Cmd-F': 'findPersistent',
|
||||
'Ctrl-F': 'findPersistent',
|
||||
'Cmd-F': (cm) => {
|
||||
cm.execCommand('findPersistent');
|
||||
this._bindSearchHandler();
|
||||
this._appendSearchResultsCount();
|
||||
},
|
||||
'Ctrl-F': (cm) => {
|
||||
cm.execCommand('findPersistent');
|
||||
this._bindSearchHandler();
|
||||
this._appendSearchResultsCount();
|
||||
},
|
||||
'Cmd-H': 'replace',
|
||||
'Ctrl-H': 'replace',
|
||||
Tab: function (cm) {
|
||||
@@ -308,6 +319,8 @@ export default class CodeEditor extends React.Component {
|
||||
this.editor.off('change', this._onEdit);
|
||||
this.editor = null;
|
||||
}
|
||||
|
||||
this._unbindSearchHandler();
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -344,4 +357,62 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Bind handler to search input to count number of search results
|
||||
*/
|
||||
_bindSearchHandler = () => {
|
||||
const searchInput = document.querySelector('.CodeMirror-search-field');
|
||||
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', this._countSearchResults);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Unbind handler to search input to count number of search results
|
||||
*/
|
||||
_unbindSearchHandler = () => {
|
||||
const searchInput = document.querySelector('.CodeMirror-search-field');
|
||||
|
||||
if (searchInput) {
|
||||
searchInput.removeEventListener('input', this._countSearchResults);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Append search results count to search dialog
|
||||
*/
|
||||
_appendSearchResultsCount = () => {
|
||||
const dialog = document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top');
|
||||
|
||||
if (dialog) {
|
||||
const searchResultsCount = document.createElement('span');
|
||||
searchResultsCount.id = this.searchResultsCountElementId;
|
||||
dialog.appendChild(searchResultsCount);
|
||||
|
||||
this._countSearchResults();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Count search results and update state
|
||||
*/
|
||||
_countSearchResults = () => {
|
||||
let count = 0;
|
||||
|
||||
const searchInput = document.querySelector('.CodeMirror-search-field');
|
||||
|
||||
if (searchInput && searchInput.value.length > 0) {
|
||||
const text = new RegExp(searchInput.value, 'gi');
|
||||
const matches = this.editor.getValue().match(text);
|
||||
count = matches ? matches.length : 0;
|
||||
}
|
||||
|
||||
const searchResultsCountElement = document.querySelector(`#${this.searchResultsCountElementId}`);
|
||||
|
||||
if (searchResultsCountElement) {
|
||||
searchResultsCountElement.innerText = `${count} results`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ const Docs = ({ collection }) => {
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
/>
|
||||
) : (
|
||||
<Markdown onDoubleClick={toggleViewMode} content={docs} />
|
||||
<Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={docs} />
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -53,7 +53,7 @@ const Documentation = ({ item, collection }) => {
|
||||
mode="application/text"
|
||||
/>
|
||||
) : (
|
||||
<Markdown onDoubleClick={toggleViewMode} content={docs} />
|
||||
<Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={docs} />
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -40,10 +40,15 @@ const Wrapper = styled.div`
|
||||
color: ${(props) => props.theme.dropdown.iconColor};
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:hover:not(:disabled) {
|
||||
background-color: ${(props) => props.theme.dropdown.hoverBg};
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
color: gray;
|
||||
}
|
||||
|
||||
&.border-top {
|
||||
border-top: solid 1px ${(props) => props.theme.dropdown.separator};
|
||||
}
|
||||
|
||||
16
packages/bruno-app/src/components/Icons/Dot/index.js
Normal file
16
packages/bruno-app/src/components/Icons/Dot/index.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
|
||||
const DotIcon = ({ width }) => {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={width} height={width}
|
||||
viewBox="0 0 24 24" strokeWidth="1.5"
|
||||
stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round"
|
||||
className='inline-block'
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" stroke-width="0" fill="currentColor" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default DotIcon;
|
||||
@@ -69,6 +69,7 @@ const StyledMarkdownBodyWrapper = styled.div`
|
||||
|
||||
pre {
|
||||
background: ${(props) => props.theme.sidebar.bg};
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
|
||||
table {
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import * as MarkdownItReplaceLink from 'markdown-it-replace-link';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import React from 'react';
|
||||
|
||||
const md = new MarkdownIt();
|
||||
const Markdown = ({ collectionPath, onDoubleClick, content }) => {
|
||||
const markdownItOptions = {
|
||||
replaceLink: function (link, env) {
|
||||
return link.replace(/^\./, collectionPath);
|
||||
}
|
||||
};
|
||||
|
||||
const Markdown = ({ onDoubleClick, content }) => {
|
||||
const handleOnClick = (event) => {
|
||||
const target = event.target;
|
||||
if (target.tagName === 'A') {
|
||||
@@ -23,6 +28,8 @@ const Markdown = ({ onDoubleClick, content }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const md = new MarkdownIt(markdownItOptions).use(MarkdownItReplaceLink);
|
||||
|
||||
const htmlFromMarkdown = md.render(content || '');
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const ModalHeader = ({ title, handleCancel, customHeader }) => (
|
||||
const ModalHeader = ({ title, handleCancel, customHeader, hideClose }) => (
|
||||
<div className="bruno-modal-header">
|
||||
{customHeader ? customHeader : <>{title ? <div className="bruno-modal-header-title">{title}</div> : null}</>}
|
||||
{handleCancel ? (
|
||||
{handleCancel && !hideClose ? (
|
||||
<div className="close cursor-pointer" onClick={handleCancel ? () => handleCancel() : null}>
|
||||
×
|
||||
</div>
|
||||
@@ -63,6 +63,7 @@ const Modal = ({
|
||||
confirmDisabled,
|
||||
hideCancel,
|
||||
hideFooter,
|
||||
hideClose,
|
||||
disableCloseOnOutsideClick,
|
||||
disableEscapeKey,
|
||||
onClick,
|
||||
@@ -100,7 +101,12 @@ const Modal = ({
|
||||
return (
|
||||
<StyledWrapper className={classes} onClick={onClick ? (e) => onClick(e) : null}>
|
||||
<div className={`bruno-modal-card modal-${size}`}>
|
||||
<ModalHeader title={title} handleCancel={() => closeModal({ type: 'icon' })} customHeader={customHeader} />
|
||||
<ModalHeader
|
||||
title={title}
|
||||
hideClose={hideClose}
|
||||
handleCancel={() => closeModal({ type: 'icon' })}
|
||||
customHeader={customHeader}
|
||||
/>
|
||||
<ModalContent>{children}</ModalContent>
|
||||
<ModalFooter
|
||||
confirmText={confirmText}
|
||||
|
||||
@@ -1,39 +1,42 @@
|
||||
import React from 'react';
|
||||
import { IconSpeakerphone, IconBrandTwitter, IconBrandGithub, IconBrandDiscord, IconBook } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { useDictionary } from 'providers/Dictionary/index';
|
||||
|
||||
const Support = () => {
|
||||
const { dictionary } = useDictionary();
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="rows">
|
||||
<div className="mt-2">
|
||||
<a href="https://docs.usebruno.com" target="_blank" className="flex items-end">
|
||||
<IconBook size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Documentation</span>
|
||||
<span className="label ml-2">{dictionary.documentation}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<a href="https://github.com/usebruno/bruno/issues" target="_blank" className="flex items-end">
|
||||
<IconSpeakerphone size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Report Issues</span>
|
||||
<span className="label ml-2">{dictionary.reportIssues}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<a href="https://discord.com/invite/KgcZUncpjq" target="_blank" className="flex items-end">
|
||||
<IconBrandDiscord size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Discord</span>
|
||||
<span className="label ml-2">{dictionary.discord}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<a href="https://github.com/usebruno/bruno" target="_blank" className="flex items-end">
|
||||
<IconBrandGithub size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">GitHub</span>
|
||||
<span className="label ml-2">{dictionary.gitHub}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<a href="https://twitter.com/use_bruno" target="_blank" className="flex items-end">
|
||||
<IconBrandTwitter size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Twitter</span>
|
||||
<span className="label ml-2">{dictionary.twitter}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import React from 'react';
|
||||
import { useTheme } from 'providers/Theme/index';
|
||||
import darkTheme from 'themes/dark';
|
||||
import lightTheme from 'themes/light';
|
||||
|
||||
/**
|
||||
* Assertion operators
|
||||
@@ -81,16 +78,10 @@ const AssertionOperator = ({ operator, onChange }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<select value={operator} onChange={handleChange} className="mousetrap">
|
||||
{operators.map((operator) => (
|
||||
<option
|
||||
style={{ backgroundColor: storedTheme === 'dark' ? darkTheme.bg : lightTheme.bg }}
|
||||
key={operator}
|
||||
value={operator}
|
||||
>
|
||||
<option key={operator} value={operator}>
|
||||
{getLabel(operator)}
|
||||
</option>
|
||||
))}
|
||||
|
||||
@@ -55,6 +55,9 @@ const Wrapper = styled.div`
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
option {
|
||||
background-color: ${(props) => props.theme.bg};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
|
||||
@@ -23,6 +23,10 @@ const StyledWrapper = styled.div`
|
||||
color: ${(props) => props.theme.tabs.active.color} !important;
|
||||
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
|
||||
}
|
||||
|
||||
.content-indicator {
|
||||
color: ${(props) => props.theme.text}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -7,7 +7,7 @@ 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 DotIcon from 'components/Icons/Dot';
|
||||
import Vars from 'components/RequestPane/Vars';
|
||||
import Assertions from 'components/RequestPane/Assertions';
|
||||
import Script from 'components/RequestPane/Script';
|
||||
@@ -16,6 +16,12 @@ import StyledWrapper from './StyledWrapper';
|
||||
import { find, get } from 'lodash';
|
||||
import Documentation from 'components/Documentation/index';
|
||||
|
||||
const ContentIndicator = () => {
|
||||
return <sup className="ml-[.125rem] opacity-80 font-medium">
|
||||
<DotIcon width="10"></DotIcon>
|
||||
</sup>
|
||||
};
|
||||
|
||||
const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
const dispatch = useDispatch();
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
@@ -82,12 +88,17 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
|
||||
const isMultipleContentTab = ['params', 'script', 'vars', 'auth', 'docs'].includes(focusedTab.requestPaneTab);
|
||||
|
||||
// get the length of active params, headers, asserts and vars
|
||||
const params = item.draft ? get(item, 'draft.request.params', []) : get(item, 'request.params', []);
|
||||
const headers = item.draft ? get(item, 'draft.request.headers', []) : get(item, 'request.headers', []);
|
||||
const assertions = item.draft ? get(item, 'draft.request.assertions', []) : get(item, 'request.assertions', []);
|
||||
const requestVars = item.draft ? get(item, 'draft.request.vars.req', []) : get(item, 'request.vars.req', []);
|
||||
const responseVars = item.draft ? get(item, 'draft.request.vars.res', []) : get(item, 'request.vars.res', []);
|
||||
// get the length of active params, headers, asserts and vars as well as the contents of the body, tests and script
|
||||
const getPropertyFromDraftOrRequest = (propertyKey) =>
|
||||
item.draft ? get(item, `draft.${propertyKey}`, []) : get(item, propertyKey, []);
|
||||
const params = getPropertyFromDraftOrRequest('request.params');
|
||||
const body = getPropertyFromDraftOrRequest('request.body');
|
||||
const headers = getPropertyFromDraftOrRequest('request.headers');
|
||||
const script = getPropertyFromDraftOrRequest('request.script');
|
||||
const assertions = getPropertyFromDraftOrRequest('request.assertions');
|
||||
const tests = getPropertyFromDraftOrRequest('request.tests');
|
||||
const requestVars = getPropertyFromDraftOrRequest('request.vars.req');
|
||||
const responseVars = getPropertyFromDraftOrRequest('request.vars.res');
|
||||
|
||||
const activeParamsLength = params.filter((param) => param.enabled).length;
|
||||
const activeHeadersLength = headers.filter((header) => header.enabled).length;
|
||||
@@ -105,10 +116,11 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
</div>
|
||||
<div className={getTabClassname('body')} role="tab" onClick={() => selectTab('body')}>
|
||||
Body
|
||||
{body.mode !== 'none' && <ContentIndicator />}
|
||||
</div>
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
||||
Headers
|
||||
{activeHeadersLength > 0 && <sup className="ml-1 font-medium">{activeHeadersLength}</sup>}
|
||||
{activeHeadersLength > 0 && <sup className="ml-[.125rem] font-medium">{activeHeadersLength}</sup>}
|
||||
</div>
|
||||
<div className={getTabClassname('auth')} role="tab" onClick={() => selectTab('auth')}>
|
||||
Auth
|
||||
@@ -119,6 +131,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
</div>
|
||||
<div className={getTabClassname('script')} role="tab" onClick={() => selectTab('script')}>
|
||||
Script
|
||||
{(script.req || script.res) && <ContentIndicator />}
|
||||
</div>
|
||||
<div className={getTabClassname('assert')} role="tab" onClick={() => selectTab('assert')}>
|
||||
Assert
|
||||
@@ -126,6 +139,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
</div>
|
||||
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
|
||||
Tests
|
||||
{tests && <ContentIndicator />}
|
||||
</div>
|
||||
<div className={getTabClassname('docs')} role="tab" onClick={() => selectTab('docs')}>
|
||||
Docs
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import has from 'lodash/has';
|
||||
import Tooltip from 'components/Tooltip';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
@@ -103,7 +103,7 @@ const QueryParams = ({ item, collection }) => {
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col">
|
||||
<div className="flex-1 mt-2">
|
||||
<div className="mb-1 title text-xs">Query</div>
|
||||
<div className="mb-2 title text-xs">Query</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -173,7 +173,22 @@ const QueryParams = ({ item, collection }) => {
|
||||
<button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={handleAddQueryParam}>
|
||||
+ <span>Add Param</span>
|
||||
</button>
|
||||
<div className="mb-1 title text-xs">Path</div>
|
||||
<div className="mb-2 title text-xs flex items-stretch">
|
||||
<span>Path</span>
|
||||
<Tooltip
|
||||
text={`
|
||||
<div>
|
||||
Path variables are automatically added whenever the
|
||||
<code className="font-mono mx-2">:name</code>
|
||||
template is used in the URL. <br/> For example:
|
||||
<code className="font-mono mx-2">
|
||||
https://example.com/v1/users/<span>:id</span>
|
||||
</code>
|
||||
</div>
|
||||
`}
|
||||
tooltipId="path-param-tooltip"
|
||||
/>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -224,6 +239,11 @@ const QueryParams = ({ item, collection }) => {
|
||||
: null}
|
||||
</tbody>
|
||||
</table>
|
||||
{!(pathParams && pathParams.length) ?
|
||||
<div className="title pr-2 py-3 mt-2 text-xs">
|
||||
|
||||
</div>
|
||||
: null}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -30,7 +30,7 @@ const RequestNotFound = ({ itemUid }) => {
|
||||
|
||||
return (
|
||||
<div className="mt-6 px-6">
|
||||
<div className="p-4 bg-orange-100 border-l-4 border-yellow-500 text-yellow-700 bg-yellow-100 p-4">
|
||||
<div className="p-4 bg-orange-100 border-l-4 border-yellow-500 text-yellow-700">
|
||||
<div>Request no longer exists.</div>
|
||||
<div className="mt-2">
|
||||
This can happen when the .bru file associated with this request was deleted on your filesystem.
|
||||
|
||||
@@ -18,6 +18,7 @@ import CollectionSettings from 'components/CollectionSettings';
|
||||
import { DocExplorer } from '@usebruno/graphql-docs';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import SecuritySettings from 'components/SecuritySettings';
|
||||
import FolderSettings from 'components/FolderSettings';
|
||||
|
||||
const MIN_LEFT_PANE_WIDTH = 300;
|
||||
@@ -137,6 +138,10 @@ const RequestTabPanel = () => {
|
||||
return <FolderSettings collection={collection} folder={folder} />;
|
||||
}
|
||||
|
||||
if (focusedTab.type === 'security-settings') {
|
||||
return <SecuritySettings collection={collection} />;
|
||||
}
|
||||
|
||||
const item = findItemInCollection(collection, activeTabUid);
|
||||
if (!item || !item.uid) {
|
||||
return <RequestNotFound itemUid={activeTabUid} />;
|
||||
|
||||
@@ -5,6 +5,7 @@ import EnvironmentSelector from 'components/Environments/EnvironmentSelector';
|
||||
import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import JsSandboxMode from 'components/SecuritySettings/JsSandboxMode';
|
||||
|
||||
const CollectionToolBar = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -47,6 +48,9 @@ const CollectionToolBar = ({ collection }) => {
|
||||
<span className="ml-2 mr-4 font-semibold">{collection?.name}</span>
|
||||
</div>
|
||||
<div className="flex flex-1 items-center justify-end">
|
||||
<span className="mr-2">
|
||||
<JsSandboxMode collection={collection} />
|
||||
</span>
|
||||
<span className="mr-2">
|
||||
<IconRun className="cursor-pointer" size={20} strokeWidth={1.5} onClick={handleRun} />
|
||||
</span>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { IconVariable, IconSettings, IconRun, IconFolder } from '@tabler/icons';
|
||||
import { IconVariable, IconSettings, IconRun, IconFolder, IconShieldLock } from '@tabler/icons';
|
||||
|
||||
const SpecialTab = ({ handleCloseClick, type, tabName }) => {
|
||||
const getTabInfo = (type, tabName) => {
|
||||
@@ -12,6 +12,14 @@ const SpecialTab = ({ handleCloseClick, type, tabName }) => {
|
||||
</>
|
||||
);
|
||||
}
|
||||
case 'security-settings': {
|
||||
return (
|
||||
<>
|
||||
<IconShieldLock size={18} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<span className="ml-1">Security</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
case 'folder-settings': {
|
||||
return (
|
||||
<div className="flex items-center flex-nowrap overflow-hidden">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useRef, Fragment } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
@@ -12,12 +12,18 @@ import ConfirmRequestClose from './ConfirmRequestClose';
|
||||
import RequestTabNotFound from './RequestTabNotFound';
|
||||
import SpecialTab from './SpecialTab';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import CloneCollectionItem from 'components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index';
|
||||
import NewRequest from 'components/Sidebar/NewRequest/index';
|
||||
|
||||
const RequestTab = ({ tab, collection, folderUid }) => {
|
||||
const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUid }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const [showConfirmClose, setShowConfirmClose] = useState(false);
|
||||
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const handleCloseClick = (event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
@@ -28,6 +34,19 @@ const RequestTab = ({ tab, collection, folderUid }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const handleRightClick = (_event) => {
|
||||
const menuDropdown = dropdownTippyRef.current;
|
||||
if (!menuDropdown) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (menuDropdown.state.isShown) {
|
||||
menuDropdown.hide();
|
||||
} else {
|
||||
menuDropdown.show();
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = (e) => {
|
||||
if (e.button === 1) {
|
||||
e.stopPropagation();
|
||||
@@ -43,45 +62,11 @@ const RequestTab = ({ tab, collection, folderUid }) => {
|
||||
|
||||
const getMethodColor = (method = '') => {
|
||||
const theme = storedTheme === 'dark' ? darkTheme : lightTheme;
|
||||
|
||||
let color = '';
|
||||
method = method.toLocaleLowerCase();
|
||||
|
||||
switch (method) {
|
||||
case 'get': {
|
||||
color = theme.request.methods.get;
|
||||
break;
|
||||
}
|
||||
case 'post': {
|
||||
color = theme.request.methods.post;
|
||||
break;
|
||||
}
|
||||
case 'put': {
|
||||
color = theme.request.methods.put;
|
||||
break;
|
||||
}
|
||||
case 'delete': {
|
||||
color = theme.request.methods.delete;
|
||||
break;
|
||||
}
|
||||
case 'patch': {
|
||||
color = theme.request.methods.patch;
|
||||
break;
|
||||
}
|
||||
case 'options': {
|
||||
color = theme.request.methods.options;
|
||||
break;
|
||||
}
|
||||
case 'head': {
|
||||
color = theme.request.methods.head;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return color;
|
||||
return theme.request.methods[method.toLocaleLowerCase()];
|
||||
};
|
||||
|
||||
const folder = folderUid ? findItemInCollection(collection, folderUid) : null;
|
||||
if (['collection-settings', 'folder-settings', 'variables', 'collection-runner'].includes(tab.type)) {
|
||||
if (['collection-settings', 'folder-settings', 'variables', 'collection-runner', 'security-settings'].includes(tab.type)) {
|
||||
return (
|
||||
<StyledWrapper className="flex items-center justify-between tab-container px-1">
|
||||
{tab.type === 'folder-settings' ? (
|
||||
@@ -143,6 +128,7 @@ const RequestTab = ({ tab, collection, folderUid }) => {
|
||||
)}
|
||||
<div
|
||||
className="flex items-baseline tab-label pl-2"
|
||||
onContextMenu={handleRightClick}
|
||||
onMouseUp={(e) => {
|
||||
if (!item.draft) return handleMouseUp(e);
|
||||
|
||||
@@ -159,6 +145,15 @@ const RequestTab = ({ tab, collection, folderUid }) => {
|
||||
<span className="ml-1 tab-name" title={item.name}>
|
||||
{item.name}
|
||||
</span>
|
||||
<RequestTabMenu
|
||||
onDropdownCreate={onDropdownCreate}
|
||||
tabIndex={tabIndex}
|
||||
collectionRequestTabs={collectionRequestTabs}
|
||||
tabItem={item}
|
||||
collection={collection}
|
||||
dropdownTippyRef={dropdownTippyRef}
|
||||
dispatch={dispatch}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="flex px-2 close-icon-container"
|
||||
@@ -195,4 +190,124 @@ const RequestTab = ({ tab, collection, folderUid }) => {
|
||||
);
|
||||
};
|
||||
|
||||
function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, collection, dropdownTippyRef, dispatch }) {
|
||||
const [showCloneRequestModal, setShowCloneRequestModal] = useState(false);
|
||||
const [showAddNewRequestModal, setShowAddNewRequestModal] = useState(false);
|
||||
|
||||
const totalTabs = collectionRequestTabs.length || 0;
|
||||
const currentTabUid = collectionRequestTabs[tabIndex]?.uid;
|
||||
const currentTabItem = findItemInCollection(collection, currentTabUid);
|
||||
|
||||
const hasLeftTabs = tabIndex !== 0;
|
||||
const hasRightTabs = totalTabs > tabIndex + 1;
|
||||
const hasOtherTabs = totalTabs > 1;
|
||||
|
||||
async function handleCloseTab(event, tabUid) {
|
||||
event.stopPropagation();
|
||||
dropdownTippyRef.current.hide();
|
||||
|
||||
if (!tabUid) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const item = findItemInCollection(collection, tabUid);
|
||||
// silently save unsaved changes before closing the tab
|
||||
if (item.draft) {
|
||||
await dispatch(saveRequest(item.uid, collection.uid, true));
|
||||
}
|
||||
|
||||
dispatch(closeTabs({ tabUids: [tabUid] }));
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
function handleCloseOtherTabs(event) {
|
||||
dropdownTippyRef.current.hide();
|
||||
|
||||
const otherTabs = collectionRequestTabs.filter((_, index) => index !== tabIndex);
|
||||
otherTabs.forEach((tab) => handleCloseTab(event, tab.uid));
|
||||
}
|
||||
|
||||
function handleCloseTabsToTheLeft(event) {
|
||||
dropdownTippyRef.current.hide();
|
||||
|
||||
const leftTabs = collectionRequestTabs.filter((_, index) => index < tabIndex);
|
||||
leftTabs.forEach((tab) => handleCloseTab(event, tab.uid));
|
||||
}
|
||||
|
||||
function handleCloseTabsToTheRight(event) {
|
||||
dropdownTippyRef.current.hide();
|
||||
|
||||
const rightTabs = collectionRequestTabs.filter((_, index) => index > tabIndex);
|
||||
rightTabs.forEach((tab) => handleCloseTab(event, tab.uid));
|
||||
}
|
||||
|
||||
function handleCloseSavedTabs(event) {
|
||||
event.stopPropagation();
|
||||
|
||||
const savedTabs = collection.items.filter((item) => !item.draft);
|
||||
const savedTabIds = savedTabs.map((item) => item.uid) || [];
|
||||
dispatch(closeTabs({ tabUids: savedTabIds }));
|
||||
}
|
||||
|
||||
function handleCloseAllTabs(event) {
|
||||
collectionRequestTabs.forEach((tab) => handleCloseTab(event, tab.uid));
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{showAddNewRequestModal && (
|
||||
<NewRequest collection={collection} onClose={() => setShowAddNewRequestModal(false)} />
|
||||
)}
|
||||
|
||||
{showCloneRequestModal && (
|
||||
<CloneCollectionItem
|
||||
item={currentTabItem}
|
||||
collection={collection}
|
||||
onClose={() => setShowCloneRequestModal(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<span></span>} placement="bottom-start">
|
||||
<button
|
||||
className="dropdown-item w-full"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
setShowAddNewRequestModal(true);
|
||||
}}
|
||||
>
|
||||
New Request
|
||||
</button>
|
||||
<button
|
||||
className="dropdown-item w-full"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
setShowCloneRequestModal(true);
|
||||
}}
|
||||
>
|
||||
Clone Request
|
||||
</button>
|
||||
<button className="dropdown-item w-full" onClick={(e) => handleCloseTab(e, currentTabUid)}>
|
||||
Close
|
||||
</button>
|
||||
<button disabled={!hasOtherTabs} className="dropdown-item w-full" onClick={handleCloseOtherTabs}>
|
||||
Close Others
|
||||
</button>
|
||||
<button disabled={!hasLeftTabs} className="dropdown-item w-full" onClick={handleCloseTabsToTheLeft}>
|
||||
Close to the Left
|
||||
</button>
|
||||
<button disabled={!hasRightTabs} className="dropdown-item w-full" onClick={handleCloseTabsToTheRight}>
|
||||
Close to the Right
|
||||
</button>
|
||||
<button className="dropdown-item w-full" onClick={handleCloseSavedTabs}>
|
||||
Close Saved
|
||||
</button>
|
||||
<button className="dropdown-item w-full" onClick={handleCloseAllTabs}>
|
||||
Close All
|
||||
</button>
|
||||
</Dropdown>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export default RequestTab;
|
||||
|
||||
@@ -7,7 +7,6 @@ const Wrapper = styled.div`
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
position: relative;
|
||||
overflow: scroll;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
|
||||
@@ -110,7 +110,14 @@ const RequestTabs = () => {
|
||||
role="tab"
|
||||
onClick={() => handleClick(tab)}
|
||||
>
|
||||
<RequestTab key={tab.uid} tab={tab} collection={activeCollection} folderUid={tab.folderUid} />
|
||||
<RequestTab
|
||||
collectionRequestTabs={collectionRequestTabs}
|
||||
tabIndex={index}
|
||||
key={tab.uid}
|
||||
tab={tab}
|
||||
collection={activeCollection}
|
||||
folderUid={tab.folderUid}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ import styled from 'styled-components';
|
||||
const StyledWrapper = styled.div`
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: calc(100% - 0.75rem);
|
||||
z-index: 1;
|
||||
background-color: ${(props) => props.theme.requestTabPanel.responseOverlayBg};
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ const ResponseLoadingOverlay = ({ item, collection }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="px-3 w-full">
|
||||
<StyledWrapper className="w-full">
|
||||
<div className="overlay">
|
||||
<div style={{ marginBottom: 15, fontSize: 26 }}>
|
||||
<div style={{ display: 'inline-block', fontSize: 20, marginLeft: 5, marginRight: 5 }}>
|
||||
|
||||
@@ -46,7 +46,7 @@ const QueryResultFilter = ({ filter, onChange, mode }) => {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
'response-filter absolute bottom-2 w-full justify-end right-0 flex flex-row items-center gap-2 py-4 px-2'
|
||||
'response-filter absolute bottom-2 w-full justify-end right-0 flex flex-row items-center gap-2 py-4 px-2 pointer-events-none'
|
||||
}
|
||||
>
|
||||
{tooltipText && !isExpanded && <ReactTooltip anchorId={'request-filter-icon'} html={tooltipText} />}
|
||||
@@ -61,11 +61,11 @@ const QueryResultFilter = ({ filter, onChange, mode }) => {
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
className={`block ml-14 p-2 py-1 sm:text-sm transition-all duration-200 ease-in-out border border-gray-300 rounded-md ${
|
||||
isExpanded ? 'w-full opacity-100' : 'w-[0] opacity-0'
|
||||
isExpanded ? 'w-full opacity-100 pointer-events-auto' : 'w-[0] opacity-0'
|
||||
}`}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<div className="text-gray-500 sm:text-sm cursor-pointer" id="request-filter-icon" onClick={handleFilterClick}>
|
||||
<div className="text-gray-500 sm:text-sm cursor-pointer pointer-events-auto" id="request-filter-icon" onClick={handleFilterClick}>
|
||||
{isExpanded ? <IconX size={20} strokeWidth={1.5} /> : <IconFilter size={20} strokeWidth={1.5} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -97,6 +97,8 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const responseHeadersCount = typeof response.headers === 'object' ? Object.entries(response.headers).length : 0;
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
<div className="flex flex-wrap items-center pl-3 pr-4 tabs" role="tablist">
|
||||
@@ -105,7 +107,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
</div>
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
||||
Headers
|
||||
{response.headers?.length > 0 && <sup className="ml-1 font-medium">{response.headers.length}</sup>}
|
||||
{responseHeadersCount > 0 && <sup className="ml-1 font-medium">{responseHeadersCount}</sup>}
|
||||
</div>
|
||||
<div className={getTabClassname('timeline')} role="tab" onClick={() => selectTab('timeline')}>
|
||||
Timeline
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.safe-mode {
|
||||
padding: 0.15rem 0.3rem;
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
border: solid 1px ${(props) => props.theme.colors.text.green} !important;
|
||||
}
|
||||
.developer-mode {
|
||||
padding: 0.15rem 0.3rem;
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
border: solid 1px ${(props) => props.theme.colors.text.yellow} !important;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { IconShieldLock } from '@tabler/icons';
|
||||
import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { uuid } from 'utils/common/index';
|
||||
import JsSandboxModeModal from '../JsSandboxModeModal';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const JsSandboxMode = ({ collection }) => {
|
||||
const jsSandboxMode = collection?.securityConfig?.jsSandboxMode;
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const viewSecuritySettings = () => {
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: uuid(),
|
||||
collectionUid: collection.uid,
|
||||
type: 'security-settings'
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className='flex'>
|
||||
{jsSandboxMode === 'safe' && (
|
||||
<div
|
||||
className="flex items-center border rounded-md text-xs cursor-pointer safe-mode"
|
||||
onClick={viewSecuritySettings}
|
||||
>
|
||||
Safe Mode
|
||||
</div>
|
||||
)}
|
||||
{jsSandboxMode === 'developer' && (
|
||||
<div
|
||||
className="flex items-center border rounded-md text-xs cursor-pointer developer-mode"
|
||||
onClick={viewSecuritySettings}
|
||||
>
|
||||
Developer Mode
|
||||
</div>
|
||||
)}
|
||||
{!jsSandboxMode ? <JsSandboxModeModal collection={collection} /> : null}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default JsSandboxMode;
|
||||
@@ -0,0 +1,22 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
max-width: 800px;
|
||||
|
||||
span.beta-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.1rem 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
border: solid 1px ${(props) => props.theme.colors.text.green} !important;
|
||||
}
|
||||
|
||||
span.developer-mode-warning {
|
||||
font-weight: 400;
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,98 @@
|
||||
import { saveCollectionSecurityConfig } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useState } from 'react';
|
||||
import Portal from 'components/Portal';
|
||||
import Modal from 'components/Modal';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const JsSandboxModeModal = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const [jsSandboxMode, setJsSandboxMode] = useState(collection?.securityConfig?.jsSandboxMode || 'safe');
|
||||
|
||||
const handleChange = (e) => {
|
||||
setJsSandboxMode(e.target.value);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
dispatch(
|
||||
saveCollectionSecurityConfig(collection?.uid, {
|
||||
jsSandboxMode: jsSandboxMode
|
||||
})
|
||||
)
|
||||
.then(() => {
|
||||
toast.success('Sandbox mode updated successfully');
|
||||
})
|
||||
.catch((err) => console.log(err) && toast.error('Failed to update sandbox mode'));
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
size="sm"
|
||||
title={'JavaScript Sandbox'}
|
||||
confirmText="Save"
|
||||
handleConfirm={handleSave}
|
||||
hideCancel={true}
|
||||
hideClose={true}
|
||||
disableCloseOnOutsideClick={true}
|
||||
disableEscapeKey={true}
|
||||
>
|
||||
<StyledWrapper>
|
||||
<div>
|
||||
The collection might include JavaScript code in Variables, Scripts, Tests, and Assertions.
|
||||
</div>
|
||||
|
||||
<div className='text-muted mt-6'>
|
||||
Please choose the security level for the JavaScript code execution.
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col mt-4">
|
||||
<label htmlFor="safe" className="flex flex-row items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
id="safe"
|
||||
name="jsSandboxMode"
|
||||
value="safe"
|
||||
checked={jsSandboxMode === 'safe'}
|
||||
onChange={handleChange}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
<span className={jsSandboxMode === 'safe' ? 'font-medium' : 'font-normal'}>
|
||||
Safe Mode
|
||||
</span>
|
||||
<span className='beta-tag'>BETA</span>
|
||||
</label>
|
||||
<p className='text-sm text-muted mt-1'>
|
||||
JavaScript code is executed in a secure sandbox and cannot access your filesystem or execute system commands.
|
||||
</p>
|
||||
|
||||
<label htmlFor="developer" className="flex flex-row gap-2 mt-6 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
id="developer"
|
||||
name="jsSandboxMode"
|
||||
value="developer"
|
||||
checked={jsSandboxMode === 'developer'}
|
||||
onChange={handleChange}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
<span className={jsSandboxMode === 'developer' ? 'font-medium' : 'font-normal'}>
|
||||
Developer Mode
|
||||
<span className='ml-1 developer-mode-warning'>(use only if you trust the collections authors)</span>
|
||||
</span>
|
||||
</label>
|
||||
<p className='text-sm text-muted mt-1'>
|
||||
JavaScript code has access to the filesystem, can execute system commands and access sensitive information.
|
||||
</p>
|
||||
<small className='text-muted mt-6'>
|
||||
* SAFE mode has been introduced v1.26 onwards and is in beta. Please report any issues on github.
|
||||
</small>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default JsSandboxModeModal;
|
||||
@@ -0,0 +1,22 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
max-width: 800px;
|
||||
|
||||
span.beta-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.1rem 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
border: solid 1px ${(props) => props.theme.colors.text.green} !important;
|
||||
}
|
||||
|
||||
span.developer-mode-warning {
|
||||
font-weight: 400;
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
86
packages/bruno-app/src/components/SecuritySettings/index.js
Normal file
86
packages/bruno-app/src/components/SecuritySettings/index.js
Normal file
@@ -0,0 +1,86 @@
|
||||
import { useState } from 'react';
|
||||
import { saveCollectionSecurityConfig } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import toast from 'react-hot-toast';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
const SecuritySettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const [jsSandboxMode, setJsSandboxMode] = useState(collection?.securityConfig?.jsSandboxMode || 'safe');
|
||||
|
||||
const handleChange = (e) => {
|
||||
setJsSandboxMode(e.target.value);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
dispatch(
|
||||
saveCollectionSecurityConfig(collection?.uid, {
|
||||
jsSandboxMode: jsSandboxMode
|
||||
})
|
||||
)
|
||||
.then(() => {
|
||||
toast.success('Sandbox mode updated successfully');
|
||||
})
|
||||
.catch((err) => console.log(err) && toast.error('Failed to update sandbox mode'));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative px-4 py-4">
|
||||
<div className='font-semibold mt-2'>JavaScript Sandbox</div>
|
||||
|
||||
<div className='mt-4'>
|
||||
The collection might include JavaScript code in Variables, Scripts, Tests, and Assertions.
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col mt-4">
|
||||
<div className="flex flex-col">
|
||||
<label htmlFor="safe" className="flex flex-row items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
id="safe"
|
||||
name="jsSandboxMode"
|
||||
value="safe"
|
||||
checked={jsSandboxMode === 'safe'}
|
||||
onChange={handleChange}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
<span className={jsSandboxMode === 'safe' ? 'font-medium' : 'font-normal'}>
|
||||
Safe Mode
|
||||
</span>
|
||||
<span className='beta-tag'>BETA</span>
|
||||
</label>
|
||||
<p className='text-sm text-muted mt-1'>
|
||||
JavaScript code is executed in a secure sandbox and cannot access your filesystem or execute system commands.
|
||||
</p>
|
||||
|
||||
<label htmlFor="developer" className="flex flex-row gap-2 mt-6 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
id="developer"
|
||||
name="jsSandboxMode"
|
||||
value="developer"
|
||||
checked={jsSandboxMode === 'developer'}
|
||||
onChange={handleChange}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
<span className={jsSandboxMode === 'developer' ? 'font-medium' : 'font-normal'}>
|
||||
Developer Mode
|
||||
<span className='ml-1 developer-mode-warning'>(use only if you trust the collections authors)</span>
|
||||
</span>
|
||||
</label>
|
||||
<p className='text-sm text-muted mt-1'>
|
||||
JavaScript code has access to the filesystem, can execute system commands and access sensitive information.
|
||||
</p>
|
||||
</div>
|
||||
<button onClick={handleSave} className="submit btn btn-sm btn-secondary w-fit mt-6">
|
||||
Save
|
||||
</button>
|
||||
<small className='text-muted mt-6'>
|
||||
* SAFE mode has been introduced v1.26 onwards and is in beta. Please report any issues on github.
|
||||
</small>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default SecuritySettings;
|
||||
@@ -23,7 +23,9 @@ const RequestMethod = ({ item }) => {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className={getClassname(item.request.method)}>
|
||||
<span className="uppercase">{item.request.method}</span>
|
||||
<span className="uppercase">
|
||||
{item.request.method.length > 5 ? item.request.method.substring(0, 3) : item.request.method}
|
||||
</span>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -115,7 +115,7 @@ const Collections = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-col overflow-y-auto absolute top-32 bottom-10 left-0 right-0">
|
||||
<div className="mt-4 flex flex-col overflow-hidden hover:overflow-y-auto absolute top-32 bottom-10 left-0 right-0">
|
||||
{collections && collections.length
|
||||
? collections.map((c) => {
|
||||
return (
|
||||
|
||||
@@ -115,7 +115,7 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName, trans
|
||||
collectionLocation: Yup.string()
|
||||
.min(1, 'must be at least 1 character')
|
||||
.max(500, 'must be 500 characters or less')
|
||||
.required('name is required')
|
||||
.required('Location is required')
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
handleSubmit(values.collectionLocation);
|
||||
@@ -124,7 +124,9 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName, trans
|
||||
const browse = () => {
|
||||
dispatch(browseDirectory())
|
||||
.then((dirPath) => {
|
||||
formik.setFieldValue('collectionLocation', dirPath);
|
||||
if (typeof dirPath === 'string' && dirPath.length > 0) {
|
||||
formik.setFieldValue('collectionLocation', dirPath);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
formik.setFieldValue('collectionLocation', '');
|
||||
|
||||
@@ -109,7 +109,8 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item ? item.uid : null,
|
||||
headers: request.headers,
|
||||
body: request.body
|
||||
body: request.body,
|
||||
auth: request.auth
|
||||
})
|
||||
)
|
||||
.then(() => onClose())
|
||||
|
||||
@@ -129,7 +129,7 @@ const Sidebar = () => {
|
||||
Star
|
||||
</GitHubButton> */}
|
||||
</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.25.0</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.26.2</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,6 @@ const Tooltip = ({ text, tooltipId }) => {
|
||||
fill="currentColor"
|
||||
className="inline-block ml-2 cursor-pointer"
|
||||
viewBox="0 0 16 16"
|
||||
style={{ marginTop: 1 }}
|
||||
>
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
|
||||
<path d="M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z" />
|
||||
|
||||
@@ -9,9 +9,11 @@ import CreateCollection from 'components/Sidebar/CreateCollection';
|
||||
import ImportCollection from 'components/Sidebar/ImportCollection';
|
||||
import ImportCollectionLocation from 'components/Sidebar/ImportCollectionLocation';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { useDictionary } from 'providers/Dictionary/index';
|
||||
|
||||
const Welcome = () => {
|
||||
const dispatch = useDispatch();
|
||||
const { dictionary } = useDictionary();
|
||||
const [importedCollection, setImportedCollection] = useState(null);
|
||||
const [importedTranslationLog, setImportedTranslationLog] = useState({});
|
||||
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
|
||||
@@ -20,7 +22,7 @@ const Welcome = () => {
|
||||
|
||||
const handleOpenCollection = () => {
|
||||
dispatch(openCollection()).catch(
|
||||
(err) => console.log(err) && toast.error('An error occurred while opening the collection')
|
||||
(err) => console.log(err) && toast.error(dictionary.errorWhileOpeningCollection)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -38,12 +40,12 @@ const Welcome = () => {
|
||||
.then(() => {
|
||||
setImportCollectionLocationModalOpen(false);
|
||||
setImportedCollection(null);
|
||||
toast.success('Collection imported successfully');
|
||||
toast.success(dictionary.collectionImportedSuccessfully);
|
||||
})
|
||||
.catch((err) => {
|
||||
setImportCollectionLocationModalOpen(false);
|
||||
console.error(err);
|
||||
toast.error('An error occurred while importing the collection. Check the logs for more information.');
|
||||
toast.error(dictionary.errorWhileImportingCollection);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -66,46 +68,45 @@ const Welcome = () => {
|
||||
<Bruno width={50} />
|
||||
</div>
|
||||
<div className="text-xl font-semibold select-none">bruno</div>
|
||||
<div className="mt-4">Opensource IDE for exploring and testing APIs</div>
|
||||
<div className="mt-4">{dictionary.aboutBruno}</div>
|
||||
|
||||
<div className="uppercase font-semibold heading mt-10">Collections</div>
|
||||
<div className="uppercase font-semibold heading mt-10">{dictionary.collections}</div>
|
||||
<div className="mt-4 flex items-center collection-options select-none">
|
||||
<div className="flex items-center" onClick={() => setCreateCollectionModalOpen(true)}>
|
||||
<IconPlus size={18} strokeWidth={2} />
|
||||
<span className="label ml-2" id="create-collection">
|
||||
Create Collection
|
||||
{dictionary.createCollection}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center ml-6" onClick={handleOpenCollection}>
|
||||
<IconFolders size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Open Collection</span>
|
||||
<span className="label ml-2">{dictionary.openCollection}</span>
|
||||
</div>
|
||||
<div className="flex items-center ml-6" onClick={() => setImportCollectionModalOpen(true)}>
|
||||
<IconDownload size={18} strokeWidth={2} />
|
||||
<span className="label ml-2" id="import-collection">
|
||||
Import Collection
|
||||
{dictionary.importCollection}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="uppercase font-semibold heading mt-10 pt-6">Links</div>
|
||||
<div className="mt-4 flex flex-col collection-options select-none">
|
||||
<div className="flex items-center mt-2">
|
||||
<a href="https://docs.usebruno.com" target="_blank" className="inline-flex items-center">
|
||||
<IconBook size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Documentation</span>
|
||||
<span className="label ml-2">{dictionary.documentation}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex items-center mt-2">
|
||||
<a href="https://github.com/usebruno/bruno/issues" target="_blank" className="inline-flex items-center">
|
||||
<IconSpeakerphone size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Report Issues</span>
|
||||
<span className="label ml-2">{dictionary.reportIssues}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex items-center mt-2">
|
||||
<a href="https://github.com/usebruno/bruno" target="_blank" className="flex items-center">
|
||||
<IconBrandGithub size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">GitHub</span>
|
||||
<span className="label ml-2">{dictionary.gitHub}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
16
packages/bruno-app/src/dictionaries/en.js
Normal file
16
packages/bruno-app/src/dictionaries/en.js
Normal file
@@ -0,0 +1,16 @@
|
||||
export default {
|
||||
aboutBruno: 'Opensource IDE for exploring and testing APIs',
|
||||
collections: 'Collections',
|
||||
createCollection: 'Create Collection',
|
||||
openCollection: 'Open Collection',
|
||||
importCollection: 'Import Collection',
|
||||
documentation: 'Documentation',
|
||||
reportIssues: 'Report Issues',
|
||||
gitHub: 'GitHub',
|
||||
collectionImportedSuccessfully: 'Collection imported successfully',
|
||||
errorWhileOpeningCollection: 'An error occurred while opening the collection',
|
||||
errorWhileImportingCollection:
|
||||
'An error occurred while importing the collection. Check the logs for more information.',
|
||||
discord: 'Discord',
|
||||
twitter: 'Twitter'
|
||||
};
|
||||
5
packages/bruno-app/src/dictionaries/index.js
Normal file
5
packages/bruno-app/src/dictionaries/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import en from './en.js';
|
||||
|
||||
export const dictionaries = {
|
||||
en
|
||||
};
|
||||
@@ -14,6 +14,16 @@ import 'codemirror/lib/codemirror.css';
|
||||
import 'graphiql/graphiql.min.css';
|
||||
import 'react-tooltip/dist/react-tooltip.css';
|
||||
import '@usebruno/graphql-docs/dist/esm/index.css';
|
||||
import '@fontsource/inter/100.css';
|
||||
import '@fontsource/inter/200.css';
|
||||
import '@fontsource/inter/300.css';
|
||||
import '@fontsource/inter/400.css';
|
||||
import '@fontsource/inter/500.css';
|
||||
import '@fontsource/inter/600.css';
|
||||
import '@fontsource/inter/700.css';
|
||||
import '@fontsource/inter/800.css';
|
||||
import '@fontsource/inter/900.css';
|
||||
import { DictionaryProvider } from 'providers/Dictionary/index';
|
||||
|
||||
function SafeHydrate({ children }) {
|
||||
return <div suppressHydrationWarning>{typeof window === 'undefined' ? null : children}</div>;
|
||||
@@ -59,13 +69,15 @@ function MyApp({ Component, pageProps }) {
|
||||
<NoSsr>
|
||||
<Provider store={ReduxStore}>
|
||||
<ThemeProvider>
|
||||
<ToastProvider>
|
||||
<AppProvider>
|
||||
<HotkeysProvider>
|
||||
<Component {...pageProps} />
|
||||
</HotkeysProvider>
|
||||
</AppProvider>
|
||||
</ToastProvider>
|
||||
<DictionaryProvider>
|
||||
<ToastProvider>
|
||||
<AppProvider>
|
||||
<HotkeysProvider>
|
||||
<Component {...pageProps} />
|
||||
</HotkeysProvider>
|
||||
</AppProvider>
|
||||
</ToastProvider>
|
||||
</DictionaryProvider>
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
</NoSsr>
|
||||
|
||||
@@ -30,12 +30,7 @@ export default class MyDocument extends Document {
|
||||
render() {
|
||||
return (
|
||||
<Html>
|
||||
<Head>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</Head>
|
||||
<Head />
|
||||
<body id="bruno-app-body">
|
||||
<Main />
|
||||
<NextScript />
|
||||
|
||||
@@ -60,7 +60,7 @@ const trackStart = () => {
|
||||
event: 'start',
|
||||
properties: {
|
||||
os: platformLib.os.family,
|
||||
version: '1.25.0'
|
||||
version: '1.26.2'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
28
packages/bruno-app/src/providers/Dictionary/index.js
Normal file
28
packages/bruno-app/src/providers/Dictionary/index.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import { useState, useContext } from 'react';
|
||||
import { dictionaries } from 'src/dictionaries/index';
|
||||
|
||||
export const DictionaryContext = React.createContext();
|
||||
|
||||
const DictionaryProvider = (props) => {
|
||||
const [language, setLanguage] = useState('en');
|
||||
const dictionary = dictionaries[language] ?? dictionaries.en;
|
||||
|
||||
return (
|
||||
<DictionaryContext.Provider {...props} value={{ language, setLanguage, dictionary }}>
|
||||
<>{props.children}</>
|
||||
</DictionaryContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const useDictionary = () => {
|
||||
const context = useContext(DictionaryContext);
|
||||
|
||||
if (context === undefined) {
|
||||
throw new Error(`useDictionary must be used within a DictionaryProvider`);
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export { useDictionary, DictionaryProvider };
|
||||
@@ -154,6 +154,31 @@ export const HotkeysProvider = (props) => {
|
||||
};
|
||||
}, [activeTabUid]);
|
||||
|
||||
// close all tabs
|
||||
useEffect(() => {
|
||||
Mousetrap.bind(['command+shift+w', 'ctrl+shift+w'], (e) => {
|
||||
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
if (activeTab) {
|
||||
const collection = findCollectionByUid(collections, activeTab.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const tabUids = tabs.filter((tab) => tab.collectionUid === collection.uid).map((tab) => tab.uid);
|
||||
dispatch(
|
||||
closeTabs({
|
||||
tabUids: tabUids
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return false; // this stops the event bubbling
|
||||
});
|
||||
|
||||
return () => {
|
||||
Mousetrap.unbind(['command+shift+w', 'ctrl+shift+w']);
|
||||
};
|
||||
}, [activeTabUid, tabs, collections, dispatch]);
|
||||
|
||||
return (
|
||||
<HotkeysContext.Provider {...props} value="hotkey">
|
||||
{showSaveRequestModal && (
|
||||
|
||||
@@ -33,13 +33,14 @@ import {
|
||||
requestCancelled,
|
||||
resetRunResults,
|
||||
responseReceived,
|
||||
updateLastAction
|
||||
updateLastAction,
|
||||
setCollectionSecurityConfig
|
||||
} from './index';
|
||||
|
||||
import { each } from 'lodash';
|
||||
import { closeAllCollectionTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
import { resolveRequestFilename } from 'utils/common/platform';
|
||||
import { parseQueryParams, splitOnFirst } from 'utils/url/index';
|
||||
import { parsePathParams, parseQueryParams, splitOnFirst } from 'utils/url/index';
|
||||
import { sendCollectionOauth2Request as _sendCollectionOauth2Request } from 'utils/network/index';
|
||||
import { name } from 'file-loader';
|
||||
|
||||
@@ -373,6 +374,7 @@ export const newFolder = (folderName, collectionUid, itemUid) => (dispatch, getS
|
||||
});
|
||||
};
|
||||
|
||||
// rename item
|
||||
export const renameItem = (newName, itemUid, collectionUid) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
@@ -698,7 +700,7 @@ export const moveItemToRootOfCollection = (collectionUid, draggedItemUid) => (di
|
||||
};
|
||||
|
||||
export const newHttpRequest = (params) => (dispatch, getState) => {
|
||||
const { requestName, requestType, requestUrl, requestMethod, collectionUid, itemUid, headers, body } = params;
|
||||
const { requestName, requestType, requestUrl, requestMethod, collectionUid, itemUid, headers, body, auth } = params;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const state = getState();
|
||||
@@ -708,11 +710,20 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
|
||||
}
|
||||
|
||||
const parts = splitOnFirst(requestUrl, '?');
|
||||
const params = parseQueryParams(parts[1]);
|
||||
each(params, (urlParam) => {
|
||||
const queryParams = parseQueryParams(parts[1]);
|
||||
each(queryParams, (urlParam) => {
|
||||
urlParam.enabled = true;
|
||||
urlParam.type = 'query';
|
||||
});
|
||||
|
||||
const pathParams = parsePathParams(requestUrl);
|
||||
each(pathParams, (pathParm) => {
|
||||
pathParams.enabled = true;
|
||||
pathParm.type = 'path';
|
||||
});
|
||||
|
||||
const params = [...queryParams, ...pathParams];
|
||||
|
||||
const item = {
|
||||
uid: uuid(),
|
||||
type: requestType,
|
||||
@@ -730,6 +741,9 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
|
||||
sparql: null,
|
||||
multipartForm: null,
|
||||
formUrlEncoded: null
|
||||
},
|
||||
auth: auth ?? {
|
||||
mode: 'none'
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1039,11 +1053,13 @@ export const openCollectionEvent = (uid, pathname, brunoConfig) => (dispatch, ge
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
collectionSchema
|
||||
.validate(collection)
|
||||
.then(() => dispatch(_createCollection(collection)))
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
ipcRenderer.invoke('renderer:get-collection-security-config', pathname).then((securityConfig) => {
|
||||
collectionSchema
|
||||
.validate(collection)
|
||||
.then(() => dispatch(_createCollection({ ...collection, securityConfig })))
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1108,3 +1124,19 @@ export const importCollection = (collection, collectionLocation) => (dispatch, g
|
||||
ipcRenderer.invoke('renderer:import-collection', collection, collectionLocation).then(resolve).catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
export const saveCollectionSecurityConfig = (collectionUid, securityConfig) => (dispatch, getState) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
const state = getState();
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
|
||||
ipcRenderer
|
||||
.invoke('renderer:save-collection-security-config', collection?.pathname, securityConfig)
|
||||
.then(async () => {
|
||||
await dispatch(setCollectionSecurityConfig({ collectionUid, securityConfig }));
|
||||
resolve();
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -33,7 +33,6 @@ export const collectionsSlice = createSlice({
|
||||
const collection = action.payload;
|
||||
|
||||
collection.settingsSelectedTab = 'headers';
|
||||
|
||||
collection.folderLevelSettingsSelectedTab = {};
|
||||
|
||||
// TODO: move this to use the nextAction approach
|
||||
@@ -51,6 +50,12 @@ export const collectionsSlice = createSlice({
|
||||
state.collections.push(collection);
|
||||
}
|
||||
},
|
||||
setCollectionSecurityConfig: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
if (collection) {
|
||||
collection.securityConfig = action.payload.securityConfig;
|
||||
}
|
||||
},
|
||||
brunoConfigUpdateEvent: (state, action) => {
|
||||
const { collectionUid, brunoConfig } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
@@ -1622,6 +1627,7 @@ export const collectionsSlice = createSlice({
|
||||
|
||||
export const {
|
||||
createCollection,
|
||||
setCollectionSecurityConfig,
|
||||
brunoConfigUpdateEvent,
|
||||
renameCollection,
|
||||
removeCollection,
|
||||
|
||||
@@ -24,7 +24,9 @@ export const tabsSlice = createSlice({
|
||||
return;
|
||||
}
|
||||
|
||||
if (['variables', 'collection-settings', 'collection-runner'].includes(action.payload.type)) {
|
||||
if (
|
||||
['variables', 'collection-settings', 'collection-runner', 'security-settings'].includes(action.payload.type)
|
||||
) {
|
||||
const tab = tabTypeAlreadyExists(state.tabs, action.payload.collectionUid, action.payload.type);
|
||||
if (tab) {
|
||||
state.activeTabUid = tab.uid;
|
||||
|
||||
@@ -50,7 +50,11 @@ const createPostData = (body) => {
|
||||
mimeType: contentType,
|
||||
params: body[body.mode]
|
||||
.filter((param) => param.enabled)
|
||||
.map((param) => ({ name: param.name, value: param.value }))
|
||||
.map((param) => ({
|
||||
name: param.name,
|
||||
value: param.value,
|
||||
...(param.type === 'file' && { fileName: param.value })
|
||||
}))
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
|
||||
@@ -3,7 +3,7 @@ import filter from 'lodash/filter';
|
||||
import find from 'lodash/find';
|
||||
|
||||
export const doesRequestMatchSearchText = (request, searchText = '') => {
|
||||
return request.name.toLowerCase().includes(searchText.toLowerCase());
|
||||
return request?.name?.toLowerCase().includes(searchText.toLowerCase());
|
||||
};
|
||||
|
||||
export const doesFolderHaveItemsMatchSearchText = (item, searchText = '') => {
|
||||
|
||||
@@ -123,7 +123,8 @@ const curlToJson = (curlCommand) => {
|
||||
request.urlWithoutQuery = 'http://' + request.urlWithoutQuery;
|
||||
}
|
||||
|
||||
requestJson.url = request.urlWithoutQuery
|
||||
requestJson.url = request.urlWithoutQuery;
|
||||
requestJson.raw_url = request.url;
|
||||
requestJson.method = request.method;
|
||||
|
||||
if (request.cookies) {
|
||||
@@ -159,14 +160,15 @@ const curlToJson = (curlCommand) => {
|
||||
}
|
||||
|
||||
if (request.auth) {
|
||||
const splitAuth = request.auth.split(':');
|
||||
const user = splitAuth[0] || '';
|
||||
const password = splitAuth[1] || '';
|
||||
|
||||
requestJson.auth = {
|
||||
user: repr(user),
|
||||
password: repr(password)
|
||||
};
|
||||
if(request.auth.mode === 'basic'){
|
||||
requestJson.auth = {
|
||||
mode: 'basic',
|
||||
basic: {
|
||||
username: repr(request.auth.basic?.username),
|
||||
password: repr(request.auth.basic?.password)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(requestJson).length ? requestJson : {};
|
||||
|
||||
@@ -75,4 +75,15 @@ describe('curlToJson', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should return and parse a simple curl command with a trailing slash', () => {
|
||||
const curlCommand = 'curl https://www.usebruno.com/';
|
||||
const result = curlToJson(curlCommand);
|
||||
|
||||
expect(result).toEqual({
|
||||
url: 'https://www.usebruno.com/',
|
||||
raw_url: 'https://www.usebruno.com/',
|
||||
method: 'get'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -56,7 +56,8 @@ export const getRequestFromCurlCommand = (curlCommand) => {
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
body,
|
||||
headers: headers
|
||||
headers: headers,
|
||||
auth: request.auth
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
@@ -36,7 +36,8 @@ const parseCurlCommand = (curlCommand) => {
|
||||
boolean: ['I', 'head', 'compressed', 'L', 'k', 'silent', 's', 'G', 'get'],
|
||||
alias: {
|
||||
H: 'header',
|
||||
A: 'user-agent'
|
||||
A: 'user-agent',
|
||||
u: 'user'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -187,10 +188,21 @@ const parseCurlCommand = (curlCommand) => {
|
||||
}
|
||||
|
||||
urlObject.search = null; // Clean out the search/query portion.
|
||||
|
||||
let urlWithoutQuery = URL.format(urlObject);
|
||||
let urlHost = urlObject?.host;
|
||||
if (!url?.includes(`${urlHost}/`)) {
|
||||
if (urlWithoutQuery && urlHost) {
|
||||
const [beforeHost, afterHost] = urlWithoutQuery.split(urlHost);
|
||||
urlWithoutQuery = beforeHost + urlHost + afterHost?.slice(1);
|
||||
}
|
||||
}
|
||||
|
||||
const request = {
|
||||
url: url,
|
||||
urlWithoutQuery: URL.format(urlObject)
|
||||
url,
|
||||
urlWithoutQuery
|
||||
};
|
||||
|
||||
if (compressed) {
|
||||
request.compressed = true;
|
||||
}
|
||||
@@ -226,12 +238,19 @@ const parseCurlCommand = (curlCommand) => {
|
||||
request.data = parsedArguments['data-urlencode'];
|
||||
}
|
||||
|
||||
if (parsedArguments.u) {
|
||||
request.auth = parsedArguments.u;
|
||||
}
|
||||
if (parsedArguments.user) {
|
||||
request.auth = parsedArguments.user;
|
||||
if (parsedArguments.user && typeof parsedArguments.user === 'string') {
|
||||
const basicAuth = parsedArguments.user.split(':')
|
||||
const username = basicAuth[0] || ''
|
||||
const password = basicAuth[1] || ''
|
||||
request.auth = {
|
||||
mode: 'basic',
|
||||
basic: {
|
||||
username,
|
||||
password
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(request.data)) {
|
||||
request.dataArray = request.data;
|
||||
request.data = request.data.join('&');
|
||||
|
||||
@@ -12,6 +12,7 @@ export const exportCollection = (collection) => {
|
||||
const generateInfoSection = () => {
|
||||
return {
|
||||
name: collection.name,
|
||||
description: collection.root?.docs,
|
||||
schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'
|
||||
};
|
||||
};
|
||||
@@ -137,6 +138,11 @@ export const exportCollection = (collection) => {
|
||||
}
|
||||
}
|
||||
};
|
||||
case 'graphql':
|
||||
return {
|
||||
mode: 'graphql',
|
||||
graphql: body.graphql
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -201,6 +207,8 @@ export const exportCollection = (collection) => {
|
||||
const requestObject = {
|
||||
method: itemRequest.method,
|
||||
header: generateHeaders(itemRequest.headers),
|
||||
auth: generateAuth(itemRequest.auth),
|
||||
description: itemRequest.docs,
|
||||
url: {
|
||||
raw: itemRequest.url,
|
||||
host: generateHost(itemRequest.url),
|
||||
|
||||
@@ -59,12 +59,15 @@ const transformOpenapiRequestItem = (request) => {
|
||||
operationName = `${request.method} ${request.path}`;
|
||||
}
|
||||
|
||||
// replace OpenAPI links in path by Bruno variables
|
||||
let path = request.path.replace(/{([a-zA-Z]+)}/g, `{{${_operationObject.operationId}_$1}}`);
|
||||
|
||||
const brunoRequestItem = {
|
||||
uid: uuid(),
|
||||
name: operationName,
|
||||
type: 'http-request',
|
||||
request: {
|
||||
url: ensureUrl(request.global.server + '/' + request.path),
|
||||
url: ensureUrl(request.global.server + '/' + path),
|
||||
method: request.method.toUpperCase(),
|
||||
auth: {
|
||||
mode: 'none',
|
||||
@@ -81,6 +84,9 @@ const transformOpenapiRequestItem = (request) => {
|
||||
xml: null,
|
||||
formUrlEncoded: [],
|
||||
multipartForm: []
|
||||
},
|
||||
script: {
|
||||
res: null
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -195,6 +201,26 @@ const transformOpenapiRequestItem = (request) => {
|
||||
}
|
||||
}
|
||||
|
||||
// build the extraction scripts from responses that have links
|
||||
// https://swagger.io/docs/specification/links/
|
||||
let script = [];
|
||||
each(_operationObject.responses || [], (response, responseStatus) => {
|
||||
if (Object.hasOwn(response, 'links')) {
|
||||
// only extract if the status code matches the response
|
||||
script.push(`if (res.status === ${responseStatus}) {`);
|
||||
each(response.links, (link) => {
|
||||
each(link.parameters || [], (expression, parameter) => {
|
||||
let value = openAPIRuntimeExpressionToScript(expression);
|
||||
script.push(` bru.setVar('${link.operationId}_${parameter}', ${value});`);
|
||||
});
|
||||
});
|
||||
script.push(`}`);
|
||||
}
|
||||
});
|
||||
if (script.length > 0) {
|
||||
brunoRequestItem.request.script.res = script.join('\n');
|
||||
}
|
||||
|
||||
return brunoRequestItem;
|
||||
};
|
||||
|
||||
@@ -305,6 +331,18 @@ const getSecurity = (apiSpec) => {
|
||||
};
|
||||
};
|
||||
|
||||
const openAPIRuntimeExpressionToScript = (expression) => {
|
||||
// see https://swagger.io/docs/specification/links/#runtime-expressions
|
||||
if (expression === '$response.body') {
|
||||
return 'res.body';
|
||||
} else if (expression.startsWith('$response.body#')) {
|
||||
let pointer = expression.substring(15);
|
||||
// could use https://www.npmjs.com/package/json-pointer for better support
|
||||
return `res.body${pointer.replace('/', '.')}`;
|
||||
}
|
||||
return expression;
|
||||
};
|
||||
|
||||
const parseOpenApiCollection = (data) => {
|
||||
const brunoCollection = {
|
||||
name: '',
|
||||
@@ -348,7 +386,7 @@ const parseOpenApiCollection = (data) => {
|
||||
.map(([method, operationObject]) => {
|
||||
return {
|
||||
method: method,
|
||||
path: path,
|
||||
path: path.replace(/{([^}]+)}/g, ':$1'), // Replace placeholders enclosed in curly braces with colons
|
||||
operationObject: operationObject,
|
||||
global: {
|
||||
server: baseUrl,
|
||||
|
||||
@@ -113,7 +113,8 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) =
|
||||
xml: null,
|
||||
formUrlEncoded: [],
|
||||
multipartForm: []
|
||||
}
|
||||
},
|
||||
docs: i.request.description
|
||||
}
|
||||
};
|
||||
/* struct of translation log
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"url": "git+https://github.com/usebruno/bruno.git"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "jest"
|
||||
"test": "node --experimental-vm-modules $(npx which jest)"
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
|
||||
@@ -190,6 +190,10 @@ const getFolderRoot = (dir) => {
|
||||
return collectionBruToJson(content);
|
||||
};
|
||||
|
||||
const getJsSandboxRuntime = (sandbox) => {
|
||||
return sandbox === 'safe' ? 'quickjs' : 'vm2';
|
||||
};
|
||||
|
||||
const builder = async (yargs) => {
|
||||
yargs
|
||||
.option('r', {
|
||||
@@ -215,6 +219,11 @@ const builder = async (yargs) => {
|
||||
describe: 'Overwrite a single environment variable, multiple usages possible',
|
||||
type: 'string'
|
||||
})
|
||||
.option('sandbox', {
|
||||
describe: 'Javscript sandbox to use; available sandboxes are "developer" (default) or "safe"',
|
||||
default: 'developer',
|
||||
type: 'string'
|
||||
})
|
||||
.option('output', {
|
||||
alias: 'o',
|
||||
describe: 'Path to write file results to',
|
||||
@@ -232,7 +241,7 @@ const builder = async (yargs) => {
|
||||
})
|
||||
.option('tests-only', {
|
||||
type: 'boolean',
|
||||
description: 'Only run requests that have a test'
|
||||
description: 'Only run requests that have a test or active assertion'
|
||||
})
|
||||
.option('bail', {
|
||||
type: 'boolean',
|
||||
@@ -282,6 +291,7 @@ const handler = async function (argv) {
|
||||
r: recursive,
|
||||
output: outputPath,
|
||||
format,
|
||||
sandbox,
|
||||
testsOnly,
|
||||
bail
|
||||
} = argv;
|
||||
@@ -451,6 +461,7 @@ const handler = async function (argv) {
|
||||
}
|
||||
}
|
||||
|
||||
const runtime = getJsSandboxRuntime(sandbox);
|
||||
let currentRequestIndex = 0;
|
||||
let nJumps = 0; // count the number of jumps to avoid infinite loops
|
||||
while (currentRequestIndex < bruJsons.length) {
|
||||
@@ -466,7 +477,8 @@ const handler = async function (argv) {
|
||||
envVars,
|
||||
processEnvVars,
|
||||
brunoConfig,
|
||||
collectionRoot
|
||||
collectionRoot,
|
||||
runtime
|
||||
);
|
||||
|
||||
results.push({
|
||||
|
||||
@@ -21,6 +21,10 @@ const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../utils/proxy-util'
|
||||
const path = require('path');
|
||||
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
|
||||
|
||||
const onConsoleLog = (type, args) => {
|
||||
console[type](...args);
|
||||
};
|
||||
|
||||
const runSingleRequest = async function (
|
||||
filename,
|
||||
bruJson,
|
||||
@@ -29,7 +33,8 @@ const runSingleRequest = async function (
|
||||
envVariables,
|
||||
processEnvVars,
|
||||
brunoConfig,
|
||||
collectionRoot
|
||||
collectionRoot,
|
||||
runtime
|
||||
) {
|
||||
try {
|
||||
let request;
|
||||
@@ -38,6 +43,7 @@ const runSingleRequest = async function (
|
||||
request = prepareRequest(bruJson.request, collectionRoot);
|
||||
|
||||
const scriptingConfig = get(brunoConfig, 'scripts', {});
|
||||
scriptingConfig.runtime = runtime;
|
||||
|
||||
// make axios work in node using form data
|
||||
// reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
|
||||
@@ -57,7 +63,7 @@ const runSingleRequest = async function (
|
||||
// run pre-request vars
|
||||
const preRequestVars = get(bruJson, 'request.vars.req');
|
||||
if (preRequestVars?.length) {
|
||||
const varsRuntime = new VarsRuntime();
|
||||
const varsRuntime = new VarsRuntime({ runtime: scriptingConfig?.runtime });
|
||||
varsRuntime.runPreRequestVars(
|
||||
preRequestVars,
|
||||
request,
|
||||
@@ -74,14 +80,14 @@ const runSingleRequest = async function (
|
||||
get(bruJson, 'request.script.req')
|
||||
]).join(os.EOL);
|
||||
if (requestScriptFile?.length) {
|
||||
const scriptRuntime = new ScriptRuntime();
|
||||
const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });
|
||||
const result = await scriptRuntime.runRequestScript(
|
||||
decomment(requestScriptFile),
|
||||
request,
|
||||
envVariables,
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
null,
|
||||
onConsoleLog,
|
||||
processEnvVars,
|
||||
scriptingConfig
|
||||
);
|
||||
@@ -276,7 +282,7 @@ const runSingleRequest = async function (
|
||||
// run post-response vars
|
||||
const postResponseVars = get(bruJson, 'request.vars.res');
|
||||
if (postResponseVars?.length) {
|
||||
const varsRuntime = new VarsRuntime();
|
||||
const varsRuntime = new VarsRuntime({ runtime: scriptingConfig?.runtime });
|
||||
varsRuntime.runPostResponseVars(
|
||||
postResponseVars,
|
||||
request,
|
||||
@@ -294,7 +300,7 @@ const runSingleRequest = async function (
|
||||
get(bruJson, 'request.script.res')
|
||||
]).join(os.EOL);
|
||||
if (responseScriptFile?.length) {
|
||||
const scriptRuntime = new ScriptRuntime();
|
||||
const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });
|
||||
const result = await scriptRuntime.runResponseScript(
|
||||
decomment(responseScriptFile),
|
||||
request,
|
||||
@@ -315,7 +321,7 @@ const runSingleRequest = async function (
|
||||
let assertionResults = [];
|
||||
const assertions = get(bruJson, 'request.assertions');
|
||||
if (assertions) {
|
||||
const assertRuntime = new AssertRuntime();
|
||||
const assertRuntime = new AssertRuntime({ runtime: scriptingConfig?.runtime });
|
||||
assertionResults = assertRuntime.runAssertions(
|
||||
assertions,
|
||||
request,
|
||||
@@ -339,7 +345,7 @@ const runSingleRequest = async function (
|
||||
let testResults = [];
|
||||
const testFile = compact([get(collectionRoot, 'request.tests'), get(bruJson, 'request.tests')]).join(os.EOL);
|
||||
if (typeof testFile === 'string') {
|
||||
const testRuntime = new TestRuntime();
|
||||
const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime });
|
||||
const result = await testRuntime.runTests(
|
||||
decomment(testFile),
|
||||
request,
|
||||
|
||||
@@ -22,13 +22,13 @@
|
||||
"@rollup/plugin-commonjs": "^23.0.2",
|
||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||
"@rollup/plugin-typescript": "^9.0.2",
|
||||
"rollup": "3.2.5",
|
||||
"rollup":"3.29.4",
|
||||
"rollup-plugin-dts": "^5.0.0",
|
||||
"rollup-plugin-peer-deps-external": "^2.2.4",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"typescript": "^4.8.4"
|
||||
},
|
||||
"overrides": {
|
||||
"rollup": "3.2.5"
|
||||
"rollup":"3.29.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,7 +294,7 @@ describe('interpolate - recursive', () => {
|
||||
|
||||
const result = interpolate(inputString, inputObject);
|
||||
|
||||
expect(result).toBe('{{recursion2}}');
|
||||
expect(result).toBe('{{recursion3}}');
|
||||
});
|
||||
|
||||
it('should replace repetead placeholders with 1 level of recursion with values from the object', () => {
|
||||
@@ -335,4 +335,22 @@ describe('interpolate - recursive', () => {
|
||||
|
||||
expect(result).toBe(new Array(24).fill('repetead4').join(' '));
|
||||
});
|
||||
|
||||
it('should replace mutiple interdependent variables in the same input string', () => {
|
||||
const inputString = `{
|
||||
"x": "{{v2}} {{v1}}"
|
||||
}`;
|
||||
const inputObject = {
|
||||
foo: 'bar',
|
||||
v1: '{{foo}}',
|
||||
v2: '{{bar}}',
|
||||
bar: 'baz'
|
||||
};
|
||||
|
||||
const result = interpolate(inputString, inputObject);
|
||||
|
||||
expect(result).toBe(`{
|
||||
"x": "baz bar"
|
||||
}`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,32 +27,41 @@ const interpolate = (str: string, obj: Record<string, any>): string => {
|
||||
const replace = (
|
||||
str: string,
|
||||
flattenedObj: Record<string, any>,
|
||||
visited = new Set<String>(),
|
||||
visited = new Set<string>(),
|
||||
results = new Map<string, string>()
|
||||
): string => {
|
||||
const patternRegex = /\{\{([^}]+)\}\}/g;
|
||||
let resultStr = str;
|
||||
let matchFound = true;
|
||||
|
||||
return str.replace(patternRegex, (match, placeholder) => {
|
||||
const replacement = flattenedObj[placeholder];
|
||||
while (matchFound) {
|
||||
const patternRegex = /\{\{([^}]+)\}\}/g;
|
||||
matchFound = false;
|
||||
resultStr = resultStr.replace(patternRegex, (match, placeholder) => {
|
||||
const replacement = flattenedObj[placeholder];
|
||||
|
||||
if (results.has(match)) {
|
||||
return results.get(match);
|
||||
}
|
||||
if (results.has(match)) {
|
||||
return results.get(match);
|
||||
}
|
||||
|
||||
if (patternRegex.test(replacement) && !visited.has(match)) {
|
||||
visited.add(match);
|
||||
const result = replace(replacement, flattenedObj, visited, results);
|
||||
results.set(match, result);
|
||||
|
||||
matchFound = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (patternRegex.test(replacement) && !visited.has(match)) {
|
||||
visited.add(match);
|
||||
const result = replace(replacement, flattenedObj, visited, results);
|
||||
const result = replacement !== undefined ? replacement : match;
|
||||
results.set(match, result);
|
||||
|
||||
matchFound = true;
|
||||
return result;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
visited.add(match);
|
||||
const result = replacement !== undefined ? replacement : match;
|
||||
results.set(match, result);
|
||||
|
||||
return result;
|
||||
});
|
||||
return resultStr;
|
||||
};
|
||||
|
||||
export default interpolate;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "v1.25.0",
|
||||
"version": "v1.26.2",
|
||||
"name": "bruno",
|
||||
"description": "Opensource API Client for Exploring and Testing APIs",
|
||||
"homepage": "https://www.usebruno.com",
|
||||
@@ -16,7 +16,7 @@
|
||||
"dist:rpm": "electron-builder --linux rpm --config electron-builder-config.js",
|
||||
"dist:snap": "electron-builder --linux snap --config electron-builder-config.js",
|
||||
"pack": "electron-builder --dir",
|
||||
"test": "jest"
|
||||
"test": "node --experimental-vm-modules $(npx which jest)"
|
||||
},
|
||||
"jest": {
|
||||
"modulePaths": ["node_modules"]
|
||||
@@ -65,7 +65,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "31.2.1",
|
||||
"electron-builder": "23.0.2",
|
||||
"electron-icon-maker": "^0.0.5"
|
||||
"electron-builder": "23.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,9 +464,10 @@ class Watcher {
|
||||
.on('unlink', (pathname) => unlink(win, pathname, collectionUid, watchPath))
|
||||
.on('unlinkDir', (pathname) => unlinkDir(win, pathname, collectionUid, watchPath))
|
||||
.on('error', (error) => {
|
||||
// `EMFILE` is an error code thrown when to many files are watched at the same time see: https://github.com/usebruno/bruno/issues/627
|
||||
// `ENOSPC` stands for "Error No space" but is also thrown if the file watcher limit is reached.
|
||||
// To prevent loops `!forcePolling` is checked.
|
||||
if (error.code === 'ENOSPC' && !startedNewWatcher && !forcePolling) {
|
||||
if ((error.code === 'ENOSPC' || error.code === 'EMFILE') && !startedNewWatcher && !forcePolling) {
|
||||
// This callback is called for every file the watcher is trying to watch. To prevent a spam of messages and
|
||||
// Multiple watcher being started `startedNewWatcher` is set to prevent this.
|
||||
startedNewWatcher = true;
|
||||
|
||||
@@ -13,15 +13,19 @@ const {
|
||||
browseFiles,
|
||||
createDirectory,
|
||||
searchForBruFiles,
|
||||
sanitizeDirectoryName
|
||||
sanitizeDirectoryName,
|
||||
isWSLPath,
|
||||
normalizeWslPath,
|
||||
} = require('../utils/filesystem');
|
||||
const { openCollectionDialog } = require('../app/collections');
|
||||
const { generateUidBasedOnHash, stringifyJson, safeParseJSON, safeStringifyJSON } = require('../utils/common');
|
||||
const { moveRequestUid, deleteRequestUid } = require('../cache/requestUids');
|
||||
const { deleteCookiesForDomain, getDomainsWithCookies } = require('../utils/cookies');
|
||||
const EnvironmentSecretsStore = require('../store/env-secrets');
|
||||
const CollectionSecurityStore = require('../store/collection-security');
|
||||
|
||||
const environmentSecretsStore = new EnvironmentSecretsStore();
|
||||
const collectionSecurityStore = new CollectionSecurityStore();
|
||||
|
||||
const envHasSecrets = (environment = {}) => {
|
||||
const secrets = _.filter(environment.variables, (v) => v.secret);
|
||||
@@ -324,6 +328,14 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
// rename item
|
||||
ipcMain.handle('renderer:rename-item', async (event, oldPath, newPath, newName) => {
|
||||
try {
|
||||
// Normalize paths if they are WSL paths
|
||||
if (isWSLPath(oldPath)) {
|
||||
oldPath = normalizeWslPath(oldPath);
|
||||
}
|
||||
if (isWSLPath(newPath)) {
|
||||
newPath = normalizeWslPath(newPath);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(oldPath)) {
|
||||
throw new Error(`path: ${oldPath} does not exist`);
|
||||
}
|
||||
@@ -665,6 +677,24 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
return Promise.reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('renderer:save-collection-security-config', async (event, collectionPath, securityConfig) => {
|
||||
try {
|
||||
collectionSecurityStore.setSecurityConfigForCollection(collectionPath, {
|
||||
jsSandboxMode: securityConfig.jsSandboxMode
|
||||
});
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('renderer:get-collection-security-config', async (event, collectionPath) => {
|
||||
try {
|
||||
return collectionSecurityStore.getSecurityConfigForCollection(collectionPath);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const registerMainEventHandlers = (mainWindow, watcher, lastOpenedCollections) => {
|
||||
|
||||
@@ -29,7 +29,7 @@ const { makeAxiosInstance } = require('./axios-instance');
|
||||
const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-helper');
|
||||
const { addDigestInterceptor } = require('./digestauth-helper');
|
||||
const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../../utils/proxy-util');
|
||||
const { chooseFileToSave, writeBinaryFile } = require('../../utils/filesystem');
|
||||
const { chooseFileToSave, writeBinaryFile, writeFile } = require('../../utils/filesystem');
|
||||
const { getCookieStringForUrl, addCookieToJar, getDomainsWithCookies } = require('../../utils/cookies');
|
||||
const {
|
||||
resolveOAuth2AuthorizationCodeAccessToken,
|
||||
@@ -81,6 +81,11 @@ const getEnvVars = (environment = {}) => {
|
||||
};
|
||||
};
|
||||
|
||||
const getJsSandboxRuntime = (collection) => {
|
||||
const securityConfig = get(collection, 'securityConfig', {});
|
||||
return securityConfig.jsSandboxMode === 'safe' ? 'quickjs' : 'vm2';
|
||||
};
|
||||
|
||||
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
|
||||
|
||||
const configureRequest = async (
|
||||
@@ -315,7 +320,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
// run pre-request vars
|
||||
const preRequestVars = get(request, 'vars.req', []);
|
||||
if (preRequestVars?.length) {
|
||||
const varsRuntime = new VarsRuntime();
|
||||
const varsRuntime = new VarsRuntime({ runtime: scriptingConfig?.runtime });
|
||||
varsRuntime.runPreRequestVars(
|
||||
preRequestVars,
|
||||
request,
|
||||
@@ -330,7 +335,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
let scriptResult;
|
||||
const requestScript = compact([get(collectionRoot, 'request.script.req'), get(request, 'script.req')]).join(os.EOL);
|
||||
if (requestScript?.length) {
|
||||
const scriptRuntime = new ScriptRuntime();
|
||||
const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });
|
||||
scriptResult = await scriptRuntime.runRequestScript(
|
||||
decomment(requestScript),
|
||||
request,
|
||||
@@ -382,7 +387,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
// run post-response vars
|
||||
const postResponseVars = get(request, 'vars.res', []);
|
||||
if (postResponseVars?.length) {
|
||||
const varsRuntime = new VarsRuntime();
|
||||
const varsRuntime = new VarsRuntime({ runtime: scriptingConfig?.runtime });
|
||||
const result = varsRuntime.runPostResponseVars(
|
||||
postResponseVars,
|
||||
request,
|
||||
@@ -416,7 +421,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
let scriptResult;
|
||||
if (responseScript?.length) {
|
||||
const scriptRuntime = new ScriptRuntime();
|
||||
const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });
|
||||
scriptResult = await scriptRuntime.runResponseScript(
|
||||
decomment(responseScript),
|
||||
request,
|
||||
@@ -460,6 +465,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
const processEnvVars = getProcessEnvVars(collectionUid);
|
||||
const brunoConfig = getBrunoConfig(collectionUid);
|
||||
const scriptingConfig = get(brunoConfig, 'scripts', {});
|
||||
scriptingConfig.runtime = getJsSandboxRuntime(collection);
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
@@ -575,7 +581,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
// run assertions
|
||||
const assertions = get(request, 'assertions');
|
||||
if (assertions) {
|
||||
const assertRuntime = new AssertRuntime();
|
||||
const assertRuntime = new AssertRuntime({ runtime: scriptingConfig?.runtime });
|
||||
const results = assertRuntime.runAssertions(
|
||||
assertions,
|
||||
request,
|
||||
@@ -603,7 +609,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
]).join(os.EOL);
|
||||
|
||||
if (typeof testFile === 'string') {
|
||||
const testRuntime = new TestRuntime();
|
||||
const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime });
|
||||
const testResults = await testRuntime.runTests(
|
||||
decomment(testFile),
|
||||
request,
|
||||
@@ -661,6 +667,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
const processEnvVars = getProcessEnvVars(collectionUid);
|
||||
const brunoConfig = getBrunoConfig(collectionUid);
|
||||
const scriptingConfig = get(brunoConfig, 'scripts', {});
|
||||
scriptingConfig.runtime = getJsSandboxRuntime(collection);
|
||||
|
||||
await runPreRequest(
|
||||
request,
|
||||
@@ -766,6 +773,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
const processEnvVars = getProcessEnvVars(collectionUid);
|
||||
const brunoConfig = getBrunoConfig(collection.uid);
|
||||
const scriptingConfig = get(brunoConfig, 'scripts', {});
|
||||
scriptingConfig.runtime = getJsSandboxRuntime(collection);
|
||||
|
||||
await runPreRequest(
|
||||
request,
|
||||
@@ -832,6 +840,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
const cancelTokenUid = uuid();
|
||||
const brunoConfig = getBrunoConfig(collectionUid);
|
||||
const scriptingConfig = get(brunoConfig, 'scripts', {});
|
||||
scriptingConfig.runtime = getJsSandboxRuntime(collection);
|
||||
const collectionRoot = get(collection, 'root', {});
|
||||
|
||||
const abortController = new AbortController();
|
||||
@@ -1028,7 +1037,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
// run assertions
|
||||
const assertions = get(item, 'request.assertions');
|
||||
if (assertions) {
|
||||
const assertRuntime = new AssertRuntime();
|
||||
const assertRuntime = new AssertRuntime({ runtime: scriptingConfig?.runtime });
|
||||
const results = assertRuntime.runAssertions(
|
||||
assertions,
|
||||
request,
|
||||
@@ -1055,7 +1064,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
]).join(os.EOL);
|
||||
|
||||
if (typeof testFile === 'string') {
|
||||
const testRuntime = new TestRuntime();
|
||||
const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime });
|
||||
const testResults = await testRuntime.runTests(
|
||||
decomment(testFile),
|
||||
request,
|
||||
@@ -1180,7 +1189,13 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
const fileName = determineFileName();
|
||||
const filePath = await chooseFileToSave(mainWindow, fileName);
|
||||
if (filePath) {
|
||||
await writeBinaryFile(filePath, Buffer.from(response.dataBuffer, getEncodingFormat()));
|
||||
const encoding = getEncodingFormat();
|
||||
const data = Buffer.from(response.dataBuffer, 'base64')
|
||||
if (encoding === 'utf-8') {
|
||||
await writeFile(filePath, data);
|
||||
} else {
|
||||
await writeBinaryFile(filePath, data);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
|
||||
@@ -32,9 +32,6 @@ const resolveOAuth2AuthorizationCodeAccessToken = async (request, collectionUid)
|
||||
client_secret: clientSecret,
|
||||
state: state
|
||||
};
|
||||
if (scope) {
|
||||
data['scope'] = scope;
|
||||
}
|
||||
if (pkce) {
|
||||
data['code_verifier'] = codeVerifier;
|
||||
}
|
||||
|
||||
39
packages/bruno-electron/src/store/collection-security.js
Normal file
39
packages/bruno-electron/src/store/collection-security.js
Normal file
@@ -0,0 +1,39 @@
|
||||
const _ = require('lodash');
|
||||
const Store = require('electron-store');
|
||||
|
||||
class CollectionSecurityStore {
|
||||
constructor() {
|
||||
this.store = new Store({
|
||||
name: 'collection-security',
|
||||
clearInvalidConfig: true
|
||||
});
|
||||
}
|
||||
|
||||
setSecurityConfigForCollection(collectionPathname, securityConfig) {
|
||||
const collections = this.store.get('collections') || [];
|
||||
const collection = _.find(collections, (c) => c.path === collectionPathname);
|
||||
|
||||
if (!collection) {
|
||||
collections.push({
|
||||
path: collectionPathname,
|
||||
securityConfig: {
|
||||
jsSandboxMode: securityConfig.jsSandboxMode
|
||||
}
|
||||
});
|
||||
|
||||
this.store.set('collections', collections);
|
||||
return;
|
||||
}
|
||||
|
||||
collection.securityConfig = securityConfig || {};
|
||||
this.store.set('collections', collections);
|
||||
}
|
||||
|
||||
getSecurityConfigForCollection(collectionPathname) {
|
||||
const collections = this.store.get('collections') || [];
|
||||
const collection = _.find(collections, (c) => c.path === collectionPathname);
|
||||
return collection?.securityConfig || {};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CollectionSecurityStore;
|
||||
@@ -86,7 +86,11 @@ function decryptString(str) {
|
||||
}
|
||||
|
||||
if (algo === ELECTRONSAFESTORAGE_ALGO) {
|
||||
return safeStorageDecrypt(encryptedString);
|
||||
if (safeStorage && safeStorage.isEncryptionAvailable()) {
|
||||
return safeStorageDecrypt(encryptedString);
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
if (algo === AES256_ALGO) {
|
||||
|
||||
@@ -50,6 +50,18 @@ const normalizeAndResolvePath = (pathname) => {
|
||||
return path.resolve(pathname);
|
||||
};
|
||||
|
||||
function isWSLPath(pathname) {
|
||||
// Check if the path starts with the WSL prefix
|
||||
// eg. "\\wsl.localhost\Ubuntu\home\user\bruno\collection\scripting\api\req\getHeaders.bru"
|
||||
return pathname.startsWith('/wsl.localhost/') || pathname.startsWith('\\wsl.localhost\\');
|
||||
}
|
||||
|
||||
function normalizeWslPath(pathname) {
|
||||
// Replace the WSL path prefix and convert forward slashes to backslashes
|
||||
// This is done to achieve WSL paths (linux style) to Windows UNC equivalent (Universal Naming Conversion)
|
||||
return pathname.replace(/^\/wsl.localhost/, '\\\\wsl.localhost').replace(/\//g, '\\');
|
||||
}
|
||||
|
||||
const writeFile = async (pathname, content) => {
|
||||
try {
|
||||
fs.writeFileSync(pathname, content, {
|
||||
@@ -143,6 +155,8 @@ const searchForBruFiles = (dir) => {
|
||||
return searchForFiles(dir, '.bru');
|
||||
};
|
||||
|
||||
// const isW
|
||||
|
||||
const sanitizeDirectoryName = (name) => {
|
||||
return name.replace(/[<>:"/\\|?*\x00-\x1F]+/g, '-');
|
||||
};
|
||||
@@ -154,6 +168,8 @@ module.exports = {
|
||||
isFile,
|
||||
isDirectory,
|
||||
normalizeAndResolvePath,
|
||||
isWSLPath,
|
||||
normalizeWslPath,
|
||||
writeFile,
|
||||
writeBinaryFile,
|
||||
hasJsonExtension,
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"postcss": "^8.4.18",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"rollup": "3.2.5",
|
||||
"rollup":"3.29.4",
|
||||
"rollup-plugin-dts": "^5.0.0",
|
||||
"rollup-plugin-peer-deps-external": "^2.2.4",
|
||||
"rollup-plugin-postcss": "^4.0.2",
|
||||
@@ -34,6 +34,6 @@
|
||||
"markdown-it": "^13.0.1"
|
||||
},
|
||||
"overrides": {
|
||||
"rollup": "3.2.5"
|
||||
"rollup":"3.29.4"
|
||||
}
|
||||
}
|
||||
|
||||
1
packages/bruno-js/.gitignore
vendored
Normal file
1
packages/bruno-js/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
src/sandbox/bundle-browser-rollup.js
|
||||
@@ -11,7 +11,8 @@
|
||||
"@n8n/vm2": "^3.9.23"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "jest --testPathIgnorePatterns test.js"
|
||||
"test": "node --experimental-vm-modules $(npx which jest) --testPathIgnorePatterns test.js",
|
||||
"sandbox:bundle-libraries": "node ./src/sandbox/bundle-libraries.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@usebruno/common": "0.1.0",
|
||||
@@ -24,12 +25,29 @@
|
||||
"chai": "^4.3.7",
|
||||
"chai-string": "^1.5.0",
|
||||
"crypto-js": "^4.1.1",
|
||||
"crypto-js-3.1.9-1": "npm:crypto-js@^3.1.9-1",
|
||||
"json-query": "^2.2.2",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"nanoid": "3.3.4",
|
||||
"node-fetch": "2.*",
|
||||
"node-fetch": "^2.7.0",
|
||||
"node-vault": "^0.10.2",
|
||||
"path": "^0.12.7",
|
||||
"quickjs-emscripten": "^0.29.2",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^23.0.2",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||
"rollup": "3.2.5",
|
||||
"rollup-plugin-node-builtins": "^2.1.2",
|
||||
"rollup-plugin-node-globals": "^1.4.0",
|
||||
"rollup-plugin-polyfill-node": "^0.13.0",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"stream": "^0.0.2",
|
||||
"terser": "^5.31.1",
|
||||
"uglify-js": "^3.18.0",
|
||||
"util": "^0.12.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +100,10 @@ class Bru {
|
||||
setNextRequest(nextRequest) {
|
||||
this.nextRequest = nextRequest;
|
||||
}
|
||||
|
||||
sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Bru;
|
||||
|
||||
@@ -5,6 +5,7 @@ const Bru = require('../bru');
|
||||
const BrunoRequest = require('../bruno-request');
|
||||
const { evaluateJsTemplateLiteral, evaluateJsExpression, createResponseParser } = require('../utils');
|
||||
const { interpolateString } = require('../interpolate-string');
|
||||
const { executeQuickJsVm } = require('../sandbox/quickjs');
|
||||
|
||||
const { expect } = chai;
|
||||
chai.use(require('chai-string'));
|
||||
@@ -161,7 +162,31 @@ const isUnaryOperator = (operator) => {
|
||||
return unaryOperators.includes(operator);
|
||||
};
|
||||
|
||||
const evaluateRhsOperand = (rhsOperand, operator, context) => {
|
||||
const evaluateJsTemplateLiteralBasedOnRuntime = (literal, context, runtime) => {
|
||||
if (runtime === 'quickjs') {
|
||||
return executeQuickJsVm({
|
||||
script: literal,
|
||||
context,
|
||||
scriptType: 'template-literal'
|
||||
});
|
||||
}
|
||||
|
||||
return evaluateJsTemplateLiteral(literal, context);
|
||||
};
|
||||
|
||||
const evaluateJsExpressionBasedOnRuntime = (expr, context, runtime) => {
|
||||
if (runtime === 'quickjs') {
|
||||
return executeQuickJsVm({
|
||||
script: expr,
|
||||
context,
|
||||
scriptType: 'expression'
|
||||
});
|
||||
}
|
||||
|
||||
return evaluateJsExpression(expr, context);
|
||||
};
|
||||
|
||||
const evaluateRhsOperand = (rhsOperand, operator, context, runtime) => {
|
||||
if (isUnaryOperator(operator)) {
|
||||
return;
|
||||
}
|
||||
@@ -181,13 +206,17 @@ const evaluateRhsOperand = (rhsOperand, operator, context) => {
|
||||
|
||||
return rhsOperand
|
||||
.split(',')
|
||||
.map((v) => evaluateJsTemplateLiteral(interpolateString(v.trim(), interpolationContext), context));
|
||||
.map((v) =>
|
||||
evaluateJsTemplateLiteralBasedOnRuntime(interpolateString(v.trim(), interpolationContext), context, runtime)
|
||||
);
|
||||
}
|
||||
|
||||
if (operator === 'between') {
|
||||
const [lhs, rhs] = rhsOperand
|
||||
.split(',')
|
||||
.map((v) => evaluateJsTemplateLiteral(interpolateString(v.trim(), interpolationContext), context));
|
||||
.map((v) =>
|
||||
evaluateJsTemplateLiteralBasedOnRuntime(interpolateString(v.trim(), interpolationContext), context, runtime)
|
||||
);
|
||||
return [lhs, rhs];
|
||||
}
|
||||
|
||||
@@ -200,10 +229,14 @@ const evaluateRhsOperand = (rhsOperand, operator, context) => {
|
||||
return interpolateString(rhsOperand, interpolationContext);
|
||||
}
|
||||
|
||||
return evaluateJsTemplateLiteral(interpolateString(rhsOperand, interpolationContext), context);
|
||||
return evaluateJsTemplateLiteralBasedOnRuntime(interpolateString(rhsOperand, interpolationContext), context, runtime);
|
||||
};
|
||||
|
||||
class AssertRuntime {
|
||||
constructor(props) {
|
||||
this.runtime = props?.runtime || 'vm2';
|
||||
}
|
||||
|
||||
runAssertions(assertions, request, response, envVariables, runtimeVariables, processEnvVars) {
|
||||
const requestVariables = request?.requestVariables || {};
|
||||
const enabledAssertions = _.filter(assertions, (a) => a.enabled);
|
||||
@@ -238,8 +271,8 @@ class AssertRuntime {
|
||||
const { operator, value: rhsOperand } = parseAssertionOperator(rhsExpr);
|
||||
|
||||
try {
|
||||
const lhs = evaluateJsExpression(lhsExpr, context);
|
||||
const rhs = evaluateRhsOperand(rhsOperand, operator, context);
|
||||
const lhs = evaluateJsExpressionBasedOnRuntime(lhsExpr, context, this.runtime);
|
||||
const rhs = evaluateRhsOperand(rhsOperand, operator, context, this.runtime);
|
||||
|
||||
switch (operator) {
|
||||
case 'eq':
|
||||
|
||||
@@ -28,9 +28,12 @@ const fetch = require('node-fetch');
|
||||
const chai = require('chai');
|
||||
const CryptoJS = require('crypto-js');
|
||||
const NodeVault = require('node-vault');
|
||||
const { executeQuickJsVmAsync } = require('../sandbox/quickjs');
|
||||
|
||||
class ScriptRuntime {
|
||||
constructor() {}
|
||||
constructor(props) {
|
||||
this.runtime = props?.runtime || 'vm2';
|
||||
}
|
||||
|
||||
// This approach is getting out of hand
|
||||
// Need to refactor this to use a single arg (object) instead of 7
|
||||
@@ -86,6 +89,22 @@ class ScriptRuntime {
|
||||
};
|
||||
}
|
||||
|
||||
if (this.runtime === 'quickjs') {
|
||||
await executeQuickJsVmAsync({
|
||||
script: script,
|
||||
context: context,
|
||||
collectionPath
|
||||
});
|
||||
|
||||
return {
|
||||
request,
|
||||
envVariables: cleanJson(envVariables),
|
||||
runtimeVariables: cleanJson(runtimeVariables),
|
||||
nextRequestName: bru.nextRequest
|
||||
};
|
||||
}
|
||||
|
||||
// default runtime is vm2
|
||||
const vm = new NodeVM({
|
||||
sandbox: context,
|
||||
require: {
|
||||
@@ -123,6 +142,7 @@ class ScriptRuntime {
|
||||
});
|
||||
const asyncVM = vm.run(`module.exports = async () => { ${script} }`, path.join(collectionPath, 'vm.js'));
|
||||
await asyncVM();
|
||||
|
||||
return {
|
||||
request,
|
||||
envVariables: cleanJson(envVariables),
|
||||
@@ -176,10 +196,27 @@ class ScriptRuntime {
|
||||
log: customLogger('log'),
|
||||
info: customLogger('info'),
|
||||
warn: customLogger('warn'),
|
||||
error: customLogger('error')
|
||||
error: customLogger('error'),
|
||||
debug: customLogger('debug')
|
||||
};
|
||||
}
|
||||
|
||||
if (this.runtime === 'quickjs') {
|
||||
await executeQuickJsVmAsync({
|
||||
script: script,
|
||||
context: context,
|
||||
collectionPath
|
||||
});
|
||||
|
||||
return {
|
||||
response,
|
||||
envVariables: cleanJson(envVariables),
|
||||
runtimeVariables: cleanJson(runtimeVariables),
|
||||
nextRequestName: bru.nextRequest
|
||||
};
|
||||
}
|
||||
|
||||
// default runtime is vm2
|
||||
const vm = new NodeVM({
|
||||
sandbox: context,
|
||||
require: {
|
||||
|
||||
@@ -15,7 +15,7 @@ const BrunoRequest = require('../bruno-request');
|
||||
const BrunoResponse = require('../bruno-response');
|
||||
const Test = require('../test');
|
||||
const TestResults = require('../test-results');
|
||||
const { cleanJson } = require('../utils');
|
||||
const { cleanJson, appendAwaitToTestFunc } = require('../utils');
|
||||
|
||||
// Inbuilt Library Support
|
||||
const ajv = require('ajv');
|
||||
@@ -30,9 +30,12 @@ const axios = require('axios');
|
||||
const fetch = require('node-fetch');
|
||||
const CryptoJS = require('crypto-js');
|
||||
const NodeVault = require('node-vault');
|
||||
const { executeQuickJsVmAsync } = require('../sandbox/quickjs');
|
||||
|
||||
class TestRuntime {
|
||||
constructor() {}
|
||||
constructor(props) {
|
||||
this.runtime = props?.runtime || 'vm2';
|
||||
}
|
||||
|
||||
async runTests(
|
||||
testsFile,
|
||||
@@ -81,6 +84,9 @@ class TestRuntime {
|
||||
};
|
||||
}
|
||||
|
||||
// add 'await' prefix to the test function calls
|
||||
testsFile = appendAwaitToTestFunc(testsFile);
|
||||
|
||||
const context = {
|
||||
test,
|
||||
bru,
|
||||
@@ -101,48 +107,56 @@ class TestRuntime {
|
||||
log: customLogger('log'),
|
||||
info: customLogger('info'),
|
||||
warn: customLogger('warn'),
|
||||
debug: customLogger('debug'),
|
||||
error: customLogger('error')
|
||||
};
|
||||
}
|
||||
|
||||
const vm = new NodeVM({
|
||||
sandbox: context,
|
||||
require: {
|
||||
context: 'sandbox',
|
||||
external: true,
|
||||
root: [collectionPath, ...additionalContextRootsAbsolute],
|
||||
mock: {
|
||||
// node libs
|
||||
path,
|
||||
stream,
|
||||
util,
|
||||
url,
|
||||
http,
|
||||
https,
|
||||
punycode,
|
||||
zlib,
|
||||
// 3rd party libs
|
||||
ajv,
|
||||
'ajv-formats': addFormats,
|
||||
btoa,
|
||||
atob,
|
||||
lodash,
|
||||
moment,
|
||||
uuid,
|
||||
nanoid,
|
||||
axios,
|
||||
chai,
|
||||
'node-fetch': fetch,
|
||||
'crypto-js': CryptoJS,
|
||||
...whitelistedModules,
|
||||
fs: allowScriptFilesystemAccess ? fs : undefined,
|
||||
'node-vault': NodeVault
|
||||
if (this.runtime === 'quickjs') {
|
||||
await executeQuickJsVmAsync({
|
||||
script: testsFile,
|
||||
context: context
|
||||
});
|
||||
} else {
|
||||
// default runtime is vm2
|
||||
const vm = new NodeVM({
|
||||
sandbox: context,
|
||||
require: {
|
||||
context: 'sandbox',
|
||||
external: true,
|
||||
root: [collectionPath, ...additionalContextRootsAbsolute],
|
||||
mock: {
|
||||
// node libs
|
||||
path,
|
||||
stream,
|
||||
util,
|
||||
url,
|
||||
http,
|
||||
https,
|
||||
punycode,
|
||||
zlib,
|
||||
// 3rd party libs
|
||||
ajv,
|
||||
'ajv-formats': addFormats,
|
||||
btoa,
|
||||
atob,
|
||||
lodash,
|
||||
moment,
|
||||
uuid,
|
||||
nanoid,
|
||||
axios,
|
||||
chai,
|
||||
'node-fetch': fetch,
|
||||
'crypto-js': CryptoJS,
|
||||
...whitelistedModules,
|
||||
fs: allowScriptFilesystemAccess ? fs : undefined,
|
||||
'node-vault': NodeVault
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const asyncVM = vm.run(`module.exports = async () => { ${testsFile}}`, path.join(collectionPath, 'vm.js'));
|
||||
await asyncVM();
|
||||
});
|
||||
const asyncVM = vm.run(`module.exports = async () => { ${testsFile}}`, path.join(collectionPath, 'vm.js'));
|
||||
await asyncVM();
|
||||
}
|
||||
|
||||
return {
|
||||
request,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user