Compare commits

..

2 Commits

Author SHA1 Message Date
lohit
b3a99a4d85 Merge pull request #5030 from stupidly-logical/fix/gen_code_auth_header
fix: Add null check for collection root in snippet generator #5029
2025-07-03 20:31:16 +05:30
lohit
bbfa2b39a0 Merge pull request #5036 from maintainer-bruno/feat/fix-params-table-scroll
fix: params table default scroll
2025-07-03 20:30:51 +05:30
1507 changed files with 16554 additions and 116137 deletions

View File

@@ -1,66 +0,0 @@
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
language: 'en-US'
early_access: false
tone_instructions: 'You are an expert code reviewer in TypeScript, JavaScript, NodeJS, and ElectronJS. You work in an enterprise software developer team, providing concise and clear code review advice. You only elaborate or provide detailed explanations when requested.'
knowledge_base:
opt_out: false
code_guidelines:
enabled: true
filePatterns:
- '**/CODING_STANDARDS.md'
reviews:
profile: 'chill'
request_changes_workflow: false
high_level_summary: true
poem: true
review_status: true
collapse_walkthrough: false
auto_review:
enabled: true
drafts: false
base_branches: ['main', 'release/*']
path_instructions:
- path: 'tests/**/**.*'
instructions: |
Review the following e2e test code written using the Playwright test library. Ensure that:
- Follow best practices for Playwright code and e2e automation
- Try to reduce usage of `page.waitForTimeout();` in code unless absolutely necessary and the locator cannot be found using existing `expect()` playwright calls
- Avoid using `page.pause()` in code
- Use locator variables for locators
- Avoid using test.only
- Use multiple assertions
- Promote the use of `test.step` as much as possible so the generated reports are easier to read
- Ensure that the `fixtures` like the collections are nested inside the `fixtures` folder
**Fixture Example***: Here's an example of possible fixture and test pair
```
.
├── fixtures
│ └── collection
│ ├── base.bru
│ ├── bruno.json
│ ├── collection.bru
│ ├── ws-test-request-with-headers.bru
│ ├── ws-test-request-with-subproto.bru
│ └── ws-test-request.bru
├── connection.spec.ts # <- Depends on the collection in ./fixtures/collection
├── headers.spec.ts
├── persistence.spec.ts
├── variable-interpolation
│ ├── fixtures
│ │ └── collection
│ │ ├── environments
│ │ ├── bruno.json
│ │ └── ws-interpolation-test.bru
│ ├── init-user-data
│ └── variable-interpolation.spec.ts # <- Depends on the collection in ./variable-interpolation/fixtures/collection
└── subproto.spec.ts
```
chat:
auto_reply: true

2
.github/CODEOWNERS vendored
View File

@@ -1 +1 @@
* @helloanoop @maintainer-bruno @bijin-bruno @lohit-bruno @naman-bruno
* @helloanoop @maintainer-bruno @lohit-bruno @naman-bruno

View File

@@ -1,10 +1,9 @@
### Description
# Description
<!-- Explain here the changes your PR introduces and text to help us understand the context of this change. -->
#### Contribution Checklist:
### Contribution Checklist:
- [ ] **I've used AI significantly to create this pull request**
- [ ] **The pull request only addresses one issue or adds one feature.**
- [ ] **The pull request does not introduce any breaking changes**
- [ ] **I have added screenshots or gifs to help explain the change if applicable.**
@@ -13,6 +12,6 @@
Note: Keeping the PR small and focused helps make it easier to review and merge. If you have multiple changes you want to make, please consider submitting them as separate pull requests.
#### Publishing to New Package Managers
### Publishing to New Package Managers
Please see [here](../publishing.md) for more information.

View File

@@ -1,27 +0,0 @@
name: 'Setup Node Dependencies'
description: 'Install Node.js and npm dependencies'
runs:
using: 'composite'
steps:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: v22.17.0
cache: 'npm'
cache-dependency-path: './package-lock.json'
- name: Install node dependencies
shell: bash
run: npm ci --legacy-peer-deps
- name: Build libraries
shell: bash
run: |
npm run build:graphql-docs
npm run build:bruno-query
npm run build:bruno-common
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
npm run build:bruno-converters
npm run build:bruno-requests
npm run build:schema-types
npm run build:bruno-filestore

View File

@@ -1,36 +0,0 @@
name: 'Run Basic SSL CLI Tests - Linux'
description: 'Run basic SSL CLI tests on Linux'
runs:
using: 'composite'
steps:
- name: Run CLI tests
shell: bash
run: |
set -euo pipefail
# navigate to basic SSL test collection directory
cd tests/ssl/basic-ssl/collections/badssl
echo "basic ssl success"
# should pass
node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit1.xml --insecure --format junit
xmllint --xpath 'count(//testsuite[@errors="0"])' junit1.xml | grep -q "^1$" || exit 1
echo "with default/system ca certs"
# should pass
node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit2.xml --format junit
xmllint --xpath 'count(//testsuite[@errors="0"])' junit2.xml | grep -q "^1$" || exit 1
# navigate to self-signed SSL test collection directory
cd ../self-signed-badssl
echo "self-signed ssl with validation disabled"
# should pass
node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit3.xml --insecure --format junit
xmllint --xpath 'count(//testsuite[@errors="0"])' junit3.xml | grep -q "^1$" || exit 1
echo "self-signed ssl with default/system ca certs"
echo "request will error"
# should fail
node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit4.xml --format junit 2>/dev/null || true
xmllint --xpath 'count(//testsuite[@errors="1"])' junit4.xml | grep -q "^1$" || exit 1

View File

@@ -1,33 +0,0 @@
name: 'Run Custom CA Certs CLI Tests - Linux'
description: 'Run custom CA certs CLI tests on Linux'
runs:
using: 'composite'
steps:
- name: Run CLI tests
shell: bash
run: |
set -euo pipefail
# navigate to CA certificates test collection directory
cd tests/ssl/custom-ca-certs/collection
echo "custom valid ca cert"
# should pass
node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit1.xml --cacert ../server/certs/ca-cert.pem --ignore-truststore --format junit
xmllint --xpath 'count(//testsuite[@errors="0"])' junit1.xml | grep -q "^1$" || exit 1
echo "custom valid ca cert with defaults"
# should pass
node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit2.xml --cacert ../server/certs/ca-cert.pem --format junit
xmllint --xpath 'count(//testsuite[@errors="0"])' junit2.xml | grep -q "^1$" || exit 1
echo "custom invalid ca cert"
echo "request will error"
# should fail
node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit3.xml --cacert ../server/certs/ca-key.pem --ignore-truststore --format junit 2>/dev/null || true
xmllint --xpath 'count(//testsuite[@errors="1"])' junit3.xml | grep -q "^1$" || exit 1
echo "custom invalid ca cert with defaults"
# should pass
node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit4.xml --cacert ../server/certs/ca-key.pem --format junit
xmllint --xpath 'count(//testsuite[@errors="0"])' junit4.xml | grep -q "^1$" || exit 1

View File

@@ -1,19 +0,0 @@
name: 'Run SSL E2E Tests - Linux'
description: 'Run SSL E2E tests on Linux'
runs:
using: 'composite'
steps:
- name: Run E2E tests
shell: bash
run: |
set -euo pipefail
xvfb-run npm run test:e2e:ssl
- name: Upload Playwright Report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: playwright-report-linux
path: playwright-report/
retention-days: 30

View File

@@ -1,26 +0,0 @@
name: 'Setup CA Certificates - Linux'
description: 'Setup CA certificates and start test server for custom CA certs tests on Linux'
runs:
using: 'composite'
steps:
- name: Setup CA certificates
shell: bash
run: |
set -euo pipefail
cd tests/ssl/custom-ca-certs/server
echo "running certificate setup"
node scripts/generate-certs.js
- name: Start test server
shell: bash
run: |
set -euo pipefail
cd tests/ssl/custom-ca-certs/server
echo "starting server in background"
node index.js &
echo "server started with PID: $!"

View File

@@ -1,15 +0,0 @@
name: 'Setup Custom CA Certs Feature Dependencies - Linux'
description: 'Setup feature-specific dependencies for custom CA certs tests on Linux'
runs:
using: 'composite'
steps:
- name: Install additional OS dependencies for custom CA certs
shell: bash
run: |
sudo apt-get update
sudo apt-get --no-install-recommends install -y \
libglib2.0-0 libnss3 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libgtk-3-0 libasound2t64 \
xvfb libxml2-utils
sudo chown root /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox
sudo chmod 4755 /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox

View File

@@ -1,36 +0,0 @@
name: 'Run Basic SSL CLI Tests - macOS'
description: 'Run basic SSL CLI tests on macOS'
runs:
using: 'composite'
steps:
- name: Run CLI tests
shell: bash
run: |
set -euo pipefail
# navigate to basic SSL test collection directory
cd tests/ssl/basic-ssl/collections/badssl
echo "basic ssl success"
# should pass
node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit1.xml --insecure --format junit
xmllint --xpath 'count(//testsuite[@errors="0"])' junit1.xml | grep -q "^1$" || exit 1
echo "with default/system ca certs"
# should pass
node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit2.xml --format junit
xmllint --xpath 'count(//testsuite[@errors="0"])' junit2.xml | grep -q "^1$" || exit 1
# navigate to self-signed SSL test collection directory
cd ../self-signed-badssl
echo "self-signed ssl with validation disabled"
# should pass
node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit3.xml --insecure --format junit
xmllint --xpath 'count(//testsuite[@errors="0"])' junit3.xml | grep -q "^1$" || exit 1
echo "self-signed ssl with default/system ca certs"
echo "request will error"
# should fail
node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit4.xml --format junit 2>/dev/null || true
xmllint --xpath 'count(//testsuite[@errors="1"])' junit4.xml | grep -q "^1$" || exit 1

View File

@@ -1,33 +0,0 @@
name: 'Run Custom CA Certs CLI Tests - macOS'
description: 'Run custom CA certs CLI tests on macOS'
runs:
using: 'composite'
steps:
- name: Run CLI tests
shell: bash
run: |
set -euo pipefail
# navigate to CA certificates test collection directory
cd tests/ssl/custom-ca-certs/collection
echo "custom valid ca cert"
# should pass
node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit1.xml --cacert ../server/certs/ca-cert.pem --ignore-truststore --format junit
xmllint --xpath 'count(//testsuite[@errors="0"])' junit1.xml | grep -q "^1$" || exit 1
echo "custom valid ca cert with defaults"
# should pass
node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit2.xml --cacert ../server/certs/ca-cert.pem --format junit
xmllint --xpath 'count(//testsuite[@errors="0"])' junit2.xml | grep -q "^1$" || exit 1
echo "custom invalid ca cert"
echo "request will error"
# should fail
node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit3.xml --cacert ../server/certs/ca-key.pem --ignore-truststore --format junit 2>/dev/null || true
xmllint --xpath 'count(//testsuite[@errors="1"])' junit3.xml | grep -q "^1$" || exit 1
echo "custom invalid ca cert with defaults"
# should pass
node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit4.xml --cacert ../server/certs/ca-key.pem --format junit
xmllint --xpath 'count(//testsuite[@errors="0"])' junit4.xml | grep -q "^1$" || exit 1

View File

@@ -1,17 +0,0 @@
name: 'Run SSL E2E Tests - macOS'
description: 'Run SSL E2E tests on macOS'
runs:
using: 'composite'
steps:
- name: Run E2E tests
shell: bash
run: |
npm run test:e2e:ssl
- name: Upload Playwright Report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: playwright-report-macos
path: playwright-report/
retention-days: 30

View File

@@ -1,26 +0,0 @@
name: 'Setup CA Certificates - macOS'
description: 'Setup CA certificates and start test server for custom CA certs tests on macOS'
runs:
using: 'composite'
steps:
- name: Setup CA certificates
shell: bash
run: |
set -euo pipefail
cd tests/ssl/custom-ca-certs/server
echo "running certificate setup"
node scripts/generate-certs.js
- name: Start test server
shell: bash
run: |
set -euo pipefail
cd tests/ssl/custom-ca-certs/server
echo "starting server in background"
node index.js &
echo "server started with PID: $!"

View File

@@ -1,9 +0,0 @@
name: 'Setup Custom CA Certs Feature Dependencies - macOS'
description: 'Setup feature-specific dependencies for custom CA certs tests on macOS'
runs:
using: 'composite'
steps:
- name: Install additional OS dependencies for custom CA certs
shell: bash
run: |
brew install libxml2

View File

@@ -1,50 +0,0 @@
name: 'Run Basic SSL CLI Tests - Windows'
description: 'Run basic SSL CLI tests on Windows'
runs:
using: 'composite'
steps:
- name: Run CLI tests
shell: pwsh
run: |
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
# navigate to basic SSL test collection directory
Set-Location tests\ssl\basic-ssl\collections\badssl
Write-Host "basic ssl success"
# should pass
$process = Start-Process -FilePath "node" -ArgumentList "..\..\..\..\..\packages\bruno-cli\bin\bru.js run .\request.bru --output junit1.xml --insecure --format junit" -NoNewWindow -Wait -PassThru -RedirectStandardError "nul"
[xml]$xml1 = Get-Content junit1.xml
$testsuites1 = if ($xml1.testsuites) { $xml1.testsuites.testsuite } else { $xml1.testsuite }
$errorCount1 = ($testsuites1 | Where-Object { $_.errors -eq "0" } | Measure-Object).Count
if ($errorCount1 -ne 1) { exit 1 }
Write-Host "with default/system ca certs"
# should pass
$process = Start-Process -FilePath "node" -ArgumentList "..\..\..\..\..\packages\bruno-cli\bin\bru.js run .\request.bru --output junit2.xml --format junit" -NoNewWindow -Wait -PassThru -RedirectStandardError "nul"
[xml]$xml2 = Get-Content junit2.xml
$testsuites2 = if ($xml2.testsuites) { $xml2.testsuites.testsuite } else { $xml2.testsuite }
$errorCount2 = ($testsuites2 | Where-Object { $_.errors -eq "0" } | Measure-Object).Count
if ($errorCount2 -ne 1) { exit 1 }
# navigate to self-signed SSL test collection directory
Set-Location ..\self-signed-badssl
Write-Host "self-signed ssl with validation disabled"
# should pass
$process = Start-Process -FilePath "node" -ArgumentList "..\..\..\..\..\packages\bruno-cli\bin\bru.js run .\request.bru --output junit3.xml --insecure --format junit" -NoNewWindow -Wait -PassThru -RedirectStandardError "nul"
[xml]$xml3 = Get-Content junit3.xml
$testsuites3 = if ($xml3.testsuites) { $xml3.testsuites.testsuite } else { $xml3.testsuite }
$errorCount3 = ($testsuites3 | Where-Object { $_.errors -eq "0" } | Measure-Object).Count
if ($errorCount3 -ne 1) { exit 1 }
Write-Host "self-signed ssl with default/system ca certs"
Write-Host "request will error"
# should fail
$process = Start-Process -FilePath "node" -ArgumentList "..\..\..\..\..\packages\bruno-cli\bin\bru.js run .\request.bru --output junit4.xml --format junit" -NoNewWindow -Wait -PassThru -RedirectStandardError "nul"
# Ignore the exit code - we expect this to fail
[xml]$xml4 = Get-Content junit4.xml
$testsuites4 = if ($xml4.testsuites) { $xml4.testsuites.testsuite } else { $xml4.testsuite }
$errorCount4 = ($testsuites4 | Where-Object { $_.errors -eq "1" } | Measure-Object).Count
if ($errorCount4 -ne 1) { exit 1 }

View File

@@ -1,47 +0,0 @@
name: 'Run Custom CA Certs CLI Tests - Windows'
description: 'Run custom CA certs CLI tests on Windows'
runs:
using: 'composite'
steps:
- name: Run CLI tests
shell: pwsh
run: |
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
# navigate to CA certificates test collection directory
Set-Location tests\ssl\custom-ca-certs\collection
Write-Host "custom valid ca cert"
# should pass
$process = Start-Process -FilePath "node" -ArgumentList "..\..\..\..\packages\bruno-cli\bin\bru.js run .\request.bru --output junit1.xml --cacert ..\server\certs\ca-cert.pem --ignore-truststore --format junit" -NoNewWindow -Wait -PassThru -RedirectStandardError "nul"
[xml]$xml1 = Get-Content junit1.xml
$testsuites1 = if ($xml1.testsuites) { $xml1.testsuites.testsuite } else { $xml1.testsuite }
$errorCount1 = ($testsuites1 | Where-Object { $_.errors -eq "0" } | Measure-Object).Count
if ($errorCount1 -ne 1) { exit 1 }
Write-Host "custom valid ca cert with defaults"
# should pass
$process = Start-Process -FilePath "node" -ArgumentList "..\..\..\..\packages\bruno-cli\bin\bru.js run .\request.bru --output junit2.xml --cacert ..\server\certs\ca-cert.pem --format junit" -NoNewWindow -Wait -PassThru -RedirectStandardError "nul"
[xml]$xml2 = Get-Content junit2.xml
$testsuites2 = if ($xml2.testsuites) { $xml2.testsuites.testsuite } else { $xml2.testsuite }
$errorCount2 = ($testsuites2 | Where-Object { $_.errors -eq "0" } | Measure-Object).Count
if ($errorCount2 -ne 1) { exit 1 }
Write-Host "custom invalid ca cert"
Write-Host "request will error"
# should fail
$process = Start-Process -FilePath "node" -ArgumentList "..\..\..\..\packages\bruno-cli\bin\bru.js run .\request.bru --output junit3.xml --cacert ..\server\certs\ca-key.pem --ignore-truststore --format junit" -NoNewWindow -Wait -PassThru -RedirectStandardError "nul"
# Ignore the exit code - we expect this to fail
[xml]$xml3 = Get-Content junit3.xml
$testsuites3 = if ($xml3.testsuites) { $xml3.testsuites.testsuite } else { $xml3.testsuite }
$errorCount3 = ($testsuites3 | Where-Object { $_.errors -eq "1" } | Measure-Object).Count
if ($errorCount3 -ne 1) { exit 1 }
Write-Host "custom invalid ca cert with defaults"
# should pass
$process = Start-Process -FilePath "node" -ArgumentList "..\..\..\..\packages\bruno-cli\bin\bru.js run .\request.bru --output junit4.xml --cacert ..\server\certs\ca-key.pem --format junit" -NoNewWindow -Wait -PassThru -RedirectStandardError "nul"
[xml]$xml4 = Get-Content junit4.xml
$testsuites4 = if ($xml4.testsuites) { $xml4.testsuites.testsuite } else { $xml4.testsuite }
$errorCount4 = ($testsuites4 | Where-Object { $_.errors -eq "0" } | Measure-Object).Count
if ($errorCount4 -ne 1) { exit 1 }

View File

@@ -1,17 +0,0 @@
name: 'Run SSL E2E Tests - Windows'
description: 'Run SSL E2E tests on Windows'
runs:
using: 'composite'
steps:
- name: Run E2E tests
shell: pwsh
run: |
npm run test:e2e:ssl
- name: Upload Playwright Report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: playwright-report-windows
path: playwright-report/
retention-days: 30

View File

