mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-04 01:48:33 +00:00
Compare commits
93 Commits
feat/folde
...
feat/safe-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a18e5e5b94 | ||
|
|
bde2c57a23 | ||
|
|
9955b8796e | ||
|
|
3b3fa8a856 | ||
|
|
c687856a9e | ||
|
|
033bba1805 | ||
|
|
4fcb6f0980 | ||
|
|
e4d2e5c1cf | ||
|
|
969fe8e3cf | ||
|
|
6d01c46d50 | ||
|
|
751c7aa16d | ||
|
|
b2baa1e48d | ||
|
|
f834eb4425 | ||
|
|
26f8dd7a7b | ||
|
|
7e305be817 | ||
|
|
4c3fe2f719 | ||
|
|
de226d2e44 | ||
|
|
1e0c88a291 | ||
|
|
29db85a916 | ||
|
|
edb8708dde | ||
|
|
9fdfee0083 | ||
|
|
72c3aaa5ba | ||
|
|
eb1c10fd6e | ||
|
|
911e3aa589 | ||
|
|
2358aa4cdc | ||
|
|
800dbcfdbc | ||
|
|
d7ec3d1cc5 | ||
|
|
92073e7573 | ||
|
|
3e2a3b65a4 | ||
|
|
aa4bcdca9b | ||
|
|
12fdbbb291 | ||
|
|
7cafed6c93 | ||
|
|
8a9df14e16 | ||
|
|
b2b41fec1a | ||
|
|
a8aa54cf1b | ||
|
|
60a8647e7c | ||
|
|
7ca59656f2 | ||
|
|
4598bb1bdd | ||
|
|
adb843faa7 | ||
|
|
2b0d55ce6b | ||
|
|
c5ec7eea34 | ||
|
|
741250068f | ||
|
|
7c33fd413e | ||
|
|
8f920a90c7 | ||
|
|
640623b39a | ||
|
|
37bec70fe6 | ||
|
|
98c53cf443 | ||
|
|
6fe96a8194 | ||
|
|
f2ba351f0d | ||
|
|
2e2c60d90e | ||
|
|
1d2e06d419 | ||
|
|
c99da3a581 | ||
|
|
073c1aae12 | ||
|
|
398c833393 | ||
|
|
47724b1b1e | ||
|
|
2804ce1eb3 | ||
|
|
9892f7cd40 | ||
|
|
7194998b0e | ||
|
|
ab9bcbe5ed | ||
|
|
e60aaf2ea9 | ||
|
|
81497d8397 | ||
|
|
34a961967e | ||
|
|
1b0495c7b0 | ||
|
|
73214107c7 | ||
|
|
f159f73340 | ||
|
|
c2e6dee2da | ||
|
|
f64dca16a7 | ||
|
|
b5b9e547c9 | ||
|
|
1239baf687 | ||
|
|
b2038c7cc2 | ||
|
|
9f76834b2f | ||
|
|
8094149fbe | ||
|
|
240d2d03f7 | ||
|
|
58c8085a64 | ||
|
|
e5425299a2 | ||
|
|
f1e0b112ae | ||
|
|
0988f2b86e | ||
|
|
e71e38f62e | ||
|
|
589e173256 | ||
|
|
e462eb6ecd | ||
|
|
40f7be534a | ||
|
|
c8f95a34e9 | ||
|
|
2aa7d26a89 | ||
|
|
71353b0404 | ||
|
|
01605f6f2a | ||
|
|
0d204694a6 | ||
|
|
0d3765ad66 | ||
|
|
bd61e453ee | ||
|
|
02e23df349 | ||
|
|
61e0ac03fa | ||
|
|
c895d7f357 | ||
|
|
45ff36d394 | ||
|
|
fd57b2ce94 |
39
.github/workflows/tests.yml
vendored
39
.github/workflows/tests.yml
vendored
@@ -15,6 +15,8 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: './package-lock.json'
|
||||
- name: Install dependencies
|
||||
run: npm ci --legacy-peer-deps
|
||||
|
||||
@@ -24,7 +26,18 @@ jobs:
|
||||
npm run build --workspace=packages/bruno-common
|
||||
npm run build --workspace=packages/bruno-query
|
||||
|
||||
# test
|
||||
# rebuild isolated-vm for bruno-js
|
||||
- name: Rebuild bruno-js isolated-vm
|
||||
run: |
|
||||
cd packages/bruno-js/node_modules/isolated-vm
|
||||
npm run rebuild || true
|
||||
|
||||
# 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
|
||||
|
||||
- name: Test Package bruno-query
|
||||
run: npm run test --workspace=packages/bruno-query
|
||||
- name: Test Package bruno-lang
|
||||
@@ -33,12 +46,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
|
||||
|
||||
@@ -50,6 +59,8 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: './package-lock.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --legacy-peer-deps
|
||||
@@ -59,6 +70,12 @@ jobs:
|
||||
npm run build --workspace=packages/bruno-query
|
||||
npm run build --workspace=packages/bruno-common
|
||||
|
||||
# rebuild isolated-vm for bruno-js
|
||||
- name: Rebuild bruno-js isolated-vm
|
||||
run: |
|
||||
cd packages/bruno-js/node_modules/isolated-vm
|
||||
npm run rebuild || true
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
cd packages/bruno-tests/collection
|
||||
@@ -71,15 +88,3 @@ jobs:
|
||||
with:
|
||||
files: packages/bruno-tests/collection/junit.xml
|
||||
comment_mode: always
|
||||
prettier:
|
||||
name: Prettier
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- name: Install dependencies
|
||||
run: npm ci --legacy-peer-deps
|
||||
- name: Run Prettier
|
||||
run: npm run test:prettier:web
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npx pretty-quick --staged
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -62,6 +62,9 @@ npm run build:graphql-docs
|
||||
# construction de bruno query
|
||||
npm run build:bruno-query
|
||||
|
||||
# construction de bruno common
|
||||
npm run build:bruno-common
|
||||
|
||||
# démarrage de next (terminal 1)
|
||||
npm run dev:web
|
||||
|
||||
|
||||
84
docs/contributing/contributing_sk.md
Normal file
84
docs/contributing/contributing_sk.md
Normal file
@@ -0,0 +1,84 @@
|
||||
## Urobme bruno lepším, spoločne !!
|
||||
|
||||
Sme radi, že chcete zlepšiť bruno. Nižšie sú uvedené pokyny, ako začať s výchovou bruno na vašom počítači.
|
||||
|
||||
### Technologický zásobník
|
||||
|
||||
Bruno je vytvorené pomocou Next.js a React. Na dodávanie desktopovej verzie (ktorá podporuje lokálne kolekcie) používame aj electron.
|
||||
|
||||
Balíčky, ktoré používame:
|
||||
|
||||
- CSS - Tailwind
|
||||
- Editory kódu - Codemirror
|
||||
- Správa stavu - Redux
|
||||
- Ikony - Tabler Icons
|
||||
- Formuláre - formik
|
||||
- Overovanie schém - Yup
|
||||
- Klient požiadaviek - axios
|
||||
- Sledovač súborového systému - chokidar
|
||||
|
||||
### Závislosti
|
||||
|
||||
Budete potrebovať [NodeJS v18.x alebo najnovšiu verziu LTS](https://nodejs.org/en/) a npm versiu 8.x. V projekte používame pracovné priestory npm
|
||||
|
||||
## Vývoj
|
||||
|
||||
Bruno sa vyvíja ako desktopová aplikácia. Aplikáciu je potrebné načítať spustením aplikácie Next.js v jednom termináli a potom spustiť aplikáciu electron v inom termináli.
|
||||
|
||||
### Závislosti
|
||||
|
||||
- NodeJS v18
|
||||
|
||||
### Miestny vývoj
|
||||
|
||||
```bash
|
||||
# použite verziu nodejs 18
|
||||
nvm use
|
||||
|
||||
# nainštalovať balíčky
|
||||
npm i --legacy-peer-deps
|
||||
|
||||
# zostaviť balíčky
|
||||
npm run build:graphql-docs
|
||||
npm run build:bruno-query
|
||||
npm run build:bruno-common
|
||||
|
||||
# spustite ďalšiu aplikáciu (terminál 1)
|
||||
npm run dev:web
|
||||
|
||||
# spustite aplikáciu electron (terminál 2)
|
||||
npm run dev:electron
|
||||
```
|
||||
|
||||
### Riešenie problémov
|
||||
|
||||
Pri spustení `npm install` sa môžete stretnúť s chybou `Unsupported platform`. Ak chcete túto chybu odstrániť, musíte odstrániť súbory `node_modules`, `package-lock.json` a spustiť `npm install`. Tým by sa mali nainštalovať všetky potrebné balíky potrebné na spustenie aplikácie.
|
||||
|
||||
```shell
|
||||
# Odstrániť node_modules v podadresároch
|
||||
find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do
|
||||
rm -rf "$dir"
|
||||
done
|
||||
|
||||
# Odstráňte package-lock v podadresároch
|
||||
find . -type f -name "package-lock.json" -delete
|
||||
```
|
||||
|
||||
### Testovanie
|
||||
|
||||
````bash
|
||||
# bruno-schema
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
```
|
||||
|
||||
### Vyrobenie Pull Request
|
||||
|
||||
- Prosím, aby PR boli malé a zamerané na jednu vec
|
||||
- Prosím, dodržujte formát vytvárania vetiev
|
||||
- feature/[názov funkcie]: Táto vetva by mala obsahovať zmeny pre konkrétnu funkciu
|
||||
- Príklad: feature/dark-mode
|
||||
- bugfix/[názov chyby]: Táto vetva by mala obsahovať iba opravy konkrétnej chyby
|
||||
- Príklad: bugfix/bug-1
|
||||
@@ -103,6 +103,12 @@ Ou qualquer sistema de controle de versão de sua escolha.
|
||||
|
||||
<img src="../../assets/images/sponsors/commit-company.png" width="70"/>
|
||||
|
||||
#### Apoiadores Bronze
|
||||
|
||||
<a href="https://zuplo.link/bruno">
|
||||
<img src="../../assets/images/sponsors/zuplo.png" width="120"/>
|
||||
</a>
|
||||
|
||||
### Links Importantes 📌
|
||||
|
||||
- [Nossa Visão de Longo Prazo](https://github.com/usebruno/bruno/discussions/269)
|
||||
|
||||
26170
package-lock.json
generated
26170
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -47,12 +47,14 @@
|
||||
"test:prettier:web": "npm run test:prettier --workspace=packages/bruno-app",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
|
||||
"overrides": {
|
||||
"rollup": "3.2.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"isolated-vm": "^5.0.0",
|
||||
"json-bigint": "^1.0.0",
|
||||
"lossless-json": "^4.0.1"
|
||||
"lossless-json": "^4.0.1",
|
||||
"rollup-plugin-node-builtins": "^2.1.2",
|
||||
"rollup-plugin-node-globals": "^1.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ const StyledWrapper = styled.div`
|
||||
border: solid 1px ${(props) => props.theme.codemirror.border};
|
||||
font-family: ${(props) => (props.font ? props.font : 'default')};
|
||||
line-break: anywhere;
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
.CodeMirror-overlayscroll-horizontal div,
|
||||
|
||||
@@ -16,6 +16,7 @@ import stripJsonComments from 'strip-json-comments';
|
||||
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
const TAB_SIZE = 2;
|
||||
|
||||
if (!SERVER_RENDERED) {
|
||||
CodeMirror = require('codemirror');
|
||||
@@ -58,10 +59,13 @@ if (!SERVER_RENDERED) {
|
||||
'bru.cwd()',
|
||||
'bru.getEnvName(key)',
|
||||
'bru.getProcessEnv(key)',
|
||||
'bru.hasEnvVar(key)',
|
||||
'bru.getEnvVar(key)',
|
||||
'bru.setEnvVar(key,value)',
|
||||
'bru.hasVar(key)',
|
||||
'bru.getVar(key)',
|
||||
'bru.setVar(key,value)',
|
||||
'bru.deleteVar(key)',
|
||||
'bru.setNextRequest(requestName)'
|
||||
];
|
||||
CodeMirror.registerHelper('hint', 'brunoJS', (editor, options) => {
|
||||
@@ -118,7 +122,7 @@ export default class CodeEditor extends React.Component {
|
||||
value: this.props.value || '',
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
tabSize: 2,
|
||||
tabSize: TAB_SIZE,
|
||||
mode: this.props.mode || 'application/ld+json',
|
||||
keyMap: 'sublime',
|
||||
autoCloseBrackets: true,
|
||||
@@ -166,7 +170,33 @@ export default class CodeEditor extends React.Component {
|
||||
'Ctrl-Y': 'foldAll',
|
||||
'Cmd-Y': 'foldAll',
|
||||
'Ctrl-I': 'unfoldAll',
|
||||
'Cmd-I': 'unfoldAll'
|
||||
'Cmd-I': 'unfoldAll',
|
||||
'Cmd-/': (cm) => {
|
||||
// comment/uncomment every selected line(s)
|
||||
const selections = cm.listSelections();
|
||||
selections.forEach((range) => {
|
||||
for (let i = range.from().line; i <= range.to().line; i++) {
|
||||
const selectedLine = cm.getLine(i);
|
||||
// if commented line, remove comment
|
||||
if (selectedLine.trim().startsWith('//')) {
|
||||
cm.replaceRange(
|
||||
selectedLine.replace(/^(\s*)\/\/\s?/, '$1'),
|
||||
{ line: i, ch: 0 },
|
||||
{ line: i, ch: selectedLine.length }
|
||||
);
|
||||
continue;
|
||||
}
|
||||
// otherwise add comment
|
||||
cm.replaceRange(
|
||||
selectedLine.search(/\S|$/) >= TAB_SIZE
|
||||
? ' '.repeat(TAB_SIZE) + '// ' + selectedLine.trim()
|
||||
: '// ' + selectedLine,
|
||||
{ line: i, ch: 0 },
|
||||
{ line: i, ch: selectedLine.length }
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
foldOptions: {
|
||||
widget: (from, to) => {
|
||||
@@ -286,7 +316,7 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
return (
|
||||
<StyledWrapper
|
||||
className="h-full w-full"
|
||||
className="h-full w-full flex flex-col relative"
|
||||
aria-label="Code Editor"
|
||||
font={this.props.font}
|
||||
ref={(node) => {
|
||||
|
||||
@@ -138,6 +138,7 @@ const AwsV4Auth = ({ collection }) => {
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleSecretAccessKeyChange(val)}
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ const BasicAuth = ({ collection }) => {
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handlePasswordChange(val)}
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -37,6 +37,7 @@ const BearerAuth = ({ collection }) => {
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleTokenChange(val)}
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -62,6 +62,7 @@ const DigestAuth = ({ collection }) => {
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handlePasswordChange(val)}
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -78,7 +78,7 @@ const OAuth2AuthorizationCode = ({ collection }) => {
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label } = input;
|
||||
const { key, label, isSecret } = input;
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
|
||||
<label className="block font-medium">{label}</label>
|
||||
@@ -90,6 +90,7 @@ const OAuth2AuthorizationCode = ({ collection }) => {
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,8 @@ const inputsConfig = [
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'Client Secret'
|
||||
label: 'Client Secret',
|
||||
isSecret: true
|
||||
},
|
||||
{
|
||||
key: 'scope',
|
||||
|
||||
@@ -42,7 +42,7 @@ const OAuth2ClientCredentials = ({ collection }) => {
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label } = input;
|
||||
const { key, label, isSecret } = input;
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
|
||||
<label className="block font-medium">{label}</label>
|
||||
@@ -54,6 +54,7 @@ const OAuth2ClientCredentials = ({ collection }) => {
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,8 @@ const inputsConfig = [
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'Client Secret'
|
||||
label: 'Client Secret',
|
||||
isSecret: true
|
||||
},
|
||||
{
|
||||
key: 'scope',
|
||||
|
||||
@@ -44,7 +44,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label } = input;
|
||||
const { key, label, isSecret } = input;
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
|
||||
<label className="block font-medium">{label}</label>
|
||||
@@ -56,6 +56,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,8 @@ const inputsConfig = [
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'Client Secret'
|
||||
label: 'Client Secret',
|
||||
isSecret: true
|
||||
},
|
||||
{
|
||||
key: 'scope',
|
||||
|
||||
@@ -13,6 +13,7 @@ import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/acti
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { headers as StandardHTTPHeaders } from 'know-your-http-well';
|
||||
import { MimeTypes } from 'utils/codemirror/autocompleteConstants';
|
||||
const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
|
||||
|
||||
const Headers = ({ collection }) => {
|
||||
@@ -117,6 +118,7 @@ const Headers = ({ collection }) => {
|
||||
)
|
||||
}
|
||||
collection={collection}
|
||||
autocomplete={MimeTypes}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -74,6 +74,7 @@ const PresetsSettings = ({ collection }) => {
|
||||
id="request-url"
|
||||
type="text"
|
||||
name="requestUrl"
|
||||
placeholder='Request URL'
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.CodeMirror {
|
||||
/* todo: find a better way */
|
||||
height: calc(100vh - 240px);
|
||||
|
||||
.CodeMirror-scroll {
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
}
|
||||
.editing-mode {
|
||||
cursor: pointer;
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
|
||||
@@ -37,8 +37,8 @@ const Documentation = ({ item, collection }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-1 h-full w-full relative">
|
||||
<div className="editing-mode mb-2" role="tab" onClick={toggleViewMode}>
|
||||
<StyledWrapper className="flex flex-col gap-y-1 h-full w-full relative">
|
||||
<div className="editing-mode" role="tab" onClick={toggleViewMode}>
|
||||
{isEditing ? 'Preview' : 'Edit'}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import Portal from 'components/Portal';
|
||||
import Modal from 'components/Modal';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useFormik } from 'formik';
|
||||
import { addEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import * as Yup from 'yup';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Portal from 'components/Portal';
|
||||
import Modal from 'components/Modal';
|
||||
|
||||
const CreateEnvironment = ({ collection, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -27,7 +27,7 @@ const CreateEnvironment = ({ collection, onClose }) => {
|
||||
toast.success('Environment created in collection');
|
||||
onClose();
|
||||
})
|
||||
.catch(() => toast.error('An error occurred while created the environment'));
|
||||
.catch(() => toast.error('An error occurred while creating the environment'));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -55,19 +55,21 @@ const CreateEnvironment = ({ collection, onClose }) => {
|
||||
<label htmlFor="name" className="block font-semibold">
|
||||
Environment Name
|
||||
</label>
|
||||
<input
|
||||
id="environment-name"
|
||||
type="text"
|
||||
name="name"
|
||||
ref={inputRef}
|
||||
className="block textbox mt-2 w-full"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.name || ''}
|
||||
/>
|
||||
<div className="flex items-center mt-2">
|
||||
<input
|
||||
id="environment-name"
|
||||
type="text"
|
||||
name="name"
|
||||
ref={inputRef}
|
||||
className="block textbox w-full"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.name || ''}
|
||||
/>
|
||||
</div>
|
||||
{formik.touched.name && formik.errors.name ? (
|
||||
<div className="text-red-500">{formik.errors.name}</div>
|
||||
) : null}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { uuid } from 'utils/common';
|
||||
import { maskInputValue } from 'utils/collections';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import { variableNameRegex } from 'utils/common/regex';
|
||||
@@ -96,10 +95,10 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Enabled</td>
|
||||
<td className="text-center">Enabled</td>
|
||||
<td>Name</td>
|
||||
<td>Value</td>
|
||||
<td>Secret</td>
|
||||
<td className="text-center">Secret</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -109,7 +108,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
|
||||
<td className="text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mr-3 mousetrap"
|
||||
className="mousetrap"
|
||||
name={`${index}.enabled`}
|
||||
checked={variable.enabled}
|
||||
onChange={formik.handleChange}
|
||||
@@ -130,23 +129,22 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
|
||||
/>
|
||||
<ErrorMessage name={`${index}.name`} />
|
||||
</td>
|
||||
<td>
|
||||
{variable.secret ? (
|
||||
<div className="overflow-hidden text-ellipsis">{maskInputValue(variable.value)}</div>
|
||||
) : (
|
||||
<td className="flex flex-row flex-nowrap">
|
||||
<div className="overflow-hidden grow w-full relative">
|
||||
<SingleLineEditor
|
||||
theme={storedTheme}
|
||||
collection={collection}
|
||||
name={`${index}.value`}
|
||||
value={variable.value}
|
||||
isSecret={variable.secret}
|
||||
onChange={(newValue) => formik.setFieldValue(`${index}.value`, newValue, true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<td className="text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mr-3 mousetrap"
|
||||
className="mousetrap"
|
||||
name={`${index}.secret`}
|
||||
checked={variable.secret}
|
||||
onChange={formik.handleChange}
|
||||
|
||||
@@ -1,24 +1,38 @@
|
||||
import React from 'react';
|
||||
import Portal from 'components/Portal';
|
||||
import Modal from 'components/Modal';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import importPostmanEnvironment from 'utils/importers/postman-environment';
|
||||
import { importEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { toastError } from 'utils/common/error';
|
||||
import Modal from 'components/Modal';
|
||||
import { IconDatabaseImport } from '@tabler/icons';
|
||||
|
||||
const ImportEnvironment = ({ onClose, collection }) => {
|
||||
const ImportEnvironment = ({ collection, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleImportPostmanEnvironment = () => {
|
||||
importPostmanEnvironment()
|
||||
.then((environment) => {
|
||||
dispatch(importEnvironment(environment.name, environment.variables, collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Environment imported successfully');
|
||||
onClose();
|
||||
})
|
||||
.catch(() => toast.error('An error occurred while importing the environment'));
|
||||
.then((environments) => {
|
||||
environments
|
||||
.filter((env) =>
|
||||
env.name && env.name !== 'undefined'
|
||||
? true
|
||||
: () => {
|
||||
toast.error('Failed to import environment: env has no name');
|
||||
return false;
|
||||
}
|
||||
)
|
||||
.map((environment) => {
|
||||
dispatch(importEnvironment(environment.name, environment.variables, collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Environment imported successfully');
|
||||
})
|
||||
.catch(() => toast.error('An error occurred while importing the environment'));
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
onClose();
|
||||
})
|
||||
.catch((err) => toastError(err, 'Postman Import environment failed'));
|
||||
};
|
||||
@@ -26,11 +40,14 @@ const ImportEnvironment = ({ onClose, collection }) => {
|
||||
return (
|
||||
<Portal>
|
||||
<Modal size="sm" title="Import Environment" hideFooter={true} handleConfirm={onClose} handleCancel={onClose}>
|
||||
<div>
|
||||
<div className="text-link hover:underline cursor-pointer" onClick={handleImportPostmanEnvironment}>
|
||||
Postman Environment
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleImportPostmanEnvironment}
|
||||
className="flex justify-center flex-col items-center w-full dark:bg-zinc-700 rounded-lg border-2 border-dashed border-zinc-300 dark:border-zinc-400 p-12 text-center hover:border-zinc-400 focus:outline-none focus:ring-2 focus:ring-amber-500 focus:ring-offset-2"
|
||||
>
|
||||
<IconDatabaseImport size={64} />
|
||||
<span className="mt-2 block text-sm font-semibold">Import your Postman environments</span>
|
||||
</button>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
|
||||
@@ -4,45 +4,61 @@ import CreateEnvironment from './CreateEnvironment';
|
||||
import EnvironmentList from './EnvironmentList';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import ImportEnvironment from './ImportEnvironment';
|
||||
import { IconFileAlert } from '@tabler/icons';
|
||||
|
||||
export const SharedButton = ({ children, className, onClick }) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`rounded bg-transparent px-2.5 py-2 w-fit text-xs font-semibold text-zinc-900 dark:text-zinc-50 shadow-sm ring-1 ring-inset ring-zinc-300 dark:ring-zinc-500 hover:bg-gray-50 dark:hover:bg-zinc-700
|
||||
${className}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const DefaultTab = ({ setTab }) => {
|
||||
return (
|
||||
<div className="text-center items-center flex flex-col">
|
||||
<IconFileAlert size={64} strokeWidth={1} />
|
||||
<span className="font-semibold mt-2">No environments found</span>
|
||||
<span className="font-extralight mt-2 text-zinc-500 dark:text-zinc-400">
|
||||
Get started by using the following buttons :
|
||||
</span>
|
||||
<div className="flex items-center justify-center mt-6">
|
||||
<SharedButton onClick={() => setTab('create')}>
|
||||
<span>Create Environment</span>
|
||||
</SharedButton>
|
||||
|
||||
<span className="mx-4">Or</span>
|
||||
|
||||
<SharedButton onClick={() => setTab('import')}>
|
||||
<span>Import Environment</span>
|
||||
</SharedButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const EnvironmentSettings = ({ collection, onClose }) => {
|
||||
const [isModified, setIsModified] = useState(false);
|
||||
const { environments } = collection;
|
||||
const [openCreateModal, setOpenCreateModal] = useState(false);
|
||||
const [openImportModal, setOpenImportModal] = useState(false);
|
||||
const [selectedEnvironment, setSelectedEnvironment] = useState(null);
|
||||
|
||||
const [tab, setTab] = useState('default');
|
||||
if (!environments || !environments.length) {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<Modal
|
||||
size="md"
|
||||
title="Environments"
|
||||
confirmText={'Close'}
|
||||
handleConfirm={onClose}
|
||||
handleCancel={onClose}
|
||||
hideCancel={true}
|
||||
>
|
||||
{openCreateModal && <CreateEnvironment collection={collection} onClose={() => setOpenCreateModal(false)} />}
|
||||
{openImportModal && <ImportEnvironment collection={collection} onClose={() => setOpenImportModal(false)} />}
|
||||
<div className="text-center flex flex-col">
|
||||
<p>No environments found!</p>
|
||||
<button
|
||||
className="btn-create-environment text-link pr-2 py-3 mt-2 select-none"
|
||||
onClick={() => setOpenCreateModal(true)}
|
||||
>
|
||||
<span>Create Environment</span>
|
||||
</button>
|
||||
|
||||
<span>Or</span>
|
||||
|
||||
<button
|
||||
className="btn-import-environment text-link pl-2 pr-2 py-3 select-none"
|
||||
onClick={() => setOpenImportModal(true)}
|
||||
>
|
||||
<span>Import Environment</span>
|
||||
</button>
|
||||
</div>
|
||||
<Modal size="md" title="Environments" handleCancel={onClose} hideCancel={true} hideFooter={true}>
|
||||
{tab === 'create' ? (
|
||||
<CreateEnvironment collection={collection} onClose={() => setTab('default')} />
|
||||
) : tab === 'import' ? (
|
||||
<ImportEnvironment collection={collection} onClose={() => setTab('default')} />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<DefaultTab setTab={setTab} />
|
||||
</Modal>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -42,7 +42,7 @@ const FilePickerEditor = ({ value, onChange, collection }) => {
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
onChange('');
|
||||
onChange([]);
|
||||
};
|
||||
|
||||
const renderButtonText = (filenames) => {
|
||||
|
||||
@@ -12,8 +12,8 @@ const FolderSettings = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
let tab = 'headers';
|
||||
const { folderLevelSettingsSelectedTab } = collection;
|
||||
if (folderLevelSettingsSelectedTab?.[folder.uid]) {
|
||||
tab = folderLevelSettingsSelectedTab[folder.uid];
|
||||
if (folderLevelSettingsSelectedTab?.[folder?.uid]) {
|
||||
tab = folderLevelSettingsSelectedTab[folder?.uid];
|
||||
}
|
||||
|
||||
const setTab = (tab) => {
|
||||
|
||||
@@ -2,7 +2,6 @@ import styled from 'styled-components';
|
||||
|
||||
const StyledMarkdownBodyWrapper = styled.div`
|
||||
background: transparent;
|
||||
height: inherit;
|
||||
.markdown-body {
|
||||
background: transparent;
|
||||
overflow-y: auto;
|
||||
|
||||
@@ -1,15 +1,28 @@
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
|
||||
const md = new MarkdownIt();
|
||||
|
||||
const Markdown = ({ onDoubleClick, content }) => {
|
||||
const handleOnClick = (event) => {
|
||||
const target = event.target;
|
||||
if (target.tagName === 'A') {
|
||||
event.preventDefault();
|
||||
const href = target.getAttribute('href');
|
||||
if (href) {
|
||||
window.open(href, '_blank');
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnDoubleClick = (event) => {
|
||||
if (event?.detail === 2) {
|
||||
if (event.detail === 2) {
|
||||
onDoubleClick();
|
||||
}
|
||||
};
|
||||
|
||||
const htmlFromMarkdown = md.render(content || '');
|
||||
|
||||
return (
|
||||
@@ -17,7 +30,8 @@ const Markdown = ({ onDoubleClick, content }) => {
|
||||
<div
|
||||
className="markdown-body"
|
||||
dangerouslySetInnerHTML={{ __html: htmlFromMarkdown }}
|
||||
onClick={handleOnDoubleClick}
|
||||
onClick={handleOnClick}
|
||||
onDoubleClick={handleOnDoubleClick}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -24,13 +24,15 @@ class MultiLineEditor extends Component {
|
||||
componentDidMount() {
|
||||
// Initialize CodeMirror as a single line editor
|
||||
/** @type {import("codemirror").Editor} */
|
||||
const variables = getAllVariables(this.props.collection, this.props.item);
|
||||
|
||||
this.editor = CodeMirror(this.editorRef.current, {
|
||||
lineWrapping: false,
|
||||
lineNumbers: false,
|
||||
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
|
||||
mode: 'brunovariables',
|
||||
brunoVarInfo: {
|
||||
variables: getAllVariables(this.props.collection)
|
||||
variables
|
||||
},
|
||||
scrollbarStyle: null,
|
||||
tabindex: 0,
|
||||
@@ -85,7 +87,7 @@ class MultiLineEditor extends Component {
|
||||
}
|
||||
this.editor.setValue(String(this.props.value) || '');
|
||||
this.editor.on('change', this._onEdit);
|
||||
this.addOverlay();
|
||||
this.addOverlay(variables);
|
||||
}
|
||||
|
||||
_onEdit = () => {
|
||||
@@ -103,10 +105,10 @@ class MultiLineEditor extends Component {
|
||||
// event loop.
|
||||
this.ignoreChangeEvent = true;
|
||||
|
||||
let variables = getAllVariables(this.props.collection);
|
||||
let variables = getAllVariables(this.props.collection, this.props.item);
|
||||
if (!isEqual(variables, this.variables)) {
|
||||
this.editor.options.brunoVarInfo.variables = variables;
|
||||
this.addOverlay();
|
||||
this.addOverlay(variables);
|
||||
}
|
||||
if (this.props.theme !== prevProps.theme && this.editor) {
|
||||
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
|
||||
@@ -125,10 +127,8 @@ class MultiLineEditor extends Component {
|
||||
this.editor.getWrapperElement().remove();
|
||||
}
|
||||
|
||||
addOverlay = () => {
|
||||
let variables = getAllVariables(this.props.collection);
|
||||
addOverlay = (variables) => {
|
||||
this.variables = variables;
|
||||
|
||||
defineCodeMirrorBrunoVariablesMode(variables, 'text/plain');
|
||||
this.editor.setOption('mode', 'brunovariables');
|
||||
};
|
||||
|
||||
@@ -136,6 +136,7 @@ const AwsV4Auth = ({ onTokenChange, item, collection }) => {
|
||||
onChange={(val) => handleAccessKeyIdChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -148,6 +149,8 @@ const AwsV4Auth = ({ onTokenChange, item, collection }) => {
|
||||
onChange={(val) => handleSecretAccessKeyChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
isSecret={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -160,6 +163,7 @@ const AwsV4Auth = ({ onTokenChange, item, collection }) => {
|
||||
onChange={(val) => handleSessionTokenChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -172,6 +176,7 @@ const AwsV4Auth = ({ onTokenChange, item, collection }) => {
|
||||
onChange={(val) => handleServiceChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -184,6 +189,7 @@ const AwsV4Auth = ({ onTokenChange, item, collection }) => {
|
||||
onChange={(val) => handleRegionChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -196,6 +202,7 @@ const AwsV4Auth = ({ onTokenChange, item, collection }) => {
|
||||
onChange={(val) => handleProfileNameChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -55,6 +55,7 @@ const BasicAuth = ({ item, collection }) => {
|
||||
onChange={(val) => handleUsernameChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -67,6 +68,8 @@ const BasicAuth = ({ item, collection }) => {
|
||||
onChange={(val) => handlePasswordChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
isSecret={true}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -42,6 +42,8 @@ const BearerAuth = ({ item, collection }) => {
|
||||
onChange={(val) => handleTokenChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
isSecret={true}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -55,6 +55,7 @@ const DigestAuth = ({ item, collection }) => {
|
||||
onChange={(val) => handleUsernameChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -67,6 +68,8 @@ const DigestAuth = ({ item, collection }) => {
|
||||
onChange={(val) => handlePasswordChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
isSecret={true}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -80,7 +80,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label } = input;
|
||||
const { key, label, isSecret } = input;
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
|
||||
<label className="block font-medium">{label}</label>
|
||||
@@ -92,6 +92,8 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,8 @@ const inputsConfig = [
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'Client Secret'
|
||||
label: 'Client Secret',
|
||||
isSecret: true
|
||||
},
|
||||
{
|
||||
key: 'scope',
|
||||
|
||||
@@ -43,7 +43,7 @@ const OAuth2ClientCredentials = ({ item, collection }) => {
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label } = input;
|
||||
const { key, label, isSecret } = input;
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
|
||||
<label className="block font-medium">{label}</label>
|
||||
@@ -55,6 +55,8 @@ const OAuth2ClientCredentials = ({ item, collection }) => {
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,8 @@ const inputsConfig = [
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'Client Secret'
|
||||
label: 'Client Secret',
|
||||
isSecret: true
|
||||
},
|
||||
{
|
||||
key: 'scope',
|
||||
|
||||
@@ -45,7 +45,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label } = input;
|
||||
const { key, label, isSecret } = input;
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
|
||||
<label className="block font-medium">{label}</label>
|
||||
@@ -57,6 +57,8 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,8 @@ const inputsConfig = [
|
||||
},
|
||||
{
|
||||
key: 'password',
|
||||
label: 'Password'
|
||||
label: 'Password',
|
||||
isSecret: true
|
||||
},
|
||||
{
|
||||
key: 'clientId',
|
||||
@@ -17,7 +18,8 @@ const inputsConfig = [
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'Client Secret'
|
||||
label: 'Client Secret',
|
||||
isSecret: true
|
||||
},
|
||||
{
|
||||
key: 'scope',
|
||||
|
||||
@@ -110,6 +110,7 @@ const FormUrlEncodedParams = ({ item, collection }) => {
|
||||
allowNewlines={true}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -151,7 +151,7 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
|
||||
</div>
|
||||
<GraphQLSchemaActions item={item} collection={collection} onSchemaLoad={setSchema} toggleDocs={toggleDocs} />
|
||||
</div>
|
||||
<section className="flex w-full mt-5">{getTabPanel(focusedTab.requestPaneTab)}</section>
|
||||
<section className="flex w-full mt-5 flex-1">{getTabPanel(focusedTab.requestPaneTab)}</section>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -137,7 +137,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
) : null}
|
||||
</div>
|
||||
<section
|
||||
className={classnames('flex w-full', {
|
||||
className={classnames('flex w-full flex-1', {
|
||||
'mt-5': !isMultipleContentTab
|
||||
})}
|
||||
>
|
||||
|
||||
@@ -24,7 +24,8 @@ const MultipartFormParams = ({ item, collection }) => {
|
||||
addMultipartFormParam({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
type: 'text'
|
||||
type: 'text',
|
||||
value: ''
|
||||
})
|
||||
);
|
||||
};
|
||||
@@ -34,7 +35,8 @@ const MultipartFormParams = ({ item, collection }) => {
|
||||
addMultipartFormParam({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
type: 'file'
|
||||
type: 'file',
|
||||
value: []
|
||||
})
|
||||
);
|
||||
};
|
||||
@@ -139,6 +141,7 @@ const MultipartFormParams = ({ item, collection }) => {
|
||||
onRun={handleRun}
|
||||
allowNewlines={true}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
|
||||
@@ -4,8 +4,7 @@ const StyledWrapper = styled.div`
|
||||
div.CodeMirror {
|
||||
background: ${(props) => props.theme.codemirror.bg};
|
||||
border: solid 1px ${(props) => props.theme.codemirror.border};
|
||||
/* todo: find a better way */
|
||||
height: calc(100vh - 220px);
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
textarea.cm-editor {
|
||||
|
||||
@@ -209,7 +209,7 @@ export default class QueryEditor extends React.Component {
|
||||
return (
|
||||
<>
|
||||
<StyledWrapper
|
||||
className="h-full w-full relative"
|
||||
className="h-full w-full flex flex-col relative"
|
||||
aria-label="Query Editor"
|
||||
ref={(node) => {
|
||||
this._node = node;
|
||||
|
||||
@@ -147,6 +147,7 @@ const QueryParams = ({ item, collection }) => {
|
||||
}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
@@ -214,6 +215,7 @@ const QueryParams = ({ item, collection }) => {
|
||||
}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -69,6 +69,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
onChange={(newValue) => onUrlChange(newValue)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
highlightPathParams={true}
|
||||
item={item}
|
||||
/>
|
||||
<div className="flex items-center h-full mr-2 cursor-pointer" id="send-request" onClick={handleRun}>
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
div.CodeMirror {
|
||||
/* todo: find a better way */
|
||||
height: calc(100vh - 220px);
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
|
||||
@@ -9,6 +9,7 @@ import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collection
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { headers as StandardHTTPHeaders } from 'know-your-http-well';
|
||||
import { MimeTypes } from 'utils/codemirror/autocompleteConstants';
|
||||
const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
|
||||
|
||||
const RequestHeaders = ({ item, collection }) => {
|
||||
@@ -115,8 +116,10 @@ const RequestHeaders = ({ item, collection }) => {
|
||||
)
|
||||
}
|
||||
onRun={handleRun}
|
||||
autocomplete={MimeTypes}
|
||||
allowNewlines={true}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -40,8 +40,8 @@ const Script = ({ item, collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col">
|
||||
<div className="flex-1 mt-2">
|
||||
<div className="mb-1 title text-xs">Pre Request</div>
|
||||
<div className="flex flex-col flex-1 mt-2 gap-y-2">
|
||||
<div className="title text-xs">Pre Request</div>
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
value={requestScript || ''}
|
||||
@@ -53,8 +53,8 @@ const Script = ({ item, collection }) => {
|
||||
onSave={onSave}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 mt-6">
|
||||
<div className="mt-1 mb-1 title text-xs">Post Response</div>
|
||||
<div className="flex flex-col flex-1 mt-2 gap-y-2">
|
||||
<div className="title text-xs">Post Response</div>
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
value={responseScript || ''}
|
||||
|
||||
@@ -9,11 +9,11 @@ const Vars = ({ item, collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col">
|
||||
<div className="flex-1 mt-2">
|
||||
<div className="mt-2">
|
||||
<div className="mb-1 title text-xs">Pre Request</div>
|
||||
<VarsTable item={item} collection={collection} vars={requestVars} varType="request" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div>
|
||||
<div className="mt-1 mb-1 title text-xs">Post Response</div>
|
||||
<VarsTable item={item} collection={collection} vars={responseVars} varType="response" />
|
||||
</div>
|
||||
|
||||
@@ -18,6 +18,7 @@ import CollectionSettings from 'components/CollectionSettings';
|
||||
import { DocExplorer } from '@usebruno/graphql-docs';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import SecuritySettings from 'components/SecuritySettings/index';
|
||||
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} />;
|
||||
@@ -158,10 +163,9 @@ const RequestTabPanel = () => {
|
||||
<section className="main flex flex-grow pb-4 relative">
|
||||
<section className="request-pane">
|
||||
<div
|
||||
className="px-4"
|
||||
className="px-4 h-full"
|
||||
style={{
|
||||
width: `${Math.max(leftPaneWidth, MIN_LEFT_PANE_WIDTH)}px`,
|
||||
height: `calc(100% - ${DEFAULT_PADDING}px)`
|
||||
width: `${Math.max(leftPaneWidth, MIN_LEFT_PANE_WIDTH)}px`
|
||||
}}
|
||||
>
|
||||
{item.type === 'graphql-request' ? (
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -81,7 +81,7 @@ const RequestTab = ({ tab, collection, folderUid }) => {
|
||||
return color;
|
||||
};
|
||||
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' ? (
|
||||
|
||||
@@ -14,6 +14,8 @@ const Wrapper = styled.div`
|
||||
display: none;
|
||||
}
|
||||
|
||||
scrollbar-width: none;
|
||||
|
||||
li {
|
||||
display: inline-flex;
|
||||
max-width: 150px;
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.textbox {
|
||||
border: 1px solid #ccc;
|
||||
padding: 0.2rem 0.5rem;
|
||||
box-shadow: none;
|
||||
border-radius: 0px;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
transition: border-color ease-in-out 0.1s;
|
||||
border-radius: 3px;
|
||||
background-color: ${(props) => props.theme.modal.input.bg};
|
||||
border: 1px solid ${(props) => props.theme.modal.input.border};
|
||||
}
|
||||
|
||||
.item-path {
|
||||
.link {
|
||||
color: ${(props) => props.theme.textLink};
|
||||
|
||||
@@ -23,6 +23,7 @@ const getRelativePath = (fullPath, pathname) => {
|
||||
export default function RunnerResults({ collection }) {
|
||||
const dispatch = useDispatch();
|
||||
const [selectedItem, setSelectedItem] = useState(null);
|
||||
const [delay, setDelay] = useState(null);
|
||||
|
||||
// ref for the runner output body
|
||||
const runnerBodyRef = useRef();
|
||||
@@ -78,11 +79,11 @@ export default function RunnerResults({ collection }) {
|
||||
.filter(Boolean);
|
||||
|
||||
const runCollection = () => {
|
||||
dispatch(runCollectionFolder(collection.uid, null, true));
|
||||
dispatch(runCollectionFolder(collection.uid, null, true, Number(delay)));
|
||||
};
|
||||
|
||||
const runAgain = () => {
|
||||
dispatch(runCollectionFolder(collection.uid, runnerInfo.folderUid, runnerInfo.isRecursive));
|
||||
dispatch(runCollectionFolder(collection.uid, runnerInfo.folderUid, runnerInfo.isRecursive, Number(delay)));
|
||||
};
|
||||
|
||||
const resetRunner = () => {
|
||||
@@ -116,6 +117,20 @@ export default function RunnerResults({ collection }) {
|
||||
You have <span className="font-medium">{totalRequestsInCollection}</span> requests in this collection.
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<label>Delay (in ms)</label>
|
||||
<input
|
||||
type="number"
|
||||
className="block textbox mt-2 py-5"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={delay}
|
||||
onChange={(e) => setDelay(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary mt-6" onClick={runCollection}>
|
||||
Run Collection
|
||||
</button>
|
||||
@@ -167,10 +182,14 @@ export default function RunnerResults({ collection }) {
|
||||
</span>
|
||||
{item.status !== 'error' && item.status !== 'completed' ? (
|
||||
<IconRefresh className="animate-spin ml-1" size={18} strokeWidth={1.5} />
|
||||
) : (
|
||||
) : item.responseReceived?.status ? (
|
||||
<span className="text-xs link cursor-pointer" onClick={() => setSelectedItem(item)}>
|
||||
(<span className="mr-1">{get(item.responseReceived, 'status')}</span>
|
||||
<span>{get(item.responseReceived, 'statusText')}</span>)
|
||||
(<span className="mr-1">{item.responseReceived?.status}</span>
|
||||
<span>{item.responseReceived?.statusText}</span>)
|
||||
</span>
|
||||
) : (
|
||||
<span className="danger text-xs cursor-pointer" onClick={() => setSelectedItem(item)}>
|
||||
(request failed)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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,99 @@
|
||||
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, onClose }) => {
|
||||
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');
|
||||
onClose();
|
||||
})
|
||||
.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 excess 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.25 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 excess 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.25 onwards and is in beta. Please report any issues on github.
|
||||
</small>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default SecuritySettings;
|
||||
@@ -58,6 +58,7 @@ const CloneCollectionItem = ({ collection, item, onClose }) => {
|
||||
id="collection-item-name"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder='Enter Item name'
|
||||
ref={inputRef}
|
||||
className="block textbox mt-2 w-full"
|
||||
autoComplete="off"
|
||||
|
||||
@@ -2,6 +2,7 @@ import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
position: relative;
|
||||
height: 100%;
|
||||
|
||||
.copy-to-clipboard {
|
||||
position: absolute;
|
||||
|
||||
@@ -50,6 +50,7 @@ const CodeView = ({ language, item }) => {
|
||||
</CopyToClipboard>
|
||||
<CodeEditor
|
||||
readOnly
|
||||
collection={collection}
|
||||
value={snippet}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
theme={displayedTheme}
|
||||
|
||||
@@ -73,7 +73,7 @@ const GenerateCodeItem = ({ collection, item, onClose }) => {
|
||||
const interpolatedUrl = interpolateUrl({
|
||||
url: requestUrl,
|
||||
envVars,
|
||||
collectionVariables: collection.collectionVariables,
|
||||
runtimeVariables: collection.runtimeVariables,
|
||||
processEnvVars: collection.processEnvVariables
|
||||
});
|
||||
|
||||
|
||||
@@ -91,13 +91,13 @@ const Collections = () => {
|
||||
<input
|
||||
type="text"
|
||||
name="search"
|
||||
placeholder="search"
|
||||
id="search"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
className="block w-full pl-7 py-1 sm:text-sm"
|
||||
placeholder="search"
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value.toLowerCase())}
|
||||
/>
|
||||
@@ -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 (
|
||||
|
||||
@@ -60,7 +60,7 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`rounded bg-transparent px-2.5 py-1 text-xs font-semibold text-slate-900 dark:text-slate-50 shadow-sm ring-1 ring-inset ring-zinc-300 dark:ring-zinc-500 hover:bg-gray-50 dark:hover:bg-zinc-700
|
||||
className={`rounded bg-transparent px-2.5 py-1 text-xs font-semibold text-zinc-900 dark:text-zinc-50 shadow-sm ring-1 ring-inset ring-zinc-300 dark:ring-zinc-500 hover:bg-gray-50 dark:hover:bg-zinc-700
|
||||
${className}`}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -160,7 +160,7 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName, trans
|
||||
type="text"
|
||||
name="collectionLocation"
|
||||
readOnly={true}
|
||||
className="block textbox mt-2 w-full"
|
||||
className="block textbox mt-2 w-full cursor-pointer"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
|
||||
@@ -161,7 +161,16 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<Modal size="md" title="New Request" confirmText="Create" handleConfirm={onSubmit} handleCancel={onClose}>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<form
|
||||
className="bruno-form"
|
||||
onSubmit={formik.handleSubmit}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
formik.handleSubmit();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<label htmlFor="requestName" className="block font-semibold">
|
||||
Type
|
||||
@@ -220,6 +229,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
id="request-name"
|
||||
type="text"
|
||||
name="requestName"
|
||||
placeholder="Request Name"
|
||||
ref={inputRef}
|
||||
className="block textbox mt-2 w-full"
|
||||
autoComplete="off"
|
||||
@@ -252,6 +262,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
id="request-url"
|
||||
type="text"
|
||||
name="requestUrl"
|
||||
placeholder="Request URL"
|
||||
className="px-3 w-full "
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
|
||||
@@ -129,7 +129,7 @@ const Sidebar = () => {
|
||||
Star
|
||||
</GitHubButton> */}
|
||||
</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.18.0</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.24.0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { Component } from 'react';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import { getAllVariables } from 'utils/collections';
|
||||
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
|
||||
import { defineCodeMirrorBrunoVariablesMode, MaskedEditor } from 'utils/common/codemirror';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { IconEye, IconEyeOff } from '@tabler/icons';
|
||||
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
@@ -20,12 +21,28 @@ class SingleLineEditor extends Component {
|
||||
this.cachedValue = props.value || '';
|
||||
this.editorRef = React.createRef();
|
||||
this.variables = {};
|
||||
|
||||
this.state = {
|
||||
maskInput: props.isSecret || false // Always mask the input by default (if it's a secret)
|
||||
};
|
||||
}
|
||||
componentDidMount() {
|
||||
// Initialize CodeMirror as a single line editor
|
||||
/** @type {import("codemirror").Editor} */
|
||||
const variables = getAllVariables(this.props.collection, this.props.item);
|
||||
|
||||
const runHandler = () => {
|
||||
if (this.props.onRun) {
|
||||
this.props.onRun();
|
||||
}
|
||||
};
|
||||
const saveHandler = () => {
|
||||
if (this.props.onSave) {
|
||||
this.props.onSave();
|
||||
}
|
||||
};
|
||||
const noopHandler = () => {};
|
||||
|
||||
this.editor = CodeMirror(this.editorRef.current, {
|
||||
lineWrapping: false,
|
||||
lineNumbers: false,
|
||||
@@ -37,21 +54,9 @@ class SingleLineEditor extends Component {
|
||||
scrollbarStyle: null,
|
||||
tabindex: 0,
|
||||
extraKeys: {
|
||||
Enter: () => {
|
||||
if (this.props.onRun) {
|
||||
this.props.onRun();
|
||||
}
|
||||
},
|
||||
'Ctrl-Enter': () => {
|
||||
if (this.props.onRun) {
|
||||
this.props.onRun();
|
||||
}
|
||||
},
|
||||
'Cmd-Enter': () => {
|
||||
if (this.props.onRun) {
|
||||
this.props.onRun();
|
||||
}
|
||||
},
|
||||
Enter: runHandler,
|
||||
'Ctrl-Enter': runHandler,
|
||||
'Cmd-Enter': runHandler,
|
||||
'Alt-Enter': () => {
|
||||
if (this.props.allowNewlines) {
|
||||
this.editor.setValue(this.editor.getValue() + '\n');
|
||||
@@ -60,23 +65,11 @@ class SingleLineEditor extends Component {
|
||||
this.props.onRun();
|
||||
}
|
||||
},
|
||||
'Shift-Enter': () => {
|
||||
if (this.props.onRun) {
|
||||
this.props.onRun();
|
||||
}
|
||||
},
|
||||
'Cmd-S': () => {
|
||||
if (this.props.onSave) {
|
||||
this.props.onSave();
|
||||
}
|
||||
},
|
||||
'Ctrl-S': () => {
|
||||
if (this.props.onSave) {
|
||||
this.props.onSave();
|
||||
}
|
||||
},
|
||||
'Cmd-F': () => {},
|
||||
'Ctrl-F': () => {},
|
||||
'Shift-Enter': runHandler,
|
||||
'Cmd-S': saveHandler,
|
||||
'Ctrl-S': saveHandler,
|
||||
'Cmd-F': noopHandler,
|
||||
'Ctrl-F': noopHandler,
|
||||
// Tabbing disabled to make tabindex work
|
||||
Tab: false,
|
||||
'Shift-Tab': false
|
||||
@@ -93,8 +86,24 @@ class SingleLineEditor extends Component {
|
||||
this.editor.setValue(String(this.props.value) || '');
|
||||
this.editor.on('change', this._onEdit);
|
||||
this.addOverlay(variables);
|
||||
this._enableMaskedEditor(this.props.isSecret);
|
||||
this.setState({ maskInput: this.props.isSecret });
|
||||
}
|
||||
|
||||
/** Enable or disable masking the rendered content of the editor */
|
||||
_enableMaskedEditor = (enabled) => {
|
||||
if (typeof enabled !== 'boolean') return;
|
||||
|
||||
console.log('Enabling masked editor: ' + enabled);
|
||||
if (enabled == true) {
|
||||
if (!this.maskedEditor) this.maskedEditor = new MaskedEditor(this.editor, '*');
|
||||
this.maskedEditor.enable();
|
||||
} else {
|
||||
this.maskedEditor?.disable();
|
||||
this.maskedEditor = null;
|
||||
}
|
||||
};
|
||||
|
||||
_onEdit = () => {
|
||||
if (!this.ignoreChangeEvent && this.editor) {
|
||||
this.cachedValue = this.editor.getValue();
|
||||
@@ -122,6 +131,12 @@ class SingleLineEditor extends Component {
|
||||
this.cachedValue = String(this.props.value);
|
||||
this.editor.setValue(String(this.props.value) || '');
|
||||
}
|
||||
if (!isEqual(this.props.isSecret, prevProps.isSecret)) {
|
||||
// If the secret flag has changed, update the editor to reflect the change
|
||||
this._enableMaskedEditor(this.props.isSecret);
|
||||
// also set the maskInput flag to the new value
|
||||
this.setState({ maskInput: this.props.isSecret });
|
||||
}
|
||||
this.ignoreChangeEvent = false;
|
||||
}
|
||||
|
||||
@@ -131,12 +146,39 @@ class SingleLineEditor extends Component {
|
||||
|
||||
addOverlay = (variables) => {
|
||||
this.variables = variables;
|
||||
defineCodeMirrorBrunoVariablesMode(variables, 'text/plain');
|
||||
this.editor.setOption('mode', 'combinedmode');
|
||||
defineCodeMirrorBrunoVariablesMode(variables, 'text/plain', this.props.highlightPathParams);
|
||||
this.editor.setOption('mode', 'brunovariables');
|
||||
};
|
||||
|
||||
toggleVisibleSecret = () => {
|
||||
const isVisible = !this.state.maskInput;
|
||||
this.setState({ maskInput: isVisible });
|
||||
this._enableMaskedEditor(isVisible);
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Eye icon to show/hide the secret value
|
||||
* @returns ReactComponent The eye icon
|
||||
*/
|
||||
secretEye = (isSecret) => {
|
||||
return isSecret === true ? (
|
||||
<button className="mx-2" onClick={() => this.toggleVisibleSecret()}>
|
||||
{this.state.maskInput === true ? (
|
||||
<IconEyeOff size={18} strokeWidth={2} />
|
||||
) : (
|
||||
<IconEye size={18} strokeWidth={2} />
|
||||
)}
|
||||
</button>
|
||||
) : null;
|
||||
};
|
||||
|
||||
render() {
|
||||
return <StyledWrapper ref={this.editorRef} className="single-line-editor"></StyledWrapper>;
|
||||
return (
|
||||
<div className="flex flex-row justify-between w-full">
|
||||
<StyledWrapper ref={this.editorRef} className="single-line-editor grow" />
|
||||
{this.secretEye(this.props.isSecret)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default SingleLineEditor;
|
||||
|
||||
@@ -62,10 +62,10 @@ const EnvVariables = ({ collection, theme }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const CollectionVariables = ({ collection, theme }) => {
|
||||
const collectionVariablesFound = Object.keys(collection.collectionVariables).length > 0;
|
||||
const RuntimeVariables = ({ collection, theme }) => {
|
||||
const runtimeVariablesFound = Object.keys(collection.runtimeVariables).length > 0;
|
||||
|
||||
const collectionVariableArray = Object.entries(collection.collectionVariables).map(([name, value]) => ({
|
||||
const runtimeVariableArray = Object.entries(collection.runtimeVariables).map(([name, value]) => ({
|
||||
name,
|
||||
value,
|
||||
secret: false
|
||||
@@ -73,11 +73,11 @@ const CollectionVariables = ({ collection, theme }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="font-semibold mb-2">Collection Variables</h1>
|
||||
{collectionVariablesFound ? (
|
||||
<KeyValueExplorer data={collectionVariableArray} theme={theme} />
|
||||
<h1 className="font-semibold mb-2">Runtime Variables</h1>
|
||||
{runtimeVariablesFound ? (
|
||||
<KeyValueExplorer data={runtimeVariableArray} theme={theme} />
|
||||
) : (
|
||||
<div className="muted text-xs">No collection variables found</div>
|
||||
<div className="muted text-xs">No runtime variables found</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
@@ -90,13 +90,13 @@ const VariablesEditor = ({ collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="px-4 py-4">
|
||||
<CollectionVariables collection={collection} theme={reactInspectorTheme} />
|
||||
<RuntimeVariables collection={collection} theme={reactInspectorTheme} />
|
||||
<EnvVariables collection={collection} theme={reactInspectorTheme} />
|
||||
|
||||
<div className="mt-8 muted text-xs">
|
||||
Note: As of today, collection variables can only be set via the API -{' '}
|
||||
<span className="font-medium">getVar()</span> and <span className="font-medium">setVar()</span>. <br />
|
||||
In the next release, we will add a UI to set and modify collection variables.
|
||||
Note: As of today, runtime variables can only be set via the API - <span className="font-medium">getVar()</span>{' '}
|
||||
and <span className="font-medium">setVar()</span>. <br />
|
||||
In the next release, we will add a UI to set and modify runtime variables.
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -100,6 +100,11 @@ const GlobalStyle = createGlobalStyle`
|
||||
}
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
color: ${(props) => props.theme.input.placeholder.color};
|
||||
opacity: ${(props) => props.theme.input.placeholder.opacity};
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
@@ -168,7 +173,6 @@ const GlobalStyle = createGlobalStyle`
|
||||
// (macos scrollbar styling is the ideal style reference)
|
||||
@media not all and (pointer: coarse) {
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: ${(props) => props.theme.scrollbar.color};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { get } from 'lodash';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { refreshScreenWidth } from 'providers/ReduxStore/slices/app';
|
||||
import ConfirmAppClose from './ConfirmAppClose';
|
||||
@@ -18,6 +19,13 @@ export const AppProvider = (props) => {
|
||||
dispatch(refreshScreenWidth());
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const platform = get(navigator, 'platform', '');
|
||||
if(platform && platform.toLowerCase().indexOf('mac') > -1) {
|
||||
document.body.classList.add('os-mac');
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
dispatch(refreshScreenWidth());
|
||||
|
||||
@@ -60,7 +60,7 @@ const trackStart = () => {
|
||||
event: 'start',
|
||||
properties: {
|
||||
os: platformLib.os.family,
|
||||
version: '1.18.0'
|
||||
version: '1.24.0'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -33,7 +33,8 @@ import {
|
||||
requestCancelled,
|
||||
resetRunResults,
|
||||
responseReceived,
|
||||
updateLastAction
|
||||
updateLastAction,
|
||||
setCollectionSecurityConfig
|
||||
} from './index';
|
||||
|
||||
import { each } from 'lodash';
|
||||
@@ -192,10 +193,7 @@ export const sendCollectionOauth2Request = (collectionUid, itemUid) => (dispatch
|
||||
|
||||
const environment = findEnvironmentInCollection(collectionCopy, collection.activeEnvironmentUid);
|
||||
|
||||
const externalSecrets = getExternalCollectionSecretsForActiveEnvironment({ collection });
|
||||
const secretVariables = getFormattedCollectionSecretVariables({ externalSecrets });
|
||||
|
||||
_sendCollectionOauth2Request(collection, environment, collectionCopy.collectionVariables, itemUid, secretVariables)
|
||||
_sendCollectionOauth2Request(collection, environment, collectionCopy.runtimeVariables)
|
||||
.then((response) => {
|
||||
if (response?.data?.error) {
|
||||
toast.error(response?.data?.error);
|
||||
@@ -224,7 +222,7 @@ export const sendRequest = (item, collectionUid) => (dispatch, getState) => {
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
|
||||
const environment = findEnvironmentInCollection(collectionCopy, collectionCopy.activeEnvironmentUid);
|
||||
sendNetworkRequest(itemCopy, collectionCopy, environment, collectionCopy.collectionVariables)
|
||||
sendNetworkRequest(itemCopy, collectionCopy, environment, collectionCopy.runtimeVariables)
|
||||
.then((response) => {
|
||||
return dispatch(
|
||||
responseReceived({
|
||||
@@ -284,7 +282,7 @@ export const cancelRunnerExecution = (cancelTokenUid) => (dispatch) => {
|
||||
cancelNetworkRequest(cancelTokenUid).catch((err) => console.log(err));
|
||||
};
|
||||
|
||||
export const runCollectionFolder = (collectionUid, folderUid, recursive) => (dispatch, getState) => {
|
||||
export const runCollectionFolder = (collectionUid, folderUid, recursive, delay) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
|
||||
@@ -314,8 +312,9 @@ export const runCollectionFolder = (collectionUid, folderUid, recursive) => (dis
|
||||
folder,
|
||||
collectionCopy,
|
||||
environment,
|
||||
collectionCopy.collectionVariables,
|
||||
recursive
|
||||
collectionCopy.runtimeVariables,
|
||||
recursive,
|
||||
delay
|
||||
)
|
||||
.then(resolve)
|
||||
.catch((err) => {
|
||||
@@ -1036,16 +1035,18 @@ export const openCollectionEvent = (uid, pathname, brunoConfig) => (dispatch, ge
|
||||
name: brunoConfig.name,
|
||||
pathname: pathname,
|
||||
items: [],
|
||||
collectionVariables: {},
|
||||
runtimeVariables: {},
|
||||
brunoConfig: brunoConfig
|
||||
};
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1110,3 +1111,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);
|
||||
@@ -200,7 +205,7 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
},
|
||||
scriptEnvironmentUpdateEvent: (state, action) => {
|
||||
const { collectionUid, envVariables, collectionVariables } = action.payload;
|
||||
const { collectionUid, envVariables, runtimeVariables } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if (collection) {
|
||||
@@ -230,7 +235,7 @@ export const collectionsSlice = createSlice({
|
||||
});
|
||||
}
|
||||
|
||||
collection.collectionVariables = collectionVariables;
|
||||
collection.runtimeVariables = runtimeVariables;
|
||||
}
|
||||
},
|
||||
processEnvUpdateEvent: (state, action) => {
|
||||
@@ -717,7 +722,7 @@ export const collectionsSlice = createSlice({
|
||||
uid: uuid(),
|
||||
type: action.payload.type,
|
||||
name: '',
|
||||
value: '',
|
||||
value: action.payload.value,
|
||||
description: '',
|
||||
enabled: true
|
||||
});
|
||||
@@ -1179,7 +1184,6 @@ export const collectionsSlice = createSlice({
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
type: 'request',
|
||||
enabled: true
|
||||
});
|
||||
set(folder, 'root.request.vars.req', vars);
|
||||
@@ -1189,7 +1193,6 @@ export const collectionsSlice = createSlice({
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
type: 'response',
|
||||
enabled: true
|
||||
});
|
||||
set(folder, 'root.request.vars.res', vars);
|
||||
@@ -1312,7 +1315,7 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
|
||||
if (isFolderRoot) {
|
||||
const folderPath = path.dirname(file.meta.pathname);
|
||||
const folderPath = getDirectoryName(file.meta.pathname);
|
||||
const folderItem = findItemInCollectionByPathname(collection, folderPath);
|
||||
if (folderItem) {
|
||||
folderItem.root = file.data;
|
||||
@@ -1568,29 +1571,29 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
|
||||
if (type === 'request-sent') {
|
||||
const item = collection.runnerResult.items.find((i) => i.uid === request.uid);
|
||||
const item = collection.runnerResult.items.findLast((i) => i.uid === request.uid);
|
||||
item.status = 'running';
|
||||
item.requestSent = action.payload.requestSent;
|
||||
}
|
||||
|
||||
if (type === 'response-received') {
|
||||
const item = collection.runnerResult.items.find((i) => i.uid === request.uid);
|
||||
const item = collection.runnerResult.items.findLast((i) => i.uid === request.uid);
|
||||
item.status = 'completed';
|
||||
item.responseReceived = action.payload.responseReceived;
|
||||
}
|
||||
|
||||
if (type === 'test-results') {
|
||||
const item = collection.runnerResult.items.find((i) => i.uid === request.uid);
|
||||
const item = collection.runnerResult.items.findLast((i) => i.uid === request.uid);
|
||||
item.testResults = action.payload.testResults;
|
||||
}
|
||||
|
||||
if (type === 'assertion-results') {
|
||||
const item = collection.runnerResult.items.find((i) => i.uid === request.uid);
|
||||
const item = collection.runnerResult.items.findLast((i) => i.uid === request.uid);
|
||||
item.assertionResults = action.payload.assertionResults;
|
||||
}
|
||||
|
||||
if (type === 'error') {
|
||||
const item = collection.runnerResult.items.find((i) => i.uid === request.uid);
|
||||
const item = collection.runnerResult.items.findLast((i) => i.uid === request.uid);
|
||||
item.error = action.payload.error;
|
||||
item.responseReceived = action.payload.responseReceived;
|
||||
item.status = 'error';
|
||||
@@ -1624,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;
|
||||
|
||||
@@ -58,6 +58,15 @@ body::-webkit-scrollbar-thumb,
|
||||
border-radius: 5rem;
|
||||
}
|
||||
|
||||
/*
|
||||
* Mac-specific scrollbar styling
|
||||
* This ensures that scrollbars are only visible when the user starts to scroll,
|
||||
* providing a cleaner and more minimalistic appearance.
|
||||
*/
|
||||
body.os-mac * {
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
/*
|
||||
* todo: this will be supported in the future to be changed via applying a theme
|
||||
* making all the checkboxes and radios bigger
|
||||
|
||||
@@ -20,7 +20,11 @@ const darkTheme = {
|
||||
input: {
|
||||
bg: 'rgb(65, 65, 65)',
|
||||
border: 'rgb(65, 65, 65)',
|
||||
focusBorder: 'rgb(65, 65, 65)'
|
||||
focusBorder: 'rgb(65, 65, 65)',
|
||||
placeholder: {
|
||||
color: '#a2a2a2',
|
||||
opacity: 0.75
|
||||
}
|
||||
},
|
||||
|
||||
variables: {
|
||||
@@ -154,7 +158,7 @@ const darkTheme = {
|
||||
modal: {
|
||||
title: {
|
||||
color: '#ccc',
|
||||
bg: 'rgb(48, 48, 49)',
|
||||
bg: 'rgb(38, 38, 39)',
|
||||
iconColor: '#ccc'
|
||||
},
|
||||
body: {
|
||||
|
||||
@@ -20,7 +20,11 @@ const lightTheme = {
|
||||
input: {
|
||||
bg: 'white',
|
||||
border: '#ccc',
|
||||
focusBorder: '#8b8b8b'
|
||||
focusBorder: '#8b8b8b',
|
||||
placeholder: {
|
||||
color: '#a2a2a2',
|
||||
opacity: 0.8
|
||||
}
|
||||
},
|
||||
|
||||
menubar: {
|
||||
|
||||
@@ -2,10 +2,16 @@ const createContentType = (mode) => {
|
||||
switch (mode) {
|
||||
case 'json':
|
||||
return 'application/json';
|
||||
case 'text':
|
||||
return 'text/plain';
|
||||
case 'xml':
|
||||
return 'application/xml';
|
||||
case 'sparql':
|
||||
return 'application/sparql-query';
|
||||
case 'formUrlEncoded':
|
||||
return 'application/x-www-form-urlencoded';
|
||||
case 'graphql':
|
||||
return 'application/json';
|
||||
case 'multipartForm':
|
||||
return 'multipart/form-data';
|
||||
default:
|
||||
@@ -13,13 +19,19 @@ const createContentType = (mode) => {
|
||||
}
|
||||
};
|
||||
|
||||
const createHeaders = (headers) => {
|
||||
return headers
|
||||
const createHeaders = (request, headers) => {
|
||||
const enabledHeaders = headers
|
||||
.filter((header) => header.enabled)
|
||||
.map((header) => ({
|
||||
name: header.name,
|
||||
value: header.value
|
||||
}));
|
||||
|
||||
const contentType = createContentType(request.body?.mode);
|
||||
if (contentType !== '') {
|
||||
enabledHeaders.push({ name: 'content-type', value: contentType });
|
||||
}
|
||||
return enabledHeaders;
|
||||
};
|
||||
|
||||
const createQuery = (queryParams = []) => {
|
||||
@@ -54,7 +66,7 @@ export const buildHarRequest = ({ request, headers }) => {
|
||||
url: encodeURI(request.url),
|
||||
httpVersion: 'HTTP/1.1',
|
||||
cookies: [],
|
||||
headers: createHeaders(headers),
|
||||
headers: createHeaders(request, headers),
|
||||
queryString: createQuery(request.params),
|
||||
postData: createPostData(request.body),
|
||||
headersSize: 0,
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
export const MimeTypes = [
|
||||
'application/atom+xml',
|
||||
'application/ecmascript',
|
||||
'application/json',
|
||||
'application/vnd.api+json',
|
||||
'application/javascript',
|
||||
'application/octet-stream',
|
||||
'application/ogg',
|
||||
'application/pdf',
|
||||
'application/postscript',
|
||||
'application/rdf+xml',
|
||||
'application/rss+xml',
|
||||
'application/soap+xml',
|
||||
'application/font-woff',
|
||||
'application/x-yaml',
|
||||
'application/xhtml+xml',
|
||||
'application/xml',
|
||||
'application/xml-dtd',
|
||||
'application/xop+xml',
|
||||
'application/zip',
|
||||
'application/gzip',
|
||||
'application/graphql',
|
||||
'application/x-www-form-urlencoded',
|
||||
'audio/basic',
|
||||
'audio/L24',
|
||||
'audio/mp4',
|
||||
'audio/mpeg',
|
||||
'audio/ogg',
|
||||
'audio/vorbis',
|
||||
'audio/vnd.rn-realaudio',
|
||||
'audio/vnd.wave',
|
||||
'audio/webm',
|
||||
'image/gif',
|
||||
'image/jpeg',
|
||||
'image/pjpeg',
|
||||
'image/png',
|
||||
'image/svg+xml',
|
||||
'image/tiff',
|
||||
'message/http',
|
||||
'message/imdn+xml',
|
||||
'message/partial',
|
||||
'message/rfc822',
|
||||
'multipart/mixed',
|
||||
'multipart/alternative',
|
||||
'multipart/related',
|
||||
'multipart/form-data',
|
||||
'multipart/signed',
|
||||
'multipart/encrypted',
|
||||
'text/cmd',
|
||||
'text/css',
|
||||
'text/csv',
|
||||
'text/html',
|
||||
'text/plain',
|
||||
'text/vcard',
|
||||
'text/xml'
|
||||
];
|
||||
@@ -31,8 +31,8 @@ if (!SERVER_RENDERED) {
|
||||
if (str.startsWith('{{')) {
|
||||
variableName = str.replace('{{', '').replace('}}', '').trim();
|
||||
variableValue = interpolate(get(options.variables, variableName), options.variables);
|
||||
} else if (str.startsWith(':')) {
|
||||
variableName = str.replace(':', '').trim();
|
||||
} else if (str.startsWith('/:')) {
|
||||
variableName = str.replace('/:', '').trim();
|
||||
variableValue =
|
||||
options.variables && options.variables.pathParams ? options.variables.pathParams[variableName] : undefined;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import isEqual from 'lodash/isEqual';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { uuid } from 'utils/common';
|
||||
import path from 'path';
|
||||
import slash from 'utils/common/slash';
|
||||
|
||||
const replaceTabsWithSpaces = (str, numSpaces = 2) => {
|
||||
if (!str || !str.length || !isString(str)) {
|
||||
@@ -98,7 +99,7 @@ export const findCollectionByItemUid = (collections, itemUid) => {
|
||||
};
|
||||
|
||||
export const findItemByPathname = (items = [], pathname) => {
|
||||
return find(items, (i) => i.pathname === pathname);
|
||||
return find(items, (i) => slash(i.pathname) === slash(pathname));
|
||||
};
|
||||
|
||||
export const findItemInCollectionByPathname = (collection, pathname) => {
|
||||
@@ -380,6 +381,55 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
}
|
||||
}
|
||||
|
||||
if (si.type == 'folder' && si?.root) {
|
||||
di.root = {
|
||||
request: {}
|
||||
};
|
||||
|
||||
let { request, meta } = si?.root || {};
|
||||
let { headers, script = {}, vars = {}, tests } = request || {};
|
||||
|
||||
// folder level headers
|
||||
if (headers?.length) {
|
||||
di.root.request.headers = headers;
|
||||
}
|
||||
// folder level script
|
||||
if (Object.keys(script)?.length) {
|
||||
di.root.request.script = {};
|
||||
if (script?.req?.length) {
|
||||
di.root.request.script.req = script?.req;
|
||||
}
|
||||
if (script?.res?.length) {
|
||||
di.root.request.script.res = script?.res;
|
||||
}
|
||||
}
|
||||
// folder level vars
|
||||
if (Object.keys(vars)?.length) {
|
||||
di.root.request.vars = {};
|
||||
if (vars?.req?.length) {
|
||||
di.root.request.vars.req = vars?.req;
|
||||
}
|
||||
if (vars?.res?.length) {
|
||||
di.root.request.vars.res = vars?.res;
|
||||
}
|
||||
}
|
||||
// folder level tests
|
||||
if (tests?.length) {
|
||||
di.root.request.tests = tests;
|
||||
}
|
||||
|
||||
if (meta?.name) {
|
||||
di.root.meta = {};
|
||||
di.root.meta.name = meta?.name;
|
||||
}
|
||||
if (!Object.keys(di.root.request)?.length) {
|
||||
delete di.root.request;
|
||||
}
|
||||
if (!Object.keys(di.root)?.length) {
|
||||
delete di.root;
|
||||
}
|
||||
}
|
||||
|
||||
if (si.type === 'js') {
|
||||
di.fileContent = si.raw;
|
||||
}
|
||||
@@ -403,6 +453,60 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
collectionToSave.activeEnvironmentUid = collection.activeEnvironmentUid;
|
||||
collectionToSave.environments = collection.environments || [];
|
||||
|
||||
collectionToSave.root = {
|
||||
request: {}
|
||||
};
|
||||
|
||||
let { request, docs, meta } = collection?.root || {};
|
||||
let { auth, headers, script = {}, vars = {}, tests } = request || {};
|
||||
|
||||
// collection level auth
|
||||
if (auth?.mode) {
|
||||
collectionToSave.root.request.auth = auth;
|
||||
}
|
||||
// collection level headers
|
||||
if (headers?.length) {
|
||||
collectionToSave.root.request.headers = headers;
|
||||
}
|
||||
// collection level script
|
||||
if (Object.keys(script)?.length) {
|
||||
collectionToSave.root.request.script = {};
|
||||
if (script?.req?.length) {
|
||||
collectionToSave.root.request.script.req = script?.req;
|
||||
}
|
||||
if (script?.res?.length) {
|
||||
collectionToSave.root.request.script.res = script?.res;
|
||||
}
|
||||
}
|
||||
// collection level vars
|
||||
if (Object.keys(vars)?.length) {
|
||||
collectionToSave.root.request.vars = {};
|
||||
if (vars?.req?.length) {
|
||||
collectionToSave.root.request.vars.req = vars?.req;
|
||||
}
|
||||
if (vars?.res?.length) {
|
||||
collectionToSave.root.request.vars.res = vars?.res;
|
||||
}
|
||||
}
|
||||
// collection level tests
|
||||
if (tests?.length) {
|
||||
collectionToSave.root.request.tests = tests;
|
||||
}
|
||||
// collection level docs
|
||||
if (docs?.length) {
|
||||
collectionToSave.root.docs = docs;
|
||||
}
|
||||
if (meta?.name) {
|
||||
collectionToSave.root.meta = {};
|
||||
collectionToSave.root.meta.name = meta?.name;
|
||||
}
|
||||
if (!Object.keys(collectionToSave.root.request)?.length) {
|
||||
delete collectionToSave.root.request;
|
||||
}
|
||||
if (!Object.keys(collectionToSave.root)?.length) {
|
||||
delete collectionToSave.root;
|
||||
}
|
||||
|
||||
collectionToSave.brunoConfig = cloneDeep(collection?.brunoConfig);
|
||||
|
||||
// delete proxy password if present
|
||||
@@ -684,11 +788,17 @@ export const getTotalRequestCountInCollection = (collection) => {
|
||||
|
||||
export const getAllVariables = (collection, item) => {
|
||||
const environmentVariables = getEnvironmentVariables(collection);
|
||||
let requestVariables = {};
|
||||
if (item?.request) {
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
||||
requestVariables = mergeFolderLevelVars(item?.request, requestTreePath);
|
||||
}
|
||||
const pathParams = getPathParams(item);
|
||||
|
||||
return {
|
||||
...environmentVariables,
|
||||
...collection.collectionVariables,
|
||||
...requestVariables,
|
||||
...collection.runtimeVariables,
|
||||
pathParams: {
|
||||
...pathParams
|
||||
},
|
||||
@@ -710,3 +820,36 @@ export const maskInputValue = (value) => {
|
||||
.map(() => '*')
|
||||
.join('');
|
||||
};
|
||||
|
||||
const getTreePathFromCollectionToItem = (collection, _item) => {
|
||||
let path = [];
|
||||
let item = findItemInCollection(collection, _item?.uid);
|
||||
while (item) {
|
||||
path.unshift(item);
|
||||
item = findParentItemInCollection(collection, item?.uid);
|
||||
}
|
||||
return path;
|
||||
};
|
||||
|
||||
const mergeFolderLevelVars = (request, requestTreePath = []) => {
|
||||
let requestVariables = {};
|
||||
for (let i of requestTreePath) {
|
||||
if (i.type === 'folder') {
|
||||
let vars = get(i, 'root.request.vars.req', []);
|
||||
vars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
requestVariables[_var.name] = _var.value;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
let vars = get(i, 'request.vars.req', []);
|
||||
vars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
requestVariables[_var.name] = _var.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return requestVariables;
|
||||
};
|
||||
|
||||
@@ -12,8 +12,67 @@ const pathFoundInVariables = (path, obj) => {
|
||||
return value !== undefined;
|
||||
};
|
||||
|
||||
export const defineCodeMirrorBrunoVariablesMode = (variables, mode) => {
|
||||
CodeMirror.defineMode('combinedmode', function (config, parserConfig) {
|
||||
/**
|
||||
* Changes the render behaviour for a given CodeMirror editor.
|
||||
* Replaces all **rendered** characters, not the actual value, with the provided character.
|
||||
*/
|
||||
export class MaskedEditor {
|
||||
/**
|
||||
* @param {import('codemirror').Editor} editor CodeMirror editor instance
|
||||
* @param {string} maskChar Target character being applied to all content
|
||||
*/
|
||||
constructor(editor, maskChar) {
|
||||
this.editor = editor;
|
||||
this.maskChar = maskChar;
|
||||
this.enabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set and apply new masking character
|
||||
*/
|
||||
enable = () => {
|
||||
this.enabled = true;
|
||||
this.editor.setValue(this.editor.getValue());
|
||||
this.editor.on('inputRead', this.maskContent);
|
||||
this.update();
|
||||
};
|
||||
|
||||
/** Disables masking of the editor field. */
|
||||
disable = () => {
|
||||
this.enabled = false;
|
||||
this.editor.off('inputRead', this.maskContent);
|
||||
this.editor.setValue(this.editor.getValue());
|
||||
};
|
||||
|
||||
/** Updates the rendered content if enabled. */
|
||||
update = () => {
|
||||
if (this.enabled) this.maskContent();
|
||||
};
|
||||
|
||||
/** Replaces all rendered characters, with the provided character. */
|
||||
maskContent = () => {
|
||||
const content = this.editor.getValue();
|
||||
this.editor.operation(() => {
|
||||
// Clear previous masked text
|
||||
this.editor.getAllMarks().forEach((mark) => mark.clear());
|
||||
// Apply new masked text
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
if (content[i] !== '\n') {
|
||||
const maskedNode = document.createTextNode(this.maskChar);
|
||||
this.editor.markText(
|
||||
{ line: this.editor.posFromIndex(i).line, ch: this.editor.posFromIndex(i).ch },
|
||||
{ line: this.editor.posFromIndex(i + 1).line, ch: this.editor.posFromIndex(i + 1).ch },
|
||||
{ replacedWith: maskedNode, handleMouseEvents: true }
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export const defineCodeMirrorBrunoVariablesMode = (_variables, mode, highlightPathParams) => {
|
||||
CodeMirror.defineMode('brunovariables', function (config, parserConfig) {
|
||||
const { pathParams = {}, ...variables } = _variables || {};
|
||||
const variablesOverlay = {
|
||||
token: function (stream) {
|
||||
if (stream.match('{{', true)) {
|
||||
@@ -37,13 +96,13 @@ export const defineCodeMirrorBrunoVariablesMode = (variables, mode) => {
|
||||
|
||||
const urlPathParamsOverlay = {
|
||||
token: function (stream) {
|
||||
if (stream.match(':', true)) {
|
||||
if (stream.match('/:', true)) {
|
||||
let ch;
|
||||
let word = '';
|
||||
while ((ch = stream.next()) != null) {
|
||||
if (ch === '/' || ch === '?' || ch === '&' || ch === '=') {
|
||||
stream.backUp(1);
|
||||
const found = pathFoundInVariables(word, variables?.pathParams);
|
||||
const found = pathFoundInVariables(word, pathParams);
|
||||
const status = found ? 'valid' : 'invalid';
|
||||
const randomClass = `random-${(Math.random() + 1).toString(36).substring(9)}`;
|
||||
return `variable-${status} ${randomClass}`;
|
||||
@@ -53,21 +112,24 @@ export const defineCodeMirrorBrunoVariablesMode = (variables, mode) => {
|
||||
|
||||
// If we've consumed all characters and the word is not empty, it might be a path parameter at the end of the URL.
|
||||
if (word) {
|
||||
const found = pathFoundInVariables(word, variables?.pathParams);
|
||||
const found = pathFoundInVariables(word, pathParams);
|
||||
const status = found ? 'valid' : 'invalid';
|
||||
const randomClass = `random-${(Math.random() + 1).toString(36).substring(9)}`;
|
||||
return `variable-${status} ${randomClass}`;
|
||||
}
|
||||
}
|
||||
stream.skipTo(':') || stream.skipToEnd();
|
||||
stream.skipTo('/:') || stream.skipToEnd();
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return CodeMirror.overlayMode(
|
||||
CodeMirror.overlayMode(CodeMirror.getMode(config, parserConfig.backdrop || mode), variablesOverlay),
|
||||
urlPathParamsOverlay
|
||||
);
|
||||
let baseMode = CodeMirror.overlayMode(CodeMirror.getMode(config, parserConfig.backdrop || mode), variablesOverlay);
|
||||
|
||||
if (highlightPathParams) {
|
||||
return CodeMirror.overlayMode(baseMode, urlPathParamsOverlay);
|
||||
} else {
|
||||
return baseMode;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -72,11 +72,10 @@ const parseCurlCommand = (curlCommand) => {
|
||||
parsedArguments.header.forEach((header) => {
|
||||
if (header.indexOf('Cookie') !== -1) {
|
||||
cookieString = header;
|
||||
} else {
|
||||
const components = header.split(/:(.*)/);
|
||||
if (components[1]) {
|
||||
headers[components[0]] = components[1].trim();
|
||||
}
|
||||
}
|
||||
const components = header.split(/:(.*)/);
|
||||
if (components[1]) {
|
||||
headers[components[0]] = components[1].trim();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -119,15 +118,16 @@ const parseCurlCommand = (curlCommand) => {
|
||||
cookies = cookie.parse(cookieString.replace(/^Cookie: /gi, ''), cookieParseOptions);
|
||||
}
|
||||
let method;
|
||||
if (parsedArguments.X === 'POST') {
|
||||
let parsedMethodArgument = parsedArguments.X || parsedArguments.request || parsedArguments.T;
|
||||
if (parsedMethodArgument === 'POST') {
|
||||
method = 'post';
|
||||
} else if (parsedArguments.X === 'PUT' || parsedArguments.T) {
|
||||
} else if (parsedMethodArgument === 'PUT') {
|
||||
method = 'put';
|
||||
} else if (parsedArguments.X === 'PATCH') {
|
||||
} else if (parsedMethodArgument === 'PATCH') {
|
||||
method = 'patch';
|
||||
} else if (parsedArguments.X === 'DELETE') {
|
||||
} else if (parsedMethodArgument === 'DELETE') {
|
||||
method = 'delete';
|
||||
} else if (parsedArguments.X === 'OPTIONS') {
|
||||
} else if (parsedMethodArgument === 'OPTIONS') {
|
||||
method = 'options';
|
||||
} else if (
|
||||
(parsedArguments.d ||
|
||||
|
||||
@@ -57,10 +57,20 @@ const parsePostmanEnvironment = (str) => {
|
||||
|
||||
const importEnvironment = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fileDialog({ accept: 'application/json' })
|
||||
.then(readFile)
|
||||
.then(parsePostmanEnvironment)
|
||||
.then((environment) => resolve(environment))
|
||||
fileDialog({ multiple: true, accept: 'application/json' })
|
||||
.then((files) => {
|
||||
return Promise.all(
|
||||
Object.values(files ?? {}).map((file) =>
|
||||
readFile([file])
|
||||
.then(parsePostmanEnvironment)
|
||||
.catch((err) => {
|
||||
console.error(`Error processing file: ${file.name || 'undefined'}`, err);
|
||||
throw err;
|
||||
})
|
||||
)
|
||||
);
|
||||
})
|
||||
.then((environments) => resolve(environments))
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
reject(new BrunoError('Import Environment failed'));
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { safeStringifyJSON } from 'utils/common';
|
||||
|
||||
export const sendNetworkRequest = async (item, collection, environment, collectionVariables) => {
|
||||
export const sendNetworkRequest = async (item, collection, environment, runtimeVariables) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (['http-request', 'graphql-request'].includes(item.type)) {
|
||||
sendHttpRequest(item, collection, environment, collectionVariables)
|
||||
sendHttpRequest(item, collection, environment, runtimeVariables)
|
||||
.then((response) => {
|
||||
resolve({
|
||||
state: 'success',
|
||||
@@ -22,22 +22,22 @@ export const sendNetworkRequest = async (item, collection, environment, collecti
|
||||
});
|
||||
};
|
||||
|
||||
const sendHttpRequest = async (item, collection, environment, collectionVariables) => {
|
||||
const sendHttpRequest = async (item, collection, environment, runtimeVariables) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
ipcRenderer
|
||||
.invoke('send-http-request', item, collection, environment, collectionVariables)
|
||||
.invoke('send-http-request', item, collection, environment, runtimeVariables)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
export const sendCollectionOauth2Request = async (collection, environment, collectionVariables) => {
|
||||
export const sendCollectionOauth2Request = async (collection, environment, runtimeVariables) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer
|
||||
.invoke('send-collection-oauth2-request', collection, environment, collectionVariables)
|
||||
.invoke('send-collection-oauth2-request', collection, environment, runtimeVariables)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
|
||||
@@ -107,14 +107,14 @@ export const isValidUrl = (url) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const interpolateUrl = ({ url, envVars, collectionVariables, processEnvVars }) => {
|
||||
export const interpolateUrl = ({ url, envVars, runtimeVariables, processEnvVars }) => {
|
||||
if (!url || !url.length || typeof url !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
return interpolate(url, {
|
||||
...envVars,
|
||||
...collectionVariables,
|
||||
...runtimeVariables,
|
||||
process: {
|
||||
env: {
|
||||
...processEnvVars
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user