forked from extern/bruno
Merge branch 'main' into add-documentaion-bengali
This commit is contained in:
commit
a11247d359
@ -1,4 +1,4 @@
|
||||
**English** | [Українська](docs/contributing/contributing_ua.md) | [Русский](docs/contributing/contributing_ru.md) | [Türkçe](docs/contributing/contributing_tr.md) | [Deutsch](docs/contributing/contributing_de.md) | [Français](docs/contributing/contributing_fr.md) | [বাংলা](docs/contributing/contributing_bn.md)
|
||||
**English** | [Українська](docs/contributing/contributing_ua.md) | [Русский](docs/contributing/contributing_ru.md) | [Türkçe](docs/contributing/contributing_tr.md) | [Deutsch](docs/contributing/contributing_de.md) | [Français](docs/contributing/contributing_fr.md) | [Português (BR)](docs/contributing/contributing_pt_br.md) | [বাংলা](docs/contributing/contributing_bn.md)
|
||||
|
||||
## Let's make bruno better, together !!
|
||||
|
||||
|
85
docs/contributing/contributing_pt_br.md
Normal file
85
docs/contributing/contributing_pt_br.md
Normal file
@ -0,0 +1,85 @@
|
||||
## Vamos tornar o Bruno melhor, juntos!!
|
||||
|
||||
Estamos felizes que você queira ajudar a melhorar o Bruno. Abaixo estão as diretrizes e orientações para começar a executar o Bruno no seu computador.
|
||||
|
||||
### Stack de Tecnologias
|
||||
|
||||
O Bruno é construído usando Next.js e React. Também usamos o Electron para disponibilizar uma versão para desktop (que suporta coleções locais).
|
||||
|
||||
Bibliotecas que utilizamos:
|
||||
|
||||
- CSS - Tailwind
|
||||
- Editor de Código - Codemirror
|
||||
- Gerenciador de Estado - Redux
|
||||
- Ícones - Tabler Icons
|
||||
- Formulários - formik
|
||||
- Validador de Schema - Yup
|
||||
- Cliente de Requisições - axios
|
||||
- Monitor de Arquivos - chokidar
|
||||
|
||||
### Dependências
|
||||
|
||||
Você precisará do [Node v18.x (ou da versão LTS mais recente)](https://nodejs.org/en/) e do npm na versão 8.x. Nós utilizamos npm workspaces no projeto.
|
||||
|
||||
## Desenvolvimento
|
||||
|
||||
Bruno está sendo desenvolvido como um aplicativo de desktop. Você precisa carregar o programa executando o aplicativo Next.js em um terminal e, em seguida, executar o aplicativo Electron em outro terminal.
|
||||
|
||||
### Dependências
|
||||
|
||||
- NodeJS v18
|
||||
|
||||
### Desenvolvimento Local
|
||||
|
||||
```bash
|
||||
# use nodejs 18 version
|
||||
nvm use
|
||||
|
||||
# install deps
|
||||
npm i --legacy-peer-deps
|
||||
|
||||
# build graphql docs
|
||||
npm run build:graphql-docs
|
||||
|
||||
# build bruno query
|
||||
npm run build:bruno-query
|
||||
|
||||
# run next app (terminal 1)
|
||||
npm run dev:web
|
||||
|
||||
# run electron app (terminal 2)
|
||||
npm run dev:electron
|
||||
```
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
Você pode se deparar com o erro `Unsupported platform` ao executar o comando `npm install`. Para corrigir isso, você precisará excluir a pasta `node_modules` e o arquivo `package-lock.json` e, em seguida, executar o comando `npm install` novamente. Isso deve instalar todos os pacotes necessários para executar o aplicativo.
|
||||
|
||||
```shell
|
||||
# delete node_modules in sub-directories
|
||||
find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do
|
||||
rm -rf "$dir"
|
||||
done
|
||||
|
||||
# delete package-lock in sub-directories
|
||||
find . -type f -name "package-lock.json" -delete
|
||||
```
|
||||
|
||||
### Testando
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
```
|
||||
|
||||
### Envio de Pull Request
|
||||
|
||||
- Por favor, mantenha os PRs pequenos e focados em uma única coisa.
|
||||
- Siga o formato de criação de branches.
|
||||
- feature/[nome da funcionalidade]: Esta branch deve conter alterações para uma funcionalidade específica.
|
||||
- Exemplo: feature/dark-mode
|
||||
- bugfix/[nome do bug]: Esta branch deve conter apenas correções para um bug específico.
|
||||
- Exemplo: bugfix/bug-1
|
5
docs/publishing/publishing_pt_br.md
Normal file
5
docs/publishing/publishing_pt_br.md
Normal file
@ -0,0 +1,5 @@
|
||||
### Publicando Bruno em um novo gerenciador de pacotes
|
||||
|
||||
Embora nosso código seja de código aberto e esteja disponível para todos usarem, pedimos gentilmente que entre em contato conosco antes de considerar a publicação em novos gerenciadores de pacotes. Como o criador da ferramenta, mantenho a marca registrada `Bruno` para este projeto e gostaria de gerenciar sua distribuição. Se deseja ver o Bruno em um novo gerenciador de pacotes, por favor, solicite através de uma issue no GitHub.
|
||||
|
||||
Embora a maioria de nossas funcionalidades seja gratuita e de código aberto (o que abrange API's REST e GraphQL), buscamos alcançar um equilíbrio harmonioso entre os princípios de código aberto e sustentabilidade. - https://github.com/usebruno/bruno/discussions/269
|
121
docs/readme/readme_kr.md
Normal file
121
docs/readme/readme_kr.md
Normal file
@ -0,0 +1,121 @@
|
||||
<br />
|
||||
<img src="../../assets/images/logo-transparent.png" width="80"/>
|
||||
|
||||
### Bruno - API 탐색 및 테스트를 위한 오픈소스 IDE.
|
||||
|
||||
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[![CI](https://github.com/usebruno/bruno/actions/workflows/unit-tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/workflows/unit-tests.yml)
|
||||
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
|
||||
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)
|
||||
[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
|
||||
[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)
|
||||
|
||||
Bruno는 새롭고 혁신적인 API 클라이언트로, Postman과 유사한 툴들을 혁신하는 것을 목표로 합니다.
|
||||
|
||||
Bruno는 사용자의 컬렉션을 파일 시스템의 폴더에 직접 저장합니다. 일반 텍스트 마크업 언어인 Bru를 사용해 API 요청에 대한 정보를 저장합니다.
|
||||
|
||||
Git 또는 원하는 버전 관리 도구를 사용하여 API 컬렉션을 연동할 수 있습니다.
|
||||
|
||||
브루는 오프라인 전용입니다. 브루노에 클라우드 동기화 기능을 추가할 계획은 없습니다. 저희는 사용자의 데이터 프라이버시를 소중히 여기며, 데이터는 사용자의 기기에 남아 있어야 한다고 믿습니다. 장기 비전 읽기 [링크](https://github.com/usebruno/bruno/discussions/269)
|
||||
|
||||
📢 Watch our recent talk at India FOSS 3.0 Conference [here](https://www.youtube.com/watch?v=7bSMFpbcPiY)
|
||||
|
||||
![bruno](/assets/images/landing-2.png) <br /><br />
|
||||
|
||||
### 설치
|
||||
|
||||
Bruno 는 여기에서 다운로드 받을 수 있습니다.[링크](https://www.usebruno.com/downloads) (맥, 윈도우, 리눅스)
|
||||
|
||||
Homebrew, Chocolatey, Snap, Apt 같은 패키지 관리자를 통해서도 Bruno를 설치할 수 있습니다.
|
||||
|
||||
```sh
|
||||
# On Mac via Homebrew
|
||||
brew install bruno
|
||||
|
||||
# On Windows via Chocolatey
|
||||
choco install bruno
|
||||
|
||||
# On Linux via Snap
|
||||
snap install bruno
|
||||
|
||||
# On Linux via Apt
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
|
||||
|
||||
echo "deb [signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
|
||||
sudo apt update
|
||||
sudo apt install bruno
|
||||
```
|
||||
|
||||
### 여러 플랫폼에서 실행하세요. 🖥️
|
||||
|
||||
![bruno](assets/images/run-anywhere.png) <br /><br />
|
||||
|
||||
### Git과 연동하세요. 👩💻🧑💻
|
||||
|
||||
또는 원하는 버전 관리 시스템을 선택하세요.
|
||||
|
||||
![bruno](assets/images/version-control.png) <br /><br />
|
||||
|
||||
### 중요 링크 📌
|
||||
|
||||
- [Our Long Term Vision](https://github.com/usebruno/bruno/discussions/269)
|
||||
- [Roadmap](https://github.com/usebruno/bruno/discussions/384)
|
||||
- [Documentation](https://docs.usebruno.com)
|
||||
- [Website](https://www.usebruno.com)
|
||||
- [Pricing](https://www.usebruno.com/pricing)
|
||||
- [Download](https://www.usebruno.com/downloads)
|
||||
|
||||
### 쇼케이스 🎥
|
||||
|
||||
- [Testimonials](https://github.com/usebruno/bruno/discussions/343)
|
||||
- [Knowledge Hub](https://github.com/usebruno/bruno/discussions/386)
|
||||
- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)
|
||||
|
||||
### 지원 ❤️
|
||||
|
||||
프로젝트가 마음에 들면 ⭐ 버튼을 눌러 주세요.
|
||||
|
||||
### 후기 공유 📣
|
||||
|
||||
Bruno가 여러분과 여러분의 팀에 도움이 되었다면, 잊지 말고 공유해 주세요. [Github discussion 공유 링크](https://github.com/usebruno/bruno/discussions/343)
|
||||
|
||||
### 새 패키지 관리자에게 게시
|
||||
|
||||
더 많은 정보를 확인하시려명 링크를 클릭해 주세요.[배포 가이드](publishing.md)
|
||||
|
||||
### 컨트리뷰트 👩💻🧑💻
|
||||
|
||||
컨트리뷰트에 관심이 있으시면 링크를 참고해 주세요. [컨트리뷰트 가이드](contributing.md)
|
||||
|
||||
코드를 통해 기여할 수 없더라도 사용 사례를 해결하기 위해 구현이 필요한 버그나 기능 요청을 주저하지 마시고 제출해 주세요.
|
||||
|
||||
### Authors
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/usebruno/bruno/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
### Stay in touch 🌐
|
||||
|
||||
[𝕏 (Twitter)](https://twitter.com/use_bruno) <br />
|
||||
[Website](https://www.usebruno.com) <br />
|
||||
[Discord](https://discord.com/invite/KgcZUncpjq) <br />
|
||||
[LinkedIn](https://www.linkedin.com/company/usebruno)
|
||||
|
||||
### Trademark
|
||||
|
||||
**Name**
|
||||
|
||||
`Bruno` is a trademark held by [Anoop M D](https://www.helloanoop.com/)
|
||||
|
||||
**Logo**
|
||||
|
||||
The logo is sourced from [OpenMoji](https://openmoji.org/library/emoji-1F436/). License: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)
|
||||
|
||||
### License 📄
|
||||
|
||||
[MIT](license.md)
|
121
docs/readme/readme_pt_br.md
Normal file
121
docs/readme/readme_pt_br.md
Normal file
@ -0,0 +1,121 @@
|
||||
<br />
|
||||
<img src="../../assets/images/logo-transparent.png" width="80"/>
|
||||
|
||||
### Bruno - IDE de código aberto para explorar e testar APIs.
|
||||
|
||||
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[![CI](https://github.com/usebruno/bruno/actions/workflows/unit-tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/workflows/unit-tests.yml)
|
||||
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
|
||||
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)
|
||||
[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
|
||||
[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)
|
||||
|
||||
Bruno é um novo e inovador cliente de API, com o objetivo de revolucionar o status quo representado por ferramentas como o Postman e outras semelhantes.
|
||||
|
||||
Bruno armazena suas coleções diretamente em uma pasta no seu sistema de arquivos. Utilizamos uma linguagem de marcação de texto simples, chamada Bru, para salvar informações sobre requisições de API.
|
||||
|
||||
Você pode usar o Git ou qualquer sistema de controle de versão de sua escolha para colaborar em suas coleções de API.
|
||||
|
||||
Bruno é totalmente offline. Não há planos de adicionar sincronização em nuvem ao Bruno, nunca. Valorizamos a privacidade de seus dados e acreditamos que eles devem permanecer em seu dispositivo. Saiba mais sobre nossa visão a longo prazo [aqui](https://github.com/usebruno/bruno/discussions/269).
|
||||
|
||||
📢 Assista à nossa palestra recente na India FOSS 3.0 Conference [aqui](https://www.youtube.com/watch?v=7bSMFpbcPiY).
|
||||
|
||||
![bruno](../../assets/images/landing-2.png) <br /><br />
|
||||
|
||||
### Instalação
|
||||
|
||||
Bruno está disponível para download como binário [em nosso site](https://www.usebruno.com/downloads) para Mac, Windows e Linux.
|
||||
|
||||
Você também pode instalar o Bruno via gerenciadores de pacotes como Homebrew, Chocolatey, Snap e Apt.
|
||||
|
||||
```sh
|
||||
# Mac via Homebrew
|
||||
brew install bruno
|
||||
|
||||
# Windows via Chocolatey
|
||||
choco install bruno
|
||||
|
||||
# Linux via Snap
|
||||
snap install bruno
|
||||
|
||||
# Linux via Apt
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
|
||||
|
||||
echo "deb [signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
|
||||
sudo apt update
|
||||
sudo apt install bruno
|
||||
```
|
||||
|
||||
### Execute em várias plataformas 🖥️
|
||||
|
||||
![bruno](../../assets/images/run-anywhere.png) <br /><br />
|
||||
|
||||
### Colaboração via Git 👩💻🧑💻
|
||||
|
||||
Ou qualquer sistema de controle de versão de sua escolha.
|
||||
|
||||
![bruno](../../assets/images/version-control.png) <br /><br />
|
||||
|
||||
### Links Importantes 📌
|
||||
|
||||
- [Nossa Visão de Longo Prazo](https://github.com/usebruno/bruno/discussions/269)
|
||||
- [Roadmap](https://github.com/usebruno/bruno/discussions/384)
|
||||
- [Documentação](https://docs.usebruno.com)
|
||||
- [Website](https://www.usebruno.com)
|
||||
- [Preços](https://www.usebruno.com/pricing)
|
||||
- [Download](https://www.usebruno.com/downloads)
|
||||
|
||||
### Showcase 🎥
|
||||
|
||||
- [Depoimentos](https://github.com/usebruno/bruno/discussions/343)
|
||||
- [Hub de Conhecimento](https://github.com/usebruno/bruno/discussions/386)
|
||||
- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)
|
||||
|
||||
### Apoie ❤️
|
||||
|
||||
Au-au! Se você gosta do projeto, clique no botão ⭐!!
|
||||
|
||||
### Compartilhe sua experiência 📣
|
||||
|
||||
Se o Bruno ajudou no seu trabalho e/ou no trabalho de sua equipe, por favor, não se esqueça de compartilhar seu [depoimento em nossas discussões no GitHub](https://github.com/usebruno/bruno/discussions/343).
|
||||
|
||||
### Publicando em Novos Gerenciadores de Pacotes
|
||||
|
||||
Por favor, verifique [aqui](../publishing/publishing_pt_br.md) mais informações.
|
||||
|
||||
### Colabore 👩💻🧑💻
|
||||
|
||||
Fico feliz que você queira melhorar o Bruno. Por favor, confira o [guia de colaboração](../contributing/contributing_pt_br.md).
|
||||
|
||||
Mesmo que você não possa contribuir codificando, não deixe de relatar problemas e solicitar recursos que precisam ser implementados para atender ao contexto de seu dia a dia.
|
||||
|
||||
### Authors
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/usebruno/bruno/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
### Mantenha Contato 🌐
|
||||
|
||||
[𝕏 (Twitter)](https://twitter.com/use_bruno) <br />
|
||||
[Website](https://www.usebruno.com) <br />
|
||||
[Discord](https://discord.com/invite/KgcZUncpjq) <br />
|
||||
[LinkedIn](https://www.linkedin.com/company/usebruno)
|
||||
|
||||
### Trademark
|
||||
|
||||
**Nome**
|
||||
|
||||
`Bruno` é uma marca registrada de [Anoop M D](https://www.helloanoop.com/).
|
||||
|
||||
**Logo**
|
||||
|
||||
A logo é original do [OpenMoji](https://openmoji.org/library/emoji-1F436/). Licença: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/).
|
||||
|
||||
### Licença 📄
|
||||
|
||||
[MIT](license.md)
|
1367
package-lock.json
generated
1367
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -20,10 +20,11 @@
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"@usebruno/graphql-docs": "0.1.0",
|
||||
"@usebruno/schema": "0.6.0",
|
||||
"axios": "^0.26.0",
|
||||
"axios": "^1.5.1",
|
||||
"classnames": "^2.3.1",
|
||||
"codemirror": "^5.65.2",
|
||||
"codemirror-graphql": "^1.2.5",
|
||||
"cookie": "^0.6.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"file-dialog": "^0.0.8",
|
||||
"file-saver": "^2.0.5",
|
||||
@ -36,17 +37,19 @@
|
||||
"httpsnippet": "^3.0.1",
|
||||
"idb": "^7.0.0",
|
||||
"immer": "^9.0.15",
|
||||
"jsesc": "^3.0.2",
|
||||
"know-your-http-well": "^0.5.0",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-it": "^13.0.2",
|
||||
"mousetrap": "^1.6.5",
|
||||
"nanoid": "3.3.4",
|
||||
"next": "12.3.3",
|
||||
"parse-curl": "^0.2.6",
|
||||
"path": "^0.12.7",
|
||||
"pdfjs-dist": "^3.11.174",
|
||||
"platform": "^1.3.6",
|
||||
"posthog-node": "^2.1.0",
|
||||
"qs": "^6.11.0",
|
||||
"query-string": "^7.0.1",
|
||||
"react": "18.2.0",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
@ -54,12 +57,15 @@
|
||||
"react-github-btn": "^1.4.0",
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"react-inspector": "^6.0.2",
|
||||
"react-pdf": "^7.5.1",
|
||||
"react-redux": "^7.2.6",
|
||||
"react-tooltip": "^5.5.2",
|
||||
"sass": "^1.46.0",
|
||||
"styled-components": "^5.3.3",
|
||||
"tailwindcss": "^2.2.19",
|
||||
"url": "^0.11.3",
|
||||
"xml-formatter": "^3.5.0",
|
||||
"yargs-parser": "^21.1.1",
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -11,22 +11,20 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
protocol: Yup.string().oneOf(['http', 'https', 'socks4', 'socks5']),
|
||||
hostname: Yup.string()
|
||||
.when('enabled', {
|
||||
is: true,
|
||||
is: 'true',
|
||||
then: (hostname) => hostname.required('Specify the hostname for your proxy.'),
|
||||
otherwise: (hostname) => hostname.nullable()
|
||||
})
|
||||
.max(1024),
|
||||
port: Yup.number()
|
||||
.when('enabled', {
|
||||
is: true,
|
||||
then: (port) => port.required('Specify port between 1 and 65535').typeError('Specify port between 1 and 65535'),
|
||||
otherwise: (port) => port.nullable().transform((_, val) => (val ? Number(val) : null))
|
||||
})
|
||||
.min(1)
|
||||
.max(65535),
|
||||
.max(65535)
|
||||
.typeError('Specify port between 1 and 65535')
|
||||
.nullable()
|
||||
.transform((_, val) => (val ? Number(val) : null)),
|
||||
auth: Yup.object()
|
||||
.when('enabled', {
|
||||
is: true,
|
||||
is: 'true',
|
||||
then: Yup.object({
|
||||
enabled: Yup.boolean(),
|
||||
username: Yup.string()
|
||||
|
@ -22,13 +22,11 @@ const ProxySettings = ({ close }) => {
|
||||
})
|
||||
.max(1024),
|
||||
port: Yup.number()
|
||||
.when('enabled', {
|
||||
is: true,
|
||||
then: (port) => port.required('Specify port between 1 and 65535').typeError('Specify port between 1 and 65535'),
|
||||
otherwise: (port) => port.nullable().transform((_, val) => (val ? Number(val) : null))
|
||||
})
|
||||
.min(1)
|
||||
.max(65535),
|
||||
.max(65535)
|
||||
.typeError('Specify port between 1 and 65535')
|
||||
.nullable()
|
||||
.transform((_, val) => (val ? Number(val) : null)),
|
||||
auth: Yup.object()
|
||||
.when('enabled', {
|
||||
is: true,
|
||||
|
@ -34,7 +34,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
requestUrlChanged({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
url: value
|
||||
url: value.trim()
|
||||
})
|
||||
);
|
||||
};
|
||||
|
@ -2,6 +2,11 @@ import CodeEditor from 'components/CodeEditor/index';
|
||||
import { get } from 'lodash';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { Document, Page } from 'react-pdf';
|
||||
import { useState } from 'react';
|
||||
import 'pdfjs-dist/build/pdf.worker';
|
||||
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
|
||||
import 'react-pdf/dist/esm/Page/TextLayer.css';
|
||||
|
||||
const QueryResultPreview = ({
|
||||
previewTab,
|
||||
@ -19,6 +24,10 @@ const QueryResultPreview = ({
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [numPages, setNumPages] = useState(null);
|
||||
function onDocumentLoadSuccess({ numPages }) {
|
||||
setNumPages(numPages);
|
||||
}
|
||||
// Fail safe, so we don't render anything with an invalid tab
|
||||
if (!allowedPreviewModes.includes(previewTab)) {
|
||||
return null;
|
||||
@ -45,6 +54,17 @@ const QueryResultPreview = ({
|
||||
case 'preview-image': {
|
||||
return <img src={`data:${contentType.replace(/\;(.*)/, '')};base64,${dataBuffer}`} className="mx-auto" />;
|
||||
}
|
||||
case 'preview-pdf': {
|
||||
return (
|
||||
<div className="preview-pdf" style={{ height: '100%', overflow: 'auto', maxHeight: 'calc(100vh - 220px)' }}>
|
||||
<Document file={`data:application/pdf;base64,${dataBuffer}`} onLoadSuccess={onDocumentLoadSuccess}>
|
||||
{Array.from(new Array(numPages), (el, index) => (
|
||||
<Page key={`page_${index + 1}`} pageNumber={index + 1} renderAnnotationLayer={false} />
|
||||
))}
|
||||
</Document>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
default:
|
||||
case 'raw': {
|
||||
return (
|
||||
|
@ -18,6 +18,19 @@ const StyledWrapper = styled.div`
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.react-pdf__Page {
|
||||
margin-top: 10px;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
.react-pdf__Page__textContent {
|
||||
border: 1px solid darkgrey;
|
||||
box-shadow: 5px 5px 5px 1px #ccc;
|
||||
border-radius: 0px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.react-pdf__Page__canvas {
|
||||
margin: 0 auto;
|
||||
}
|
||||
div[role='tablist'] {
|
||||
.active {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
|
@ -49,6 +49,8 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
|
||||
allowedPreviewModes.unshift('preview-web');
|
||||
} else if (mode.includes('image')) {
|
||||
allowedPreviewModes.unshift('preview-image');
|
||||
} else if (contentType.includes('pdf')) {
|
||||
allowedPreviewModes.unshift('preview-pdf');
|
||||
}
|
||||
|
||||
return allowedPreviewModes;
|
||||
|
@ -9,10 +9,11 @@ const CodeView = ({ language, item }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const { target, client, language: lang } = language;
|
||||
const headers = item.draft ? get(item, 'draft.request.headers') : get(item, 'request.headers');
|
||||
let snippet = '';
|
||||
|
||||
try {
|
||||
snippet = new HTTPSnippet(buildHarRequest(item.request)).convert(target, client);
|
||||
snippet = new HTTPSnippet(buildHarRequest({ request: item.request, headers })).convert(target, client);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
snippet = 'Error generating code snippet';
|
||||
|
@ -35,6 +35,10 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
textarea.curl-command {
|
||||
min-height: 150px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import React, { useRef, useEffect, useCallback } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import toast from 'react-hot-toast';
|
||||
@ -115,6 +115,25 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
|
||||
const onSubmit = () => formik.handleSubmit();
|
||||
|
||||
const handlePaste = useCallback(
|
||||
(event) => {
|
||||
const clipboardData = event.clipboardData || window.clipboardData;
|
||||
const pastedData = clipboardData.getData('Text');
|
||||
|
||||
// Check if pasted data looks like a cURL command
|
||||
const curlCommandRegex = /^\s*curl\s/i;
|
||||
if (curlCommandRegex.test(pastedData)) {
|
||||
// Switch to the 'from-curl' request type
|
||||
formik.setFieldValue('requestType', 'from-curl');
|
||||
formik.setFieldValue('curlCommand', pastedData);
|
||||
|
||||
// Prevent the default paste behavior to avoid pasting into the textarea
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
[formik]
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<Modal size="md" title="New Request" confirmText="Create" handleConfirm={onSubmit} handleCancel={onClose}>
|
||||
@ -174,7 +193,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
id="collection-name"
|
||||
id="request-name"
|
||||
type="text"
|
||||
name="requestName"
|
||||
ref={inputRef}
|
||||
@ -216,6 +235,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.requestUrl || ''}
|
||||
onPaste={handlePaste}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -232,8 +252,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
|
||||
<textarea
|
||||
name="curlCommand"
|
||||
placeholder="Enter cURL request here.."
|
||||
className="block textbox w-full mt-4"
|
||||
style={{ resize: 'none' }}
|
||||
className="block textbox w-full mt-4 curl-command"
|
||||
value={formik.values.curlCommand}
|
||||
onChange={formik.handleChange}
|
||||
></textarea>
|
||||
|
@ -105,7 +105,7 @@ const Sidebar = () => {
|
||||
Star
|
||||
</GitHubButton>
|
||||
</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.1.0</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.1.1</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -9,25 +9,17 @@ const createContentType = (mode) => {
|
||||
case 'multipartForm':
|
||||
return 'multipart/form-data';
|
||||
default:
|
||||
return 'application/json';
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const createHeaders = (headers, mode) => {
|
||||
const contentType = createContentType(mode);
|
||||
const headersArray = headers
|
||||
const createHeaders = (headers) => {
|
||||
return headers
|
||||
.filter((header) => header.enabled)
|
||||
.map((header) => {
|
||||
return {
|
||||
name: header.name,
|
||||
value: header.value
|
||||
};
|
||||
});
|
||||
const headerNames = headersArray.map((header) => header.name);
|
||||
if (!headerNames.includes('Content-Type')) {
|
||||
return [...headersArray, { name: 'Content-Type', value: contentType }];
|
||||
}
|
||||
return headersArray;
|
||||
.map((header) => ({
|
||||
name: header.name,
|
||||
value: header.value
|
||||
}));
|
||||
};
|
||||
|
||||
const createQuery = (queryParams = []) => {
|
||||
@ -56,13 +48,13 @@ const createPostData = (body) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const buildHarRequest = (request) => {
|
||||
export const buildHarRequest = ({ request, headers }) => {
|
||||
return {
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
httpVersion: 'HTTP/1.1',
|
||||
cookies: [],
|
||||
headers: createHeaders(request.headers, request.body.mode),
|
||||
headers: createHeaders(headers),
|
||||
queryString: createQuery(request.params),
|
||||
postData: createPostData(request.body),
|
||||
headersSize: 0,
|
||||
|
@ -1,5 +1,4 @@
|
||||
import get from 'lodash/get';
|
||||
import isString from 'lodash/isString';
|
||||
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
|
@ -94,3 +94,15 @@ export const getContentType = (headers) => {
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
export const startsWith = (str, search) => {
|
||||
if (!str || !str.length || typeof str !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!search || !search.length || typeof search !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return str.substr(0, search.length) === search;
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
const { describe, it, expect } = require('@jest/globals');
|
||||
|
||||
import { normalizeFileName } from './index';
|
||||
import { normalizeFileName, startsWith } from './index';
|
||||
|
||||
describe('common utils', () => {
|
||||
describe('normalizeFileName', () => {
|
||||
@ -16,4 +16,37 @@ describe('common utils', () => {
|
||||
expect(normalizeFileName('foo\\bar\\')).toBe('foo-bar-');
|
||||
});
|
||||
});
|
||||
|
||||
describe('startsWith', () => {
|
||||
it('should return false if str is not a string', () => {
|
||||
expect(startsWith(null, 'foo')).toBe(false);
|
||||
expect(startsWith(undefined, 'foo')).toBe(false);
|
||||
expect(startsWith(123, 'foo')).toBe(false);
|
||||
expect(startsWith({}, 'foo')).toBe(false);
|
||||
expect(startsWith([], 'foo')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if search is not a string', () => {
|
||||
expect(startsWith('foo', null)).toBe(false);
|
||||
expect(startsWith('foo', undefined)).toBe(false);
|
||||
expect(startsWith('foo', 123)).toBe(false);
|
||||
expect(startsWith('foo', {})).toBe(false);
|
||||
expect(startsWith('foo', [])).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if str does not start with search', () => {
|
||||
expect(startsWith('foo', 'bar')).toBe(false);
|
||||
expect(startsWith('foo', 'baz')).toBe(false);
|
||||
expect(startsWith('foo', 'bar')).toBe(false);
|
||||
expect(startsWith('foo', 'baz')).toBe(false);
|
||||
expect(startsWith('foo', 'bar')).toBe(false);
|
||||
expect(startsWith('foo', 'baz')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if str starts with search', () => {
|
||||
expect(startsWith('foo', 'f')).toBe(true);
|
||||
expect(startsWith('foo', 'fo')).toBe(true);
|
||||
expect(startsWith('foo', 'foo')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
169
packages/bruno-app/src/utils/curl/curl-to-json.js
Normal file
169
packages/bruno-app/src/utils/curl/curl-to-json.js
Normal file
@ -0,0 +1,169 @@
|
||||
/**
|
||||
* Copyright (c) 2014-2016 Nick Carneiro
|
||||
* https://github.com/curlconverter/curlconverter
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import parseCurlCommand from './parse-curl';
|
||||
import * as querystring from 'query-string';
|
||||
import * as jsesc from 'jsesc';
|
||||
|
||||
function getContentType(headers = {}) {
|
||||
const contentType = Object.keys(headers).find((key) => key.toLowerCase() === 'content-type');
|
||||
|
||||
return contentType ? headers[contentType] : null;
|
||||
}
|
||||
|
||||
function repr(value, isKey) {
|
||||
return isKey ? "'" + jsesc(value, { quotes: 'single' }) + "'" : value;
|
||||
}
|
||||
|
||||
function getQueries(request) {
|
||||
const queries = {};
|
||||
for (const paramName in request.query) {
|
||||
const rawValue = request.query[paramName];
|
||||
let paramValue;
|
||||
if (Array.isArray(rawValue)) {
|
||||
paramValue = rawValue.map(repr);
|
||||
} else {
|
||||
paramValue = repr(rawValue);
|
||||
}
|
||||
queries[repr(paramName)] = paramValue;
|
||||
}
|
||||
|
||||
return queries;
|
||||
}
|
||||
|
||||
function getDataString(request) {
|
||||
if (typeof request.data === 'number') {
|
||||
request.data = request.data.toString();
|
||||
}
|
||||
|
||||
const contentType = getContentType(request.headers);
|
||||
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
return { data: request.data.toString() };
|
||||
}
|
||||
|
||||
const parsedQueryString = querystring.parse(request.data, { sort: false });
|
||||
const keyCount = Object.keys(parsedQueryString).length;
|
||||
const singleKeyOnly = keyCount === 1 && !parsedQueryString[Object.keys(parsedQueryString)[0]];
|
||||
const singularData = request.isDataBinary || singleKeyOnly;
|
||||
if (singularData) {
|
||||
const data = {};
|
||||
data[repr(request.data)] = '';
|
||||
return { data: data };
|
||||
} else {
|
||||
return getMultipleDataString(request, parsedQueryString);
|
||||
}
|
||||
}
|
||||
|
||||
function getMultipleDataString(request, parsedQueryString) {
|
||||
const data = {};
|
||||
|
||||
for (const key in parsedQueryString) {
|
||||
const value = parsedQueryString[key];
|
||||
if (Array.isArray(value)) {
|
||||
data[repr(key)] = value;
|
||||
} else {
|
||||
data[repr(key)] = repr(value);
|
||||
}
|
||||
}
|
||||
|
||||
return { data: data };
|
||||
}
|
||||
|
||||
function getFilesString(request) {
|
||||
const data = {};
|
||||
|
||||
data.files = {};
|
||||
data.data = {};
|
||||
|
||||
for (const multipartKey in request.multipartUploads) {
|
||||
const multipartValue = request.multipartUploads[multipartKey];
|
||||
if (multipartValue.startsWith('@')) {
|
||||
const fileName = multipartValue.slice(1);
|
||||
data.files[repr(multipartKey)] = repr(fileName);
|
||||
} else {
|
||||
data.data[repr(multipartKey)] = repr(multipartValue);
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(data.files).length === 0) {
|
||||
delete data.files;
|
||||
}
|
||||
|
||||
if (Object.keys(data.data).length === 0) {
|
||||
delete data.data;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
const curlToJson = (curlCommand) => {
|
||||
const request = parseCurlCommand(curlCommand);
|
||||
|
||||
const requestJson = {};
|
||||
|
||||
// curl automatically prepends 'http' if the scheme is missing, but python fails and returns an error
|
||||
// we tack it on here to mimic curl
|
||||
if (!request.url.match(/https?:/)) {
|
||||
request.url = 'http://' + request.url;
|
||||
}
|
||||
if (!request.urlWithoutQuery.match(/https?:/)) {
|
||||
request.urlWithoutQuery = 'http://' + request.urlWithoutQuery;
|
||||
}
|
||||
|
||||
requestJson.url = request.urlWithoutQuery.replace(/\/$/, '');
|
||||
requestJson.raw_url = request.url;
|
||||
requestJson.method = request.method;
|
||||
|
||||
if (request.cookies) {
|
||||
const cookies = {};
|
||||
for (const cookieName in request.cookies) {
|
||||
cookies[repr(cookieName)] = repr(request.cookies[cookieName]);
|
||||
}
|
||||
|
||||
requestJson.cookies = cookies;
|
||||
}
|
||||
|
||||
if (request.headers) {
|
||||
const headers = {};
|
||||
for (const headerName in request.headers) {
|
||||
headers[repr(headerName)] = repr(request.headers[headerName]);
|
||||
}
|
||||
|
||||
requestJson.headers = headers;
|
||||
}
|
||||
|
||||
if (request.query) {
|
||||
requestJson.queries = getQueries(request);
|
||||
}
|
||||
|
||||
if (typeof request.data === 'string' || typeof request.data === 'number') {
|
||||
Object.assign(requestJson, getDataString(request));
|
||||
} else if (request.multipartUploads) {
|
||||
Object.assign(requestJson, getFilesString(request));
|
||||
}
|
||||
|
||||
if (request.insecure) {
|
||||
requestJson.insecure = false;
|
||||
}
|
||||
|
||||
if (request.auth) {
|
||||
const splitAuth = request.auth.split(':');
|
||||
const user = splitAuth[0] || '';
|
||||
const password = splitAuth[1] || '';
|
||||
|
||||
requestJson.auth = {
|
||||
user: repr(user),
|
||||
password: repr(password)
|
||||
};
|
||||
}
|
||||
|
||||
return Object.keys(requestJson).length ? requestJson : {};
|
||||
};
|
||||
|
||||
export default curlToJson;
|
62
packages/bruno-app/src/utils/curl/curl-to-json.spec.js
Normal file
62
packages/bruno-app/src/utils/curl/curl-to-json.spec.js
Normal file
@ -0,0 +1,62 @@
|
||||
const { describe, it, expect } = require('@jest/globals');
|
||||
|
||||
import curlToJson from './curl-to-json';
|
||||
|
||||
describe('curlToJson', () => {
|
||||
it('should return a parse a simple curl command', () => {
|
||||
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'
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a parse a curl command with headers', () => {
|
||||
const curlCommand = `curl https://www.usebruno.com
|
||||
-H 'Accept: application/json, text/plain, */*'
|
||||
-H 'Accept-Language: en-US,en;q=0.9,hi;q=0.8'
|
||||
`;
|
||||
|
||||
const result = curlToJson(curlCommand);
|
||||
|
||||
expect(result).toEqual({
|
||||
url: 'https://www.usebruno.com',
|
||||
raw_url: 'https://www.usebruno.com',
|
||||
method: 'get',
|
||||
headers: {
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
'Accept-Language': 'en-US,en;q=0.9,hi;q=0.8'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a parse a curl with a post body', () => {
|
||||
const curlCommand = `curl 'https://www.usebruno.com'
|
||||
-H 'Accept: application/json, text/plain, */*'
|
||||
-H 'Accept-Language: en-US,en;q=0.9,hi;q=0.8'
|
||||
-H 'Content-Type: application/json;charset=utf-8'
|
||||
-H 'Origin: https://www.usebruno.com'
|
||||
-H 'Referer: https://www.usebruno.com/'
|
||||
--data-raw '{"email":"test@usebruno.com","password":"test"}'
|
||||
`;
|
||||
|
||||
const result = curlToJson(curlCommand);
|
||||
|
||||
expect(result).toEqual({
|
||||
url: 'https://www.usebruno.com',
|
||||
raw_url: 'https://www.usebruno.com',
|
||||
method: 'post',
|
||||
headers: {
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
'Accept-Language': 'en-US,en;q=0.9,hi;q=0.8',
|
||||
'Content-Type': 'application/json;charset=utf-8',
|
||||
Origin: 'https://www.usebruno.com',
|
||||
Referer: 'https://www.usebruno.com/'
|
||||
},
|
||||
data: '{"email":"test@usebruno.com","password":"test"}'
|
||||
});
|
||||
});
|
||||
});
|
@ -1,18 +1,29 @@
|
||||
import * as parse from 'parse-curl';
|
||||
import { BrunoError } from 'utils/common/error';
|
||||
import { parseQueryParams } from 'utils/url';
|
||||
import { forOwn } from 'lodash';
|
||||
import { safeStringifyJSON } from 'utils/common';
|
||||
import curlToJson from './curl-to-json';
|
||||
|
||||
export const getRequestFromCurlCommand = (command) => {
|
||||
export const getRequestFromCurlCommand = (curlCommand) => {
|
||||
const parseFormData = (parsedBody) => {
|
||||
parseQueryParams(parsedBody);
|
||||
};
|
||||
try {
|
||||
const request = parse(command);
|
||||
const parsedHeader = request?.header;
|
||||
const headers =
|
||||
parsedHeader && Object.keys(parsedHeader).map((key) => ({ name: key, value: parsedHeader[key], enabled: true }));
|
||||
const formData = [];
|
||||
forOwn(parsedBody, (value, key) => {
|
||||
formData.push({ name: key, value, enabled: true });
|
||||
});
|
||||
|
||||
const contentType = headers?.find((h) => h.name.toLowerCase() === 'content-type');
|
||||
return formData;
|
||||
};
|
||||
|
||||
try {
|
||||
if (!curlCommand || typeof curlCommand !== 'string' || curlCommand.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const request = curlToJson(curlCommand);
|
||||
const parsedHeaders = request?.headers;
|
||||
const headers =
|
||||
parsedHeaders &&
|
||||
Object.keys(parsedHeaders).map((key) => ({ name: key, value: parsedHeaders[key], enabled: true }));
|
||||
|
||||
const contentType = headers?.find((h) => h.name.toLowerCase() === 'content-type')?.value;
|
||||
const body = {
|
||||
mode: 'none',
|
||||
json: null,
|
||||
@ -22,30 +33,25 @@ export const getRequestFromCurlCommand = (command) => {
|
||||
multipartForm: null,
|
||||
formUrlEncoded: null
|
||||
};
|
||||
const parsedBody = request?.body;
|
||||
if (parsedBody && contentType) {
|
||||
switch (contentType.value.toLowerCase()) {
|
||||
case 'application/json':
|
||||
body.mode = 'json';
|
||||
body.json = parsedBody;
|
||||
break;
|
||||
case 'text/xml':
|
||||
body.mode = 'xml';
|
||||
body.xml = parsedBody;
|
||||
break;
|
||||
case 'application/x-www-form-urlencoded':
|
||||
body.mode = 'formUrlEncoded';
|
||||
body.formUrlEncoded = parseFormData(parsedBody);
|
||||
break;
|
||||
case 'multipart/form-data':
|
||||
body.mode = 'multipartForm';
|
||||
body.multipartForm = parsedBody;
|
||||
break;
|
||||
case 'text/plain':
|
||||
default:
|
||||
body.mode = 'text';
|
||||
body.text = parsedBody;
|
||||
break;
|
||||
const parsedBody = request.data;
|
||||
if (parsedBody && contentType && typeof contentType === 'string') {
|
||||
if (contentType.includes('application/json')) {
|
||||
body.mode = 'json';
|
||||
body.json = safeStringifyJSON(parsedBody);
|
||||
} else if (contentType.includes('text/xml')) {
|
||||
body.mode = 'xml';
|
||||
body.xml = parsedBody;
|
||||
} else if (contentType.includes('application/x-www-form-urlencoded')) {
|
||||
body.mode = 'formUrlEncoded';
|
||||
console.log(parsedBody);
|
||||
console.log(parseFormData(parsedBody));
|
||||
body.formUrlEncoded = parseFormData(parsedBody);
|
||||
} else if (contentType.includes('multipart/form-data')) {
|
||||
body.mode = 'multipartForm';
|
||||
body.multipartForm = parsedBody;
|
||||
} else if (contentType.includes('text/plain')) {
|
||||
body.mode = 'text';
|
||||
body.text = parsedBody;
|
||||
}
|
||||
}
|
||||
return {
|
||||
@ -55,6 +61,7 @@ export const getRequestFromCurlCommand = (command) => {
|
||||
headers: headers
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
238
packages/bruno-app/src/utils/curl/parse-curl.js
Normal file
238
packages/bruno-app/src/utils/curl/parse-curl.js
Normal file
@ -0,0 +1,238 @@
|
||||
/**
|
||||
* Copyright (c) 2014-2016 Nick Carneiro
|
||||
* https://github.com/curlconverter/curlconverter
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import * as cookie from 'cookie';
|
||||
import * as URL from 'url';
|
||||
import * as querystring from 'query-string';
|
||||
import yargs from 'yargs-parser';
|
||||
|
||||
const parseCurlCommand = (curlCommand) => {
|
||||
// Remove newlines (and from continuations)
|
||||
curlCommand = curlCommand.replace(/\\\r|\\\n/g, '');
|
||||
|
||||
// Remove extra whitespace
|
||||
curlCommand = curlCommand.replace(/\s+/g, ' ');
|
||||
|
||||
// yargs parses -XPOST as separate arguments. just prescreen for it.
|
||||
curlCommand = curlCommand.replace(/ -XPOST/, ' -X POST');
|
||||
curlCommand = curlCommand.replace(/ -XGET/, ' -X GET');
|
||||
curlCommand = curlCommand.replace(/ -XPUT/, ' -X PUT');
|
||||
curlCommand = curlCommand.replace(/ -XPATCH/, ' -X PATCH');
|
||||
curlCommand = curlCommand.replace(/ -XDELETE/, ' -X DELETE');
|
||||
curlCommand = curlCommand.replace(/ -XOPTIONS/, ' -X OPTIONS');
|
||||
// Safari adds `-Xnull` if is unable to determine the request type, it can be ignored
|
||||
curlCommand = curlCommand.replace(/ -Xnull/, ' ');
|
||||
curlCommand = curlCommand.trim();
|
||||
|
||||
const parsedArguments = yargs(curlCommand, {
|
||||
boolean: ['I', 'head', 'compressed', 'L', 'k', 'silent', 's'],
|
||||
alias: {
|
||||
H: 'header',
|
||||
A: 'user-agent'
|
||||
}
|
||||
});
|
||||
|
||||
let cookieString;
|
||||
let cookies;
|
||||
let url = parsedArguments._[1] || '';
|
||||
|
||||
// remove surrounding quotes if present
|
||||
if (url && url.length) {
|
||||
url = url.replace(/^['"]|['"]$/g, '');
|
||||
}
|
||||
|
||||
// if url argument wasn't where we expected it, try to find it in the other arguments
|
||||
if (!url) {
|
||||
for (const argName in parsedArguments) {
|
||||
if (typeof parsedArguments[argName] === 'string') {
|
||||
if (parsedArguments[argName].indexOf('http') === 0 || parsedArguments[argName].indexOf('www.') === 0) {
|
||||
url = parsedArguments[argName];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let headers;
|
||||
|
||||
if (parsedArguments.header) {
|
||||
if (!headers) {
|
||||
headers = {};
|
||||
}
|
||||
if (!Array.isArray(parsedArguments.header)) {
|
||||
parsedArguments.header = [parsedArguments.header];
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (parsedArguments['user-agent']) {
|
||||
if (!headers) {
|
||||
headers = {};
|
||||
}
|
||||
headers['User-Agent'] = parsedArguments['user-agent'];
|
||||
}
|
||||
|
||||
if (parsedArguments.b) {
|
||||
cookieString = parsedArguments.b;
|
||||
}
|
||||
if (parsedArguments.cookie) {
|
||||
cookieString = parsedArguments.cookie;
|
||||
}
|
||||
let multipartUploads;
|
||||
if (parsedArguments.F) {
|
||||
multipartUploads = {};
|
||||
if (!Array.isArray(parsedArguments.F)) {
|
||||
parsedArguments.F = [parsedArguments.F];
|
||||
}
|
||||
parsedArguments.F.forEach((multipartArgument) => {
|
||||
// input looks like key=value. value could be json or a file path prepended with an @
|
||||
const splitArguments = multipartArgument.split('=', 2);
|
||||
const key = splitArguments[0];
|
||||
const value = splitArguments[1];
|
||||
multipartUploads[key] = value;
|
||||
});
|
||||
}
|
||||
if (cookieString) {
|
||||
const cookieParseOptions = {
|
||||
decode: function (s) {
|
||||
return s;
|
||||
}
|
||||
};
|
||||
// separate out cookie headers into separate data structure
|
||||
// note: cookie is case insensitive
|
||||
cookies = cookie.parse(cookieString.replace(/^Cookie: /gi, ''), cookieParseOptions);
|
||||
}
|
||||
let method;
|
||||
if (parsedArguments.X === 'POST') {
|
||||
method = 'post';
|
||||
} else if (parsedArguments.X === 'PUT' || parsedArguments.T) {
|
||||
method = 'put';
|
||||
} else if (parsedArguments.X === 'PATCH') {
|
||||
method = 'patch';
|
||||
} else if (parsedArguments.X === 'DELETE') {
|
||||
method = 'delete';
|
||||
} else if (parsedArguments.X === 'OPTIONS') {
|
||||
method = 'options';
|
||||
} else if (
|
||||
(parsedArguments.d ||
|
||||
parsedArguments.data ||
|
||||
parsedArguments['data-ascii'] ||
|
||||
parsedArguments['data-binary'] ||
|
||||
parsedArguments['data-raw'] ||
|
||||
parsedArguments.F ||
|
||||
parsedArguments.form) &&
|
||||
!(parsedArguments.G || parsedArguments.get)
|
||||
) {
|
||||
method = 'post';
|
||||
} else if (parsedArguments.I || parsedArguments.head) {
|
||||
method = 'head';
|
||||
} else {
|
||||
method = 'get';
|
||||
}
|
||||
|
||||
const compressed = !!parsedArguments.compressed;
|
||||
const urlObject = URL.parse(url || '');
|
||||
|
||||
// if GET request with data, convert data to query string
|
||||
// NB: the -G flag does not change the http verb. It just moves the data into the url.
|
||||
if (parsedArguments.G || parsedArguments.get) {
|
||||
urlObject.query = urlObject.query ? urlObject.query : '';
|
||||
const option = 'd' in parsedArguments ? 'd' : 'data' in parsedArguments ? 'data' : null;
|
||||
if (option) {
|
||||
let urlQueryString = '';
|
||||
|
||||
if (url.indexOf('?') < 0) {
|
||||
url += '?';
|
||||
} else {
|
||||
urlQueryString += '&';
|
||||
}
|
||||
|
||||
if (typeof parsedArguments[option] === 'object') {
|
||||
urlQueryString += parsedArguments[option].join('&');
|
||||
} else {
|
||||
urlQueryString += parsedArguments[option];
|
||||
}
|
||||
urlObject.query += urlQueryString;
|
||||
url += urlQueryString;
|
||||
delete parsedArguments[option];
|
||||
}
|
||||
}
|
||||
if (urlObject.query && urlObject.query.endsWith('&')) {
|
||||
urlObject.query = urlObject.query.slice(0, -1);
|
||||
}
|
||||
const query = querystring.parse(urlObject.query, { sort: false });
|
||||
for (const param in query) {
|
||||
if (query[param] === null) {
|
||||
query[param] = '';
|
||||
}
|
||||
}
|
||||
|
||||
urlObject.search = null; // Clean out the search/query portion.
|
||||
const request = {
|
||||
url: url,
|
||||
urlWithoutQuery: URL.format(urlObject)
|
||||
};
|
||||
if (compressed) {
|
||||
request.compressed = true;
|
||||
}
|
||||
|
||||
if (Object.keys(query).length > 0) {
|
||||
request.query = query;
|
||||
}
|
||||
if (headers) {
|
||||
request.headers = headers;
|
||||
}
|
||||
request.method = method;
|
||||
|
||||
if (cookies) {
|
||||
request.cookies = cookies;
|
||||
request.cookieString = cookieString.replace('Cookie: ', '');
|
||||
}
|
||||
if (multipartUploads) {
|
||||
request.multipartUploads = multipartUploads;
|
||||
}
|
||||
if (parsedArguments.data) {
|
||||
request.data = parsedArguments.data;
|
||||
} else if (parsedArguments['data-binary']) {
|
||||
request.data = parsedArguments['data-binary'];
|
||||
request.isDataBinary = true;
|
||||
} else if (parsedArguments.d) {
|
||||
request.data = parsedArguments.d;
|
||||
} else if (parsedArguments['data-ascii']) {
|
||||
request.data = parsedArguments['data-ascii'];
|
||||
} else if (parsedArguments['data-raw']) {
|
||||
request.data = parsedArguments['data-raw'];
|
||||
request.isDataRaw = true;
|
||||
}
|
||||
|
||||
if (parsedArguments.u) {
|
||||
request.auth = parsedArguments.u;
|
||||
}
|
||||
if (parsedArguments.user) {
|
||||
request.auth = parsedArguments.user;
|
||||
}
|
||||
if (Array.isArray(request.data)) {
|
||||
request.dataArray = request.data;
|
||||
request.data = request.data.join('&');
|
||||
}
|
||||
|
||||
if (parsedArguments.k || parsedArguments.insecure) {
|
||||
request.insecure = true;
|
||||
}
|
||||
return request;
|
||||
};
|
||||
|
||||
export default parseCurlCommand;
|
@ -187,18 +187,24 @@ const transformOpenapiRequestItem = (request) => {
|
||||
return brunoRequestItem;
|
||||
};
|
||||
|
||||
const resolveRefs = (spec, components = spec.components) => {
|
||||
const resolveRefs = (spec, components = spec.components, visitedItems = new Set()) => {
|
||||
if (!spec || typeof spec !== 'object') {
|
||||
return spec;
|
||||
}
|
||||
|
||||
if (Array.isArray(spec)) {
|
||||
return spec.map((item) => resolveRefs(item, components));
|
||||
return spec.map((item) => resolveRefs(item, components, visitedItems));
|
||||
}
|
||||
|
||||
if ('$ref' in spec) {
|
||||
const refPath = spec.$ref;
|
||||
|
||||
if (visitedItems.has(refPath)) {
|
||||
return spec;
|
||||
} else {
|
||||
visitedItems.add(refPath);
|
||||
}
|
||||
|
||||
if (refPath.startsWith('#/components/')) {
|
||||
// Local reference within components
|
||||
const refKeys = refPath.replace('#/components/', '').split('/');
|
||||
@ -213,7 +219,7 @@ const resolveRefs = (spec, components = spec.components) => {
|
||||
}
|
||||
}
|
||||
|
||||
return resolveRefs(ref, components);
|
||||
return resolveRefs(ref, components, visitedItems);
|
||||
} else {
|
||||
// Handle external references (not implemented here)
|
||||
// You would need to fetch the external reference and resolve it.
|
||||
@ -223,7 +229,7 @@ const resolveRefs = (spec, components = spec.components) => {
|
||||
|
||||
// Recursively resolve references in nested objects
|
||||
for (const prop in spec) {
|
||||
spec[prop] = resolveRefs(spec[prop], components);
|
||||
spec[prop] = resolveRefs(spec[prop], components, visitedItems);
|
||||
}
|
||||
|
||||
return spec;
|
||||
@ -267,12 +273,7 @@ const getDefaultUrl = (serverObject) => {
|
||||
};
|
||||
|
||||
const getSecurity = (apiSpec) => {
|
||||
let supportedSchemes = apiSpec.security || [];
|
||||
if (supportedSchemes.length === 0) {
|
||||
return {
|
||||
supported: []
|
||||
};
|
||||
}
|
||||
let defaultSchemes = apiSpec.security || [];
|
||||
|
||||
let securitySchemes = get(apiSpec, 'components.securitySchemes', {});
|
||||
if (Object.keys(securitySchemes) === 0) {
|
||||
@ -282,7 +283,7 @@ const getSecurity = (apiSpec) => {
|
||||
}
|
||||
|
||||
return {
|
||||
supported: supportedSchemes.map((scheme) => {
|
||||
supported: defaultSchemes.map((scheme) => {
|
||||
var schemeName = Object.keys(scheme)[0];
|
||||
return securitySchemes[schemeName];
|
||||
}),
|
||||
|
@ -1,5 +1,9 @@
|
||||
# Changelog
|
||||
|
||||
## 1.1.0
|
||||
|
||||
- Upgraded axios to 1.5.1
|
||||
|
||||
## 1.0.0
|
||||
|
||||
- Announcing Stable Release
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@usebruno/cli",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.1",
|
||||
"license": "MIT",
|
||||
"main": "src/index.js",
|
||||
"bin": {
|
||||
@ -24,7 +24,7 @@
|
||||
"package.json"
|
||||
],
|
||||
"dependencies": {
|
||||
"@usebruno/js": "0.9.1",
|
||||
"@usebruno/js": "0.9.2",
|
||||
"@usebruno/lang": "0.9.0",
|
||||
"axios": "^1.5.1",
|
||||
"chai": "^4.3.7",
|
||||
|
@ -3,7 +3,7 @@ const qs = require('qs');
|
||||
const chalk = require('chalk');
|
||||
const decomment = require('decomment');
|
||||
const fs = require('fs');
|
||||
const { forOwn, each, extend, get, compact } = require('lodash');
|
||||
const { forOwn, isUndefined, isNull, each, extend, get, compact } = require('lodash');
|
||||
const FormData = require('form-data');
|
||||
const prepareRequest = require('./prepare-request');
|
||||
const interpolateVars = require('./interpolate-vars');
|
||||
@ -136,14 +136,15 @@ const runSingleRequest = async function (
|
||||
const proxyAuthEnabled = get(brunoConfig, 'proxy.auth.enabled', false);
|
||||
const socksEnabled = proxyProtocol.includes('socks');
|
||||
|
||||
let uriPort = isUndefined(proxyPort) || isNull(proxyPort) ? '' : `:${proxyPort}`;
|
||||
let proxyUri;
|
||||
if (proxyAuthEnabled) {
|
||||
const proxyAuthUsername = interpolateString(get(brunoConfig, 'proxy.auth.username'), interpolationOptions);
|
||||
const proxyAuthPassword = interpolateString(get(brunoConfig, 'proxy.auth.password'), interpolationOptions);
|
||||
|
||||
proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}:${proxyPort}`;
|
||||
proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}${uriPort}`;
|
||||
} else {
|
||||
proxyUri = `${proxyProtocol}://${proxyHostname}:${proxyPort}`;
|
||||
proxyUri = `${proxyProtocol}://${proxyHostname}${uriPort}`;
|
||||
}
|
||||
|
||||
if (socksEnabled) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "v1.1.0",
|
||||
"version": "v1.1.1",
|
||||
"name": "bruno",
|
||||
"description": "Opensource API Client for Exploring and Testing APIs",
|
||||
"homepage": "https://www.usebruno.com",
|
||||
@ -20,7 +20,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/credential-providers": "^3.425.0",
|
||||
"@usebruno/js": "0.9.1",
|
||||
"@usebruno/js": "0.9.2",
|
||||
"@usebruno/lang": "0.9.0",
|
||||
"@usebruno/schema": "0.6.0",
|
||||
"about-window": "^1.15.2",
|
||||
|
@ -14,16 +14,18 @@ const { loadWindowState, saveBounds, saveMaximized } = require('./utils/window')
|
||||
|
||||
const lastOpenedCollections = new LastOpenedCollections();
|
||||
|
||||
// Reference: https://content-security-policy.com/
|
||||
const contentSecurityPolicy = [
|
||||
isDev ? "default-src 'self' 'unsafe-inline' 'unsafe-eval'" : "default-src 'self'",
|
||||
"connect-src 'self' https://api.github.com/repos/usebruno/bruno",
|
||||
"font-src 'self' https://fonts.gstatic.com",
|
||||
"default-src 'self'",
|
||||
"script-src * 'unsafe-inline' 'unsafe-eval'",
|
||||
"connect-src 'self' api.github.com",
|
||||
"font-src 'self' https:",
|
||||
"form-action 'none'",
|
||||
"img-src 'self' blob: data:",
|
||||
"style-src 'self' https://fonts.googleapis.com"
|
||||
"img-src 'self' blob: data: https:",
|
||||
"style-src 'self' 'unsafe-inline' https:"
|
||||
];
|
||||
|
||||
setContentSecurityPolicy(contentSecurityPolicy.join(';'));
|
||||
setContentSecurityPolicy(contentSecurityPolicy.join(';') + ';');
|
||||
|
||||
const menu = Menu.buildFromTemplate(menuTemplate);
|
||||
Menu.setApplicationMenu(menu);
|
||||
|
@ -6,11 +6,10 @@ const axios = require('axios');
|
||||
const path = require('path');
|
||||
const decomment = require('decomment');
|
||||
const Mustache = require('mustache');
|
||||
const FormData = require('form-data');
|
||||
const contentDispositionParser = require('content-disposition');
|
||||
const mime = require('mime-types');
|
||||
const { ipcMain } = require('electron');
|
||||
const { forOwn, extend, each, get, compact } = require('lodash');
|
||||
const { isUndefined, isNull, each, get, compact } = require('lodash');
|
||||
const { VarsRuntime, AssertRuntime, ScriptRuntime, TestRuntime } = require('@usebruno/js');
|
||||
const prepareRequest = require('./prepare-request');
|
||||
const prepareGqlIntrospectionRequest = require('./prepare-gql-introspection-request');
|
||||
@ -72,22 +71,6 @@ const getEnvVars = (environment = {}) => {
|
||||
};
|
||||
};
|
||||
|
||||
const getSize = (data) => {
|
||||
if (!data) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (typeof data === 'string') {
|
||||
return Buffer.byteLength(data, 'utf8');
|
||||
}
|
||||
|
||||
if (typeof data === 'object') {
|
||||
return Buffer.byteLength(safeStringifyJSON(data), 'utf8');
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
const configureRequest = async (
|
||||
collectionUid,
|
||||
request,
|
||||
@ -143,7 +126,7 @@ const configureRequest = async (
|
||||
|
||||
// proxy configuration
|
||||
let proxyConfig = get(brunoConfig, 'proxy', {});
|
||||
let proxyEnabled = get(proxyConfig, 'enabled', false);
|
||||
let proxyEnabled = get(proxyConfig, 'enabled', 'global');
|
||||
if (proxyEnabled === 'global') {
|
||||
proxyConfig = preferencesUtil.getGlobalProxyConfig();
|
||||
proxyEnabled = get(proxyConfig, 'enabled', false);
|
||||
@ -156,14 +139,15 @@ const configureRequest = async (
|
||||
const proxyAuthEnabled = get(proxyConfig, 'auth.enabled', false);
|
||||
const socksEnabled = proxyProtocol.includes('socks');
|
||||
|
||||
let uriPort = isUndefined(proxyPort) || isNull(proxyPort) ? '' : `:${proxyPort}`;
|
||||
let proxyUri;
|
||||
if (proxyAuthEnabled) {
|
||||
const proxyAuthUsername = interpolateString(get(proxyConfig, 'auth.username'), interpolationOptions);
|
||||
const proxyAuthPassword = interpolateString(get(proxyConfig, 'auth.password'), interpolationOptions);
|
||||
|
||||
proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}:${proxyPort}`;
|
||||
proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}${uriPort}`;
|
||||
} else {
|
||||
proxyUri = `${proxyProtocol}://${proxyHostname}:${proxyPort}`;
|
||||
proxyUri = `${proxyProtocol}://${proxyHostname}${uriPort}`;
|
||||
}
|
||||
|
||||
if (socksEnabled) {
|
||||
@ -203,10 +187,10 @@ const configureRequest = async (
|
||||
const parseDataFromResponse = (response) => {
|
||||
const dataBuffer = Buffer.from(response.data);
|
||||
// Parse the charset from content type: https://stackoverflow.com/a/33192813
|
||||
const charset = /charset=([^()<>@,;:\"/[\]?.=\s]*)/i.exec(response.headers['Content-Type'] || '');
|
||||
const charset = /charset=([^()<>@,;:"/[\]?.=\s]*)/i.exec(response.headers['Content-Type'] || '');
|
||||
// Overwrite the original data for backwards compatability
|
||||
let data = dataBuffer.toString(charset || 'utf-8');
|
||||
// Try to parse response to JSON, this can quitly fail
|
||||
// Try to parse response to JSON, this can quietly fail
|
||||
try {
|
||||
data = JSON.parse(response.data);
|
||||
} catch {}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@usebruno/js",
|
||||
"version": "0.9.1",
|
||||
"version": "0.9.2",
|
||||
"license": "MIT",
|
||||
"main": "src/index.js",
|
||||
"files": [
|
||||
@ -17,7 +17,7 @@
|
||||
"@usebruno/query": "0.1.0",
|
||||
"ajv": "^8.12.0",
|
||||
"atob": "^2.1.2",
|
||||
"axios": "^0.26.0",
|
||||
"axios": "^1.5.1",
|
||||
"btoa": "^1.2.1",
|
||||
"chai": "^4.3.7",
|
||||
"chai-string": "^1.5.0",
|
||||
|
@ -45,7 +45,7 @@ class Bru {
|
||||
|
||||
setEnvVar(key, value) {
|
||||
if (!key) {
|
||||
throw new Error('Key is required');
|
||||
throw new Error('Creating a env variable without specifying a name is not allowed.');
|
||||
}
|
||||
|
||||
// gracefully ignore if key is not present in environment
|
||||
@ -58,7 +58,7 @@ class Bru {
|
||||
|
||||
setVar(key, value) {
|
||||
if (!key) {
|
||||
throw new Error('Key is required');
|
||||
throw new Error('Creating a variable without specifying a name is not allowed.');
|
||||
}
|
||||
|
||||
if (envVariableNameRegex.test(key) === false) {
|
||||
|
@ -1,3 +1,5 @@
|
||||
**English** | [Português (BR)](docs/publishing/publishing_pt_br.md)
|
||||
|
||||
### Publishing Bruno to a new package manager
|
||||
|
||||
While our code is open source and available for everyone to use, we kindly request that you reach out to us before considering publication on new package managers. As the creator of Bruno, I hold the trademark `Bruno` for this project and would like to manage its distribution. If you'd like to see Bruno on a new package manager, please raise a GitHub issue.
|
||||
|
@ -10,7 +10,7 @@
|
||||
[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
|
||||
[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)
|
||||
|
||||
**English** | [Українська](docs/readme/readme_ua.md) | [Русский](docs/readme/readme_ru.md) | [Türkçe](docs/readme/readme_tr.md) | [Deutsch](docs/readme/readme_de.md) | [Français](docs/readme/readme_fr.md) | [বাংলা](docs/readme/readme_bn.md)
|
||||
**English** | [Українська](docs/readme/readme_ua.md) | [Русский](docs/readme/readme_ru.md) | [Türkçe](docs/readme/readme_tr.md) | [Deutsch](docs/readme/readme_de.md) | [Français](docs/readme/readme_fr.md) | [Português (BR)](docs/readme/readme_pt_br.md)) | [한국어](docs/readme/readme_kr.md) ) | [বাংলা](docs/readme/readme_bn.md)
|
||||
|
||||
Bruno is a new and innovative API client, aimed at revolutionizing the status quo represented by Postman and similar tools out there.
|
||||
|
||||
@ -65,6 +65,7 @@ Or any version control system of your choice
|
||||
- [Our Long Term Vision](https://github.com/usebruno/bruno/discussions/269)
|
||||
- [Roadmap](https://github.com/usebruno/bruno/discussions/384)
|
||||
- [Documentation](https://docs.usebruno.com)
|
||||
- [Stack Overflow](https://stackoverflow.com/questions/tagged/bruno)
|
||||
- [Website](https://www.usebruno.com)
|
||||
- [Pricing](https://www.usebruno.com/pricing)
|
||||
- [Download](https://www.usebruno.com/downloads)
|
||||
|
Loading…
Reference in New Issue
Block a user