@@ -1,25 +0,0 @@
name: 'Setup CA Certificates - Windows'
description: 'Setup CA certificates and start test server for custom CA certs tests on Windows'
runs:
using: 'composite'
steps:
- name: Setup CA certificates
shell: pwsh
run: |
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
Set-Location tests\ssl\custom-ca-certs\server
Write-Host "running certificate setup"
node scripts/generate-certs.js
- name: Start test server
shell: pwsh
run: |
Set-StrictMode -Version Latest
Set-Location tests\ssl\custom-ca-certs\server
Write-Host "starting server in background"
Start-Process -FilePath "node" -ArgumentList "index.js" -PassThru -WindowStyle Hidden

View File

@@ -25,8 +25,8 @@ jobs:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v5
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -40,7 +40,7 @@ jobs:
run: |
cd packages/bruno-tests/collection
npm install
bru run --env Prod --output junit.xml --format junit --sandbox developer
bru run --env Prod --output junit.xml --format junit
- name: Publish Test Report
uses: dorny/test-reporter@v2

View File

@@ -1,91 +0,0 @@
name: SSL Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
tests-for-linux:
name: SSL Tests - Linux
timeout-minutes: 60
runs-on: ubuntu-latest
permissions:
checks: write
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v6
- name: Setup Node Dependencies
uses: ./.github/actions/common/setup-node-deps
- name: Setup Feature Dependencies
uses: ./.github/actions/ssl/linux/setup-feature-specific-deps
- name: Setup CA Certificates
uses: ./.github/actions/ssl/linux/setup-ca-certs
- name: Run Basic SSL CLI Tests
uses: ./.github/actions/ssl/linux/run-basic-ssl-cli-tests
- name: Run Custom CA Certs CLI Tests
uses: ./.github/actions/ssl/linux/run-custom-ca-certs-cli-tests
- name: Run Custom CA Certs E2E Tests
uses: ./.github/actions/ssl/linux/run-ssl-e2e-tests
tests-for-macos:
name: SSL Tests - macOS
timeout-minutes: 60
runs-on: macos-latest
permissions:
checks: write
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v6
- name: Setup Node Dependencies
uses: ./.github/actions/common/setup-node-deps
- name: Setup Feature Dependencies
uses: ./.github/actions/ssl/macos/setup-feature-specific-deps
- name: Setup CA Certificates
uses: ./.github/actions/ssl/macos/setup-ca-certs
- name: Run Basic SSL CLI Tests
uses: ./.github/actions/ssl/macos/run-basic-ssl-cli-tests
- name: Run Custom CA Certs CLI Tests
uses: ./.github/actions/ssl/macos/run-custom-ca-certs-cli-tests
- name: Run Custom CA Certs E2E Tests
uses: ./.github/actions/ssl/macos/run-ssl-e2e-tests
tests-for-windows:
name: SSL Tests - Windows
timeout-minutes: 60
runs-on: windows-latest
permissions:
checks: write
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v6
- name: Setup Node Dependencies
uses: ./.github/actions/common/setup-node-deps
- name: Setup CA Certificates
uses: ./.github/actions/ssl/windows/setup-ca-certs
- name: Run Basic SSL CLI Tests
uses: ./.github/actions/ssl/windows/run-basic-ssl-cli-tests
- name: Run Custom CA Certs CLI Tests
uses: ./.github/actions/ssl/windows/run-custom-ca-certs-cli-tests
- name: Run Custom CA Certs E2E Tests
uses: ./.github/actions/ssl/windows/run-ssl-e2e-tests

View File

@@ -13,8 +13,8 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v5
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
@@ -30,13 +30,9 @@ jobs:
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
npm run build --workspace=packages/bruno-converters
npm run build --workspace=packages/bruno-requests
npm run build --workspace=packages/bruno-schema-types
npm run build --workspace=packages/bruno-filestore
- name: Lint Check
run: npm run lint
env:
ESLINT_PLUGIN_DIFF_COMMIT: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.ref || 'main' }}
# tests
- name: Test Package bruno-js
@@ -67,8 +63,8 @@ jobs:
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v5
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
@@ -84,19 +80,12 @@ jobs:
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
npm run build --workspace=packages/bruno-converters
npm run build --workspace=packages/bruno-requests
npm run build --workspace=packages/bruno-schema-types
npm run build --workspace=packages/bruno-filestore
- name: Run Local Testbench
run: |
npm start --workspace=packages/bruno-tests &
sleep 5
- name: Run tests
run: |
cd packages/bruno-tests/collection
npm install
node ../../bruno-cli/bin/bru.js run --env Prod --output junit.xml --format junit --sandbox developer
node ../../bruno-cli/bin/bru.js run --env Prod --output junit.xml --format junit
- name: Publish Test Report
uses: EnricoMi/publish-unit-test-result-action@v2
@@ -110,8 +99,8 @@ jobs:
timeout-minutes: 60
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v5
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: v22.11.x
- name: Install dependencies
@@ -136,13 +125,11 @@ jobs:
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
npm run build:bruno-converters
npm run build:bruno-requests
npm run build:schema-types
npm run build:bruno-filestore
- name: Run Playwright tests
run: |
xvfb-run npm run test:e2e
- uses: actions/upload-artifact@v5
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report

9
.gitignore vendored
View File

@@ -51,12 +51,3 @@ bruno.iml
# Playwright
/blob-report/
# Development plan files
*.plan.md
# packages dist
packages/bruno-filestore/dist
packages/bruno-requests/dist
packages/bruno-schema-types/dist
packages/bruno-converters/dist

View File

@@ -1 +0,0 @@
npx nano-staged

View File

@@ -1,78 +0,0 @@
# Bruno Coding Standards
- No diffs unless an actual change is made, the code changes need to be as minimal as possible, avoid making un-necessary whitespace diffs. This is already handled by eslint but make sure you check your code changes before commiting and raising a PR.
## General Style Rules
- Use 2 spaces for indentation. No tabs, just spaces keeps everything neat and uniform.
- Stick to single quotes for strings. Double quotes are cool elsewhere, but here we go single.
- Always add semicolons at the end of statements. It's like putting a period at the end of a sentence clarity matters.
- JSX is enabled, so feel free to use it where it makes sense.
## Punctuation and Spacing
- No trailing commas. Keep it clean, no extra commas hanging around.
- Always use parentheses around parameters in arrow functions. Even for single params consistency is key.
- For multiline constructs, put opening braces on the same line, and ensure consistency. Minimum 2 elements for multiline.
- No newlines inside function parentheses. Keep 'em tight.
- Space before and after the arrow in arrow functions. `() => {}` is good.
- No space between function name and parentheses. `func()` not `func ()`.
- Semicolons go at the end of the line, not on a new line.
- No strict max length write readable code, not cramped lines.
- Multiple expressions per line in JSX are fine flexibility is nice.
Remember, these rules are here to make our codebase harmonious. If something doesn't fit perfectly, let's chat about it. Happy coding! 🚀
## Tests
- Add tests for any new functionality or meaningful changes. If code is added, removed, or significantly modified, corresponding tests should be updated or created.
- Prioritise high-value tests over maximum coverage. Focus on testing behaviour that is critical, complex, or likely to break—dont chase coverage numbers for their own sake.
- Write behaviour-driven tests, not implementation-driven ones. Tests should validate real expected output and observable behaviour, not internal details or mocked-out logic unless absolutely necessary.
- Minimise mocking unless it meaningfully increases clarity or isolates external dependencies. Prefer real flows where practical; only mock external services, slow systems, or non-deterministic behaviour.
- Keep tests readable and maintainable. Optimise for clarity over cleverness. Name tests descriptively, keep setup minimal, and avoid unnecessary abstraction.
- Aim for tests that fail usefully. When a test fails, it should clearly indicate what behaviour broke and why.
- Cover both the “happy path” and the realistically problematic paths. Validate expected success behaviour, but also validate error handling, edge cases, and degraded-mode behaviour when appropriate.
- Ensure tests are deterministic and reproducible. No randomness, timing dependencies, or environment-specific assumptions without explicit control.
- Avoid overfitting tests to current behaviour if future flexibility matters. Only assert what needs to be true, not incidental details.
- Use consistent patterns and helper utilities where they improve clarity. Prefer shared test utilities over copy-pasted setup code, but only when it actually reduces complexity.
- Tests should be fast enough to run continuously. Avoid long-running operations unless absolutely necessary; prefer lightweight fixtures and isolated units.
## UI Specific instructions
### React
- Use styled component's theme prop to manage CSS colors and not CSS variables when in the context of a styled component or any react component using the styled component
- Styled Components are used as wrappers to define both self and children components style, tailwind classes are used specifically for layout based styles.
- Styled Component CSS might also change layout but tailwind classes shouldn't define colors.
## Readability and Abstractions
- Avoid abstractions unless the exact same code is being used in more than 3 places.
- Names for functions need to be concise and descriptive.
- Add in JSDoc comments to add more details to the abstractions if needed.
- Follow functional programming but just enough to be readable, we don't need to go as deep as ADTs and Monads, we want to keep the code pipeline obvious and easy for everyone to read and contribute to.
- Avoid single line abstractions where all that's being done is increasing the call stack with one additional function.
- Add in meaningful comments instead of obvious ones where complex code flow is explained properly.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 813 KiB

After

Width:  |  Height:  |  Size: 537 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 409 KiB

View File

@@ -16,7 +16,6 @@
| [日本語](docs/contributing/contributing_ja.md)
| [हिंदी](docs/contributing/contributing_hi.md)
| [Dutch](docs/contributing/contributing_nl.md)
| [فارسی](docs/contributing/contributing_fa.md)
## Let's make Bruno better, together!!
@@ -70,13 +69,10 @@ npm run build:bruno-query
npm run build:bruno-common
npm run build:bruno-converters
npm run build:bruno-requests
npm run build:schema-types
npm run build:bruno-filestore
# bundle js sandbox libraries
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
```
##### Option 2
```bash
@@ -97,22 +93,18 @@ npm run dev:electron
```
##### Option 2
```bash
# run electron and react app concurrently
npm run dev
```
#### Customize Electron `userData` path
If `ELECTRON_USER_DATA_PATH` env-variable is present and its development mode, then `userData` path is modified accordingly.
e.g.
```sh
ELECTRON_USER_DATA_PATH=$(realpath ~/Desktop/bruno-test) npm run dev:electron
```
This will create a `bruno-test` folder on your Desktop and use it as the `userData` path.
### Troubleshooting

View File

@@ -1,92 +0,0 @@
[English](../../contributing.md)
## با هم، Bruno را بهتر می‌کنیم!
خوشحالم که قصد دارید Bruno را بهبود ببخشید. در ادامه قوانین و راهنماها برای راه‌اندازی Bruno روی سیستم شما آورده شده است.
### فناوری‌های استفاده‌شده
به فارسی برونو Bruno با استفاده از Next.js و React ساخته شده است. همچنین از Electron برای بسته‌بندی نسخه دسکتاپ (که امکان مجموعه‌های محلی را فراهم می‌کند) استفاده می‌کنیم.
کتابخانه‌هایی که استفاده می‌کنیم:
- CSS - Tailwind استایل
- Codemirror - ویرایشگر کد
- Redux - مدیریت وضعیت
- Tabler Icons - آیکون‌ها
- formik - فرم‌ها
- Yup اعتبارسنجی اسکیمـا
- axios - کلاینت درخواست
- chokidar - پایش‌گر سیستم فایل
### پیش‌نیازها
شما به [نود v20.x یا اخرین نسخه پایدار](https://nodejs.org/en/) و npm 8.x نیاز دارید. در این پروژه از فضای کاری npm (npm workspaces) استفاده می‌کنیم.
### شروع به کدنویسی
برای راه‌اندازی محیط توسعه محلی به فایل [مستندات توسعه](docs/development_fa.md) مراجعه کنید:
### ارسال Pull Request
1 - لطفاً Pull Requestها (PR) را کوتاه و متمرکز نگه دارید و تنها یک هدف مشخص را دنبال کنند. </br>
2 - لطفاً از فرمت نام‌گذاری شاخه‌ها استفاده کنید:
- feature/[name]: این شاخه باید شامل یک قابلیت مشخص باشد.
- feature/dark-mode : مثال
- bugfix/[name]: این شاخه باید تنها شامل رفع یک باگ مشخص باشد.
- bugfix/bug-1 : مثال
## توسعه
به فارسی برونو یا Bruno به‌صورت یک اپلیکیشن «سنگین» توسعه داده می‌شود. برای اجرا باید ابتدا Next.js را در یک پنجره ترمینال اجرا کنید و سپس اپلیکیشن Electron را در پنجره ترمینال دیگری راه‌اندازی نمایید.
### نیازمندی توسعه
- NodeJS v18
### اجرای محلی
```bash
# از ورژن NodeJS 18 استفاده کنید
nvm use
# نصب وابستگی‌ها
npm i --legacy-peer-deps
# ساخت مستندات GraphQL
npm run build:graphql-docs
# ساخت bruno-query
npm run build:bruno-query
# اجرای اپ Next (ترمینال 1)
npm run dev:web
# اجرای اپ Electron (ترمینال 2)
npm run dev:electron
```
### عیب‌یابی
ممکن است هنگام اجرای `npm install` خطای `Unsupported platform` ببینید. برای رفع این مشکل، پوشه `node_modules` و فایل `package-lock.json` را حذف کرده و سپس دوباره `npm install` را اجرا کنید. این کار معمولاً همه پکیج‌های لازم را نصب می‌کند.
```shell
# حذف پوشه node_modules در زیردایرکتوری‌ها
find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do
rm -rf "$dir"
done
# حذف فایل package-lock.json در زیردایرکتوری‌ها
find . -type f -name "package-lock.json" -delete
```
### تست‌ها
```bash
# اجرای تست‌های schema مربوط به bruno
npm test --workspace=packages/bruno-schema
# اجرای تست‌ها در همه فضاهای کاری (در صورت وجود)
npm test --workspaces --if-present
```

View File

@@ -1,470 +0,0 @@
# Playwright Testing Guide for Bruno
This guide explains how to create and run Playwright test cases for the Bruno application using the UI.
## Table of Contents
- [Overview](#overview)
- [Prerequisites](#prerequisites)
- [Creating Tests Using Codegen](#creating-tests-using-codegen)
- [Manual Test Creation](#manual-test-creation)
- [Test Structure and Organization](#test-structure-and-organization)
- [Available Test Fixtures](#available-test-fixtures)
- [Running Tests](#running-tests)
- [Best Practices](#best-practices)
- [Examples](#examples)
- [Troubleshooting](#troubleshooting)
## Overview
Bruno uses Playwright for end-to-end testing of its Electron application. The testing setup includes custom fixtures for Electron app testing and utilities for managing test data.
## Prerequisites
- Node.js installed
- All dependencies installed (`npm install`)
- Electron app can be built and run
## Creating Tests Using Codegen
The easiest way to create tests is using Playwright's codegen feature, which records your UI interactions and generates test code.
### Using the Built-in Codegen Script
```bash
# Generate a test with a specific name
npm run test:codegen my-new-test
# Generate a test without specifying a name (will prompt for input)
npm run test:codegen
```
### What Happens During Codegen
1. The Electron app launches automatically
2. Playwright Inspector opens in a separate window
3. You interact with the Bruno UI
4. Actions are recorded and converted to test code
5. The generated test file is saved in `e2e-tests/`
### Codegen Workflow
1. **Start Recording**: Run the codegen command
2. **Interact with UI**: Perform the actions you want to test
3. **Add Assertions**: Use the inspector to add assertions
4. **Save Test**: The test file is automatically generated
5. **Review and Refine**: Edit the generated test as needed
## Manual Test Creation
You can also create tests manually by following the established patterns.
### Basic Test Structure
```typescript
import { test, expect } from '../../playwright';
test('Test description', async ({ page }) => {
// Test steps here
await page.getByLabel('Some Label').click();
// Assertions
await expect(page.getByText('Expected Text')).toBeVisible();
});
```
### Test with Temporary Data
```typescript
import { test, expect } from '../../playwright';
test('Test with temporary data', async ({ page, createTmpDir }) => {
// Create temporary directory for test data
const testDir = await createTmpDir('test-collection');
// Test steps
await page.getByLabel('Create Collection').click();
await page.getByLabel('Name').fill('test-collection');
await page.getByLabel('Location').fill(testDir);
// Assertions
await expect(page.getByText('test-collection')).toBeVisible();
});
```
## Test Structure and Organization
### Directory Structure
```
e2e-tests/
├── 001-sanity-tests/ # Basic functionality tests
│ ├── 001-home-screen.spec.ts
│ └── 002-create-new-collection-and-new-request.spec.ts
├── 002-feature-tests/ # Specific feature tests
├── 003-integration-tests/ # Complex workflow tests
└── bruno-testbench/ # Test utilities and helpers
```
### Naming Conventions
- **Files**: Use descriptive names with `.spec.ts` extension
- **Tests**: Use clear, descriptive test names
- **Folders**: Use numbered prefixes for ordering
### Test File Template
```typescript
import { test, expect } from '../../playwright';
test.describe('Feature Name', () => {
test('should perform specific action', async ({ page }) => {
// Arrange
// Act
// Assert
});
test('should handle error case', async ({ page }) => {
// Test error scenarios
});
});
```
## Available Test Fixtures
The Bruno Playwright setup provides several custom fixtures:
### Core Fixtures
- `page`: Main page for testing
- `context`: Browser context
- `electronApp`: Electron application instance
### Utility Fixtures
- `createTmpDir`: Creates temporary directories for test data
- `newPage`: Creates a new page instance
- `pageWithUserData`: Page with custom user data
- `launchElectronApp`: Launches a new Electron app instance
- `reuseOrLaunchElectronApp`: Reuses existing app or launches new one
### Using Fixtures
```typescript
test('Test with multiple fixtures', async ({ page, createTmpDir, electronApp }) => {
const testDir = await createTmpDir('test-data');
// Your test logic here
});
```
## Running Tests
### Basic Commands
```bash
# Run all tests
npm run test:e2e
# Run specific test file
npx playwright test e2e-tests/001-sanity-tests/001-home-screen.spec.ts
# Run tests in a specific folder
npx playwright test e2e-tests/001-sanity-tests/
```
### Advanced Options
```bash
# Run with UI mode (for debugging)
npx playwright test --ui
# Run in headed mode (see browser)
npx playwright test --headed
# Run with specific browser
npx playwright test --project="Bruno Electron App"
# Run with debugging
npx playwright test --debug
# Run with trace recording
npx playwright test --trace on
```
### CI/CD Integration
```bash
# Install browsers for CI
npx playwright install
# Run tests in CI mode
npm run test:e2e
```
## Best Practices
### 1. Use Semantic Selectors
**Preferred:**
```typescript
await page.getByRole('button', { name: 'Create' }).click();
await page.getByLabel('Collection Name').fill('test');
await page.getByText('Success message').toBeVisible();
```
**Avoid:**
```typescript
await page.locator('.btn-primary').click();
await page.locator('#collection-name').fill('test');
```
### 2. Create Isolated Tests
Each test should be independent and not rely on other tests:
```typescript
test('should create collection', async ({ page, createTmpDir }) => {
const testDir = await createTmpDir('collection-test');
// Test creates its own data
await page.getByLabel('Create Collection').click();
await page.getByLabel('Name').fill('test-collection');
await page.getByLabel('Location').fill(testDir);
// Clean up happens automatically via createTmpDir
});
```
### 3. Add Meaningful Assertions
Always verify the expected outcomes:
```typescript
test('should save request successfully', async ({ page }) => {
// Arrange
await page.getByLabel('Create Collection').click();
// Act
await page.getByRole('button', { name: 'Save' }).click();
// Assert
await expect(page.getByText('Request saved successfully')).toBeVisible();
await expect(page.getByRole('tab', { name: 'GET request' })).toBeVisible();
});
```
### 4. Handle Async Operations
```typescript
test('should wait for network requests', async ({ page }) => {
// Wait for specific network request
await page.waitForResponse((response) => response.url().includes('/api/endpoint'));
// Or wait for element to be stable
await page.waitForSelector('[data-testid="loading"]', { state: 'hidden' });
});
```
### 5. Use Test Data Management
```typescript
test('should work with test data', async ({ page, createTmpDir }) => {
const testDir = await createTmpDir('test-data');
// Create test files
await fs.writeFile(path.join(testDir, 'test.bru'), testContent);
// Use in test
await page.getByLabel('Open Collection').click();
await page.getByText(testDir).click();
});
```
## Examples
### Example 1: Basic Collection Creation
```typescript
import { test, expect } from '../../playwright';
test('should create a new collection', async ({ page, createTmpDir }) => {
const testDir = await createTmpDir('new-collection');
await page.getByLabel('Create Collection').click();
await page.getByLabel('Name').fill('My Test Collection');
await page.getByLabel('Location').fill(testDir);
await page.getByRole('button', { name: 'Create' }).click();
await expect(page.getByText('My Test Collection')).toBeVisible();
});
```
### Example 2: Request Creation and Execution
```typescript
import { test, expect } from '../../playwright';
test('should create and execute HTTP request', async ({ page, createTmpDir }) => {
const testDir = await createTmpDir('request-test');
// Create collection
await page.getByLabel('Create Collection').click();
await page.getByLabel('Name').fill('Request Test');
await page.getByLabel('Location').fill(testDir);
await page.getByRole('button', { name: 'Create' }).click();
// Create request
await page.locator('#create-new-tab').getByRole('img').click();
await page.getByPlaceholder('Request Name').fill('Test Request');
await page.locator('#new-request-url .CodeMirror').click();
await page.locator('textarea').fill('http://localhost:8081/ping');
await page.getByRole('button', { name: 'Create' }).click();
// Execute request
await page.locator('#send-request').getByRole('img').nth(2).click();
// Verify response
await expect(page.getByRole('main')).toContainText('200 OK');
});
```
### Example 3: Environment Management
```typescript
import { test, expect } from '../../playwright';
test('should create and use environment variables', async ({ page, createTmpDir }) => {
const testDir = await createTmpDir('env-test');
// Setup collection
await page.getByLabel('Create Collection').click();
await page.getByLabel('Name').fill('Environment Test');
await page.getByLabel('Location').fill(testDir);
await page.getByRole('button', { name: 'Create' }).click();
// Create environment
await page.getByRole('button', { name: 'Environments' }).click();
await page.getByRole('button', { name: 'Add Environment' }).click();
await page.getByLabel('Environment Name').fill('Development');
await page.getByRole('button', { name: 'Create' }).click();
// Add variable
await page.getByRole('button', { name: 'Add Variable' }).click();
await page.getByLabel('Variable Name').fill('API_URL');
await page.getByLabel('Variable Value').fill('http://localhost:3000');
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByText('API_URL')).toBeVisible();
});
```
## Troubleshooting
### Common Issues
1. **Electron App Not Starting**
```bash
# Ensure dependencies are installed
npm install
# Try running the app manually first
npm run dev:electron
```
2. **Tests Timing Out**
```typescript
// Increase timeout for specific test
test('slow test', async ({ page }) => {
test.setTimeout(60000); // 60 seconds
// Test steps
});
```
3. **Element Not Found**
```typescript
// Wait for element to be present
await page.waitForSelector('[data-testid="element"]');
// Or use more specific selectors
await page.getByRole('button', { name: 'Exact Button Text' }).click();
```
4. **Flaky Tests**
```typescript
// Use stable selectors
await page.getByTestId('stable-id').click();
// Wait for state changes
await page.waitForLoadState('networkidle');
```
### Debug Mode
```bash
# Run with debug mode
npx playwright test --debug
# Run specific test in debug mode
npx playwright test --debug e2e-tests/001-sanity-tests/001-home-screen.spec.ts
```
### Trace Analysis
```bash
# Run with trace recording
npx playwright test --trace on
# View trace in browser
npx playwright show-trace test-results/trace-*.zip
```
## Configuration
The Playwright configuration is in `playwright.config.ts`:
```typescript
export default defineConfig({
testDir: './e2e-tests',
fullyParallel: false,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 1 : 0,
workers: process.env.CI ? undefined : 1,
projects: [
{
name: 'Bruno Electron App'
}
],
webServer: [
{
command: 'npm run dev:web',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI
},
{
command: 'npm start --workspace=packages/bruno-tests',
url: 'http://localhost:8081/ping',
reuseExistingServer: !process.env.CI
}
]
});
```
## Additional Resources
- [Playwright Documentation](https://playwright.dev/)
- [Playwright Test API](https://playwright.dev/docs/api/class-test)
- [Electron Testing with Playwright](https://playwright.dev/docs/api/class-electronapplication)
- [Bruno Project Structure](../readme.md)
---
For questions or issues with testing, please refer to the project's contributing guidelines or create an issue in the repository.

View File

@@ -1,8 +0,0 @@
[English](../../publishing.md)
### انتشار Bruno در یک پکیج منیجر جدید
اگرچه کد ما متن‌باز است و همه می‌توانند از آن استفاده کنند، لطفاً قبل از انتشار Bruno در مدیر بسته‌های جدید با ما تماس بگیرید. به عنوان سازنده Bruno، علامت تجاری `Bruno` را برای این پروژه دارم و مایلم توزیع آن را مدیریت کنم. اگر دوست دارید Bruno را در یک مدیر بسته جدید ببینید، لطفاً یک issue در گیت‌هاب ثبت کنید.
اگرچه بیشتر قابلیت‌های ما رایگان و متن‌باز هستند (شامل REST و GraphQL Apis)،
ما تلاش می‌کنیم بین اصول متن‌باز و توسعه پایدار تعادل مناسبی برقرار کنیم - https://github.com/usebruno/bruno/discussions/269

View File

@@ -41,6 +41,13 @@
![bruno](/assets/images/landing-2.png) <br /><br />
### الطبعة الذهبية ✨
غالبية ميزاتنا مجانية ومفتوحة المصدر.
نحن نسعى لتحقيق توازن متناغم بين [مبادئ الشفافية والاستدامة](https://github.com/usebruno/bruno/discussions/269)
طلبات الشراء لـ [الطبعة الذهبية](https://www.usebruno.com/pricing) ستطلق قريبًا بسعر ~~$19~~ **$9** ! <br/>
[اشترك هنا](https://usebruno.ck.page/4c65576bd4) لتصلك إشعارات عند الإطلاق.
### التثبيت
@@ -67,13 +74,10 @@ flatpak install com.usebruno.Bruno
# على نظام Linux عبر Apt
sudo mkdir -p /etc/apt/keyrings
sudo apt update && sudo apt install gpg curl
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
| gpg --dearmor \
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
echo "deb [arch=amd64 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 gpg
sudo gpg --list-keys
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
echo "deb [arch=amd64 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
```

