diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6eb759301..468bf9ebc 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -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 @@ -50,6 +52,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 @@ -71,15 +75,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 diff --git a/docs/contributing/contributing_fr.md b/docs/contributing/contributing_fr.md index d3cf0e79b..9bc867d08 100644 --- a/docs/contributing/contributing_fr.md +++ b/docs/contributing/contributing_fr.md @@ -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 diff --git a/docs/contributing/contributing_sk.md b/docs/contributing/contributing_sk.md new file mode 100644 index 000000000..e2c4aabe7 --- /dev/null +++ b/docs/contributing/contributing_sk.md @@ -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 diff --git a/docs/readme/readme_pt_br.md b/docs/readme/readme_pt_br.md index 577ff1d42..0d390dbd4 100644 --- a/docs/readme/readme_pt_br.md +++ b/docs/readme/readme_pt_br.md @@ -103,6 +103,12 @@ Ou qualquer sistema de controle de versão de sua escolha. +#### Apoiadores Bronze + + + + + ### Links Importantes 📌 - [Nossa Visão de Longo Prazo](https://github.com/usebruno/bruno/discussions/269) diff --git a/package-lock.json b/package-lock.json index 6792725f9..e021bf38f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19668,7 +19668,7 @@ }, "packages/bruno-electron": { "name": "bruno", - "version": "v1.21.0", + "version": "v1.23.1", "dependencies": { "@aws-sdk/credential-providers": "3.525.0", "@usebruno/common": "0.1.0", diff --git a/packages/bruno-app/src/components/CodeEditor/StyledWrapper.js b/packages/bruno-app/src/components/CodeEditor/StyledWrapper.js index 3623d406d..232f964ae 100644 --- a/packages/bruno-app/src/components/CodeEditor/StyledWrapper.js +++ b/packages/bruno-app/src/components/CodeEditor/StyledWrapper.js @@ -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, diff --git a/packages/bruno-app/src/components/CodeEditor/index.js b/packages/bruno-app/src/components/CodeEditor/index.js index ddc38aff8..85f6c2da4 100644 --- a/packages/bruno-app/src/components/CodeEditor/index.js +++ b/packages/bruno-app/src/components/CodeEditor/index.js @@ -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'); @@ -121,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, @@ -169,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) => { @@ -289,7 +316,7 @@ export default class CodeEditor extends React.Component { } return ( { diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/AwsV4Auth/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/AwsV4Auth/index.js index bc9cb67b5..38fae3447 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/AwsV4Auth/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/AwsV4Auth/index.js @@ -138,6 +138,7 @@ const AwsV4Auth = ({ collection }) => { onSave={handleSave} onChange={(val) => handleSecretAccessKeyChange(val)} collection={collection} + isSecret={true} /> diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/BasicAuth/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/BasicAuth/index.js index b09cf1175..3c29895ed 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/BasicAuth/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/BasicAuth/index.js @@ -62,6 +62,7 @@ const BasicAuth = ({ collection }) => { onSave={handleSave} onChange={(val) => handlePasswordChange(val)} collection={collection} + isSecret={true} /> diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/BearerAuth/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/BearerAuth/index.js index a8b341a8c..82f8be12c 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/BearerAuth/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/BearerAuth/index.js @@ -37,6 +37,7 @@ const BearerAuth = ({ collection }) => { onSave={handleSave} onChange={(val) => handleTokenChange(val)} collection={collection} + isSecret={true} /> diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/DigestAuth/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/DigestAuth/index.js index 3e084c06d..5ac6b1e26 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/DigestAuth/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/DigestAuth/index.js @@ -62,6 +62,7 @@ const DigestAuth = ({ collection }) => { onSave={handleSave} onChange={(val) => handlePasswordChange(val)} collection={collection} + isSecret={true} /> diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/AuthorizationCode/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/AuthorizationCode/index.js index 8ec71a69a..8f3dc1601 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/AuthorizationCode/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/AuthorizationCode/index.js @@ -78,7 +78,7 @@ const OAuth2AuthorizationCode = ({ collection }) => { return ( {inputsConfig.map((input) => { - const { key, label } = input; + const { key, label, isSecret } = input; return (
@@ -90,6 +90,7 @@ const OAuth2AuthorizationCode = ({ collection }) => { onChange={(val) => handleChange(key, val)} onRun={handleRun} collection={collection} + isSecret={isSecret} />
diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/AuthorizationCode/inputsConfig.js b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/AuthorizationCode/inputsConfig.js index 67bc277aa..a100ce8e5 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/AuthorizationCode/inputsConfig.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/AuthorizationCode/inputsConfig.js @@ -17,7 +17,8 @@ const inputsConfig = [ }, { key: 'clientSecret', - label: 'Client Secret' + label: 'Client Secret', + isSecret: true }, { key: 'scope', diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/ClientCredentials/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/ClientCredentials/index.js index 5be4fde1d..d69122b48 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/ClientCredentials/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/ClientCredentials/index.js @@ -42,7 +42,7 @@ const OAuth2ClientCredentials = ({ collection }) => { return ( {inputsConfig.map((input) => { - const { key, label } = input; + const { key, label, isSecret } = input; return (
@@ -54,6 +54,7 @@ const OAuth2ClientCredentials = ({ collection }) => { onChange={(val) => handleChange(key, val)} onRun={handleRun} collection={collection} + isSecret={isSecret} />
diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/ClientCredentials/inputsConfig.js b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/ClientCredentials/inputsConfig.js index 164dcaab4..f2cd88ae3 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/ClientCredentials/inputsConfig.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/ClientCredentials/inputsConfig.js @@ -9,7 +9,8 @@ const inputsConfig = [ }, { key: 'clientSecret', - label: 'Client Secret' + label: 'Client Secret', + isSecret: true }, { key: 'scope', diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/PasswordCredentials/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/PasswordCredentials/index.js index 44598da1a..d2d9eed1f 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/PasswordCredentials/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/PasswordCredentials/index.js @@ -44,7 +44,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => { return ( {inputsConfig.map((input) => { - const { key, label } = input; + const { key, label, isSecret } = input; return (
@@ -56,6 +56,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => { onChange={(val) => handleChange(key, val)} onRun={handleRun} collection={collection} + isSecret={isSecret} />
diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/PasswordCredentials/inputsConfig.js b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/PasswordCredentials/inputsConfig.js index 6366bb5e7..ec9efb1a8 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/PasswordCredentials/inputsConfig.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/PasswordCredentials/inputsConfig.js @@ -17,7 +17,8 @@ const inputsConfig = [ }, { key: 'clientSecret', - label: 'Client Secret' + label: 'Client Secret', + isSecret: true }, { key: 'scope', diff --git a/packages/bruno-app/src/components/CollectionSettings/Presets/index.js b/packages/bruno-app/src/components/CollectionSettings/Presets/index.js index 734bd90ef..e16884e16 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Presets/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Presets/index.js @@ -74,6 +74,7 @@ const PresetsSettings = ({ collection }) => { id="request-url" type="text" name="requestUrl" + placeholder='Request URL' className="block textbox" autoComplete="off" autoCorrect="off" diff --git a/packages/bruno-app/src/components/Documentation/StyledWrapper.js b/packages/bruno-app/src/components/Documentation/StyledWrapper.js index f0ffee808..f159d94dc 100644 --- a/packages/bruno-app/src/components/Documentation/StyledWrapper.js +++ b/packages/bruno-app/src/components/Documentation/StyledWrapper.js @@ -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}; diff --git a/packages/bruno-app/src/components/Documentation/index.js b/packages/bruno-app/src/components/Documentation/index.js index d4b790965..5a391db8a 100644 --- a/packages/bruno-app/src/components/Documentation/index.js +++ b/packages/bruno-app/src/components/Documentation/index.js @@ -37,8 +37,8 @@ const Documentation = ({ item, collection }) => { } return ( - -
+ +
{isEditing ? 'Preview' : 'Edit'}
diff --git a/packages/bruno-app/src/components/Dropdown/StyledWrapper.js b/packages/bruno-app/src/components/Dropdown/StyledWrapper.js index 6ad94e289..7af8b9081 100644 --- a/packages/bruno-app/src/components/Dropdown/StyledWrapper.js +++ b/packages/bruno-app/src/components/Dropdown/StyledWrapper.js @@ -40,10 +40,15 @@ const Wrapper = styled.div` color: ${(props) => props.theme.dropdown.iconColor}; } - &:hover { + &:hover:not(:disabled) { background-color: ${(props) => props.theme.dropdown.hoverBg}; } + &:disabled { + cursor: not-allowed; + color: gray; + } + &.border-top { border-top: solid 1px ${(props) => props.theme.dropdown.separator}; } diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js index 1f36d05ea..45a43a6a9 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js @@ -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 - + - + @@ -109,7 +108,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original - -
EnabledEnabled Name ValueSecretSecret
- {variable.secret ? ( -
{maskInputValue(variable.value)}
- ) : ( +
+
formik.setFieldValue(`${index}.value`, newValue, true)} /> - )} +
+ {
Pre and post-request scripts that will run before and after any request inside this folder is sent.
-
-
Pre Request
+
+
Pre Request
{ font={get(preferences, 'font.codeFont', 'default')} />
-
-
Post Response
+
+
Post Response
{ }; return ( - +
setTab('headers')}> diff --git a/packages/bruno-app/src/components/MarkDown/StyledWrapper.js b/packages/bruno-app/src/components/MarkDown/StyledWrapper.js index 65cb9c23b..f834fdaba 100644 --- a/packages/bruno-app/src/components/MarkDown/StyledWrapper.js +++ b/packages/bruno-app/src/components/MarkDown/StyledWrapper.js @@ -2,7 +2,6 @@ import styled from 'styled-components'; const StyledMarkdownBodyWrapper = styled.div` background: transparent; - height: inherit; .markdown-body { background: transparent; overflow-y: auto; diff --git a/packages/bruno-app/src/components/Preferences/Support/index.js b/packages/bruno-app/src/components/Preferences/Support/index.js index dfd6fabed..5e1b0dacc 100644 --- a/packages/bruno-app/src/components/Preferences/Support/index.js +++ b/packages/bruno-app/src/components/Preferences/Support/index.js @@ -1,39 +1,42 @@ import React from 'react'; import { IconSpeakerphone, IconBrandTwitter, IconBrandGithub, IconBrandDiscord, IconBook } from '@tabler/icons'; import StyledWrapper from './StyledWrapper'; +import { useDictionary } from 'providers/Dictionary/index'; const Support = () => { + const { dictionary } = useDictionary(); + return ( diff --git a/packages/bruno-app/src/components/RequestPane/Auth/AwsV4Auth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/AwsV4Auth/index.js index 41820a0c8..a44cecc1b 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/AwsV4Auth/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/AwsV4Auth/index.js @@ -150,6 +150,7 @@ const AwsV4Auth = ({ onTokenChange, item, collection }) => { onRun={handleRun} collection={collection} item={item} + isSecret={true} />
diff --git a/packages/bruno-app/src/components/RequestPane/Auth/BasicAuth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/BasicAuth/index.js index bbe16ec70..8582a53cd 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/BasicAuth/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/BasicAuth/index.js @@ -69,6 +69,7 @@ const BasicAuth = ({ item, collection }) => { onRun={handleRun} collection={collection} item={item} + isSecret={true} />
diff --git a/packages/bruno-app/src/components/RequestPane/Auth/BearerAuth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/BearerAuth/index.js index 1dfa42b15..bef4d062a 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/BearerAuth/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/BearerAuth/index.js @@ -43,6 +43,7 @@ const BearerAuth = ({ item, collection }) => { onRun={handleRun} collection={collection} item={item} + isSecret={true} />
diff --git a/packages/bruno-app/src/components/RequestPane/Auth/DigestAuth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/DigestAuth/index.js index 24f4610f0..e91ed8d1f 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/DigestAuth/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/DigestAuth/index.js @@ -69,6 +69,7 @@ const DigestAuth = ({ item, collection }) => { onRun={handleRun} collection={collection} item={item} + isSecret={true} />
diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/index.js index 793be57f0..2bb5dcc35 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/index.js @@ -80,7 +80,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => { return ( {inputsConfig.map((input) => { - const { key, label } = input; + const { key, label, isSecret } = input; return (
@@ -93,6 +93,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => { onRun={handleRun} collection={collection} item={item} + isSecret={isSecret} />
diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/inputsConfig.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/inputsConfig.js index 67bc277aa..a100ce8e5 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/inputsConfig.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/inputsConfig.js @@ -17,7 +17,8 @@ const inputsConfig = [ }, { key: 'clientSecret', - label: 'Client Secret' + label: 'Client Secret', + isSecret: true }, { key: 'scope', diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/index.js index df08475e8..a43c8f0ad 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/index.js @@ -43,7 +43,7 @@ const OAuth2ClientCredentials = ({ item, collection }) => { return ( {inputsConfig.map((input) => { - const { key, label } = input; + const { key, label, isSecret } = input; return (
@@ -56,6 +56,7 @@ const OAuth2ClientCredentials = ({ item, collection }) => { onRun={handleRun} collection={collection} item={item} + isSecret={isSecret} />
diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/inputsConfig.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/inputsConfig.js index 164dcaab4..f2cd88ae3 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/inputsConfig.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/inputsConfig.js @@ -9,7 +9,8 @@ const inputsConfig = [ }, { key: 'clientSecret', - label: 'Client Secret' + label: 'Client Secret', + isSecret: true }, { key: 'scope', diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/index.js index cfcff9784..4ec8c1faa 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/index.js @@ -45,7 +45,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => { return ( {inputsConfig.map((input) => { - const { key, label } = input; + const { key, label, isSecret } = input; return (
@@ -58,6 +58,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => { onRun={handleRun} collection={collection} item={item} + isSecret={isSecret} />
diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/inputsConfig.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/inputsConfig.js index 6366bb5e7..32f2c999c 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/inputsConfig.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/inputsConfig.js @@ -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', diff --git a/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js b/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js index df90082c6..c7d66aadb 100644 --- a/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js +++ b/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js @@ -137,7 +137,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => { ) : null}
diff --git a/packages/bruno-app/src/components/RequestPane/QueryEditor/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/QueryEditor/StyledWrapper.js index 06f9e4b78..3832f60c0 100644 --- a/packages/bruno-app/src/components/RequestPane/QueryEditor/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestPane/QueryEditor/StyledWrapper.js @@ -4,6 +4,7 @@ const StyledWrapper = styled.div` div.CodeMirror { background: ${(props) => props.theme.codemirror.bg}; border: solid 1px ${(props) => props.theme.codemirror.border}; + flex: 1 1 0; } textarea.cm-editor { diff --git a/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js b/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js index 03b60d1d7..0c2707ac8 100644 --- a/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js +++ b/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js @@ -74,7 +74,7 @@ const QueryUrl = ({ item, collection, handleRun }) => { />
{ e.stopPropagation(); if (!item.draft) return; diff --git a/packages/bruno-app/src/components/RequestPane/RequestBody/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/RequestBody/StyledWrapper.js index 83ebd8140..42da81d61 100644 --- a/packages/bruno-app/src/components/RequestPane/RequestBody/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestPane/RequestBody/StyledWrapper.js @@ -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; diff --git a/packages/bruno-app/src/components/RequestPane/Script/index.js b/packages/bruno-app/src/components/RequestPane/Script/index.js index 935b52ede..acd674ee0 100644 --- a/packages/bruno-app/src/components/RequestPane/Script/index.js +++ b/packages/bruno-app/src/components/RequestPane/Script/index.js @@ -40,8 +40,8 @@ const Script = ({ item, collection }) => { return ( -
-
Pre Request
+
+
Pre Request
{ onSave={onSave} />
-
-
Post Response
+
+
Post Response
{ return ( -
+
Pre Request
-
+
Post Response
diff --git a/packages/bruno-app/src/components/RequestTabPanel/RequestNotFound/index.js b/packages/bruno-app/src/components/RequestTabPanel/RequestNotFound/index.js index db0e45e41..cb62ac8a0 100644 --- a/packages/bruno-app/src/components/RequestTabPanel/RequestNotFound/index.js +++ b/packages/bruno-app/src/components/RequestTabPanel/RequestNotFound/index.js @@ -30,7 +30,7 @@ const RequestNotFound = ({ itemUid }) => { return (
-
+
Request no longer exists.
This can happen when the .bru file associated with this request was deleted on your filesystem. diff --git a/packages/bruno-app/src/components/RequestTabPanel/index.js b/packages/bruno-app/src/components/RequestTabPanel/index.js index 2fd253f4b..ecec4bc9d 100644 --- a/packages/bruno-app/src/components/RequestTabPanel/index.js +++ b/packages/bruno-app/src/components/RequestTabPanel/index.js @@ -158,10 +158,9 @@ const RequestTabPanel = () => {
{item.type === 'graphql-request' ? ( diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js index 680782169..8b9bb0c35 100644 --- a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js +++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useRef, Fragment } from 'react'; import get from 'lodash/get'; import { closeTabs } from 'providers/ReduxStore/slices/tabs'; import { saveRequest } from 'providers/ReduxStore/slices/collections/actions'; @@ -12,12 +12,18 @@ import ConfirmRequestClose from './ConfirmRequestClose'; import RequestTabNotFound from './RequestTabNotFound'; import SpecialTab from './SpecialTab'; import StyledWrapper from './StyledWrapper'; +import Dropdown from 'components/Dropdown'; +import CloneCollectionItem from 'components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index'; +import NewRequest from 'components/Sidebar/NewRequest/index'; -const RequestTab = ({ tab, collection, folderUid }) => { +const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUid }) => { const dispatch = useDispatch(); const { storedTheme } = useTheme(); const [showConfirmClose, setShowConfirmClose] = useState(false); + const dropdownTippyRef = useRef(); + const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref); + const handleCloseClick = (event) => { event.stopPropagation(); event.preventDefault(); @@ -28,6 +34,19 @@ const RequestTab = ({ tab, collection, folderUid }) => { ); }; + const handleRightClick = (_event) => { + const menuDropdown = dropdownTippyRef.current; + if (!menuDropdown) { + return; + } + + if (menuDropdown.state.isShown) { + menuDropdown.hide(); + } else { + menuDropdown.show(); + } + }; + const handleMouseUp = (e) => { if (e.button === 1) { e.stopPropagation(); @@ -143,6 +162,7 @@ const RequestTab = ({ tab, collection, folderUid }) => { )}
{ if (!item.draft) return handleMouseUp(e); @@ -159,6 +179,15 @@ const RequestTab = ({ tab, collection, folderUid }) => { {item.name} +
{ ); }; +function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, collection, dropdownTippyRef, dispatch }) { + const [showCloneRequestModal, setShowCloneRequestModal] = useState(false); + const [showAddNewRequestModal, setShowAddNewRequestModal] = useState(false); + + const totalTabs = collectionRequestTabs.length || 0; + const currentTabUid = collectionRequestTabs[tabIndex]?.uid; + const currentTabItem = findItemInCollection(collection, currentTabUid); + + const hasLeftTabs = tabIndex !== 0; + const hasRightTabs = totalTabs > tabIndex + 1; + const hasOtherTabs = totalTabs > 1; + + async function handleCloseTab(event, tabUid) { + event.stopPropagation(); + dropdownTippyRef.current.hide(); + + if (!tabUid) { + return; + } + + try { + const item = findItemInCollection(collection, tabUid); + // silently save unsaved changes before closing the tab + if (item.draft) { + await dispatch(saveRequest(item.uid, collection.uid, true)); + } + + dispatch(closeTabs({ tabUids: [tabUid] })); + } catch (err) {} + } + + function handleCloseOtherTabs(event) { + dropdownTippyRef.current.hide(); + + const otherTabs = collectionRequestTabs.filter((_, index) => index !== tabIndex); + otherTabs.forEach((tab) => handleCloseTab(event, tab.uid)); + } + + function handleCloseTabsToTheLeft(event) { + dropdownTippyRef.current.hide(); + + const leftTabs = collectionRequestTabs.filter((_, index) => index < tabIndex); + leftTabs.forEach((tab) => handleCloseTab(event, tab.uid)); + } + + function handleCloseTabsToTheRight(event) { + dropdownTippyRef.current.hide(); + + const rightTabs = collectionRequestTabs.filter((_, index) => index > tabIndex); + rightTabs.forEach((tab) => handleCloseTab(event, tab.uid)); + } + + function handleCloseSavedTabs(event) { + event.stopPropagation(); + + const savedTabs = collection.items.filter((item) => !item.draft); + const savedTabIds = savedTabs.map((item) => item.uid) || []; + dispatch(closeTabs({ tabUids: savedTabIds })); + } + + function handleCloseAllTabs(event) { + collectionRequestTabs.forEach((tab) => handleCloseTab(event, tab.uid)); + } + + return ( + + {showAddNewRequestModal && ( + setShowAddNewRequestModal(false)} /> + )} + + {showCloneRequestModal && ( + setShowCloneRequestModal(false)} + /> + )} + + } placement="bottom-start"> + + + + + + + + + + + ); +} + export default RequestTab; diff --git a/packages/bruno-app/src/components/RequestTabs/StyledWrapper.js b/packages/bruno-app/src/components/RequestTabs/StyledWrapper.js index ec76ec5b5..93829cca9 100644 --- a/packages/bruno-app/src/components/RequestTabs/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestTabs/StyledWrapper.js @@ -7,13 +7,14 @@ const Wrapper = styled.div` padding: 0; margin: 0; display: flex; - position: relative; overflow: scroll; &::-webkit-scrollbar { display: none; } + scrollbar-width: none; + li { display: inline-flex; max-width: 150px; diff --git a/packages/bruno-app/src/components/RequestTabs/index.js b/packages/bruno-app/src/components/RequestTabs/index.js index fbafb55cf..d0cd0b459 100644 --- a/packages/bruno-app/src/components/RequestTabs/index.js +++ b/packages/bruno-app/src/components/RequestTabs/index.js @@ -110,7 +110,14 @@ const RequestTabs = () => { role="tab" onClick={() => handleClick(tab)} > - + ); }) diff --git a/packages/bruno-app/src/components/RunnerResults/StyledWrapper.js b/packages/bruno-app/src/components/RunnerResults/StyledWrapper.js index 0178b90d7..38dd7511e 100644 --- a/packages/bruno-app/src/components/RunnerResults/StyledWrapper.js +++ b/packages/bruno-app/src/components/RunnerResults/StyledWrapper.js @@ -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}; diff --git a/packages/bruno-app/src/components/RunnerResults/index.jsx b/packages/bruno-app/src/components/RunnerResults/index.jsx index e415aeb3c..4b0b68cba 100644 --- a/packages/bruno-app/src/components/RunnerResults/index.jsx +++ b/packages/bruno-app/src/components/RunnerResults/index.jsx @@ -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 {totalRequestsInCollection} requests in this collection.
+
+ + setDelay(e.target.value)} + /> +
+ @@ -167,10 +182,14 @@ export default function RunnerResults({ collection }) { {item.status !== 'error' && item.status !== 'completed' ? ( - ) : ( + ) : item.responseReceived?.status ? ( setSelectedItem(item)}> - ({get(item.responseReceived, 'status')} - {get(item.responseReceived, 'statusText')}) + ({item.responseReceived?.status} + {item.responseReceived?.statusText}) + + ) : ( + setSelectedItem(item)}> + (request failed) )}
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index.js index 55c2b86dd..0dd96e197 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index.js @@ -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" diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/StyledWrapper.js index 418658f03..ff06f4f31 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/StyledWrapper.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/StyledWrapper.js @@ -2,6 +2,7 @@ import styled from 'styled-components'; const StyledWrapper = styled.div` position: relative; + height: 100%; .copy-to-clipboard { position: absolute; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/index.js b/packages/bruno-app/src/components/Sidebar/Collections/index.js index e5a657ef9..6553be58f 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/index.js @@ -91,13 +91,13 @@ const Collections = () => { setSearchText(e.target.value.toLowerCase())} /> diff --git a/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js b/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js index 4211e8ff1..7dd827298 100644 --- a/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js +++ b/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js @@ -115,7 +115,7 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName, trans collectionLocation: Yup.string() .min(1, 'must be at least 1 character') .max(500, 'must be 500 characters or less') - .required('name is required') + .required('Location is required') }), onSubmit: (values) => { handleSubmit(values.collectionLocation); @@ -124,7 +124,9 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName, trans const browse = () => { dispatch(browseDirectory()) .then((dirPath) => { - formik.setFieldValue('collectionLocation', dirPath); + if (typeof dirPath === 'string' && dirPath.length > 0) { + formik.setFieldValue('collectionLocation', dirPath); + } }) .catch((error) => { formik.setFieldValue('collectionLocation', ''); @@ -160,7 +162,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" diff --git a/packages/bruno-app/src/components/Sidebar/NewRequest/index.js b/packages/bruno-app/src/components/Sidebar/NewRequest/index.js index 8d8125e94..50e7be277 100644 --- a/packages/bruno-app/src/components/Sidebar/NewRequest/index.js +++ b/packages/bruno-app/src/components/Sidebar/NewRequest/index.js @@ -109,7 +109,8 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => { collectionUid: collection.uid, itemUid: item ? item.uid : null, headers: request.headers, - body: request.body + body: request.body, + auth: request.auth }) ) .then(() => onClose()) @@ -161,7 +162,16 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => { return ( -
+ { + if (e.key === 'Enter') { + e.preventDefault(); + formik.handleSubmit(); + } + }} + >
-
v1.22.0
+
v1.25.0
diff --git a/packages/bruno-app/src/components/SingleLineEditor/index.js b/packages/bruno-app/src/components/SingleLineEditor/index.js index dbb46191b..31d0875fd 100644 --- a/packages/bruno-app/src/components/SingleLineEditor/index.js +++ b/packages/bruno-app/src/components/SingleLineEditor/index.js @@ -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; } @@ -135,8 +150,35 @@ class SingleLineEditor extends Component { 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 ? ( + + ) : null; + }; + render() { - return ; + return ( +
+ + {this.secretEye(this.props.isSecret)} +
+ ); } } export default SingleLineEditor; diff --git a/packages/bruno-app/src/components/Welcome/index.js b/packages/bruno-app/src/components/Welcome/index.js index 385a71486..54f7b5378 100644 --- a/packages/bruno-app/src/components/Welcome/index.js +++ b/packages/bruno-app/src/components/Welcome/index.js @@ -9,9 +9,11 @@ import CreateCollection from 'components/Sidebar/CreateCollection'; import ImportCollection from 'components/Sidebar/ImportCollection'; import ImportCollectionLocation from 'components/Sidebar/ImportCollectionLocation'; import StyledWrapper from './StyledWrapper'; +import { useDictionary } from 'providers/Dictionary/index'; const Welcome = () => { const dispatch = useDispatch(); + const { dictionary } = useDictionary(); const [importedCollection, setImportedCollection] = useState(null); const [importedTranslationLog, setImportedTranslationLog] = useState({}); const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false); @@ -20,7 +22,7 @@ const Welcome = () => { const handleOpenCollection = () => { dispatch(openCollection()).catch( - (err) => console.log(err) && toast.error('An error occurred while opening the collection') + (err) => console.log(err) && toast.error(dictionary.errorWhileOpeningCollection) ); }; @@ -38,12 +40,12 @@ const Welcome = () => { .then(() => { setImportCollectionLocationModalOpen(false); setImportedCollection(null); - toast.success('Collection imported successfully'); + toast.success(dictionary.collectionImportedSuccessfully); }) .catch((err) => { setImportCollectionLocationModalOpen(false); console.error(err); - toast.error('An error occurred while importing the collection. Check the logs for more information.'); + toast.error(dictionary.errorWhileImportingCollection); }); }; @@ -66,46 +68,45 @@ const Welcome = () => {
bruno
-
Opensource IDE for exploring and testing APIs
+
{dictionary.aboutBruno}
-
Collections
+
{dictionary.collections}
setCreateCollectionModalOpen(true)}> - Create Collection + {dictionary.createCollection}
- Open Collection + {dictionary.openCollection}
setImportCollectionModalOpen(true)}> - Import Collection + {dictionary.importCollection}
-
Links
diff --git a/packages/bruno-app/src/dictionaries/en.js b/packages/bruno-app/src/dictionaries/en.js new file mode 100644 index 000000000..a9ff316cd --- /dev/null +++ b/packages/bruno-app/src/dictionaries/en.js @@ -0,0 +1,16 @@ +export default { + aboutBruno: 'Opensource IDE for exploring and testing APIs', + collections: 'Collections', + createCollection: 'Create Collection', + openCollection: 'Open Collection', + importCollection: 'Import Collection', + documentation: 'Documentation', + reportIssues: 'Report Issues', + gitHub: 'GitHub', + collectionImportedSuccessfully: 'Collection imported successfully', + errorWhileOpeningCollection: 'An error occurred while opening the collection', + errorWhileImportingCollection: + 'An error occurred while importing the collection. Check the logs for more information.', + discord: 'Discord', + twitter: 'Twitter' +}; diff --git a/packages/bruno-app/src/dictionaries/index.js b/packages/bruno-app/src/dictionaries/index.js new file mode 100644 index 000000000..fb5f797dc --- /dev/null +++ b/packages/bruno-app/src/dictionaries/index.js @@ -0,0 +1,5 @@ +import en from './en.js'; + +export const dictionaries = { + en +}; diff --git a/packages/bruno-app/src/globalStyles.js b/packages/bruno-app/src/globalStyles.js index 25a6d15bc..7839a55ac 100644 --- a/packages/bruno-app/src/globalStyles.js +++ b/packages/bruno-app/src/globalStyles.js @@ -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; diff --git a/packages/bruno-app/src/pages/_app.js b/packages/bruno-app/src/pages/_app.js index cf8b3683e..d2bf8a28d 100644 --- a/packages/bruno-app/src/pages/_app.js +++ b/packages/bruno-app/src/pages/_app.js @@ -14,6 +14,7 @@ import 'codemirror/lib/codemirror.css'; import 'graphiql/graphiql.min.css'; import 'react-tooltip/dist/react-tooltip.css'; import '@usebruno/graphql-docs/dist/esm/index.css'; +import { DictionaryProvider } from 'providers/Dictionary/index'; function SafeHydrate({ children }) { return
{typeof window === 'undefined' ? null : children}
; @@ -59,13 +60,15 @@ function MyApp({ Component, pageProps }) { - - - - - - - + + + + + + + + + diff --git a/packages/bruno-app/src/providers/App/index.js b/packages/bruno-app/src/providers/App/index.js index c54d53867..7664ae03e 100644 --- a/packages/bruno-app/src/providers/App/index.js +++ b/packages/bruno-app/src/providers/App/index.js @@ -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()); diff --git a/packages/bruno-app/src/providers/App/useTelemetry.js b/packages/bruno-app/src/providers/App/useTelemetry.js index ae379e91e..bd5551c91 100644 --- a/packages/bruno-app/src/providers/App/useTelemetry.js +++ b/packages/bruno-app/src/providers/App/useTelemetry.js @@ -60,7 +60,7 @@ const trackStart = () => { event: 'start', properties: { os: platformLib.os.family, - version: '1.22.0' + version: '1.25.0' } }); }; diff --git a/packages/bruno-app/src/providers/Dictionary/index.js b/packages/bruno-app/src/providers/Dictionary/index.js new file mode 100644 index 000000000..75a399f27 --- /dev/null +++ b/packages/bruno-app/src/providers/Dictionary/index.js @@ -0,0 +1,28 @@ +import React from 'react'; +import { useState, useContext } from 'react'; +import { dictionaries } from 'src/dictionaries/index'; + +export const DictionaryContext = React.createContext(); + +const DictionaryProvider = (props) => { + const [language, setLanguage] = useState('en'); + const dictionary = dictionaries[language] ?? dictionaries.en; + + return ( + + <>{props.children} + + ); +}; + +const useDictionary = () => { + const context = useContext(DictionaryContext); + + if (context === undefined) { + throw new Error(`useDictionary must be used within a DictionaryProvider`); + } + + return context; +}; + +export { useDictionary, DictionaryProvider }; diff --git a/packages/bruno-app/src/providers/Hotkeys/index.js b/packages/bruno-app/src/providers/Hotkeys/index.js index 8b0503b1c..53a0fc263 100644 --- a/packages/bruno-app/src/providers/Hotkeys/index.js +++ b/packages/bruno-app/src/providers/Hotkeys/index.js @@ -154,6 +154,31 @@ export const HotkeysProvider = (props) => { }; }, [activeTabUid]); + // close all tabs + useEffect(() => { + Mousetrap.bind(['command+shift+w', 'ctrl+shift+w'], (e) => { + const activeTab = find(tabs, (t) => t.uid === activeTabUid); + if (activeTab) { + const collection = findCollectionByUid(collections, activeTab.collectionUid); + + if (collection) { + const tabUids = tabs.filter((tab) => tab.collectionUid === collection.uid).map((tab) => tab.uid); + dispatch( + closeTabs({ + tabUids: tabUids + }) + ); + } + } + + return false; // this stops the event bubbling + }); + + return () => { + Mousetrap.unbind(['command+shift+w', 'ctrl+shift+w']); + }; + }, [activeTabUid, tabs, collections, dispatch]); + return ( {showSaveRequestModal && ( diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js index 5e8984823..b434f6a96 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -39,7 +39,7 @@ import { import { each } from 'lodash'; import { closeAllCollectionTabs } from 'providers/ReduxStore/slices/tabs'; import { resolveRequestFilename } from 'utils/common/platform'; -import { parseQueryParams, splitOnFirst } from 'utils/url/index'; +import { parsePathParams, parseQueryParams, splitOnFirst } from 'utils/url/index'; import { sendCollectionOauth2Request as _sendCollectionOauth2Request } from 'utils/network/index'; import { name } from 'file-loader'; @@ -192,10 +192,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.runtimeVariables, itemUid, secretVariables) + _sendCollectionOauth2Request(collection, environment, collectionCopy.runtimeVariables) .then((response) => { if (response?.data?.error) { toast.error(response?.data?.error); @@ -284,7 +281,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); @@ -315,7 +312,8 @@ export const runCollectionFolder = (collectionUid, folderUid, recursive) => (dis collectionCopy, environment, collectionCopy.runtimeVariables, - recursive + recursive, + delay ) .then(resolve) .catch((err) => { @@ -700,7 +698,7 @@ export const moveItemToRootOfCollection = (collectionUid, draggedItemUid) => (di }; export const newHttpRequest = (params) => (dispatch, getState) => { - const { requestName, requestType, requestUrl, requestMethod, collectionUid, itemUid, headers, body } = params; + const { requestName, requestType, requestUrl, requestMethod, collectionUid, itemUid, headers, body, auth } = params; return new Promise((resolve, reject) => { const state = getState(); @@ -710,11 +708,20 @@ export const newHttpRequest = (params) => (dispatch, getState) => { } const parts = splitOnFirst(requestUrl, '?'); - const params = parseQueryParams(parts[1]); - each(params, (urlParam) => { + const queryParams = parseQueryParams(parts[1]); + each(queryParams, (urlParam) => { urlParam.enabled = true; + urlParam.type = 'query'; }); + const pathParams = parsePathParams(requestUrl); + each(pathParams, (pathParm) => { + pathParams.enabled = true; + pathParm.type = 'path' + }); + + const params = [...queryParams, ...pathParams]; + const item = { uid: uuid(), type: requestType, @@ -733,6 +740,9 @@ export const newHttpRequest = (params) => (dispatch, getState) => { multipartForm: null, formUrlEncoded: null, rawFile: null + }, + auth: auth ?? { + mode: 'none' } } }; diff --git a/packages/bruno-app/src/styles/globals.css b/packages/bruno-app/src/styles/globals.css index 29e9196ea..8396c48b5 100644 --- a/packages/bruno-app/src/styles/globals.css +++ b/packages/bruno-app/src/styles/globals.css @@ -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 diff --git a/packages/bruno-app/src/themes/dark.js b/packages/bruno-app/src/themes/dark.js index bb1001f31..9e8e923aa 100644 --- a/packages/bruno-app/src/themes/dark.js +++ b/packages/bruno-app/src/themes/dark.js @@ -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: { diff --git a/packages/bruno-app/src/themes/light.js b/packages/bruno-app/src/themes/light.js index a130f2513..a25583136 100644 --- a/packages/bruno-app/src/themes/light.js +++ b/packages/bruno-app/src/themes/light.js @@ -20,7 +20,11 @@ const lightTheme = { input: { bg: 'white', border: '#ccc', - focusBorder: '#8b8b8b' + focusBorder: '#8b8b8b', + placeholder: { + color: '#a2a2a2', + opacity: 0.8 + } }, menubar: { diff --git a/packages/bruno-app/src/utils/codegenerator/har.js b/packages/bruno-app/src/utils/codegenerator/har.js index 0e3476256..c17eef866 100644 --- a/packages/bruno-app/src/utils/codegenerator/har.js +++ b/packages/bruno-app/src/utils/codegenerator/har.js @@ -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, diff --git a/packages/bruno-app/src/utils/collections/search.js b/packages/bruno-app/src/utils/collections/search.js index b420687b7..9c2f187e5 100644 --- a/packages/bruno-app/src/utils/collections/search.js +++ b/packages/bruno-app/src/utils/collections/search.js @@ -3,7 +3,7 @@ import filter from 'lodash/filter'; import find from 'lodash/find'; export const doesRequestMatchSearchText = (request, searchText = '') => { - return request.name.toLowerCase().includes(searchText.toLowerCase()); + return request?.name?.toLowerCase().includes(searchText.toLowerCase()); }; export const doesFolderHaveItemsMatchSearchText = (item, searchText = '') => { diff --git a/packages/bruno-app/src/utils/common/codemirror.js b/packages/bruno-app/src/utils/common/codemirror.js index f4013a366..cbb1a2b3a 100644 --- a/packages/bruno-app/src/utils/common/codemirror.js +++ b/packages/bruno-app/src/utils/common/codemirror.js @@ -12,6 +12,64 @@ const pathFoundInVariables = (path, obj) => { return value !== undefined; }; +/** + * 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 || {}; diff --git a/packages/bruno-app/src/utils/curl/curl-to-json.js b/packages/bruno-app/src/utils/curl/curl-to-json.js index 82eb0be95..e76f4014a 100644 --- a/packages/bruno-app/src/utils/curl/curl-to-json.js +++ b/packages/bruno-app/src/utils/curl/curl-to-json.js @@ -123,7 +123,7 @@ const curlToJson = (curlCommand) => { request.urlWithoutQuery = 'http://' + request.urlWithoutQuery; } - requestJson.url = request.urlWithoutQuery.replace(/\/$/, ''); + requestJson.url = request.urlWithoutQuery; requestJson.raw_url = request.url; requestJson.method = request.method; @@ -160,14 +160,15 @@ const curlToJson = (curlCommand) => { } if (request.auth) { - const splitAuth = request.auth.split(':'); - const user = splitAuth[0] || ''; - const password = splitAuth[1] || ''; - - requestJson.auth = { - user: repr(user), - password: repr(password) - }; + if(request.auth.mode === 'basic'){ + requestJson.auth = { + mode: 'basic', + basic: { + username: repr(request.auth.basic?.username), + password: repr(request.auth.basic?.password) + } + } + } } return Object.keys(requestJson).length ? requestJson : {}; diff --git a/packages/bruno-app/src/utils/curl/curl-to-json.spec.js b/packages/bruno-app/src/utils/curl/curl-to-json.spec.js index 2704bd4c5..2d9785154 100644 --- a/packages/bruno-app/src/utils/curl/curl-to-json.spec.js +++ b/packages/bruno-app/src/utils/curl/curl-to-json.spec.js @@ -75,4 +75,15 @@ describe('curlToJson', () => { } }); }); + + it('should return and parse a simple curl command with a trailing slash', () => { + const curlCommand = 'curl https://www.usebruno.com/'; + const result = curlToJson(curlCommand); + + expect(result).toEqual({ + url: 'https://www.usebruno.com/', + raw_url: 'https://www.usebruno.com/', + method: 'get' + }); + }); }); diff --git a/packages/bruno-app/src/utils/curl/index.js b/packages/bruno-app/src/utils/curl/index.js index 97bfbd966..e16dc68a5 100644 --- a/packages/bruno-app/src/utils/curl/index.js +++ b/packages/bruno-app/src/utils/curl/index.js @@ -56,7 +56,8 @@ export const getRequestFromCurlCommand = (curlCommand) => { url: request.url, method: request.method, body, - headers: headers + headers: headers, + auth: request.auth }; } catch (error) { console.error(error); diff --git a/packages/bruno-app/src/utils/curl/parse-curl.js b/packages/bruno-app/src/utils/curl/parse-curl.js index 810d1af8a..79db23672 100644 --- a/packages/bruno-app/src/utils/curl/parse-curl.js +++ b/packages/bruno-app/src/utils/curl/parse-curl.js @@ -36,7 +36,8 @@ const parseCurlCommand = (curlCommand) => { boolean: ['I', 'head', 'compressed', 'L', 'k', 'silent', 's', 'G', 'get'], alias: { H: 'header', - A: 'user-agent' + A: 'user-agent', + u: 'user' } }); @@ -72,11 +73,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(); } }); } @@ -188,10 +188,21 @@ const parseCurlCommand = (curlCommand) => { } urlObject.search = null; // Clean out the search/query portion. + + let urlWithoutQuery = URL.format(urlObject); + let urlHost = urlObject?.host; + if (!url?.includes(`${urlHost}/`)) { + if (urlWithoutQuery && urlHost) { + const [beforeHost, afterHost] = urlWithoutQuery.split(urlHost); + urlWithoutQuery = beforeHost + urlHost + afterHost?.slice(1); + } + } + const request = { - url: url, - urlWithoutQuery: URL.format(urlObject) + url, + urlWithoutQuery }; + if (compressed) { request.compressed = true; } @@ -227,12 +238,19 @@ const parseCurlCommand = (curlCommand) => { request.data = parsedArguments['data-urlencode']; } - if (parsedArguments.u) { - request.auth = parsedArguments.u; - } - if (parsedArguments.user) { - request.auth = parsedArguments.user; + if (parsedArguments.user && typeof parsedArguments.user === 'string') { + const basicAuth = parsedArguments.user.split(':') + const username = basicAuth[0] || '' + const password = basicAuth[1] || '' + request.auth = { + mode: 'basic', + basic: { + username, + password + } + } } + if (Array.isArray(request.data)) { request.dataArray = request.data; request.data = request.data.join('&'); diff --git a/packages/bruno-electron/package.json b/packages/bruno-electron/package.json index f1a992e47..6c70fe66e 100644 --- a/packages/bruno-electron/package.json +++ b/packages/bruno-electron/package.json @@ -1,5 +1,5 @@ { - "version": "v1.22.0", + "version": "v1.25.0", "name": "bruno", "description": "Opensource API Client for Exploring and Testing APIs", "homepage": "https://www.usebruno.com", diff --git a/packages/bruno-electron/src/app/watcher.js b/packages/bruno-electron/src/app/watcher.js index 2fbd4cc98..589cd29d8 100644 --- a/packages/bruno-electron/src/app/watcher.js +++ b/packages/bruno-electron/src/app/watcher.js @@ -428,17 +428,16 @@ class Watcher { this.watchers = {}; } - addWatcher(win, watchPath, collectionUid, brunoConfig) { + addWatcher(win, watchPath, collectionUid, brunoConfig, forcePolling = false) { if (this.watchers[watchPath]) { this.watchers[watchPath].close(); } const ignores = brunoConfig?.ignore || []; - const self = this; setTimeout(() => { const watcher = chokidar.watch(watchPath, { ignoreInitial: false, - usePolling: watchPath.startsWith('\\\\') ? true : false, + usePolling: watchPath.startsWith('\\\\') || forcePolling ? true : false, ignored: (filepath) => { const normalizedPath = filepath.replace(/\\/g, '/'); const relativePath = path.relative(watchPath, normalizedPath); @@ -457,14 +456,36 @@ class Watcher { depth: 20 }); + let startedNewWatcher = false; watcher .on('add', (pathname) => add(win, pathname, collectionUid, watchPath)) .on('addDir', (pathname) => addDirectory(win, pathname, collectionUid, watchPath)) .on('change', (pathname) => change(win, pathname, collectionUid, watchPath)) .on('unlink', (pathname) => unlink(win, pathname, collectionUid, watchPath)) - .on('unlinkDir', (pathname) => unlinkDir(win, pathname, collectionUid, watchPath)); + .on('unlinkDir', (pathname) => unlinkDir(win, pathname, collectionUid, watchPath)) + .on('error', (error) => { + // `EMFILE` is an error code thrown when to many files are watched at the same time see: https://github.com/usebruno/bruno/issues/627 + // `ENOSPC` stands for "Error No space" but is also thrown if the file watcher limit is reached. + // To prevent loops `!forcePolling` is checked. + if ((error.code === 'ENOSPC' || error.code === 'EMFILE') && !startedNewWatcher && !forcePolling) { + // This callback is called for every file the watcher is trying to watch. To prevent a spam of messages and + // Multiple watcher being started `startedNewWatcher` is set to prevent this. + startedNewWatcher = true; + watcher.close(); + console.error( + `\nCould not start watcher for ${watchPath}:`, + 'ENOSPC: System limit for number of file watchers reached!', + 'Trying again with polling, this will be slower!\n', + 'Update you system config to allow more concurrently watched files with:', + '"echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p"' + ); + this.addWatcher(win, watchPath, collectionUid, brunoConfig, true); + } else { + console.error(`An error occurred in the watcher for: ${watchPath}`, error); + } + }); - self.watchers[watchPath] = watcher; + this.watchers[watchPath] = watcher; }, 100); } diff --git a/packages/bruno-electron/src/index.js b/packages/bruno-electron/src/index.js index 7f4e58422..cad10a10c 100644 --- a/packages/bruno-electron/src/index.js +++ b/packages/bruno-electron/src/index.js @@ -50,6 +50,7 @@ app.on('ready', async () => { height, minWidth: 1000, minHeight: 640, + show: false, webPreferences: { nodeIntegration: true, contextIsolation: true, @@ -67,6 +68,9 @@ app.on('ready', async () => { mainWindow.maximize(); } + mainWindow.once('ready-to-show', () => { + mainWindow.show(); + }) const url = isDev ? 'http://localhost:3000' : format({ diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index c384ba71e..3aa819c4c 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -408,7 +408,7 @@ const registerNetworkIpc = (mainWindow) => { } // run post-response script - const responseScript = compact(scriptingConfig.flow === 'natural' ? [ + const responseScript = compact(scriptingConfig.flow === 'sequential' ? [ get(collectionRoot, 'request.script.res'), get(request, 'script.res') ] : [ get(request, 'script.res'), get(collectionRoot, 'request.script.res') @@ -596,7 +596,7 @@ const registerNetworkIpc = (mainWindow) => { // run tests const testScript = item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests'); - const testFile = compact(scriptingConfig.flow === 'natural' ? [ + const testFile = compact(scriptingConfig.flow === 'sequential' ? [ get(collectionRoot, 'request.tests'), testScript, ] : [ testScript, get(collectionRoot, 'request.tests') @@ -825,7 +825,7 @@ const registerNetworkIpc = (mainWindow) => { ipcMain.handle( 'renderer:run-collection-folder', - async (event, folder, collection, environment, runtimeVariables, recursive) => { + async (event, folder, collection, environment, runtimeVariables, recursive, delay) => { const collectionUid = collection.uid; const collectionPath = collection.pathname; const folderUid = folder ? folder.uid : null; @@ -944,6 +944,18 @@ const registerNetworkIpc = (mainWindow) => { timeStart = Date.now(); let response, responseTime; try { + if (delay && !Number.isNaN(delay) && delay > 0) { + const delayPromise = new Promise((resolve) => setTimeout(resolve, delay)); + + const cancellationPromise = new Promise((_, reject) => { + abortController.signal.addEventListener('abort', () => { + reject(new Error('Cancelled')); + }); + }); + + await Promise.race([delayPromise, cancellationPromise]); + } + /** @type {import('axios').AxiosResponse} */ response = await axiosInstance(request); timeEnd = Date.now(); @@ -1036,7 +1048,7 @@ const registerNetworkIpc = (mainWindow) => { // run tests const testScript = item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests'); - const testFile = compact(scriptingConfig.flow === 'natural' ? [ + const testFile = compact(scriptingConfig.flow === 'sequential' ? [ get(collectionRoot, 'request.tests'), testScript ] : [ testScript, get(collectionRoot, 'request.tests') @@ -1153,12 +1165,22 @@ const registerNetworkIpc = (mainWindow) => { return `response.${extension}`; }; - const fileName = - getFileNameFromContentDispositionHeader() || getFileNameFromUrlPath() || getFileNameBasedOnContentTypeHeader(); + const getEncodingFormat = () => { + const contentType = getHeaderValue('content-type'); + const extension = mime.extension(contentType) || 'txt'; + return ['json', 'xml', 'html', 'yml', 'yaml', 'txt'].includes(extension) ? 'utf-8' : 'base64'; + }; + const determineFileName = () => { + return ( + getFileNameFromContentDispositionHeader() || getFileNameFromUrlPath() || getFileNameBasedOnContentTypeHeader() + ); + }; + + const fileName = determineFileName(); const filePath = await chooseFileToSave(mainWindow, fileName); if (filePath) { - await writeBinaryFile(filePath, Buffer.from(response.dataBuffer, 'base64')); + await writeBinaryFile(filePath, Buffer.from(response.dataBuffer, getEncodingFormat())); } } catch (error) { return Promise.reject(error); diff --git a/packages/bruno-electron/src/ipc/network/oauth2-helper.js b/packages/bruno-electron/src/ipc/network/oauth2-helper.js index 216c3be97..7a1a5b503 100644 --- a/packages/bruno-electron/src/ipc/network/oauth2-helper.js +++ b/packages/bruno-electron/src/ipc/network/oauth2-helper.js @@ -32,9 +32,6 @@ const resolveOAuth2AuthorizationCodeAccessToken = async (request, collectionUid) client_secret: clientSecret, state: state }; - if (scope) { - data['scope'] = scope; - } if (pkce) { data['code_verifier'] = codeVerifier; } diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index 6e1af447b..28e13e002 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -18,8 +18,8 @@ const mergeFolderLevelHeaders = (request, requestTreePath) => { folderHeaders.set(header.name, header.value); } }); - } else { - let headers = get(i, 'request.headers', []); + } else if (i.uid === request.uid) { + const headers = i?.draft ? get(i, 'draft.request.headers', []) : get(i, 'request.headers', []); headers.forEach((header) => { if (header.enabled) { folderHeaders.set(header.name, header.value); @@ -55,8 +55,8 @@ const mergeFolderLevelVars = (request, requestTreePath) => { folderReqVars.set(_var.name, _var.value); } }); - } else { - let vars = get(i, 'request.vars.req', []); + } else if (i.uid === request.uid) { + const vars = i?.draft ? get(i, 'draft.request.vars.req', []) : get(i, 'request.vars.req', []); vars.forEach((_var) => { if (_var.enabled) { folderReqVars.set(_var.name, _var.value); @@ -91,8 +91,8 @@ const mergeFolderLevelVars = (request, requestTreePath) => { folderResVars.set(_var.name, _var.value); } }); - } else { - let vars = get(i, 'request.vars.res', []); + } else if (i.uid === request.uid) { + const vars = i?.draft ? get(i, 'draft.request.vars.res', []) : get(i, 'request.vars.res', []); vars.forEach((_var) => { if (_var.enabled) { folderResVars.set(_var.name, _var.value); @@ -147,7 +147,7 @@ const mergeFolderLevelScripts = (request, requestTreePath, scriptFlow) => { } if (folderCombinedPostResScript.length) { - if (scriptFlow === 'natural') { + if (scriptFlow === 'sequential') { request.script.res = compact([...folderCombinedPostResScript, request?.script?.res || '']).join(os.EOL); } else { request.script.res = compact([request?.script?.res || '', ...folderCombinedPostResScript.reverse()]).join(os.EOL); @@ -155,7 +155,7 @@ const mergeFolderLevelScripts = (request, requestTreePath, scriptFlow) => { } if (folderCombinedTests.length) { - if (scriptFlow === 'natural') { + if (scriptFlow === 'sequential') { request.tests = compact([...folderCombinedTests, request?.tests || '']).join(os.EOL); } else { request.tests = compact([request?.tests || '', ...folderCombinedTests.reverse()]).join(os.EOL); @@ -309,7 +309,7 @@ const prepareRequest = (item, collection) => { } }); - // scriptFlow is either "sandwich" or "natural" + // scriptFlow is either "sandwich" or "sequential" const scriptFlow = collection.brunoConfig?.scripts?.flow ?? 'sandwich'; const requestTreePath = getTreePathFromCollectionToItem(collection, item); if (requestTreePath && requestTreePath.length > 0) { diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js index 29f469922..dc410945b 100644 --- a/packages/bruno-lang/v2/src/jsonToBru.js +++ b/packages/bruno-lang/v2/src/jsonToBru.js @@ -13,7 +13,7 @@ const stripLastLine = (text) => { }; const getValueString = (value) => { - const hasNewLines = value.includes('\n'); + const hasNewLines = value?.includes('\n'); if (!hasNewLines) { return value; @@ -269,7 +269,6 @@ ${indentString(body.rawFile)} multipartForms .map((item) => { const enabled = item.enabled ? '' : '~'; - if (item.type === 'text') { return `${enabled}${item.name}: ${getValueString(item.value)}`; } diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js index 35ec2979f..97b4f4ba4 100644 --- a/packages/bruno-schema/src/collections/index.js +++ b/packages/bruno-schema/src/collections/index.js @@ -48,7 +48,7 @@ const varsSchema = Yup.object({ const requestUrlSchema = Yup.string().min(0).defined(); const requestMethodSchema = Yup.string() - .oneOf(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']) + .oneOf(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'TRACE']) .required('method is required'); const graphqlBodySchema = Yup.object({ diff --git a/packages/bruno-schema/src/collections/requestSchema.spec.js b/packages/bruno-schema/src/collections/requestSchema.spec.js index 87399c690..9fd223cb2 100644 --- a/packages/bruno-schema/src/collections/requestSchema.spec.js +++ b/packages/bruno-schema/src/collections/requestSchema.spec.js @@ -32,7 +32,7 @@ describe('Request Schema Validation', () => { return Promise.all([ expect(requestSchema.validate(request)).rejects.toEqual( validationErrorWithMessages( - 'method must be one of the following values: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS' + 'method must be one of the following values: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, TRACE' ) ) ]);