View File

@@ -59,13 +59,10 @@ snap install bruno
# Apt এর মাধ্যমে লিনাক্সে
sudo mkdir -p /etc/apt/keyrings
sudo apt update && sudo apt install gpg curl
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
| gpg --dearmor \
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
echo "deb [arch=amd64 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 gpg
sudo gpg --list-keys
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
echo "deb [arch=amd64 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
```

View File

@@ -37,37 +37,13 @@ Bruno 直接在您的电脑文件夹中存储您的 API 信息。我们使用纯
Bruno 仅限离线使用。我们计划永不向 Bruno 添加云同步功能。我们重视您的数据隐私,并认为它应该留在您的设备上。阅读我们的长期愿景 [点击查看](https://github.com/usebruno/bruno/discussions/269)
[下载 Bruno](https://www.usebruno.com/downloads)
📢 观看我们在印度 FOSS 3.0 会议上的最新演讲 [点击查看](https://www.youtube.com/watch?v=7bSMFpbcPiY)
![bruno](../../assets/images/landing-2.png) <br /><br />
## 商业版本 ✨
### 安装
我们的大多数功能都是免费且开源的
我们致力于在 [开源与可持续性发展](https://github.com/usebruno/bruno/discussions/269) 之间取得和谐的平衡
欢迎使用我们的 [付费版本](https://www.usebruno.com/pricing) ,看看附加的功能是否对您或团队有所帮助! <br/>
## 目录
- [安装](#安装)
- [特性](#特性)
- [跨平台使用 🖥️](#跨平台使用-)
- [通过Git协作 👩‍💻🧑‍💻](#通过git协作-)
- [重要链接 📌](#重要链接-)
- [展示 🎥](#展示-)
- [分享评价 📣](#分享评价-)
- [发布到新的包管理器](#发布到新的包管理器)
- [联系方式 🌐](#联系方式-)
- [商标](#商标)
- [贡献 👩‍💻🧑‍💻](#贡献-)
- [作者](#作者)
- [许可证 📄](#许可证-)
## 安装
Bruno 可以在我们的 [网站上下载](https://www.usebruno.com/downloads) 适用于Mac、Windows 和 Linux 的可执行文件。
Bruno 可以在我们的 [网站上下载](https://www.usebruno.com/downloads) Mac、Windows 和 Linux 的可执行文件
您也可以通过包管理器如 Homebrew、Chocolatey、Scoop、Snap 和 Apt 安装 Bruno。
@@ -82,71 +58,79 @@ choco install bruno
scoop bucket add extras
scoop install bruno
# 在 Windows 上用 winget 安装
winget install Bruno.Bruno
# 在 Linux 上用 Snap 安装
snap install bruno
# 在 Linux 上用 Flatpak 安装
flatpak install com.usebruno.Bruno
# 在 Linux 上用 Apt 安装
sudo mkdir -p /etc/apt/keyrings
sudo apt update && sudo apt install gpg curl
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
| gpg --dearmor \
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
echo "deb [arch=amd64 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 gpg
sudo gpg --list-keys
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
echo "deb [arch=amd64 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
```
## 特性
### 跨平台使用 🖥️
### 在 Mac 上通过 Homebrew 安装 🖥️
![bruno](../../assets/images/run-anywhere.png) <br /><br />
### 通过Git协作 👩‍💻🧑‍💻
### Collaborate 安装 👩‍💻🧑‍💻
或者任何您选择的版本控制系统
![bruno](../../assets/images/version-control.png) <br /><br />
## 重要链接 📌
### 重要链接 📌
- [我们的愿景](https://github.com/usebruno/bruno/discussions/269)
- [路线图](https://www.usebruno.com/roadmap)
- [路线图](https://github.com/usebruno/bruno/discussions/384)
- [文档](https://docs.usebruno.com)
- [Stack Overflow](https://stackoverflow.com/questions/tagged/bruno)
- [网站](https://www.usebruno.com)
- [价格](https://www.usebruno.com/pricing)
- [下载](https://www.usebruno.com/downloads)
- [GitHub 赞助](https://github.com/sponsors/helloanoop).
## 展示 🎥
### 展示 🎥
- [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 Sponsors](https://github.com/sponsors/helloanoop) 来赞助我们。
### 分享评价 📣
如果 Bruno 在您的工作和团队中帮助了您,请不要忘记在我们的 GitHub 讨论上分享您的 [评价](https://github.com/usebruno/bruno/discussions/343)
## 发布到新的包管理器
### 发布到新的包管理器
如需了解更多信息,请参见 [此处](../publishing/publishing_cn.md) 。
有关更多信息,请参见 [此处](../publishing/publishing_cn.md) 。
## 联系方式 🌐
### 贡献 👩‍💻🧑‍💻
我很高兴您希望改进 bruno。请查看 [贡献指南](../contributing/contributing_cn.md)。
即使您无法通过代码做出贡献,我们仍然欢迎您提出 BUG 和新的功能需求。
### 作者
<div align="center">
<a href="https://github.com/usebruno/bruno/graphs/contributors">
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
</a>
</div>
### 联系方式 🌐
[𝕏 (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)
## 商标
### 商标
**名称**
@@ -156,20 +140,6 @@ sudo apt update && sudo apt install bruno
Logo 源自 [OpenMoji](https://openmoji.org/library/emoji-1F436/). License: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)
## 贡献 👩‍💻🧑‍💻
很高兴您希望改进 bruno。请查看 [贡献指南](../contributing/contributing_cn.md)。
即使您无法通过代码做出贡献,我们仍然欢迎您提出 BUG 和新的功能需求。
## 作者
<div align="center">
<a href="https://github.com/usebruno/bruno/graphs/contributors">
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
</a>
</div>
## 许可证 📄
### 许可证 📄
[MIT](../../license.md)

View File

@@ -43,6 +43,13 @@ Bruno ist ein reines Offline-Tool. Es gibt keine Pläne, Bruno um eine Cloud-Syn
![bruno](/assets/images/landing-2.png) <br /><br />
### Golden Edition ✨
Die meisten unserer Funktionen sind kostenlos und quelloffen.
Wir bemühen uns um ein Gleichgewicht zwischen [Open-Source-Prinzipien und Nachhaltigkeit](https://github.com/usebruno/bruno/discussions/269)
Du kannst die [Golden Edition](https://www.usebruno.com/pricing) bestellen **$19**! <br/>
### Installation
Bruno ist als Download [auf unserer Website](https://www.usebruno.com/downloads) für Mac, Windows und Linux verfügbar.
@@ -71,13 +78,10 @@ flatpak install com.usebruno.Bruno
# Auf Linux via Apt
sudo mkdir -p /etc/apt/keyrings
sudo apt update && sudo apt install gpg curl
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
| gpg --dearmor \
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
echo "deb [arch=amd64 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 gpg
sudo gpg --list-keys
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
echo "deb [arch=amd64 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
```

View File

@@ -43,6 +43,13 @@ Bruno funciona sin conexión a internet. No tenemos intenciones de añadir sincr
![bruno](/assets/images/landing-2.png) <br /><br />
### Golden Edition ✨
La mayoría de nuestras funcionalidades son gratis y de código abierto.
Queremos alcanzar un equilibrio en armonía entre los [principios open-source y la sostenibilidad](https://github.com/usebruno/bruno/discussions/269).
¡Puedes reservar la [Golden Edition](https://www.usebruno.com/pricing) por ~~$19~~ **$9**! <br/>
### Instalación
Bruno está disponible para su descarga [en nuestro sitio web](https://www.usebruno.com/downloads) para Mac, Windows y Linux.
@@ -68,13 +75,10 @@ flatpak install com.usebruno.Bruno
# En Linux con Apt
sudo mkdir -p /etc/apt/keyrings
sudo apt update && sudo apt install gpg curl
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
| gpg --dearmor \
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
echo "deb [arch=amd64 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 gpg
sudo gpg --list-keys
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
echo "deb [arch=amd64 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
```

View File

@@ -1,143 +0,0 @@
<br />
<img src="../../assets/images/logo-transparent.png" width="80"/>
### برونو یا Bruno - محیط توسعه متن باز برای تست و توسعه API ها
[![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/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/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)
[English](../../readme.md)
| [Українська](./readme_ua.md)
| [Русский](./readme_ru.md)
| [Türkçe](./readme_tr.md)
| [Deutsch](./readme_de.md)
| [Français](./readme_fr.md)
| [Português (BR)](./readme_pt_br.md)
| [한국어](./readme_kr.md)
| [বাংলা](./readme_bn.md)
| [Español](./readme_es.md)
| **فارسی**
| [Română](./readme_ro.md)
| [Polski](./readme_pl.md)
| [简体中文](./readme_cn.md)
| [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md)
| [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
برونو یک کلاینت API جدید و نوآورانه است که هدفش تغییر وضعیت فعلی ابزارهایی مانند Postman و سایر ابزارهای مشابه است.
برونو مجموعه‌های شما را مستقیماً در یک پوشه روی فایل‌سیستم شما ذخیره می‌کند. ما از یک زبان نشانه‌گذاری ساده به نام Bru برای ذخیره اطلاعات درخواست‌های API استفاده می‌کنیم.
شما می‌توانید برای همکاری روی مجموعه‌های API خود، از Git یا هر سیستم کنترل نسخه دلخواهتان استفاده کنید.
برونو فقط به صورت آفلاین کار می‌کند. هیچ برنامه‌ای برای اضافه کردن همگام‌سازی ابری به برونو در آینده وجود ندارد. ما به حریم خصوصی داده‌های شما اهمیت می‌دهیم و معتقدیم که باید روی دستگاه خودتان باقی بمانند. می‌توانید چشم‌انداز بلندمدت ما را مطالعه کنید. [اینجا (به انگلیسی)](https://github.com/usebruno/bruno/discussions/269)
📢 جدیدترین ارائه ما را در کنفرانس India FOSS 3.0 تماشا کنید.
[اینجا](https://www.youtube.com/watch?v=7bSMFpbcPiY)
![bruno](/assets/images/landing-2.png) <br /><br />
### نصب
برونو به صورت یک فایل باینری برای دانلود در دسترس است. [بر روی وبسایت ما](https://www.usebruno.com/downloads) برای مک لینکوس و ویندوز.
همچنین می‌توانید برونو را از طریق مدیر بسته‌هایی مانند Homebrew، Chocolatey، Snap و Apt نصب کنید.
```sh
# بر روی مک از طریق brew
brew install bruno
# بر روی ویندوز از طریق Chocolatey
choco install bruno
# بر روی لینوکس از طریق Snap
snap install bruno
# بر روی لینوکس از طریق Apt
sudo mkdir -p /etc/apt/keyrings
sudo apt update && sudo apt install gpg curl
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
| gpg --dearmor \
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
echo "deb [arch=amd64 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 />
### همکاری از طریق گیت 👩‍💻🧑‍💻
یا هر سیستم کنترل نسخه‌ای که ترجیح می‌دهید
![bruno](/assets/images/version-control.png) <br /><br />
### لینک‌های مهم 📌
- [آخرین نسخه پایدار ما](https://github.com/usebruno/bruno/discussions/269)
- [نقشه راه](https://github.com/usebruno/bruno/discussions/384)
- [مستندات](https://docs.usebruno.com)
- [وبسایت](https://www.usebruno.com)
- [اشتراک ها](https://www.usebruno.com/pricing)
- [دانلود](https://www.usebruno.com/downloads)
### ویدیوها 🎥
- [تجربه ها](https://github.com/usebruno/bruno/discussions/343)
- [مرکز دانش](https://github.com/usebruno/bruno/discussions/386)
- [اسکریپ مانیا](https://github.com/usebruno/bruno/discussions/385)
### حمایت ❤️
جوون! اگر این پروژه را دوست دارید، روی دکمه ⭐ کلیک کنید!
### تجربه‌های به اشتراک گذاشته‌شده 📣
اگر برونو به شما یا تیمتان کمک کرده است، لطفاً فراموش نکنید تجربه‌های خود را به اشتراک بگذارید. [تجربه‌های خود را در بحث گیت‌هاب ما به اشتراک بگذارید](https://github.com/usebruno/bruno/discussions/343).
### انتشار برونو در یک پکیچ منیجر جدید
لطفا چک بکنید [اینجارو](../../publishing.md) برای اطلاعات بیشتر.
### مشارکت 👩‍💻🧑‍💻
خوشحالم که می‌خواهید برونو را بهتر کنید. لطفا [راهنمای مشارکت را بررسی کنید](../contributing/contributing_fa.md).
حتی اگر نمی‌توانید از طریق کدنویسی مشارکت کنید، در گزارش باگ‌ها و درخواست قابلیت‌های جدید که به حل نیازهای شما کمک می‌کند تردید نکنید.
### نویسنده ها
<div align="center">
<a href="https://github.com/usebruno/bruno/graphs/contributors">
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
</a>
</div>
### در ارتباط باشید 🌐
[𝕏 (تویتر)](https://twitter.com/use_bruno) <br />
[وبسایت](https://www.usebruno.com) <br />
[دیسکورد](https://discord.com/invite/KgcZUncpjq) <br />
[لینکدین](https://www.linkedin.com/company/usebruno)
### برند
**نام**
به فارسی برونو - `Bruno` یک علامت تجاری ثبت‌شده متعلق به [Anoop M D](https://www.helloanoop.com/)
**لوگو**
لوگو توسط [OpenMoji](https://openmoji.org/library/emoji-1F436/) ساخته شده است. مجوز: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)
### مجوز 📄
[MIT](../../license.md)

View File

@@ -63,13 +63,10 @@ snap install bruno
# Linux via Apt
sudo mkdir -p /etc/apt/keyrings
sudo apt update && sudo apt install gpg curl
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
| gpg --dearmor \
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
echo "deb [arch=amd64 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 gpg
sudo gpg --list-keys
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
echo "deb [arch=amd64 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
```

View File

@@ -75,14 +75,12 @@ flatpak install com.usebruno.Bruno
# Linux पर Apt के माध्यम से
sudo mkdir -p /etc/apt/keyrings
sudo apt update && sudo apt install gpg curl
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
| gpg --dearmor \
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
echo "deb [arch=amd64 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
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
कई प्लेटफार्मों पर चलाएं 🖥️
<br /><br />
@@ -150,3 +148,4 @@ Scriptmania
लाइसेंस 📄
MIT

View File

@@ -59,13 +59,10 @@ snap install bruno
# Su Linux tramite Apt
sudo mkdir -p /etc/apt/keyrings
sudo apt update && sudo apt install gpg curl
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
| gpg --dearmor \
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
echo "deb [arch=amd64 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 gpg
sudo gpg --list-keys
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
echo "deb [arch=amd64 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
```

View File

@@ -43,6 +43,13 @@ Bruno はオフラインのみで利用できます。Bruno にクラウド同
![bruno](/assets/images/landing-2.png) <br /><br />
### ゴールデンエディション ✨
機能のほとんどが無料で使用でき、オープンソースとなっています。
私たちは[オープンソースの原則と長期的な維持](https://github.com/usebruno/bruno/discussions/269)の間でうまくバランスを取ろうと努力しています。
[ゴールデンエディション](https://www.usebruno.com/pricing)を **19 ドル** (買い切り)で購入できます!
### インストール方法
Bruno は[私たちのウェブサイト](https://www.usebruno.com/downloads)からバイナリをダウンロードできます。Mac, Windows, Linux に対応しています。
@@ -71,13 +78,10 @@ flatpak install com.usebruno.Bruno
# LinuxでAptを使ってインストール
sudo mkdir -p /etc/apt/keyrings
sudo apt update && sudo apt install gpg curl
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
| gpg --dearmor \
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
echo "deb [arch=amd64 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 gpg
sudo gpg --list-keys
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
echo "deb [arch=amd64 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
```

View File

@@ -43,6 +43,12 @@
![bruno](../../assets/images/landing-2.png) <br /><br />
### ოქროს გამოცემა ✨
მთავარი ფუნქციების უმეტესობა უფასოა და ღია წყაროა. ჩვენ ვცდილობთ ჰარმონიული ბალანსის დაცვას [ღია წყაროების პრინციპებსა და მდგრადობას შორის](https://github.com/usebruno/bruno/discussions/269)
თქვენ შეგიძლიათ შეიძინოთ [ოქროს გამოცემა](https://www.usebruno.com/pricing) ერთჯერადი გადახდით **19 დოლარად**! <br/>
### ინსტალაცია
ბრუნო ხელმისაწვდომია როგორც ბინარული ჩამოტვირთვა [ჩვენ的网站上](https://www.usebruno.com/downloads) Mac-ის, Windows-ისა და Linux-ისთვის.
@@ -71,14 +77,12 @@ flatpak install com.usebruno.Bruno
# Linux-ზე Apt-ის საშუალებით
sudo mkdir -p /etc/apt/keyrings
sudo apt update && sudo apt install gpg curl
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
| gpg --dearmor \
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
echo "deb [arch=amd64 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
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
echo "deb [arch=amd64 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
```
### პლატფორმებს შორის მუშაობა 🖥️

View File

@@ -59,13 +59,10 @@ snap install bruno
# On Linux via Apt
sudo mkdir -p /etc/apt/keyrings
sudo apt update && sudo apt install gpg curl
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
| gpg --dearmor \
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
echo "deb [arch=amd64 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 gpg
sudo gpg --list-keys
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
echo "deb [arch=amd64 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
```

View File

@@ -26,6 +26,13 @@ Bruno is uitsluitend offline. Er zijn geen plannen om ooit cloud-synchronisatie
![bruno](/assets/images/landing-2.png) <br /><br />
### Golden Edition ✨
De meeste van onze functies zijn gratis en open source.
We streven naar een harmonieuze balans tussen [open-source principes en duurzaamheid](https://github.com/usebruno/bruno/discussions/269).
Je kunt de [Golden Edition](https://www.usebruno.com/pricing) kopen voor een eenmalige betaling van **$19**! <br/>
### Installatie
Bruno is beschikbaar als binaire download [op onze website](https://www.usebruno.com/downloads) voor Mac, Windows en Linux.
@@ -54,14 +61,12 @@ flatpak install com.usebruno.Bruno
# Op Linux via Apt
sudo mkdir -p /etc/apt/keyrings
sudo apt update && sudo apt install gpg curl
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
| gpg --dearmor \
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
echo "deb [arch=amd64 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
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
echo "deb [arch=amd64 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
```
### Draai op meerdere platformen 🖥️

View File

@@ -69,13 +69,10 @@ flatpak install com.usebruno.Bruno
# On Linux via Apt
sudo mkdir -p /etc/apt/keyrings
sudo apt update && sudo apt install gpg curl
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
| gpg --dearmor \
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
echo "deb [arch=amd64 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 gpg
sudo gpg --list-keys
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
echo "deb [arch=amd64 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
```

View File

@@ -41,6 +41,13 @@ Bruno é totalmente offline. Não há planos de adicionar sincronização em nuv
![bruno](../../assets/images/landing-2.png) <br /><br />
### Golden Edition ✨
A grande maioria dos nossos recursos são gratuitos e de código aberto.
Nós nos esforçamos para encontrar um equilíbrio harmônico entre [princípios de código aberto e sustentabilidade](https://github.com/usebruno/bruno/discussions/269)
Você pode pré encomendar o plano [Golden Edition](https://www.usebruno.com/pricing) por ~~USD $19~~ **USD $9**! <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.
@@ -69,13 +76,10 @@ flatpak install com.usebruno.Bruno
# No Linux via Apt
sudo mkdir -p /etc/apt/keyrings
sudo apt update && sudo apt install gpg curl
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
| gpg --dearmor \
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
echo "deb [arch=amd64 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 gpg
sudo gpg --list-keys
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
echo "deb [arch=amd64 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
```

View File

@@ -59,13 +59,10 @@ snap install bruno
# Pe Linux cu Apt
sudo mkdir -p /etc/apt/keyrings
sudo apt update && sudo apt install gpg curl
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
| gpg --dearmor \
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
echo "deb [arch=amd64 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 gpg
sudo gpg --list-keys
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
echo "deb [arch=amd64 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
```

View File

@@ -63,13 +63,10 @@ snap install bruno
# Apt aracılığıyla Linux'ta
sudo mkdir -p /etc/apt/keyrings
sudo apt update && sudo apt install gpg curl
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
| gpg --dearmor \
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
echo "deb [arch=amd64 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 gpg
sudo gpg --list-keys
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
echo "deb [arch=amd64 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
```

View File

@@ -63,13 +63,10 @@ snap install bruno
# 在 Linux 上使用 Apt 安裝
sudo mkdir -p /etc/apt/keyrings
sudo apt update && sudo apt install gpg curl
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
| gpg --dearmor \
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
echo "deb [arch=amd64 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 gpg
sudo gpg --list-keys
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
echo "deb [arch=amd64 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
```

View File

@@ -0,0 +1,5 @@
import { test, expect } from '../../playwright';
test('Check if the logo on top left is visible', async ({ page }) => {
await expect(page.getByRole('button', { name: 'bruno' })).toBeVisible();
});

View File

@@ -0,0 +1,31 @@
import { test, expect } from '../../playwright';
test('Create new collection and add a simple HTTP request', async ({ page, createTmpDir }) => {
await page.getByLabel('Create Collection').click();
await page.getByLabel('Name').click();
await page.getByLabel('Name').fill('test-collection');
await page.getByLabel('Name').press('Tab');
await page.getByLabel('Location').fill(await createTmpDir('test-collection'));
await page.getByRole('button', { name: 'Create', exact: true }).click();
await page.getByText('test-collection').click();
await page.getByLabel('Safe Mode').check();
await page.getByRole('button', { name: 'Save' }).click();
await page.locator('#create-new-tab').getByRole('img').click();
await page.getByPlaceholder('Request Name').fill('r1');
await page.getByPlaceholder('Request URL').click();
await page.getByPlaceholder('Request URL').fill('http://localhost:8081');
await page.getByRole('button', { name: 'Create' }).click();
await page.locator('pre').filter({ hasText: 'http://localhost:' }).click();
await page.locator('textarea').fill('/ping');
await page.locator('#send-request').getByRole('img').nth(2).click();
await expect(page.getByRole('main')).toContainText('200 OK');
await page.getByRole('tab', { name: 'GET r1' }).locator('circle').click();
await page.getByRole('button', { name: 'Save', exact: true }).click();
await page.getByText('GETr1').click();
await page.getByRole('button', { name: 'Clear response' }).click();
await page.locator('body').press('ControlOrMeta+Enter');
await expect(page.getByRole('main')).toContainText('200 OK');
});

View File

@@ -0,0 +1,49 @@
import { test, expect } from '../../playwright';
test.describe.parallel('Run Testbench Requests', () => {
test('Run bruno-testbench in Developer Mode', async ({ pageWithUserData: page }) => {
test.setTimeout(2 * 60 * 1000);
await page.getByText('bruno-testbench').click();
await page.getByLabel('Developer Mode(use only if').check();
await page.getByRole('button', { name: 'Save' }).click();
await page.locator('.environment-selector').nth(1).click();
await page.locator('.dropdown-item').getByText('Prod').click();
await page.locator('.collection-actions').hover();
await page.locator('.collection-actions .icon').click();
await page.getByText('Run', { exact: true }).click();
await page.getByRole('button', { name: 'Run Collection' }).click();
await page.getByRole('button', { name: 'Run Again' }).waitFor({ timeout: 2 * 60 * 1000 });
const result = await page.getByText('Total Requests: ').innerText();
const [totalRequests, passed, failed, skipped] = result
.match(/Total Requests: (\d+), Passed: (\d+), Failed: (\d+), Skipped: (\d+)/)
.slice(1);
await expect(parseInt(failed)).toBe(0);
await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - parseInt(failed));
});
test.fixme('Run bruno-testbench in Safe Mode', async ({ pageWithUserData: page }) => {
test.setTimeout(2 * 60 * 1000);
await page.getByText('bruno-testbench').click();
await page.getByLabel('Safe Mode').check();
await page.getByRole('button', { name: 'Save' }).click();
await page.locator('.environment-selector').nth(1).click();
await page.locator('.dropdown-item').getByText('Prod').click();
await page.locator('.collection-actions').hover();
await page.locator('.collection-actions .icon').click();
await page.getByText('Run', { exact: true }).click();
await page.getByRole('button', { name: 'Run Collection' }).click();
await page.getByRole('button', { name: 'Run Again' }).waitFor({ timeout: 2 * 60 * 1000 });
const result = await page.getByText('Total Requests: ').innerText();
const [totalRequests, passed, failed, skipped] = result
.match(/Total Requests: (\d+), Passed: (\d+), Failed: (\d+), Skipped: (\d+)/)
.slice(1);
await expect(parseInt(failed)).toBe(0);
await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - parseInt(failed));
});
});

View File

@@ -0,0 +1,27 @@
import { test, expect } from '../../playwright';
test('Should verify all support links with correct URL in preference > Support tab', async ({ page }) => {
// Open Preferences
await page.getByLabel('Open Preferences').click();
// Verify Support tab
await page.getByRole('tab', { name: 'Support' }).click();
const locator_twitter = page.getByRole('link', { name: 'Twitter' });
expect(await locator_twitter.getAttribute('href')).toEqual('https://twitter.com/use_bruno');
const locator_github = page.getByRole('link', { name: 'GitHub', exact: true });
expect(await locator_github.getAttribute('href')).toEqual('https://github.com/usebruno/bruno');
const locator_discord = page.getByRole('link', { name: 'Discord', exact: true });
expect(await locator_discord.getAttribute('href')).toEqual('https://discord.com/invite/KgcZUncpjq');
const locator_reportissues = page.getByRole('link', { name: 'Report Issues', exact: true });
expect(await locator_reportissues.getAttribute('href')).toEqual('https://github.com/usebruno/bruno/issues');
const locator_documentation = page.getByRole('link', { name: 'Documentation', exact: true });
expect(await locator_documentation.getAttribute('href')).toEqual('https://docs.usebruno.com');
});

View File

@@ -1,87 +1,11 @@
// eslint.config.js
const { defineConfig } = require('eslint/config');
const globals = require('globals');
const { fixupPluginRules } = require('@eslint/compat');
const eslintPluginDiff = require('eslint-plugin-diff');
const { defineConfig } = require("eslint/config");
const globals = require("globals");
let stylistic;
const runESMImports = async () => {
stylistic = await import('@stylistic/eslint-plugin').then((d) => d.default);
};
module.exports = runESMImports().then(() => defineConfig([
// Global ignores - must be a standalone object with ONLY ignores
module.exports = defineConfig([
{
ignores: [
'**/node_modules/**/*',
'**/dist/**/*',
'**/*.bru',
'packages/bruno-js/src/sandbox/bundle-browser-rollup.js',
'packages/bruno-app/public/static/**/*'
]
},
{
plugins: {
'diff': fixupPluginRules(eslintPluginDiff),
'@stylistic': stylistic
},
languageOptions: {
parser: require('@typescript-eslint/parser'),
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
}
},
files: [
'./eslint.config.js',
'tests/**/*.{ts,js}',
'playwright/**/*.{js,ts}',
'packages/bruno-app/**/*.{js,jsx,ts}',
'packages/bruno-app/src/test-utils/mocks/codemirror.js',
'packages/bruno-cli/**/*.js',
'packages/bruno-common/**/*.ts',
'packages/bruno-converters/**/*.js',
'packages/bruno-electron/**/*.js',
'packages/bruno-filestore/**/*.ts',
'packages/bruno-schema-types/**/*.ts',
'packages/bruno-js/**/*.js',
'packages/bruno-lang/**/*.js',
'packages/bruno-requests/**/*.ts',
'packages/bruno-requests/**/*.js',
'packages/bruno-tests/**/*.{js,ts}'
],
rules: {
...stylistic.configs.customize({
indent: 2,
quotes: 'single',
semi: true,
jsx: true
}).rules,
'@stylistic/comma-dangle': ['error', 'never'],
'@stylistic/brace-style': ['error', '1tbs', { allowSingleLine: true }],
'@stylistic/arrow-parens': ['error', 'always'],
'@stylistic/curly-newline': ['error', {
multiline: true,
minElements: 2,
consistent: true
}],
'@stylistic/function-paren-newline': ['off'],
'@stylistic/array-bracket-spacing': ['error', 'never'],
'@stylistic/arrow-spacing': ['error', { before: true, after: true }],
'@stylistic/function-call-spacing': ['error', 'never'],
'@stylistic/multiline-ternary': ['off'],
'@stylistic/padding-line-between-statements': ['off'],
'@stylistic/semi-style': ['error', 'last'],
'@stylistic/max-len': ['off'],
'@stylistic/jsx-one-expression-per-line': ['off'],
'@stylistic/max-statements-per-line': ['off'],
'@stylistic/no-mixed-operators': ['off']
}
},
{
files: ['packages/bruno-app/**/*.{js,jsx,ts}'],
ignores: ['**/*.config.js', '**/public/**/*'],
files: ["packages/bruno-app/**/*.{js,jsx,ts}"],
ignores: ["**/*.config.js"],
languageOptions: {
globals: {
...globals.browser,
@@ -89,188 +13,42 @@ module.exports = runESMImports().then(() => defineConfig([
global: false,
require: false,
Buffer: false,
process: false,
ipcRenderer: false
process: false
},
parserOptions: {
ecmaFeatures: {
jsx: true
}
}
jsx: true,
},
},
},
rules: {
'no-undef': 'error'
}
"no-undef": "error",
},
},
{
// It prevents lint errors when using CommonJS exports (module.exports) in Jest mocks.
files: ['packages/bruno-app/src/test-utils/mocks/codemirror.js'],
languageOptions: {
globals: {
...globals.node,
...globals.jest
}
},
rules: {
'no-undef': 'error'
}
},
{
files: ['packages/bruno-cli/**/*.js'],
ignores: ['**/*.config.js'],
languageOptions: {
globals: {
...globals.node,
...globals.jest
},
parserOptions: {
ecmaVersion: 'latest'
}
},
rules: {
'no-undef': 'error'
}
},
{
files: ['packages/bruno-common/**/*.ts'],
ignores: ['**/*.config.js', '**/dist/**/*'],
languageOptions: {
globals: {
...globals.node,
...globals.jest
},
parser: require('@typescript-eslint/parser'),
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: './packages/bruno-common/tsconfig.json'
}
},
rules: {
'no-undef': 'error'
}
},
{
files: ['packages/bruno-converters/**/*.js'],
ignores: ['**/*.config.js', '**/dist/**/*'],
languageOptions: {
globals: {
...globals.node,
...globals.jest
},
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
}
},
rules: {
'no-undef': 'error'
}
},
{
files: ['packages/bruno-electron/**/*.js'],
ignores: ['**/*.config.js', '**/web/**/*'],
languageOptions: {
globals: {
...globals.node,
...globals.jest
}
},
rules: {
'no-undef': 'error'
}
},
{
files: ['packages/bruno-filestore/**/*.ts'],
ignores: ['**/*.config.js', '**/dist/**/*'],
languageOptions: {
globals: {
...globals.node,
...globals.jest
},
parser: require('@typescript-eslint/parser'),
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: './packages/bruno-filestore/tsconfig.json'
}
},
rules: {
'no-undef': 'error'
}
},
{
files: ['packages/bruno-js/**/*.js'],
ignores: ['**/*.config.js', '**/dist/**/*'],
files: ["packages/bruno-app/src/test-utils/mocks/codemirror.js"],
languageOptions: {
globals: {
...globals.node,
...globals.jest,
window: false,
self: false,
HTMLElement: false,
typeDetectGlobalObject: false
},
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
}
},
rules: {
'no-undef': 'error'
}
"no-undef": "error",
},
},
{
files: ['packages/bruno-lang/**/*.js'],
ignores: ['**/*.config.js', '**/dist/**/*'],
files: ["packages/bruno-electron/**/*.{js}"],
ignores: ["**/*.config.js"],
languageOptions: {
globals: {
...globals.node,
...globals.jest
...globals.jest,
},
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
}
},
rules: {
'no-undef': 'error'
}
},
{
files: ['packages/bruno-requests/**/*.ts'],
ignores: ['**/*.config.js', '**/dist/**/*'],
languageOptions: {
globals: {
...globals.node,
...globals.jest
},
parser: require('@typescript-eslint/parser'),
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: './packages/bruno-requests/tsconfig.json'
}
"no-undef": "error",
},
rules: {
'no-undef': 'error'
}
},
{
files: ['packages/bruno-requests/**/*.js'],
ignores: ['**/*.config.js', '**/dist/**/*'],
languageOptions: {
globals: {
...globals.node,
...globals.jest
},
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
}
},
rules: {
'no-undef': 'error'
}
}
]));
]);

4910
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,37 +8,28 @@
"packages/bruno-common",
"packages/bruno-converters",
"packages/bruno-schema",
"packages/bruno-schema-types",
"packages/bruno-query",
"packages/bruno-js",
"packages/bruno-lang",
"packages/bruno-tests",
"packages/bruno-toml",
"packages/bruno-graphql-docs",
"packages/bruno-requests",
"packages/bruno-filestore"
"packages/bruno-requests"
],
"homepage": "https://usebruno.com",
"devDependencies": {
"@eslint/compat": "^1.3.2",
"@faker-js/faker": "^7.6.0",
"@jest/globals": "^29.2.0",
"@playwright/test": "^1.51.1",
"@rollup/plugin-json": "^6.1.0",
"@stylistic/eslint-plugin": "^5.3.1",
"@types/jest": "^29.5.11",
"@types/lodash-es": "^4.17.12",
"@types/node": "^22.14.1",
"@typescript-eslint/parser": "^8.39.0",
"concurrently": "^8.2.2",
"eslint": "^9.26.0",
"eslint-plugin-diff": "^2.0.3",
"fs-extra": "^11.1.1",
"globals": "^16.1.0",
"husky": "^9.1.7",
"jest": "^29.2.0",
"lodash-es": "^4.17.21",
"nano-staged": "^0.8.0",
"playwright": "^1.51.1",
"pretty-quick": "^3.1.3",
"randomstring": "^1.2.2",
@@ -49,7 +40,6 @@
"setup": "node ./scripts/setup.js",
"watch:converters": "npm run watch --workspace=packages/bruno-converters",
"dev": "concurrently --kill-others \"npm run dev:web\" \"npm run dev:electron\"",
"watch": "npm run dev:watch",
"dev:watch": "node ./scripts/dev-hot-reload.js",
"dev:web": "npm run dev --workspace=packages/bruno-app",
"build:web": "npm run build --workspace=packages/bruno-app",
@@ -58,11 +48,9 @@
"dev:electron:debug": "npm run debug --workspace=packages/bruno-electron",
"build:bruno-common": "npm run build --workspace=packages/bruno-common",
"build:bruno-requests": "npm run build --workspace=packages/bruno-requests",
"build:bruno-filestore": "npm run build --workspace=packages/bruno-filestore",
"build:bruno-converters": "npm run build --workspace=packages/bruno-converters",
"build:bruno-query": "npm run build --workspace=packages/bruno-query",
"build:graphql-docs": "npm run build --workspace=packages/bruno-graphql-docs",
"build:schema-types": "npm run build --workspace=packages/bruno-schema-types",
"build:electron": "node ./scripts/build-electron.js",
"build:electron:mac": "./scripts/build-electron.sh mac",
"build:electron:win": "./scripts/build-electron.sh win",
@@ -72,17 +60,9 @@
"build:electron:snap": "./scripts/build-electron.sh snap",
"watch:common": "npm run watch --workspace=packages/bruno-common",
"test:codegen": "node playwright/codegen.ts",
"test:e2e": "playwright test --project=default",
"test:e2e:ssl": "playwright test --project=ssl",
"test:e2e": "playwright test",
"test:prettier:web": "npm run test:prettier --workspace=packages/bruno-app",
"lint": "node --max_old_space_size=4096 $(npx which eslint)",
"lint:fix": "node --max_old_space_size=4096 $(npx which eslint) --fix",
"prepare": "husky"
},
"nano-staged": {
"*.{js,ts,jsx}": [
"npm run lint:fix"
]
"lint": "node --max_old_space_size=4096 $(npx which eslint)"
},
"overrides": {
"rollup": "3.29.5",
@@ -92,4 +72,4 @@
}
}
}
}
}

View File

@@ -6,4 +6,4 @@ module.exports = {
}]
],
plugins: ['babel-plugin-styled-components']
};
};

View File

@@ -1,10 +1,10 @@
module.exports = {
rootDir: '.',
transform: {
'^.+\\.[jt]sx?$': 'babel-jest'
'^.+\\.[jt]sx?$': 'babel-jest',
},
transformIgnorePatterns: [
'/node_modules/(?!strip-json-comments|nanoid|xml-formatter)/'
"/node_modules/(?!strip-json-comments|nanoid|xml-formatter)/",
],
moduleNameMapper: {
'^assets/(.*)$': '<rootDir>/src/assets/$1',
@@ -22,9 +22,9 @@ module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['@testing-library/jest-dom'],
setupFiles: [
'<rootDir>/jest.setup.js'
'<rootDir>/jest.setup.js',
],
testMatch: [
'<rootDir>/src/**/*.spec.[jt]s?(x)'
]
};
};

View File

@@ -16,21 +16,16 @@
"@prantlf/jsonlint": "^16.0.0",
"@reduxjs/toolkit": "^1.8.0",
"@tabler/icons": "^1.46.0",
"@testing-library/user-event": "^14.6.1",
"@tippyjs/react": "^4.2.6",
"@usebruno/common": "0.1.0",
"@usebruno/graphql-docs": "0.1.0",
"@usebruno/schema": "0.7.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0",
"classnames": "^2.3.1",
"codemirror": "5.65.2",
"codemirror-graphql": "2.1.1",
"cookie": "0.7.1",
"dompurify": "^3.2.4",
"escape-html": "^1.0.3",
"fast-fuzzy": "^1.12.0",
"fast-json-format": "~0.4.0",
"file": "^0.2.2",
"file-dialog": "^0.0.8",
"file-saver": "^2.0.5",
@@ -39,9 +34,9 @@
"graphiql": "3.7.1",
"graphql": "^16.6.0",
"graphql-request": "^3.7.0",
"hexy": "^0.3.5",
"httpsnippet": "^3.0.9",
"i18next": "24.1.2",
"iconv-lite": "^0.6.3",
"idb": "^7.0.0",
"immer": "^9.0.15",
"jsesc": "^3.0.2",
@@ -50,7 +45,6 @@
"jsonc-parser": "^3.2.1",
"jsonpath-plus": "^10.3.0",
"know-your-http-well": "^0.5.0",
"linkify-it": "^5.0.0",
"lodash": "^4.17.21",
"markdown-it": "^13.0.2",
"markdown-it-replace-link": "^1.2.0",
@@ -73,7 +67,6 @@
"react-hot-toast": "^2.4.0",
"react-i18next": "^15.0.1",
"react-inspector": "^6.0.2",
"react-json-view": "^1.21.3",
"react-pdf": "9.1.1",
"react-player": "^2.16.0",
"react-redux": "^7.2.9",
@@ -98,7 +91,7 @@
"@rsbuild/plugin-react": "^1.0.7",
"@rsbuild/plugin-sass": "^1.1.0",
"@rsbuild/plugin-styled-components": "1.1.0",
"@testing-library/dom": "^10.4.1",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"autoprefixer": "10.4.20",
@@ -117,10 +110,5 @@
"tailwindcss": "^3.4.1",
"webpack": "^5.64.4",
"webpack-cli": "^4.9.1"
},
"overrides": {
"httpsnippet": {
"form-data": "4.0.4"
}
}
}

View File

@@ -1,6 +1,6 @@
const darkTheme = {
'brand': '#546de5',
'text': 'rgb(52 52 52)',
brand: '#546de5',
text: 'rgb(52 52 52)',
'primary-text': '#ffffff',
'primary-theme': '#1e1e1e',
'secondary-text': '#929292',

View File

@@ -1,6 +1,6 @@
const lightTheme = {
'brand': '#546de5',
'text': 'rgb(52 52 52)',
brand: '#546de5',
text: 'rgb(52 52 52)',
'primary-text': 'rgb(52 52 52)',
'primary-theme': '#ffffff',
'secondary-text': '#929292',

View File

@@ -4,7 +4,7 @@ import { AccordionItem, AccordionHeader, AccordionContent } from './styledWrappe
const AccordionContext = createContext();
const Accordion = ({ children, defaultIndex, dataTestId }) => {
const Accordion = ({ children, defaultIndex }) => {
const [openIndex, setOpenIndex] = useState(defaultIndex);
const toggleItem = (index) => {
@@ -13,7 +13,7 @@ const Accordion = ({ children, defaultIndex, dataTestId }) => {
return (
<AccordionContext.Provider value={{ openIndex, toggleItem }}>
<div data-testid={dataTestId}>{children}</div>
<div>{children}</div>
</AccordionContext.Provider>
);
};

View File

@@ -1,83 +0,0 @@
import React, { useRef, forwardRef } from 'react';
import { IconCaretDown } from '@tabler/icons';
import Dropdown from 'components/Dropdown';
import { humanizeRequestBodyMode } from 'utils/collections';
const DEFAULT_MODES = [
{ key: 'multipartForm', label: 'Multipart Form', category: 'Form' },
{ key: 'formUrlEncoded', label: 'Form URL Encoded', category: 'Form' },
{ key: 'json', label: 'JSON', category: 'Raw' },
{ key: 'xml', label: 'XML', category: 'Raw' },
{ key: 'text', label: 'TEXT', category: 'Raw' },
{ key: 'sparql', label: 'SPARQL', category: 'Raw' },
{ key: 'file', label: 'File / Binary', category: 'Other' },
{ key: 'none', label: 'None', category: 'Other' }
];
const BodyModeSelector = ({
currentMode,
onModeChange,
modes = DEFAULT_MODES,
disabled = false,
className = '',
wrapperClassName = '',
showCategories = true,
placement = 'bottom-end'
}) => {
const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const Icon = forwardRef((props, ref) => {
return (
<div ref={ref} className="flex items-center justify-center pl-3 py-1 select-none selected-body-mode">
{humanizeRequestBodyMode(currentMode)}
{' '}
<IconCaretDown className="caret ml-2" size={14} strokeWidth={2} />
</div>
);
});
const onModeSelect = (mode) => {
dropdownTippyRef.current.hide();
onModeChange(mode);
};
// Group modes by category for rendering
const groupedModes = modes.reduce((acc, mode) => {
const category = mode.category || 'Other';
if (!acc[category]) {
acc[category] = [];
}
acc[category].push(mode);
return acc;
}, {});
return (
<div className={`inline-flex items-center body-mode-selector ${disabled ? 'cursor-default' : 'cursor-pointer'} ${wrapperClassName}`}>
<Dropdown
onCreate={onDropdownCreate}
icon={<Icon />}
placement={placement}
disabled={disabled}
className={className}
>
{Object.entries(groupedModes).map(([category, categoryModes]) => (
<React.Fragment key={category}>
{showCategories && <div className="label-item font-medium">{category}</div>}
{categoryModes.map((mode) => (
<div
key={mode.key}
className="dropdown-item"
onClick={() => onModeSelect(mode.key)}
>
{mode.label}
</div>
))}
</React.Fragment>
))}
</Dropdown>
</div>
);
};
export default BodyModeSelector;

View File

@@ -6,7 +6,7 @@ import StyledWrapper from './StyledWrapper';
const BrunoSupport = ({ onClose }) => {
return (
<StyledWrapper>
<Modal size="sm" title="Support" handleCancel={onClose} hideFooter={true}>
<Modal size="sm" title={'Support'} handleCancel={onClose} hideFooter={true}>
<div className="collection-options">
<div className="mt-2">
<a href="https://docs.usebruno.com" target="_blank" className="flex items-end">

View File

@@ -1,79 +0,0 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.checkbox-container {
width: 1rem;
height: 1rem;
display: flex;
justify-content: center;
align-items: center;
position: relative;
cursor: pointer;
&:disabled {
cursor: not-allowed;
opacity: 0.5;
}
}
.checkbox-checkmark {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
visibility: ${(props) => props.checked ? 'visible' : 'hidden'};
pointer-events: none;
}
.checkbox-input {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
width: 1rem;
height: 1rem;
border: 2px solid ${(props) => {
if (props.checked && props.disabled) {
return props.theme.colors.text.muted;
}
if (props.checked && !props.disabled) {
return props.theme.colors.text.yellow;
}
return props.theme.colors.text.muted;
}};
border-radius: 4px;
background-color: ${(props) => {
if (props.checked && !props.disabled) {
return props.theme.colors.text.yellow;
}
if (props.checked && props.disabled) {
return props.theme.colors.text.muted;
}
return 'transparent';
}};
cursor: pointer;
position: relative;
transition: all 0.2s ease;
outline: none;
box-shadow: none;
&:hover:not(:disabled) {
opacity: 0.8;
}
&:disabled {
cursor: not-allowed;
opacity: 0.5;
}
&:focus {
outline: none;
box-shadow: 0 0 0 2px ${(props) => props.theme.colors.text.yellow}40;
}
}
`;
export default StyledWrapper;

View File

@@ -1,45 +0,0 @@
import React from 'react';
import StyledWrapper from './StyledWrapper';
import IconCheckMark from 'components/Icons/IconCheckMark';
import { useTheme } from 'providers/Theme';
const Checkbox = ({
checked = false,
disabled = false,
onChange,
className = '',
id,
name,
value,
dataTestId = 'checkbox'
}) => {
const { theme } = useTheme();
const handleChange = (e) => {
if (!disabled && onChange) {
onChange(e);
}
};
return (
<StyledWrapper checked={checked} disabled={disabled} className={className}>
<div className="checkbox-container">
<input
type="checkbox"
id={id}
name={name}
value={value}
checked={checked}
disabled={disabled}
onChange={handleChange}
className="checkbox-input"
data-testid={dataTestId}
/>
<IconCheckMark className="checkbox-checkmark" color={theme.examples.checkbox.color} size={14} />
</div>
</StyledWrapper>
);
};
export default Checkbox;

View File

@@ -1,12 +1,6 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
&.read-only {
div.CodeMirror .CodeMirror-cursor {
display: none !important;
}
}
div.CodeMirror {
background: ${(props) => props.theme.codemirror.bg};
border: solid 1px ${(props) => props.theme.codemirror.border};
@@ -18,33 +12,6 @@ const StyledWrapper = styled.div`
flex-direction: column-reverse;
}
.CodeMirror-placeholder {
color: ${(props) => props.theme.text} !important;
opacity: 0.5 !important;
}
.CodeMirror-linenumber {
text-align: left !important;
padding-left: 3px !important;
}
/* Override default lint highlight background when emphasizing the gutter */
.CodeMirror-lint-line-error,
.CodeMirror-lint-line-warning {
background: none !important;
}
/* Style line numbers when there's a lint issue */
.CodeMirror-lint-line-error .CodeMirror-linenumber {
color: #d32f2f !important;
text-decoration: underline;
}
.CodeMirror-lint-line-warning .CodeMirror-linenumber {
color: #f57c00 !important;
text-decoration: underline;
}
/* Removes the glow outline around the folded json */
.CodeMirror-foldmarker {
text-shadow: none;
@@ -100,48 +67,41 @@ const StyledWrapper = styled.div`
}
}
.cm-s-default, .cm-s-monokai {
span.cm-def {
color: ${(props) => props.theme.codemirror.tokens.definition} !important;
}
span.cm-property {
color: ${(props) => props.theme.codemirror.tokens.property} !important;
}
span.cm-string {
color: ${(props) => props.theme.codemirror.tokens.string} !important;
}
span.cm-number {
color: ${(props) => props.theme.codemirror.tokens.number} !important;
}
span.cm-atom {
color: ${(props) => props.theme.codemirror.tokens.atom} !important;
}
span.cm-variable {
color: ${(props) => props.theme.codemirror.tokens.variable} !important;
}
span.cm-keyword {
color: ${(props) => props.theme.codemirror.tokens.keyword} !important;
}
span.cm-comment {
color: ${(props) => props.theme.codemirror.tokens.comment} !important;
}
span.cm-operator {
color: ${(props) => props.theme.codemirror.tokens.operator} !important;
}
.cm-s-monokai span.cm-property,
.cm-s-monokai span.cm-attribute {
color: #9cdcfe !important;
}
.cm-s-monokai span.cm-string {
color: #ce9178 !important;
}
.cm-s-monokai span.cm-number {
color: #b5cea8 !important;
}
.cm-s-monokai span.cm-atom {
color: #569cd6 !important;
}
/* Variable validation colors */
.cm-variable-valid {
color: #5fad89 !important; /* Soft sage */
color: green;
}
.cm-variable-invalid {
color: #d17b7b !important; /* Soft coral */
color: red;
}
.CodeMirror-search-hint {
display: inline;
}
.cm-s-default span.cm-property {
color: #1f61a0 !important;
}
.cm-s-default span.cm-variable {
color: #397d13 !important;
}
//matching bracket fix
.CodeMirror-matchingbracket {
@@ -149,42 +109,6 @@ const StyledWrapper = styled.div`
text-decoration:unset;
}
.cm-search-line-highlight {
background: ${(props) => props.theme.codemirror.searchLineHighlightCurrent};
}
.cm-search-match {
background: rgba(255, 193, 7, 0.25);
}
.cm-search-current {
background: rgba(255, 193, 7, 0.4);
}
.lint-error-tooltip {
position: fixed;
z-index: 10000;
background: ${(props) => props.theme.codemirror.bg};
border-radius: ${(props) => props.theme.border.radius.base};
padding: 8px 12px;
max-width: 400px;
box-shadow: ${(props) => props.theme.shadow.sm};
font-size: ${(props) => props.theme.font.size.xs};
line-height: 1.5;
pointer-events: none;
.lint-tooltip-message {
padding: 2px 0;
}
.lint-tooltip-message.error {
color: ${(props) => props.theme.colors.text.danger};
}
.lint-tooltip-message.warning {
color: ${(props) => props.theme.colors.text.warning};
}
}
`;
export default StyledWrapper;

View File

@@ -14,9 +14,6 @@ import * as jsonlint from '@prantlf/jsonlint';
import { JSHINT } from 'jshint';
import stripJsonComments from 'strip-json-comments';
import { getAllVariables } from 'utils/collections';
import { setupLinkAware } from 'utils/codemirror/linkAware';
import { setupLintErrorTooltip } from 'utils/codemirror/lint-errors';
import CodeMirrorSearch from 'components/CodeMirrorSearch';
const CodeMirror = require('codemirror');
window.jsonlint = jsonlint;
@@ -38,12 +35,7 @@ export default class CodeEditor extends React.Component {
this.lintOptions = {
esversion: 11,
expr: true,
asi: true,
highlightLines: true
};
this.state = {
searchBarVisible: false
asi: true
};
}
@@ -52,22 +44,19 @@ export default class CodeEditor extends React.Component {
const editor = (this.editor = CodeMirror(this._node, {
value: this.props.value || '',
placeholder: '...',
lineNumbers: true,
lineWrapping: this.props.enableLineWrapping ?? true,
lineWrapping: true,
tabSize: TAB_SIZE,
mode: this.props.mode || 'application/ld+json',
brunoVarInfo: this.props.enableBrunoVarInfo !== false ? {
variables,
collection: this.props.collection,
item: this.props.item
} : false,
brunoVarInfo: {
variables
},
keyMap: 'sublime',
autoCloseBrackets: true,
matchBrackets: true,
showCursorWhenSelecting: true,
foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'],
lint: this.lintOptions,
readOnly: this.props.readOnly,
scrollbarStyle: 'overlay',
@@ -94,18 +83,28 @@ export default class CodeEditor extends React.Component {
}
},
'Cmd-F': (cm) => {
if (!this.state.searchBarVisible) {
this.setState({ searchBarVisible: true });
if (this._isSearchOpen()) {
// replace the older search component with the new one
const search = document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top');
search && search.remove();
}
cm.execCommand('findPersistent');
this._bindSearchHandler();
this._appendSearchResultsCount();
},
'Ctrl-F': (cm) => {
if (!this.state.searchBarVisible) {
this.setState({ searchBarVisible: true });
if (this._isSearchOpen()) {
// replace the older search component with the new one
const search = document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top');
search && search.remove();
}
cm.execCommand('findPersistent');
this._bindSearchHandler();
this._appendSearchResultsCount();
},
'Cmd-H': 'replace',
'Ctrl-H': 'replace',
'Tab': function (cm) {
Tab: function (cm) {
cm.getSelection().includes('\n') || editor.getLine(cm.getCursor().line) == cm.getSelection()
? cm.execCommand('indentMore')
: cm.replaceSelection(' ', 'end');
@@ -130,11 +129,6 @@ export default class CodeEditor extends React.Component {
} else {
this.editor.toggleComment();
}
},
'Esc': () => {
if (this.state.searchBarVisible) {
this.setState({ searchBarVisible: false });
}
}
},
foldOptions: {
@@ -151,7 +145,7 @@ export default class CodeEditor extends React.Component {
} else if (this.props.mode == 'application/xml') {
var doc = new DOMParser();
try {
// add header element and remove prefix namespaces for DOMParser
//add header element and remove prefix namespaces for DOMParser
var dcm = doc.parseFromString(
'<a> ' + internal.replace(/(?<=\<|<\/)\w+:/g, '') + '</a>',
'application/xml'
@@ -188,31 +182,24 @@ export default class CodeEditor extends React.Component {
}
return found;
});
if (editor) {
editor.setOption('lint', this.props.mode && editor.getValue().trim().length > 0 ? this.lintOptions : false);
editor.on('change', this._onEdit);
editor.on('scroll', this.onScroll);
editor.scrollTo(null, this.props.initialScroll);
this.addOverlay();
const getAllVariablesHandler = () => getAllVariables(this.props.collection, this.props.item);
// Setup AutoComplete Helper for all modes
const autoCompleteOptions = {
showHintsFor: this.props.showHintsFor,
getAllVariables: getAllVariablesHandler
showHintsFor: this.props.showHintsFor
};
const getVariables = () => getAllVariables(this.props.collection, this.props.item);
this.brunoAutoCompleteCleanup = setupAutoComplete(
editor,
getVariables,
autoCompleteOptions
);
setupLinkAware(editor);
// Setup lint error tooltip on line number hover
this.cleanupLintErrorTooltip = setupLintErrorTooltip(editor);
}
}
@@ -238,52 +225,24 @@ export default class CodeEditor extends React.Component {
if (!isEqual(variables, this.variables)) {
this.addOverlay();
}
// Update collection and item when they change
if (this.props.enableBrunoVarInfo !== false && this.editor.options.brunoVarInfo) {
if (!isEqual(this.props.collection, this.editor.options.brunoVarInfo.collection)) {
this.editor.options.brunoVarInfo.collection = this.props.collection;
}
if (!isEqual(this.props.item, this.editor.options.brunoVarInfo.item)) {
this.editor.options.brunoVarInfo.item = this.props.item;
}
}
}
if (this.props.theme !== prevProps.theme && this.editor) {
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
}
if (this.props.initialScroll !== prevProps.initialScroll) {
this.editor.scrollTo(null, this.props.initialScroll);
}
if (this.props.enableLineWrapping !== prevProps.enableLineWrapping) {
this.editor.setOption('lineWrapping', this.props.enableLineWrapping);
}
if (this.props.mode !== prevProps.mode) {
this.editor.setOption('mode', this.props.mode);
}
if (this.props.readOnly !== prevProps.readOnly && this.editor) {
this.editor.setOption('readOnly', this.props.readOnly);
}
this.ignoreChangeEvent = false;
}
componentWillUnmount() {
if (this.editor) {
this.editor?._destroyLinkAware?.();
this.editor.off('change', this._onEdit);
this.editor.off('scroll', this.onScroll);
// Clean up lint error tooltip
this.cleanupLintErrorTooltip?.();
this.editor = null;
}
this._unbindSearchHandler();
if (this.brunoAutoCompleteCleanup) {
this.brunoAutoCompleteCleanup();
}
}
render() {
@@ -292,22 +251,14 @@ export default class CodeEditor extends React.Component {
}
return (
<StyledWrapper
className={`h-full w-full flex flex-col relative graphiql-container ${this.props.readOnly ? 'read-only' : ''}`}
className="h-full w-full flex flex-col relative graphiql-container"
aria-label="Code Editor"
font={this.props.font}
fontSize={this.props.fontSize}
>
<CodeMirrorSearch
visible={this.state.searchBarVisible}
editor={this.editor}
onClose={() => this.setState({ searchBarVisible: false })}
/>
<div
className={`editor-container${this.state.searchBarVisible ? ' search-bar-visible' : ''}`}
ref={(node) => { this._node = node; }}
style={{ height: '100%', width: '100%' }}
/>
</StyledWrapper>
ref={(node) => {
this._node = node;
}}
/>
);
}
@@ -316,17 +267,10 @@ export default class CodeEditor extends React.Component {
let variables = getAllVariables(this.props.collection, this.props.item);
this.variables = variables;
// Update brunoVarInfo with latest variables
if (this.props.enableBrunoVarInfo !== false && this.editor.options.brunoVarInfo) {
this.editor.options.brunoVarInfo.variables = variables;
}
defineCodeMirrorBrunoVariablesMode(variables, mode, false, this.props.enableVariableHighlighting);
this.editor.setOption('mode', 'brunovariables');
};
onScroll = (event) => this.props.onScroll?.(event);
_onEdit = () => {
if (!this.ignoreChangeEvent && this.editor) {
this.editor.setOption('lint', this.editor.getValue().trim().length > 0 ? this.lintOptions : false);
@@ -336,4 +280,67 @@ export default class CodeEditor extends React.Component {
}
}
};
_isSearchOpen = () => {
return document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top');
};
/**
* Bind handler to search input to count number of search results
*/
_bindSearchHandler = () => {
const searchInput = document.querySelector('.CodeMirror-search-field');
if (searchInput) {
searchInput.addEventListener('input', this._countSearchResults);
}
};
/**
* Unbind handler to search input to count number of search results
*/
_unbindSearchHandler = () => {
const searchInput = document.querySelector('.CodeMirror-search-field');
if (searchInput) {
searchInput.removeEventListener('input', this._countSearchResults);
}
};
/**
* Append search results count to search dialog
*/
_appendSearchResultsCount = () => {
const dialog = document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top');
if (dialog) {
const searchResultsCount = document.createElement('span');
searchResultsCount.id = this.searchResultsCountElementId;
dialog.appendChild(searchResultsCount);
this._countSearchResults();
}
};
/**
* Count search results and update state
*/
_countSearchResults = () => {
let count = 0;
const searchInput = document.querySelector('.CodeMirror-search-field');
if (searchInput && searchInput.value.length > 0) {
// Escape special characters in search input to prevent RegExp crashes. Fixes #3051
const text = new RegExp(escapeRegExp(searchInput.value), 'gi');
const matches = this.editor.getValue().match(text);
count = matches ? matches.length : 0;
}
const searchResultsCountElement = document.querySelector(`#${this.searchResultsCountElementId}`);
if (searchResultsCountElement) {
searchResultsCountElement.innerText = `${count} results`;
}
};
}

View File

@@ -10,10 +10,10 @@ jest.mock('codemirror', () => {
const MOCK_THEME = {
codemirror: {
bg: '#1e1e1e',
border: '#333'
bg: "#1e1e1e",
border: "#333",
},
textLink: '#007acc'
textLink: "#007acc",
};
const setupEditorState = (editor, { value, cursorPosition }) => {
@@ -27,8 +27,8 @@ const setupEditorState = (editor, { value, cursorPosition }) => {
});
editor.state = {
completionActive: null
};
completionActive: null,
}
};
const setupEditorWithRef = () => {
@@ -47,5 +47,5 @@ describe('CodeEditor', () => {
jest.resetModules();
});
it('add CodeEditor related tests here', () => {});
});
it("add CodeEditor related tests here", () => {});
});

View File

@@ -1,99 +0,0 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.bruno-search-bar {
position: absolute;
top: 8px;
right: 8px;
z-index: 20;
display: flex;
align-items: center;
flex-wrap: nowrap;
padding: 0 2px;
min-height: 36px;
background: ${(props) => props.theme.sidebar.search.bg} !important;
border-radius: 4px;
border: 1px solid ${(props) => props.theme.sidebar.search.bg} !important;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
width: auto;
min-width: 180px;
max-width: 320px;
}
.bruno-search-bar input {
min-width: 80px;
background: transparent;
color: inherit;
border: none;
outline: none;
padding: 1px 2px;
font-size: ${(props) => props.theme.font.size.base};
margin: 0 1px;
height: 28px;
}
.searchbar-icon-btn {
background: none;
border: none;
padding: 0 1px;
margin: 0 1px;
cursor: pointer;
color: #aaa;
border-radius: 3px;
height: 18px;
width: 18px;
display: flex;
align-items: center;
justify-content: center;
}
.searchbar-result-count {
min-width: 28px;
text-align: center;
font-size: ${(props) => props.theme.font.size.xs};
color: #aaa;
margin: 0 8px 0 1px;
white-space: nowrap;
}
.bruno-search-bar.compact {
background: ${(props) => props.theme.codemirror.bg};
color: ${(props) => props.theme.codemirror.text || props.theme.text};
border: none;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
border-radius: 4px;
padding: 1px 3px;
min-height: 22px;
display: flex;
align-items: center;
gap: 0;
}
.bruno-search-bar input {
background: transparent;
color: inherit;
border: none;
outline: none;
font-size: ${(props) => props.theme.font.size.base};
padding: 1px 2px;
min-width: 80px;
}
.searchbar-icon-btn:focus {
outline: 1px solid ${(props) => props.theme.codemirror.border};
}
.bruno-search-bar, .bruno-search-bar input {
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
}
.cm-search-line-highlight {
background: ${(props) => props.theme.codemirror.searchLineHighlightCurrent};
}
.searchbar-icon-btn.active {
color: #f39c12 !important;
}
`;
export default StyledWrapper;

View File

@@ -1,201 +0,0 @@
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { IconRegex, IconArrowUp, IconArrowDown, IconX, IconLetterCase, IconLetterW } from '@tabler/icons';
import ToolHint from 'components/ToolHint';
import StyledWrapper from './StyledWrapper';
import useDebounce from 'hooks/useDebounce';
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');
}
const CodeMirrorSearch = ({ visible, editor, onClose }) => {
const [searchText, setSearchText] = useState('');
const [regex, setRegex] = useState(false);
const [caseSensitive, setCaseSensitive] = useState(false);
const [wholeWord, setWholeWord] = useState(false);
const [matchIndex, setMatchIndex] = useState(0);
const [matchCount, setMatchCount] = useState(0);
const searchMarks = useRef([]);
const searchLineHighlight = useRef(null);
const searchMatches = useRef([]);
const debouncedSearchText = useDebounce(searchText, 150);
const memoizedMatches = useMemo(() => {
if (!editor || !visible) return [];
if (!debouncedSearchText) return [];
try {
let query, options = {};
if (regex) {
try {
query = new RegExp(debouncedSearchText, caseSensitive ? 'g' : 'gi');
} catch {
return [];
}
} else if (wholeWord) {
const escaped = escapeRegExp(debouncedSearchText);
query = new RegExp(`\\b${escaped}\\b`, caseSensitive ? 'g' : 'gi');
} else {
query = debouncedSearchText;
options = { caseFold: !caseSensitive };
}
const cursor = editor.getSearchCursor(query, { line: 0, ch: 0 }, options);
const out = [];
while (cursor.findNext()) {
out.push({ from: cursor.from(), to: cursor.to() });
}
return out;
} catch (e) {
console.error('Search error:', e);
return [];
}
}, [editor, visible, debouncedSearchText, regex, caseSensitive, wholeWord]);
const doSearch = useCallback((newIndex = 0) => {
if (!editor) return;
// Clear previous marks
searchMarks.current.forEach((mark) => mark.clear());
searchMarks.current = [];
// Clear previous line highlight
if (searchLineHighlight.current !== null) {
editor.removeLineClass(searchLineHighlight.current, 'wrap', 'cm-search-line-highlight');
searchLineHighlight.current = null;
}
if (!debouncedSearchText) {
setMatchCount(0);
setMatchIndex(0);
searchMatches.current = [];
return;
}
try {
const matches = memoizedMatches;
let matchIndex = matches.length ? Math.max(0, Math.min(newIndex, matches.length - 1)) : 0;
matches.forEach((m, i) => {
const mark = editor.markText(m.from, m.to, {
className: i === matchIndex ? 'cm-search-current' : 'cm-search-match',
clearOnEnter: true
});
searchMarks.current.push(mark);
});
if (matches.length) {
const currentLine = matches[matchIndex].from.line;
editor.addLineClass(currentLine, 'wrap', 'cm-search-line-highlight');
searchLineHighlight.current = currentLine;
editor.scrollIntoView(matches[matchIndex].from, 100);
editor.setSelection(matches[matchIndex].from, matches[matchIndex].to);
} else {
searchLineHighlight.current = null;
}
setMatchCount(matches.length);
setMatchIndex(matchIndex);
searchMatches.current = matches;
} catch (e) {
console.error('Search error:', e);
setMatchCount(0);
setMatchIndex(0);
searchMatches.current = [];
}
}, [debouncedSearchText, regex, caseSensitive, wholeWord, editor, memoizedMatches]);
useEffect(() => {
doSearch(0, debouncedSearchText);
}, [debouncedSearchText, doSearch]);
const handleSearchBarClose = useCallback(() => {
searchMarks.current.forEach((mark) => mark.clear());
searchMarks.current = [];
if (searchLineHighlight.current !== null && editor) {
editor.removeLineClass(searchLineHighlight.current, 'wrap', 'cm-search-line-highlight');
searchLineHighlight.current = null;
}
searchMatches.current = [];
if (onClose) onClose();
// Focus the editor after closing the search bar
if (editor) {
setTimeout(() => editor.focus(), 0);
}
}, [editor, onClose]);
const handleSearchTextChange = (text) => {
setSearchText(text);
setMatchIndex(0);
};
const handleToggleRegex = () => {
setRegex((prev) => !prev);
setMatchIndex(0);
doSearch(0);
};
const handleToggleCase = () => {
setCaseSensitive((prev) => !prev);
setMatchIndex(0);
doSearch(0);
};
const handleToggleWholeWord = () => {
setWholeWord((prev) => !prev);
setMatchIndex(0);
doSearch(0);
};
const handleNext = () => {
if (!searchMatches.current || !searchMatches.current.length) return;
let next = (matchIndex + 1) % searchMatches.current.length;
setMatchIndex(next);
doSearch(next);
};
const handlePrev = () => {
if (!searchMatches.current || !searchMatches.current.length) return;
let prev = (matchIndex - 1 + searchMatches.current.length) % searchMatches.current.length;
setMatchIndex(prev);
doSearch(prev);
};
if (!visible) return null;
return (
<StyledWrapper>
<div className="bruno-search-bar compact">
<input
autoFocus
type="text"
value={searchText}
onChange={(e) => handleSearchTextChange(e.target.value)}
placeholder="Search..."
spellCheck={false}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) handleNext();
if (e.key === 'Enter' && e.shiftKey) handlePrev();
if (e.key === 'Escape') handleSearchBarClose();
}}
/>
<span className="searchbar-result-count">{matchCount > 0 ? `${matchIndex + 1} / ${matchCount}` : '0 results'}</span>
<ToolHint text="Regex search" toolhintId="searchbar-regex-toolhint" place="top">
<button className={`searchbar-icon-btn ${regex ? 'active' : ''}`} onClick={handleToggleRegex}><IconRegex size={16} /></button>
</ToolHint>
<ToolHint text="Case sensitive" toolhintId="searchbar-case-toolhint" place="top">
<button className={`searchbar-icon-btn ${caseSensitive ? 'active' : ''}`} onClick={handleToggleCase}><IconLetterCase size={14} /></button>
</ToolHint>
<ToolHint text="Whole word" toolhintId="searchbar-wholeword-toolhint" place="top">
<button className={`searchbar-icon-btn ${wholeWord ? 'active' : ''}`} onClick={handleToggleWholeWord}><IconLetterW size={14} /></button>
</ToolHint>
<button className="searchbar-icon-btn" title="Previous" onClick={handlePrev}><IconArrowUp size={14} /></button>
<button className="searchbar-icon-btn" title="Next" onClick={handleNext}><IconArrowDown size={14} /></button>
<button className="searchbar-icon-btn" title="Close" onClick={handleSearchBarClose}><IconX size={14} /></button>
</div>
</StyledWrapper>
);
};
export default CodeMirrorSearch;

View File

@@ -2,7 +2,7 @@ import styled from 'styled-components';
const Wrapper = styled.div`
label {
font-size: ${(props) => props.theme.font.size.base};
font-size: 0.8125rem;
}
.single-line-editor-wrapper {

View File

@@ -6,7 +6,7 @@ import Dropdown from 'components/Dropdown';
import { useTheme } from 'providers/Theme';
import SingleLineEditor from 'components/SingleLineEditor';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import { humanizeRequestAPIKeyPlacement } from 'utils/collections';
@@ -16,9 +16,9 @@ const ApiKeyAuth = ({ collection }) => {
const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const apikeyAuth = collection.draft?.root ? get(collection, 'draft.root.request.auth.apikey', {}) : get(collection, 'root.request.auth.apikey', {});
const apikeyAuth = get(collection, 'root.request.auth.apikey', {});
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const Icon = forwardRef((props, ref) => {
return (
@@ -43,16 +43,16 @@ const ApiKeyAuth = ({ collection }) => {
};
useEffect(() => {
!apikeyAuth?.placement
&& dispatch(
updateCollectionAuth({
mode: 'apikey',
collectionUid: collection.uid,
content: {
placement: 'header'
}
})
);
!apikeyAuth?.placement &&
dispatch(
updateCollectionAuth({
mode: 'apikey',
collectionUid: collection.uid,
content: {
placement: 'header'
}
})
);
}, [apikeyAuth]);
return (

View File

@@ -1,7 +1,7 @@
import styled from 'styled-components';
const Wrapper = styled.div`
font-size: ${(props) => props.theme.font.size.base};
font-size: 0.8125rem;
.auth-mode-selector {
background: transparent;

View File

@@ -11,7 +11,7 @@ const AuthMode = ({ collection }) => {
const dispatch = useDispatch();
const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const authMode = collection.draft?.root ? get(collection, 'draft.root.request.auth.mode') : get(collection, 'root.request.auth.mode');
const authMode = get(collection, 'root.request.auth.mode');
const Icon = forwardRef((props, ref) => {
return (
@@ -87,7 +87,7 @@ const AuthMode = ({ collection }) => {
}}
>
NTLM Auth
</div>
</div>
<div
className="dropdown-item"
onClick={() => {

View File

@@ -2,7 +2,7 @@ import styled from 'styled-components';
const Wrapper = styled.div`
label {
font-size: ${(props) => props.theme.font.size.base};
font-size: 0.8125rem;
}
.single-line-editor-wrapper {

View File

@@ -1,23 +1,19 @@
import React from 'react';
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
import get from 'lodash/get';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import SingleLineEditor from 'components/SingleLineEditor';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const AwsV4Auth = ({ collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const awsv4Auth = collection.draft?.root ? get(collection, 'draft.root.request.auth.awsv4', {}) : get(collection, 'root.request.auth.awsv4', {});
const { isSensitive } = useDetectSensitiveField(collection);
const { showWarning, warningMessage } = isSensitive(awsv4Auth?.secretAccessKey);
const awsv4Auth = get(collection, 'root.request.auth.awsv4', {});
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const handleAccessKeyIdChange = (accessKeyId) => {
dispatch(
@@ -135,7 +131,7 @@ const AwsV4Auth = ({ collection }) => {
</div>
<label className="block font-medium mb-2">Secret Access Key</label>
<div className="single-line-editor-wrapper mb-2 flex items-center">
<div className="single-line-editor-wrapper mb-2">
<SingleLineEditor
value={awsv4Auth.secretAccessKey || ''}
theme={storedTheme}
@@ -144,7 +140,6 @@ const AwsV4Auth = ({ collection }) => {
collection={collection}
isSecret={true}
/>
{showWarning && <SensitiveFieldWarning fieldName="awsv4-secret-access-key" warningMessage={warningMessage} />}
</div>
<label className="block font-medium mb-2">Session Token</label>

View File

@@ -2,7 +2,7 @@ import styled from 'styled-components';
const Wrapper = styled.div`
label {
font-size: ${(props) => props.theme.font.size.base};
font-size: 0.8125rem;
}
.single-line-editor-wrapper {

View File

@@ -1,23 +1,19 @@
import React from 'react';
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
import get from 'lodash/get';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import SingleLineEditor from 'components/SingleLineEditor';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const BasicAuth = ({ collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const basicAuth = collection.draft?.root ? get(collection, 'draft.root.request.auth.basic', {}) : get(collection, 'root.request.auth.basic', {});
const { isSensitive } = useDetectSensitiveField(collection);
const { showWarning, warningMessage } = isSensitive(basicAuth?.password);
const basicAuth = get(collection, 'root.request.auth.basic', {});
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const handleUsernameChange = (username) => {
dispatch(
@@ -59,7 +55,7 @@ const BasicAuth = ({ collection }) => {
</div>
<label className="block font-medium mb-2">Password</label>
<div className="single-line-editor-wrapper flex items-center">
<div className="single-line-editor-wrapper">
<SingleLineEditor
value={basicAuth.password || ''}
theme={storedTheme}
@@ -68,7 +64,6 @@ const BasicAuth = ({ collection }) => {
collection={collection}
isSecret={true}
/>
{showWarning && <SensitiveFieldWarning fieldName="basic-password" warningMessage={warningMessage} />}
</div>
</StyledWrapper>
);

View File

@@ -2,7 +2,7 @@ import styled from 'styled-components';
const Wrapper = styled.div`
label {
font-size: ${(props) => props.theme.font.size.base};
font-size: 0.8125rem;
}
.single-line-editor-wrapper {

View File

@@ -1,23 +1,19 @@
import React from 'react';
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
import get from 'lodash/get';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import SingleLineEditor from 'components/SingleLineEditor';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const BearerAuth = ({ collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const bearerToken = collection.draft?.root ? get(collection, 'draft.root.request.auth.bearer.token', '') : get(collection, 'root.request.auth.bearer.token', '');
const { isSensitive } = useDetectSensitiveField(collection);
const { showWarning, warningMessage } = isSensitive(bearerToken);
const bearerToken = get(collection, 'root.request.auth.bearer.token', '');
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const handleTokenChange = (token) => {
dispatch(
@@ -34,7 +30,7 @@ const BearerAuth = ({ collection }) => {
return (
<StyledWrapper className="mt-2 w-full">
<label className="block font-medium mb-2">Token</label>
<div className="single-line-editor-wrapper flex items-center">
<div className="single-line-editor-wrapper">
<SingleLineEditor
value={bearerToken}
theme={storedTheme}
@@ -43,7 +39,6 @@ const BearerAuth = ({ collection }) => {
collection={collection}
isSecret={true}
/>
{showWarning && <SensitiveFieldWarning fieldName="bearer-token" warningMessage={warningMessage} />}
</div>
</StyledWrapper>
);

View File

@@ -2,7 +2,7 @@ import styled from 'styled-components';
const Wrapper = styled.div`
label {
font-size: ${(props) => props.theme.font.size.base};
font-size: 0.8125rem;
}
.single-line-editor-wrapper {

View File

@@ -1,23 +1,19 @@
import React from 'react';
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
import get from 'lodash/get';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import SingleLineEditor from 'components/SingleLineEditor';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const DigestAuth = ({ collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const digestAuth = collection.draft?.root ? get(collection, 'draft.root.request.auth.digest', {}) : get(collection, 'root.request.auth.digest', {});
const { isSensitive } = useDetectSensitiveField(collection);
const { showWarning, warningMessage } = isSensitive(digestAuth?.password);
const digestAuth = get(collection, 'root.request.auth.digest', {});
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const handleUsernameChange = (username) => {
dispatch(
@@ -59,7 +55,7 @@ const DigestAuth = ({ collection }) => {
</div>
<label className="block font-medium mb-2">Password</label>
<div className="single-line-editor-wrapper flex items-center">
<div className="single-line-editor-wrapper">
<SingleLineEditor
value={digestAuth.password || ''}
theme={storedTheme}
@@ -68,7 +64,6 @@ const DigestAuth = ({ collection }) => {
collection={collection}
isSecret={true}
/>
{showWarning && <SensitiveFieldWarning fieldName="digest-password" warningMessage={warningMessage} />}
</div>
</StyledWrapper>
);

View File

@@ -2,7 +2,7 @@ import styled from 'styled-components';
const Wrapper = styled.div`
label {
font-size: ${(props) => props.theme.font.size.base};
font-size: 0.8125rem;
}
.single-line-editor-wrapper {

View File

@@ -1,23 +1,26 @@
import React from 'react';
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
import get from 'lodash/get';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import SingleLineEditor from 'components/SingleLineEditor';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const NTLMAuth = ({ collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const ntlmAuth = collection.draft?.root ? get(collection, 'draft.root.request.auth.ntlm', {}) : get(collection, 'root.request.auth.ntlm', {});
const { isSensitive } = useDetectSensitiveField(collection);
const { showWarning, warningMessage } = isSensitive(ntlmAuth?.password);
const ntlmAuth = get(collection, 'root.request.auth.ntlm', {});
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
const handleUsernameChange = (username) => {
dispatch(
@@ -60,7 +63,10 @@ const NTLMAuth = ({ collection }) => {
}
})
);
};
};
return (
<StyledWrapper className="mt-2 w-full">
@@ -76,7 +82,7 @@ const NTLMAuth = ({ collection }) => {
</div>
<label className="block font-medium mb-2">Password</label>
<div className="single-line-editor-wrapper flex items-center">
<div className="single-line-editor-wrapper">
<SingleLineEditor
value={ntlmAuth.password || ''}
theme={storedTheme}
@@ -85,7 +91,6 @@ const NTLMAuth = ({ collection }) => {
collection={collection}
isSecret={true}
/>
{showWarning && <SensitiveFieldWarning fieldName="ntlm-password" warningMessage={warningMessage} />}
</div>
<label className="block font-medium mb-2">Domain</label>

View File

@@ -2,7 +2,7 @@ import styled from 'styled-components';
const Wrapper = styled.div`
label {
font-size: ${(props) => props.theme.font.size.base};
font-size: 0.8125rem;
}
.single-line-editor-wrapper {
max-width: 400px;

View File

@@ -1,23 +1,22 @@
import React from 'react';
import get from 'lodash/get';
import StyledWrapper from './StyledWrapper';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import OAuth2AuthorizationCode from 'components/RequestPane/Auth/OAuth2/AuthorizationCode/index';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/index';
import { useDispatch } from 'react-redux';
import OAuth2PasswordCredentials from 'components/RequestPane/Auth/OAuth2/PasswordCredentials/index';
import OAuth2ClientCredentials from 'components/RequestPane/Auth/OAuth2/ClientCredentials/index';
import OAuth2Implicit from 'components/RequestPane/Auth/OAuth2/Implicit/index';
import GrantTypeSelector from 'components/RequestPane/Auth/OAuth2/GrantTypeSelector/index';
const GrantTypeComponentMap = ({ collection }) => {
const GrantTypeComponentMap = ({collection }) => {
const dispatch = useDispatch();
const save = () => {
dispatch(saveCollectionSettings(collection.uid));
dispatch(saveCollectionRoot(collection.uid));
};
let request = collection.draft?.root ? get(collection, 'draft.root.request', {}) : get(collection, 'root.request', {});
let request = collection.draft ? get(collection, 'draft.request', {}) : get(collection, 'root.request', {});
const grantType = get(request, 'auth.oauth2.grantType', {});
switch (grantType) {
@@ -30,9 +29,6 @@ const GrantTypeComponentMap = ({ collection }) => {
case 'client_credentials':
return <OAuth2ClientCredentials save={save} request={request} updateAuth={updateCollectionAuth} collection={collection} />;
break;
case 'implicit':
return <OAuth2Implicit save={save} request={request} updateAuth={updateCollectionAuth} collection={collection} />;
break;
default:
return <div>TBD</div>;
break;
@@ -40,7 +36,7 @@ const GrantTypeComponentMap = ({ collection }) => {
};
const OAuth2 = ({ collection }) => {
let request = collection.draft?.root ? get(collection, 'draft.root.request', {}) : get(collection, 'root.request', {});
let request = collection.draft ? get(collection, 'draft.request', {}) : get(collection, 'root.request', {});
return (
<StyledWrapper className="mt-2 w-full">

View File

@@ -2,7 +2,7 @@ import styled from 'styled-components';
const Wrapper = styled.div`
label {
font-size: ${(props) => props.theme.font.size.base};
font-size: 0.8125rem;
}
.single-line-editor-wrapper {

View File

@@ -1,23 +1,19 @@
import React from 'react';
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
import get from 'lodash/get';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import SingleLineEditor from 'components/SingleLineEditor';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const WsseAuth = ({ collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const wsseAuth = collection.draft?.root ? get(collection, 'draft.root.request.auth.wsse', {}) : get(collection, 'root.request.auth.wsse', {});
const { isSensitive } = useDetectSensitiveField(collection);
const { showWarning, warningMessage } = isSensitive(wsseAuth?.password);
const wsseAuth = get(collection, 'root.request.auth.wsse', {});
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const handleUserChange = (username) => {
dispatch(
@@ -59,16 +55,14 @@ const WsseAuth = ({ collection }) => {
</div>
<label className="block font-medium mb-2">Password</label>
<div className="single-line-editor-wrapper flex items-center">
<div className="single-line-editor-wrapper">
<SingleLineEditor
value={wsseAuth.password || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handlePasswordChange(val)}
collection={collection}
isSecret={true}
/>
{showWarning && <SensitiveFieldWarning fieldName="wsse-password" warningMessage={warningMessage} />}
</div>
</StyledWrapper>
);

View File

@@ -8,16 +8,17 @@ import BasicAuth from './BasicAuth';
import DigestAuth from './DigestAuth';
import WsseAuth from './WsseAuth';
import ApiKeyAuth from './ApiKeyAuth/';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import OAuth2 from './OAuth2';
import NTLMAuth from './NTLMAuth';
const Auth = ({ collection }) => {
const authMode = collection.draft?.root ? get(collection, 'draft.root.request.auth.mode') : get(collection, 'root.request.auth.mode');
const authMode = get(collection, 'root.request.auth.mode');
const dispatch = useDispatch();
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const getAuthView = () => {
switch (authMode) {
@@ -35,7 +36,7 @@ const Auth = ({ collection }) => {
}
case 'ntlm': {
return <NTLMAuth collection={collection} />;
}
}
case 'oauth2': {
return <OAuth2 collection={collection} />;
}

View File

@@ -38,48 +38,6 @@ const StyledWrapper = styled.div`
outline: none !important;
}
}
.protocol-placeholder {
height: 100%;
position: relative;
display: inline-block;
width: 60px;
overflow: hidden;
}
.protocol-https,
.protocol-grpcs {
position: absolute;
right: 8px;
top: 0;
bottom: 0;
transition: transform 0.3s ease-in-out;
display: flex;
align-items: center;
justify-content: center;
}
.protocol-https {
animation: slideUpDown 6s infinite;
transform: translateY(0);
}
.protocol-grpcs {
animation: slideUpDown 6s infinite 3s;
transform: translateY(100%);
}
@keyframes slideUpDown {
0%, 45% {
transform: translateY(0);
}
50%, 95% {
transform: translateY(100%);
}
100% {
transform: translateY(0);
}
}
`;
export default StyledWrapper;

View File

@@ -1,30 +1,19 @@
import React from 'react';
import { IconCertificate, IconTrash, IconWorld } from '@tabler/icons';
import { useFormik } from 'formik';
import { uuid } from 'utils/common';
import * as Yup from 'yup';
import { IconEye, IconEyeOff } from '@tabler/icons';
import { useState } from 'react';
import StyledWrapper from './StyledWrapper';
import { useRef } from 'react';
import path from 'utils/common/path';
import SensitiveFieldWarning from 'components/SensitiveFieldWarning/index';
import SingleLineEditor from 'components/SingleLineEditor/index';
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField/index';
import { useTheme } from 'styled-components';
import { useDispatch } from 'react-redux';
import { updateCollectionClientCertificates } from 'providers/ReduxStore/slices/collections';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import get from 'lodash/get';
const ClientCertSettings = ({ collection }) => {
const dispatch = useDispatch();
// Get client certs from draft if exists, otherwise from brunoConfig
const clientCertConfig = collection.draft?.brunoConfig
? get(collection, 'draft.brunoConfig.clientCertificates.certs', [])
: get(collection, 'brunoConfig.clientCertificates.certs', []);
const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
const certFilePathInputRef = useRef();
const keyFilePathInputRef = useRef();
const pfxFilePathInputRef = useRef();
const { storedTheme } = useTheme();
const formik = useFormik({
initialValues: {
@@ -39,7 +28,7 @@ const ClientCertSettings = ({ collection }) => {
domain: Yup.string()
.required()
.trim()
.test('not-empty-after-trim', 'Domain is required', (value) => value && value.trim().length > 0),
.test('not-empty-after-trim', 'Domain is required', value => value && value.trim().length > 0),
type: Yup.string().required().oneOf(['cert', 'pfx']),
certFilePath: Yup.string().when('type', {
is: (type) => type == 'cert',
@@ -73,47 +62,28 @@ const ClientCertSettings = ({ collection }) => {
passphrase: values.passphrase
};
}
// Add the new cert to the existing certs in draft
const updatedCerts = [...clientCertConfig, relevantValues];
const clientCertificates = {
enabled: true,
certs: updatedCerts
};
dispatch(updateCollectionClientCertificates({
collectionUid: collection.uid,
clientCertificates
}));
onUpdate(relevantValues);
formik.resetForm();
resetFileInputFields();
}
});
const { isSensitive } = useDetectSensitiveField(collection);
const { showWarning, warningMessage } = isSensitive(formik.values.passphrase);
const getFile = (e) => {
const filePath = window?.ipcRenderer?.getFilePath(e?.files?.[0]);
if (filePath) {
let relativePath = path.relative(collection.pathname, filePath);
let relativePath = path.relative(root, filePath);
formik.setFieldValue(e.name, relativePath);
}
};
const resetFileInputFields = () => {
if (certFilePathInputRef.current) {
certFilePathInputRef.current.value = '';
}
if (keyFilePathInputRef.current) {
keyFilePathInputRef.current.value = '';
}
if (pfxFilePathInputRef.current) {
pfxFilePathInputRef.current.value = '';
}
certFilePathInputRef.current.value = '';
keyFilePathInputRef.current.value = '';
pfxFilePathInputRef.current.value = '';
};
const [passwordVisible, setPasswordVisible] = useState(false);
const handleTypeChange = (e) => {
formik.setFieldValue('type', e.target.value);
if (e.target.value === 'cert') {
@@ -127,49 +97,34 @@ const ClientCertSettings = ({ collection }) => {
}
};
const handleRemove = (indexToRemove) => {
const updatedCerts = clientCertConfig.filter((cert, index) => index !== indexToRemove);
const clientCertificates = {
enabled: true,
certs: updatedCerts
};
dispatch(updateCollectionClientCertificates({
collectionUid: collection.uid,
clientCertificates
}));
};
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
return (
<StyledWrapper className="w-full h-full">
<div className="text-xs mb-4 text-muted">Add client certificates to be used for specific domains.</div>
<h1 className="font-medium">Client Certificates</h1>
<h1 className="font-semibold">Client Certificates</h1>
<ul className="mt-4">
{!clientCertConfig.length
? 'No client certificates added'
: clientCertConfig.map((clientCert, index) => (
<li key={`client-cert-${index}`} className="flex items-center available-certificates p-2 rounded-lg mb-2">
<div className="flex items-center w-full justify-between">
<div className="flex w-full items-center">
<IconWorld className="mr-2" size={18} strokeWidth={1.5} />
{clientCert.domain}
</div>
<div className="flex w-full items-center">
<IconCertificate className="mr-2 flex-shrink-0" size={18} strokeWidth={1.5} />
{clientCert.type === 'cert' ? clientCert.certFilePath : clientCert.pfxFilePath}
</div>
<button onClick={() => handleRemove(index)} className="remove-certificate ml-2">
<IconTrash size={18} strokeWidth={1.5} />
</button>
<li key={`client-cert-${index}`} className="flex items-center available-certificates p-2 rounded-lg mb-2">
<div className="flex items-center w-full justify-between">
<div className="flex w-full items-center">
<IconWorld className="mr-2" size={18} strokeWidth={1.5} />
{clientCert.domain}
</div>
</li>
))}
<div className="flex w-full items-center">
<IconCertificate className="mr-2 flex-shrink-0" size={18} strokeWidth={1.5} />
{clientCert.type === 'cert' ? clientCert.certFilePath : clientCert.pfxFilePath}
</div>
<button onClick={() => onRemove(clientCert)} className="remove-certificate ml-2">
<IconTrash size={18} strokeWidth={1.5} />
</button>
</div>
</li>
))}
</ul>
<h1 className="font-medium mt-8 mb-2">Add Client Certificate</h1>
<h1 className="font-semibold mt-8 mb-2">Add Client Certificate</h1>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="domain">
@@ -177,10 +132,7 @@ const ClientCertSettings = ({ collection }) => {
</label>
<div className="relative flex items-center">
<div className="absolute left-0 pl-2 text-gray-400 pointer-events-none flex items-center h-full">
<span className="protocol-placeholder">
<span className="protocol-https">https://</span>
<span className="protocol-grpcs">grpcs://</span>
</span>
https://
</div>
<input
id="domain"
@@ -359,27 +311,30 @@ const ClientCertSettings = ({ collection }) => {
Passphrase
</label>
<div className="textbox flex flex-row items-center w-[300px] h-[1.70rem] relative">
<SingleLineEditor
<input
id="passphrase"
type={passwordVisible ? 'text' : 'password'}
name="passphrase"
className="outline-none w-64 bg-transparent"
onChange={formik.handleChange}
value={formik.values.passphrase || ''}
theme={storedTheme}
onChange={(val) => formik.setFieldValue('passphrase', val)}
collection={collection}
isSecret={true}
/>
{showWarning && <SensitiveFieldWarning fieldName="basic-password" warningMessage={warningMessage} />}
<button
type="button"
className="btn btn-sm absolute right-0 l"
onClick={() => setPasswordVisible(!passwordVisible)}
>
{passwordVisible ? <IconEyeOff size={18} strokeWidth={1.5} /> : <IconEye size={18} strokeWidth={1.5} />}
</button>
</div>
{formik.touched.passphrase && formik.errors.passphrase ? (
<div className="ml-1 text-red-500">{formik.errors.passphrase}</div>
) : null}
</div>
<div className="mt-6 flex flex-row gap-2 items-center">
<div className="mt-6">
<button type="submit" className="submit btn btn-sm btn-secondary">
Add
</button>
<div className="h-4 border-l border-gray-600"></div>
<button type="button" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
Save
</button>
</div>
</form>
</StyledWrapper>

View File

@@ -1,10 +1,10 @@
import 'github-markdown-css/github-markdown.css';
import get from 'lodash/get';
import { updateCollectionDocs, deleteCollectionDraft } from 'providers/ReduxStore/slices/collections';
import { updateCollectionDocs } from 'providers/ReduxStore/slices/collections';
import { useTheme } from 'providers/Theme';
import { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import Markdown from 'components/MarkDown';
import CodeEditor from 'components/CodeEditor';
import StyledWrapper from './StyledWrapper';
@@ -14,7 +14,7 @@ const Docs = ({ collection }) => {
const dispatch = useDispatch();
const { displayedTheme } = useTheme();
const [isEditing, setIsEditing] = useState(false);
const docs = collection.draft?.root ? get(collection, 'draft.root.docs', '') : get(collection, 'root.docs', '');
const docs = get(collection, 'root.docs', '');
const preferences = useSelector((state) => state.app.preferences);
const toggleViewMode = () => {
@@ -31,28 +31,28 @@ const Docs = ({ collection }) => {
};
const handleDiscardChanges = () => {
dispatch((
dispatch(
updateCollectionDocs({
collectionUid: collection.uid,
docs: docs
}))
})
);
toggleViewMode();
};
}
const onSave = () => {
dispatch(saveCollectionSettings(collection.uid));
dispatch(saveCollectionRoot(collection.uid));
toggleViewMode();
};
}
return (
<StyledWrapper className="h-full w-full relative flex flex-col">
<div className="flex flex-row w-full justify-between items-center mb-4">
<div className="text-lg font-medium flex items-center gap-2">
<StyledWrapper className="mt-1 h-full w-full relative flex flex-col">
<div className='flex flex-row w-full justify-between items-center mb-4'>
<div className='text-lg font-medium flex items-center gap-2'>
<IconFileText size={20} strokeWidth={1.5} />
Documentation
</div>
<div className="flex flex-row gap-2 items-center justify-center">
<div className='flex flex-row gap-2 items-center justify-center'>
{isEditing ? (
<>
<div className="editing-mode" role="tab" onClick={handleDiscardChanges}>
@@ -81,13 +81,14 @@ const Docs = ({ collection }) => {
fontSize={get(preferences, 'font.codeFontSize')}
/>
) : (
<div className="h-full overflow-auto pl-1">
<div className="h-[1px] min-h-[500px]">
<div className='h-full overflow-auto pl-1'>
<div className='h-[1px] min-h-[500px]'>
{
docs?.length > 0
? <Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={docs} />
: <Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={documentationPlaceholder} />
}
docs?.length > 0 ?
<Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={docs} />
:
<Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={documentationPlaceholder} />
}
</div>
</div>
)}
@@ -97,6 +98,7 @@ const Docs = ({ collection }) => {
export default Docs;
const documentationPlaceholder = `
Welcome to your collection documentation! This space is designed to help you document your API collection effectively.

View File

@@ -6,7 +6,7 @@ const Wrapper = styled.div`
table {
width: 100%;
border-collapse: collapse;
font-weight: 500;
font-weight: 600;
table-layout: fixed;
thead,
@@ -16,7 +16,7 @@ const Wrapper = styled.div`
thead {
color: ${(props) => props.theme.table.thead.color};
font-size: ${(props) => props.theme.font.size.base};
font-size: 0.8125rem;
user-select: none;
}
td {
@@ -33,7 +33,7 @@ const Wrapper = styled.div`
}
.btn-add-header {
font-size: ${(props) => props.theme.font.size.base};
font-size: 0.8125rem;
}
input[type='text'] {

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React from 'react';
import get from 'lodash/get';
import cloneDeep from 'lodash/cloneDeep';
import { IconTrash } from '@tabler/icons';
@@ -7,30 +7,19 @@ import { useTheme } from 'providers/Theme';
import {
addCollectionHeader,
updateCollectionHeader,
deleteCollectionHeader,
setCollectionHeaders
deleteCollectionHeader
} from 'providers/ReduxStore/slices/collections';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import SingleLineEditor from 'components/SingleLineEditor';
import StyledWrapper from './StyledWrapper';
import { headers as StandardHTTPHeaders } from 'know-your-http-well';
import { MimeTypes } from 'utils/codemirror/autocompleteConstants';
import BulkEditor from 'components/BulkEditor/index';
const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
const Headers = ({ collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const headers = collection.draft?.root ? get(collection, 'draft.root.request.headers', []) : get(collection, 'root.request.headers', []);
const [isBulkEditMode, setIsBulkEditMode] = useState(false);
const toggleBulkEditMode = () => {
setIsBulkEditMode(!isBulkEditMode);
};
const handleBulkHeadersChange = (newHeaders) => {
dispatch(setCollectionHeaders({ collectionUid: collection.uid, headers: newHeaders }));
};
const headers = get(collection, 'root.request.headers', []);
const addHeader = () => {
dispatch(
@@ -40,13 +29,12 @@ const Headers = ({ collection }) => {
);
};
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const handleHeaderValueChange = (e, _header, type) => {
const header = cloneDeep(_header);
switch (type) {
case 'name': {
// Strip newlines from header keys
header.name = e.target.value.replace(/[\r\n]/g, '');
header.name = e.target.value;
break;
}
case 'value': {
@@ -75,22 +63,6 @@ const Headers = ({ collection }) => {
);
};
if (isBulkEditMode) {
return (
<StyledWrapper className="h-full w-full">
<div className="text-xs mb-4 text-muted">
Add request headers that will be sent with every request in this collection.
</div>
<BulkEditor
params={headers}
onChange={handleBulkHeadersChange}
onToggle={toggleBulkEditMode}
onSave={handleSave}
/>
</StyledWrapper>
);
}
return (
<StyledWrapper className="h-full w-full">
<div className="text-xs mb-4 text-muted">
@@ -123,7 +95,8 @@ const Headers = ({ collection }) => {
},
header,
'name'
)}
)
}
autocomplete={headerAutoCompleteList}
collection={collection}
/>
@@ -142,7 +115,8 @@ const Headers = ({ collection }) => {
},
header,
'value'
)}
)
}
collection={collection}
autocomplete={MimeTypes}
/>
@@ -167,14 +141,9 @@ const Headers = ({ collection }) => {
: null}
</tbody>
</table>
<div className="flex justify-between mt-2">
<button className="btn-add-header text-link pr-2 py-3 select-none" onClick={addHeader}>
+ Add Header
</button>
<button className="text-link select-none" onClick={toggleBulkEditMode}>
Bulk Edit
</button>
</div>
<button className="btn-add-header text-link pr-2 py-3 mt-2 select-none" onClick={addHeader}>
+ Add Header
</button>
<div className="mt-6">
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>

View File

@@ -1,9 +1,9 @@
import React from 'react';
import React from "react";
import { getTotalRequestCountInCollection } from 'utils/collections/';
import { IconBox, IconFolder, IconWorld, IconApi, IconShare } from '@tabler/icons';
import { areItemsLoading, getItemsLoadStats } from 'utils/collections/index';
import { useState } from 'react';
import ShareCollection from 'components/ShareCollection/index';
import { IconFolder, IconWorld, IconApi, IconShare } from '@tabler/icons';
import { areItemsLoading, getItemsLoadStats } from "utils/collections/index";
import { useState } from "react";
import ShareCollection from "components/ShareCollection/index";
const Info = ({ collection }) => {
const totalRequestsInCollection = getTotalRequestCountInCollection(collection);
@@ -11,23 +11,23 @@ const Info = ({ collection }) => {
const isCollectionLoading = areItemsLoading(collection);
const { loading: itemsLoadingCount, total: totalItems } = getItemsLoadStats(collection);
const [showShareCollectionModal, toggleShowShareCollectionModal] = useState(false);
const handleToggleShowShareCollectionModal = (value) => (e) => {
toggleShowShareCollectionModal(value);
};
}
return (
<div className="w-full flex flex-col h-fit">
<div className="rounded-lg py-6">
<div className="grid gap-5">
<div className="grid gap-6">
{/* Location Row */}
<div className="flex items-start">
<div className="flex-shrink-0 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<IconFolder className="w-5 h-5 text-blue-500" stroke={1.5} />
</div>
<div className="ml-4">
<div className="font-medium">Location</div>
<div className="mt-1 text-muted break-all text-xs">
<div className="font-semibold text-sm">Location</div>
<div className="mt-1 text-sm text-muted break-all">
{collection.pathname}
</div>
</div>
@@ -39,8 +39,8 @@ const Info = ({ collection }) => {
<IconWorld className="w-5 h-5 text-green-500" stroke={1.5} />
</div>
<div className="ml-4">
<div className="font-medium">Environments</div>
<div className="mt-1 text-muted text-xs">
<div className="font-semibold text-sm">Environments</div>
<div className="mt-1 text-sm text-muted">
{collection.environments?.length || 0} environment{collection.environments?.length !== 1 ? 's' : ''} configured
</div>
</div>
@@ -52,10 +52,10 @@ const Info = ({ collection }) => {
<IconApi className="w-5 h-5 text-purple-500" stroke={1.5} />
</div>
<div className="ml-4">
<div className="font-medium">Requests</div>
<div className="mt-1 text-muted text-xs">
<div className="font-semibold text-sm">Requests</div>
<div className="mt-1 text-sm text-muted font-mono">
{
isCollectionLoading ? `${totalItems - itemsLoadingCount} out of ${totalItems} requests in the collection loaded` : `${totalRequestsInCollection} request${totalRequestsInCollection !== 1 ? 's' : ''} in collection`
isCollectionLoading? `${totalItems - itemsLoadingCount} out of ${totalItems} requests in the collection loaded` : `${totalRequestsInCollection} request${totalRequestsInCollection !== 1 ? 's' : ''} in collection`
}
</div>
</div>
@@ -66,8 +66,8 @@ const Info = ({ collection }) => {
<IconShare className="w-5 h-5 text-indigo-500" stroke={1.5} />
</div>
<div className="ml-4 h-full flex flex-col justify-start">
<div className="font-medium h-fit my-auto">Share</div>
<div className="group-hover:underline text-link text-xs">
<div className="font-semibold text-sm h-fit my-auto">Share</div>
<div className="mt-1 text-sm group-hover:underline text-link">
Share Collection
</div>
</div>
@@ -79,4 +79,4 @@ const Info = ({ collection }) => {
);
};
export default Info;
export default Info;

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { flattenItems } from 'utils/collections';
import { flattenItems } from "utils/collections";
import { IconAlertTriangle } from '@tabler/icons';
import StyledWrapper from './StyledWrapper';
import StyledWrapper from "./StyledWrapper";
import { useDispatch, useSelector } from 'react-redux';
import { isItemARequest, itemIsOpenedInTabs } from 'utils/tabs/index';
import { getDefaultRequestPaneTab } from 'utils/collections/index';
@@ -12,13 +12,13 @@ const RequestsNotLoaded = ({ collection }) => {
const dispatch = useDispatch();
const tabs = useSelector((state) => state.tabs.tabs);
const flattenedItems = flattenItems(collection.items);
const itemsFailedLoading = flattenedItems?.filter((item) => item?.partial && !item?.loading);
const itemsFailedLoading = flattenedItems?.filter(item => item?.partial && !item?.loading);
if (!itemsFailedLoading?.length) {
return null;
}
const handleRequestClick = (item) => (e) => {
const handleRequestClick = (item) => e => {
e.preventDefault();
if (isItemARequest(item)) {
dispatch(hideHomePage());
@@ -39,7 +39,7 @@ const RequestsNotLoaded = ({ collection }) => {
);
return;
}
};
}
return (
<StyledWrapper className="w-full card my-2">
@@ -61,7 +61,7 @@ const RequestsNotLoaded = ({ collection }) => {
<tbody>
{flattenedItems?.map((item, index) => (
item?.partial && !item?.loading ? (
<tr key={index} className="cursor-pointer" onClick={handleRequestClick(item)}>
<tr key={index} className='cursor-pointer' onClick={handleRequestClick(item)}>
<td className="py-1.5 px-3">
{item?.pathname?.split(`${collection?.pathname}/`)?.[1]}
</td>

View File

@@ -1,16 +1,16 @@
import StyledWrapper from './StyledWrapper';
import Docs from '../Docs';
import Info from './Info';
import StyledWrapper from "./StyledWrapper";
import Docs from "../Docs";
import Info from "./Info";
import { IconBox } from '@tabler/icons';
import RequestsNotLoaded from './RequestsNotLoaded';
import RequestsNotLoaded from "./RequestsNotLoaded";
const Overview = ({ collection }) => {
return (
<div className="h-full">
<div className="grid grid-cols-5 gap-5 h-full">
<div className="grid grid-cols-5 gap-4 h-full">
<div className="col-span-2">
<div className="text-lg font-medium flex items-center gap-2">
<IconBox size={20} stroke={1.5} />
<div className="text-xl font-semibold flex items-center gap-2">
<IconBox size={24} stroke={1.5} />
{collection?.name}
</div>
<Info collection={collection} />
@@ -22,6 +22,6 @@ const Overview = ({ collection }) => {
</div>
</div>
);
};
}
export default Overview;
export default Overview;

View File

@@ -0,0 +1,29 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
max-width: 800px;
.settings-label {
width: 110px;
}
.textbox {
border: 1px solid #ccc;
padding: 0.15rem 0.45rem;
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};
&:focus {
border: solid 1px ${(props) => props.theme.modal.input.focusBorder} !important;
outline: none !important;
}
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,100 @@
import React, { useEffect } from 'react';
import { useFormik } from 'formik';
import { useDispatch } from 'react-redux';
import StyledWrapper from './StyledWrapper';
import toast from 'react-hot-toast';
import { updateBrunoConfig } from 'providers/ReduxStore/slices/collections/actions';
import cloneDeep from 'lodash/cloneDeep';
const PresetsSettings = ({ collection }) => {
const dispatch = useDispatch();
const {
brunoConfig: { presets: presets = {} }
} = collection;
const formik = useFormik({
enableReinitialize: true,
initialValues: {
requestType: presets.requestType || 'http',
requestUrl: presets.requestUrl || ''
},
onSubmit: (newPresets) => {
const brunoConfig = cloneDeep(collection.brunoConfig);
brunoConfig.presets = newPresets;
dispatch(updateBrunoConfig(brunoConfig, collection.uid));
toast.success('Collection presets updated');
}
});
return (
<StyledWrapper className="h-full w-full">
<div className="text-xs mb-4 text-muted">
These presets will be used as the default values for new requests in this collection.
</div>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<div className="mb-3 flex items-center">
<label className="settings-label flex items-center" htmlFor="enabled">
Request Type
</label>
<div className="flex items-center">
<input
id="http"
className="cursor-pointer"
type="radio"
name="requestType"
onChange={formik.handleChange}
value="http"
checked={formik.values.requestType === 'http'}
/>
<label htmlFor="http" className="ml-1 cursor-pointer select-none">
HTTP
</label>
<input
id="graphql"
className="ml-4 cursor-pointer"
type="radio"
name="requestType"
onChange={formik.handleChange}
value="graphql"
checked={formik.values.requestType === 'graphql'}
/>
<label htmlFor="graphql" className="ml-1 cursor-pointer select-none">
GraphQL
</label>
</div>
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="requestUrl">
Base URL
</label>
<div className="flex items-center w-full">
<div className="flex items-center flex-grow input-container h-full">
<input
id="request-url"
type="text"
name="requestUrl"
placeholder='Request URL'
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.requestUrl || ''}
style={{ width: '100%' }}
/>
</div>
</div>
</div>
<div className="mt-6">
<button type="submit" className="submit btn btn-sm btn-secondary">
Save
</button>
</div>
</form>
</StyledWrapper>
);
};
export default PresetsSettings;

Some files were not shown because too many files have changed in this diff Show More