mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-03 17:38:36 +00:00
Compare commits
7 Commits
v3.1.3
...
bugfix/def
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90a4f8271e | ||
|
|
0c62ead394 | ||
|
|
b82b3088c1 | ||
|
|
72dccdb8e3 | ||
|
|
013c128c78 | ||
|
|
9053c47789 | ||
|
|
f3fd8a1ce0 |
70
.github/scripts/comment-on-flaky-tests.js
vendored
70
.github/scripts/comment-on-flaky-tests.js
vendored
@@ -1,70 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
// Check if flaky-tests.json exists
|
||||
if (!fs.existsSync('flaky-tests.json')) {
|
||||
console.log('No flaky-tests.json found');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Get changed files in PR
|
||||
let changedFiles = [];
|
||||
try {
|
||||
changedFiles = execSync('git diff --name-only origin/main...HEAD')
|
||||
.toString()
|
||||
.split('\n')
|
||||
.filter(f => f.endsWith('.spec.ts'));
|
||||
} catch (error) {
|
||||
console.log('Could not determine changed files:', error.message);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (changedFiles.length === 0) {
|
||||
console.log('No test files were modified in this PR');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Read flaky tests
|
||||
const flakyTests = JSON.parse(fs.readFileSync('flaky-tests.json', 'utf8'));
|
||||
|
||||
if (flakyTests.length === 0) {
|
||||
console.log('No flaky/failed tests found');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Find modified flaky tests
|
||||
const modifiedFlakyTests = flakyTests.filter(test =>
|
||||
changedFiles.some(file => test.file.includes(file))
|
||||
);
|
||||
|
||||
if (modifiedFlakyTests.length === 0) {
|
||||
console.log('No modified test files are flaky');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Generate comment markdown
|
||||
let comment = '## ⚠️ Warning: You modified flaky/failed test files\n\n';
|
||||
comment += 'The following test files you modified have reliability issues:\n\n';
|
||||
|
||||
modifiedFlakyTests.forEach(test => {
|
||||
const testType = test.status === 'failed' ? '❌ Failed' : '⚠️ Flaky';
|
||||
comment += `### ${testType}: \`${test.file}\`\n`;
|
||||
comment += `**Test:** ${test.testTitle}\n`;
|
||||
comment += `**Status:** ${test.status}\n`;
|
||||
if (test.retryAttempt > 0) {
|
||||
comment += `**Retry Attempt:** ${test.retryAttempt}\n`;
|
||||
}
|
||||
comment += '\n**To debug locally, run:**\n';
|
||||
comment += '```bash\n';
|
||||
comment += `npx playwright test ${test.file} --repeat-each=5 --workers=1\n`;
|
||||
comment += '```\n\n';
|
||||
});
|
||||
|
||||
comment += '---\n';
|
||||
comment += '**Note:** Flaky tests passed after retrying, failed tests did not pass. ';
|
||||
comment += 'Please investigate and fix the root cause before merging.\n';
|
||||
|
||||
// Save comment to file for GitHub Action to post
|
||||
fs.writeFileSync('pr-comment.md', comment);
|
||||
|
||||
console.log(`Found ${modifiedFlakyTests.length} modified flaky tests`);
|
||||
78
.github/scripts/detect-flaky-tests.js
vendored
78
.github/scripts/detect-flaky-tests.js
vendored
@@ -1,78 +0,0 @@
|
||||
const fs = require('fs');
|
||||
|
||||
|
||||
// Read Playwright JSON report
|
||||
const resultsPath = 'playwright-report/results.json';
|
||||
|
||||
if (!fs.existsSync(resultsPath)) {
|
||||
console.log('No Playwright results found at', resultsPath);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const results = JSON.parse(fs.readFileSync(resultsPath, 'utf8'));
|
||||
|
||||
// Extract flaky tests
|
||||
// A test is flaky if: status === "passed" AND retry > 0
|
||||
// A test is failed if: status === "failed"
|
||||
// This means it failed initially but passed on retry OR failed completely
|
||||
const flakyTests = [];
|
||||
|
||||
function traverseSuites(suites) {
|
||||
for (const suite of suites) {
|
||||
// Process specs in this suite
|
||||
for (const spec of suite.specs || []) {
|
||||
for (const test of spec.tests || []) {
|
||||
// Check each test result
|
||||
for (const result of test.results || []) {
|
||||
// Track two types of problematic tests:
|
||||
// 1. Flaky: passed on a retry attempt (retry > 0)
|
||||
// 2. Failed: failed on all attempts
|
||||
if ((result.status === 'passed' && result.retry > 0) || result.status === 'failed') {
|
||||
flakyTests.push({
|
||||
file: spec.file,
|
||||
title: spec.title,
|
||||
testTitle: spec.title,
|
||||
line: spec.line,
|
||||
status: result.status,
|
||||
retryAttempt: result.retry
|
||||
});
|
||||
break; // Only record once per test
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively process nested suites
|
||||
if (suite.suites && suite.suites.length > 0) {
|
||||
traverseSuites(suite.suites);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
traverseSuites(results.suites || []);
|
||||
|
||||
// Save flaky tests to JSON
|
||||
fs.writeFileSync('flaky-tests.json', JSON.stringify(flakyTests, null, 2));
|
||||
|
||||
// Generate markdown report
|
||||
let markdown = '## ⚠️ Flaky/Failed Tests Detected\n\n';
|
||||
markdown += 'The following tests are problematic:\n\n';
|
||||
|
||||
flakyTests.forEach(test => {
|
||||
const testType = test.status === 'failed' ? '❌ Failed' : '⚠️ Flaky';
|
||||
markdown += `### ${testType}: \`${test.file}\`\n`;
|
||||
markdown += `- **Test:** ${test.testTitle}\n`;
|
||||
markdown += `- **Status:** ${test.status}\n`;
|
||||
if (test.retryAttempt > 0) {
|
||||
markdown += `- **Retry Attempt:** ${test.retryAttempt}\n`;
|
||||
}
|
||||
markdown += `- **Debug command:**\n`;
|
||||
markdown += '```bash\n';
|
||||
markdown += `npx playwright test ${test.file} --repeat-each=5 --workers=1\n`;
|
||||
markdown += '```\n\n';
|
||||
});
|
||||
|
||||
fs.writeFileSync('flaky-report.md', markdown);
|
||||
|
||||
console.log(`Found ${flakyTests.length} flaky/failed tests`);
|
||||
process.exit(flakyTests.length > 0 ? 1 : 0);
|
||||
119
.github/workflows/flaky-test-detector.yml
vendored
119
.github/workflows/flaky-test-detector.yml
vendored
@@ -1,119 +0,0 @@
|
||||
name: Flaky Test Detector
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'tests/**/*.spec.ts'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
checks: write
|
||||
|
||||
jobs:
|
||||
detect-flaky-tests:
|
||||
name: Detect Flaky Tests
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0 # Need full history to compare with main
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install system dependencies
|
||||
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
|
||||
|
||||
- name: Install npm dependencies
|
||||
run: |
|
||||
npm ci --legacy-peer-deps
|
||||
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
|
||||
|
||||
- name: Install test collection dependencies
|
||||
run: npm ci --prefix packages/bruno-tests/collection
|
||||
|
||||
- name: Build libraries
|
||||
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
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: xvfb-run npm run test:e2e
|
||||
continue-on-error: true # Continue even if tests fail
|
||||
|
||||
- name: Detect flaky tests
|
||||
id: detect
|
||||
run: node .github/scripts/detect-flaky-tests.js
|
||||
continue-on-error: true # Don't fail workflow if flaky tests found
|
||||
|
||||
- name: Check modified flaky tests
|
||||
id: check-modified
|
||||
run: node .github/scripts/comment-on-flaky-tests.js
|
||||
continue-on-error: true
|
||||
|
||||
- name: Post PR comment
|
||||
if: hashFiles('pr-comment.md') != ''
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const comment = fs.readFileSync('pr-comment.md', 'utf8');
|
||||
|
||||
// Check if we already commented
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number
|
||||
});
|
||||
|
||||
const botComment = comments.find(c =>
|
||||
c.user.type === 'Bot' && c.body.includes('Warning: You modified flaky/failed test files')
|
||||
);
|
||||
|
||||
if (botComment) {
|
||||
// Update existing comment
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: botComment.id,
|
||||
body: comment
|
||||
});
|
||||
} else {
|
||||
// Create new comment
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: comment
|
||||
});
|
||||
}
|
||||
|
||||
- name: Upload flaky test artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: flaky-test-results
|
||||
path: |
|
||||
flaky-tests.json
|
||||
flaky-report.md
|
||||
playwright-report/
|
||||
retention-days: 30
|
||||
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
@@ -58,8 +58,6 @@ jobs:
|
||||
run: npm run test --workspace=packages/bruno-converters
|
||||
- name: Test Package bruno-electron
|
||||
run: npm run test --workspace=packages/bruno-electron
|
||||
- name: Test Package bruno-requests
|
||||
run: npm run test --workspace=packages/bruno-requests
|
||||
|
||||
cli-test:
|
||||
name: CLI Tests
|
||||
@@ -144,7 +142,7 @@ jobs:
|
||||
- name: Run Playwright tests
|
||||
run: |
|
||||
xvfb-run npm run test:e2e
|
||||
- uses: actions/upload-artifact@v6
|
||||
- uses: actions/upload-artifact@v5
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: playwright-report
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -48,14 +48,11 @@ yarn-error.log*
|
||||
bruno.iml
|
||||
.idea
|
||||
.vscode
|
||||
.cursor
|
||||
|
||||
# Playwright
|
||||
/blob-report/
|
||||
|
||||
# Development plan files
|
||||
CLAUDE.md
|
||||
AGENTS.md
|
||||
*.plan.md
|
||||
|
||||
# packages dist
|
||||
|
||||
7
.prettierrc.json
Normal file
7
.prettierrc.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"printWidth": 120
|
||||
}
|
||||
@@ -66,16 +66,7 @@ Remember, these rules are here to make our codebase harmonious. If something doe
|
||||
|
||||
- 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.
|
||||
- MUST: Prefer custom hooks for business logic, data fetching, and side-effects.
|
||||
- MUST: Avoid `useEffect` unless absolutely needed. Prefer derived state, event handlers.
|
||||
- SHOULD: Memoize only when necessary (`useMemo`/`useCallback`), and prefer moving logic into hooks first.
|
||||
- MUST: Do not use namespace access for hooks in app code (e.g., `React.useCallback`, `React.useMemo`, `React.useState`). Import hooks directly.
|
||||
- Correct: `import { useCallback, useMemo, useState } from "react";`
|
||||
- Avoid: `import * as React from "react";` then `React.useCallback(...)`
|
||||
- Add `data-testid` to testable elements for Playwright
|
||||
- Co-locate utilities that are truly component-specific next to the component, otherwise place shared items under a common folder
|
||||
|
||||
- Styled Component CSS might also change layout but tailwind classes shouldn't define colors.
|
||||
|
||||
## Readability and Abstractions
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 346 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 347 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 584 KiB After Width: | Height: | Size: 813 KiB |
@@ -3,7 +3,7 @@
|
||||
|
||||
### برونو - بيئة تطوير مفتوحة المصدر لاستكشاف واختبار واجهات برمجة التطبيقات (APIs).
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### ব্রুনো - API অন্বেষণ এবং পরীক্ষা করার জন্য ওপেনসোর্স IDE।
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - 开源 IDE,用于探索和测试 API。
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - Opensource IDE zum Erkunden und Testen von APIs.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - IDE de código abierto para explorar y probar APIs.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### برونو یا Bruno - محیط توسعه متن باز برای تست و توسعه API ها
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - IDE Opensource pour explorer et tester des APIs.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - Opensource IDE per esplorare e testare gli APIs.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - API の検証・動作テストのためのオープンソース IDE.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### ბრუნო - ღია წყაროების IDE API-ების შესწავლისა და ტესტირებისათვის.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - API 탐색 및 테스트를 위한 오픈소스 IDE.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - Open source IDE voor het verkennen en testen van API's.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - Otwartoźródłowe IDE do eksploracji i testów APIs.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - IDE de código aberto para explorar e testar APIs.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - Mediu integrat de dezvoltare cu sursă deschisă pentru explorarea și testarea API-urilor.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - IDE с открытым исходным кодом для изучения и тестирования API.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - API'leri keşfetmek ve test etmek için açık kaynaklı IDE.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - IDE із відкритим кодом для тестування та дослідження API
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - 探索和測試 API 的開源 IDE 工具
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -117,18 +117,6 @@ module.exports = runESMImports().then(() => defineConfig([
|
||||
'no-undef': 'error'
|
||||
}
|
||||
},
|
||||
{
|
||||
// Storybook config files use CommonJS with __dirname and module.exports
|
||||
files: ['packages/bruno-app/storybook/**/*.js'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'no-undef': 'error'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['packages/bruno-cli/**/*.js'],
|
||||
ignores: ['**/*.config.js'],
|
||||
|
||||
3220
package-lock.json
generated
3220
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
22
package.json
22
package.json
@@ -23,20 +23,14 @@
|
||||
"@eslint/compat": "^1.3.2",
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@jest/globals": "^29.2.0",
|
||||
"@opencollection/types": "~0.7.0",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@storybook/addon-webpack5-compiler-babel": "^4.0.0",
|
||||
"@storybook/builder-webpack5": "^10.1.10",
|
||||
"@storybook/react": "^10.1.10",
|
||||
"@storybook/react-webpack5": "^10.1.10",
|
||||
"@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",
|
||||
"cross-env": "10.1.0",
|
||||
"eslint": "^9.26.0",
|
||||
"eslint-plugin-diff": "^2.0.3",
|
||||
"fs-extra": "^11.1.1",
|
||||
@@ -49,20 +43,19 @@
|
||||
"pretty-quick": "^3.1.3",
|
||||
"randomstring": "^1.2.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"storybook": "^10.1.10",
|
||||
"ts-jest": "^29.2.6"
|
||||
},
|
||||
"scripts": {
|
||||
"setup": "node ./scripts/setup.js",
|
||||
"watch:converters": "npm run watch --workspace=packages/bruno-converters",
|
||||
"dev": "node ./scripts/dev.js",
|
||||
"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",
|
||||
"prettier:web": "npm run prettier --workspace=packages/bruno-app",
|
||||
"dev:electron": "npm run dev --workspace=packages/bruno-electron",
|
||||
"dev:electron:debug": "npm run debug --workspace=packages/bruno-electron",
|
||||
"storybook": "npm run storybook --workspace=packages/bruno-app",
|
||||
"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",
|
||||
@@ -78,12 +71,12 @@
|
||||
"build:electron:rpm": "./scripts/build-electron.sh rpm",
|
||||
"build:electron:snap": "./scripts/build-electron.sh snap",
|
||||
"watch:common": "npm run watch --workspace=packages/bruno-common",
|
||||
"watch:requests": "npm run watch --workspace=packages/bruno-requests",
|
||||
"test:codegen": "node playwright/codegen.ts",
|
||||
"test:e2e": "playwright test --project=default",
|
||||
"test:e2e:ssl": "playwright test --project=ssl",
|
||||
"lint": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" npx eslint",
|
||||
"lint:fix": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" npx eslint --fix",
|
||||
"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": {
|
||||
@@ -98,8 +91,5 @@
|
||||
"json-schema-typed": "8.0.1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"ajv": "^8.17.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
4
packages/bruno-app/.gitignore
vendored
4
packages/bruno-app/.gitignore
vendored
@@ -22,7 +22,6 @@ build
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
*.log
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
@@ -34,5 +33,4 @@ yarn-error.log*
|
||||
.next/
|
||||
dist/
|
||||
|
||||
.env
|
||||
storybook-static/
|
||||
.env
|
||||
7
packages/bruno-app/.prettierrc.json
Normal file
7
packages/bruno-app/.prettierrc.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"printWidth": 120
|
||||
}
|
||||
@@ -8,8 +8,8 @@
|
||||
"build": "rsbuild build -m production",
|
||||
"preview": "rsbuild preview",
|
||||
"test": "jest",
|
||||
"storybook": "storybook dev -p 6006 --config-dir storybook",
|
||||
"build-storybook": "storybook build --config-dir storybook"
|
||||
"test:prettier": "prettier --check \"./src/**/*.{js,jsx,json,ts,tsx}\"",
|
||||
"prettier": "prettier --write \"./src/**/*.{js,jsx,json,ts,tsx}\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/inter": "^5.0.15",
|
||||
@@ -44,19 +44,18 @@
|
||||
"i18next": "24.1.2",
|
||||
"idb": "^7.0.0",
|
||||
"immer": "^9.0.15",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsesc": "^3.0.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"xml2js": "^0.6.2",
|
||||
"jshint": "^2.13.6",
|
||||
"json5": "^2.2.3",
|
||||
"jsonc-parser": "^3.2.1",
|
||||
"jsonpath-plus": "^10.3.0",
|
||||
"jsonschema": "^1.5.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",
|
||||
"mime-types": "^3.0.2",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.5.47",
|
||||
"mousetrap": "^1.6.5",
|
||||
@@ -64,10 +63,9 @@
|
||||
"path": "^0.12.7",
|
||||
"pdfjs-dist": "4.4.168",
|
||||
"platform": "^1.3.6",
|
||||
"polished": "^4.3.1",
|
||||
"posthog-node": "4.2.1",
|
||||
"prettier": "^2.7.1",
|
||||
"qs": "^6.14.1",
|
||||
"qs": "^6.11.0",
|
||||
"query-string": "^7.0.1",
|
||||
"react": "19.0.0",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
@@ -82,7 +80,6 @@
|
||||
"react-player": "^2.16.0",
|
||||
"react-redux": "^7.2.9",
|
||||
"react-tooltip": "^5.5.2",
|
||||
"react-virtuoso": "^4.18.1",
|
||||
"sass": "^1.46.0",
|
||||
"semver": "^7.7.1",
|
||||
"shell-quote": "^1.8.3",
|
||||
@@ -92,7 +89,6 @@
|
||||
"system": "^2.0.1",
|
||||
"url": "^0.11.3",
|
||||
"xml-formatter": "^3.5.0",
|
||||
"xml2js": "^0.6.2",
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -130,4 +126,4 @@
|
||||
"form-data": "4.0.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,10 +55,10 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
.cm-variable-valid {
|
||||
color: ${(props) => props.theme.codemirror.variable.valid};
|
||||
color: green;
|
||||
}
|
||||
.cm-variable-invalid {
|
||||
color: ${(props) => props.theme.codemirror.variable.invalid};
|
||||
color: red;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ const FileEditor = ({ apiSpec }) => {
|
||||
/>
|
||||
<IconDeviceFloppy
|
||||
onClick={onSave}
|
||||
color={hasChanges ? theme.draftColor : theme.requestTabs.icon.color}
|
||||
color={hasChanges ? theme.colors.text.yellow : theme.requestTabs.icon.color}
|
||||
strokeWidth={1.5}
|
||||
size={22}
|
||||
className={`absolute right-0 top-0 m-4 ${
|
||||
|
||||
@@ -7,7 +7,7 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
div.dropdown-item.menu-item {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
color: ${(props) => props.theme.colors.danger};
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.colors.bg.danger};
|
||||
color: white;
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
-webkit-app-region: no-drag;
|
||||
|
||||
.shortcut {
|
||||
font-size: 11px;
|
||||
color: ${(props) => props.theme.dropdown.mutedText};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,154 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { IconMenu2 } from '@tabler/icons';
|
||||
import MenuDropdown from 'ui/MenuDropdown';
|
||||
import ActionIcon from 'ui/ActionIcon';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const AppMenu = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
id: 'file',
|
||||
label: 'File',
|
||||
submenu: [
|
||||
{
|
||||
id: 'open-collection',
|
||||
label: 'Open Collection',
|
||||
onClick: () => ipcRenderer?.invoke('renderer:open-collection')
|
||||
},
|
||||
{ type: 'divider', id: 'file-div-1' },
|
||||
{
|
||||
id: 'preferences',
|
||||
label: 'Preferences',
|
||||
rightSection: <span className="shortcut">Ctrl+,</span>,
|
||||
onClick: () => ipcRenderer?.invoke('renderer:open-preferences')
|
||||
},
|
||||
{ type: 'divider', id: 'file-div-2' },
|
||||
{
|
||||
id: 'quit',
|
||||
label: 'Quit',
|
||||
rightSection: <span className="shortcut">Alt+F4</span>,
|
||||
onClick: () => ipcRenderer?.send('renderer:window-close')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'edit',
|
||||
label: 'Edit',
|
||||
submenu: [
|
||||
{
|
||||
id: 'undo',
|
||||
label: 'Undo',
|
||||
rightSection: <span className="shortcut">Ctrl+Z</span>,
|
||||
onClick: () => document.execCommand('undo')
|
||||
},
|
||||
{
|
||||
id: 'redo',
|
||||
label: 'Redo',
|
||||
rightSection: <span className="shortcut">Ctrl+Y</span>,
|
||||
onClick: () => document.execCommand('redo')
|
||||
},
|
||||
{ type: 'divider', id: 'edit-div-1' },
|
||||
{
|
||||
id: 'cut',
|
||||
label: 'Cut',
|
||||
rightSection: <span className="shortcut">Ctrl+X</span>,
|
||||
onClick: () => document.execCommand('cut')
|
||||
},
|
||||
{
|
||||
id: 'copy',
|
||||
label: 'Copy',
|
||||
rightSection: <span className="shortcut">Ctrl+C</span>,
|
||||
onClick: () => document.execCommand('copy')
|
||||
},
|
||||
{
|
||||
id: 'paste',
|
||||
label: 'Paste',
|
||||
rightSection: <span className="shortcut">Ctrl+V</span>,
|
||||
onClick: () => document.execCommand('paste')
|
||||
},
|
||||
{ type: 'divider', id: 'edit-div-2' },
|
||||
{
|
||||
id: 'select-all',
|
||||
label: 'Select All',
|
||||
rightSection: <span className="shortcut">Ctrl+A</span>,
|
||||
onClick: () => document.execCommand('selectAll')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'view',
|
||||
label: 'View',
|
||||
submenu: [
|
||||
{
|
||||
id: 'toggle-devtools',
|
||||
label: 'Developer Tools',
|
||||
rightSection: <span className="shortcut">Ctrl+Shift+I</span>,
|
||||
onClick: () => ipcRenderer?.invoke('renderer:toggle-devtools')
|
||||
},
|
||||
{ type: 'divider', id: 'view-div-1' },
|
||||
{
|
||||
id: 'reset-zoom',
|
||||
label: 'Reset Zoom',
|
||||
rightSection: <span className="shortcut">Ctrl+0</span>,
|
||||
onClick: () => ipcRenderer?.invoke('renderer:reset-zoom')
|
||||
},
|
||||
{
|
||||
id: 'zoom-in',
|
||||
label: 'Zoom In',
|
||||
rightSection: <span className="shortcut">Ctrl++</span>,
|
||||
onClick: () => ipcRenderer?.invoke('renderer:zoom-in')
|
||||
},
|
||||
{
|
||||
id: 'zoom-out',
|
||||
label: 'Zoom Out',
|
||||
rightSection: <span className="shortcut">Ctrl+-</span>,
|
||||
onClick: () => ipcRenderer?.invoke('renderer:zoom-out')
|
||||
},
|
||||
{ type: 'divider', id: 'view-div-2' },
|
||||
{
|
||||
id: 'toggle-fullscreen',
|
||||
label: 'Full Screen',
|
||||
rightSection: <span className="shortcut">F11</span>,
|
||||
onClick: () => ipcRenderer?.invoke('renderer:toggle-fullscreen')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'help',
|
||||
label: 'Help',
|
||||
submenu: [
|
||||
{
|
||||
id: 'about',
|
||||
label: 'About Bruno',
|
||||
onClick: () => ipcRenderer?.invoke('renderer:open-about')
|
||||
},
|
||||
{
|
||||
id: 'documentation',
|
||||
label: 'Documentation',
|
||||
onClick: () => ipcRenderer?.invoke('renderer:open-docs')
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<MenuDropdown
|
||||
opened={isOpen}
|
||||
onChange={setIsOpen}
|
||||
placement="bottom-start"
|
||||
showTickMark={false}
|
||||
items={menuItems}
|
||||
>
|
||||
<ActionIcon label="Menu" size="lg">
|
||||
<IconMenu2 size={16} stroke={1.5} />
|
||||
</ActionIcon>
|
||||
</MenuDropdown>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppMenu;
|
||||
@@ -5,6 +5,7 @@ const Wrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: ${(props) => props.theme.sidebar.bg};
|
||||
border-bottom: 1px solid ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
-webkit-app-region: drag;
|
||||
user-select: none;
|
||||
|
||||
@@ -21,7 +22,7 @@ const Wrapper = styled.div`
|
||||
|
||||
/* When in full screen, no traffic lights so reduce padding */
|
||||
&.fullscreen .titlebar-content {
|
||||
padding-left: 6px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
/* Remove drag region from interactive elements */
|
||||
@@ -102,13 +103,6 @@ const Wrapper = styled.div`
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-shrink: 0;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
/* App action buttons container */
|
||||
.titlebar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Workspace Dropdown Styles */
|
||||
@@ -187,67 +181,16 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
/* Adjust for non-macOS platforms */
|
||||
&:not(.os-mac) .titlebar-content {
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
/* Windows-specific styles */
|
||||
&.os-windows .titlebar-content {
|
||||
padding-right: 0px;
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
&.os-windows .titlebar-left {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
&.os-linux .titlebar-content {
|
||||
padding-right: 0px;
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
&.os-linux .titlebar-left {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.app-menu {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
/* Custom window control buttons for Windows - always interactive, above modal overlay */
|
||||
.window-controls {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
height: 36px;
|
||||
margin-left: 8px;
|
||||
position: relative;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.window-control-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 46px;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: ${(props) => props.theme.text};
|
||||
cursor: pointer;
|
||||
transition: background-color 0.1s ease;
|
||||
-webkit-app-region: no-drag;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
body:not(.os-mac) & {
|
||||
.titlebar-content {
|
||||
padding-left: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
}
|
||||
|
||||
&.close:hover {
|
||||
background: #e81123;
|
||||
color: white;
|
||||
/* Leave room for Windows caption buttons when the overlay is enabled */
|
||||
body.os-windows & {
|
||||
.titlebar-content {
|
||||
padding-right: 120px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import { IconCheck, IconChevronDown, IconFolder, IconHome, IconPin, IconPinned, IconPlus, IconDownload, IconSettings, IconMinus, IconSquare, IconX, IconCopy } from '@tabler/icons';
|
||||
import { IconCheck, IconChevronDown, IconFolder, IconHome, IconLayoutColumns, IconLayoutRows, IconPin, IconPinned, IconPlus } from '@tabler/icons';
|
||||
import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { savePreferences, showHomePage, showManageWorkspacePage, toggleSidebarCollapse } from 'providers/ReduxStore/slices/app';
|
||||
import { savePreferences, showHomePage, toggleSidebarCollapse } from 'providers/ReduxStore/slices/app';
|
||||
import { closeConsole, openConsole } from 'providers/ReduxStore/slices/logs';
|
||||
import { openWorkspaceDialog, switchWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
|
||||
import { sortWorkspaces, toggleWorkspacePin } from 'utils/workspaces';
|
||||
@@ -14,36 +14,14 @@ import MenuDropdown from 'ui/MenuDropdown';
|
||||
import ActionIcon from 'ui/ActionIcon';
|
||||
import IconSidebarToggle from 'components/Icons/IconSidebarToggle';
|
||||
import CreateWorkspace from 'components/WorkspaceSidebar/CreateWorkspace';
|
||||
import ImportWorkspace from 'components/WorkspaceSidebar/ImportWorkspace';
|
||||
|
||||
import IconBottombarToggle from 'components/Icons/IconBottombarToggle/index';
|
||||
import AppMenu from './AppMenu';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import ResponseLayoutToggle from 'components/ResponsePane/ResponseLayoutToggle';
|
||||
import { isMacOS, isWindowsOS, isLinuxOS } from 'utils/common/platform';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const getOsClass = () => {
|
||||
if (isMacOS()) return 'os-mac';
|
||||
if (isWindowsOS()) return 'os-windows';
|
||||
if (isLinuxOS()) return 'os-linux';
|
||||
return 'os-other';
|
||||
};
|
||||
|
||||
// Helper to get display name for workspace
|
||||
export const getWorkspaceDisplayName = (name) => {
|
||||
if (!name) return 'Untitled Workspace';
|
||||
return name;
|
||||
};
|
||||
import { toTitleCase } from 'utils/common/index';
|
||||
|
||||
const AppTitleBar = () => {
|
||||
const dispatch = useDispatch();
|
||||
const [isFullScreen, setIsFullScreen] = useState(false);
|
||||
const [isMaximized, setIsMaximized] = useState(false);
|
||||
const osClass = getOsClass();
|
||||
const isWindows = osClass === 'os-windows';
|
||||
const isLinux = osClass === 'os-linux';
|
||||
const showWindowControls = isWindows || isLinux;
|
||||
|
||||
// Listen for fullscreen changes
|
||||
useEffect(() => {
|
||||
@@ -64,46 +42,6 @@ const AppTitleBar = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showWindowControls) return;
|
||||
const { ipcRenderer } = window;
|
||||
if (!ipcRenderer) return;
|
||||
|
||||
ipcRenderer.invoke('renderer:window-is-maximized')
|
||||
.then((maximized) => {
|
||||
setIsMaximized(maximized);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error getting initial maximized state:', error);
|
||||
});
|
||||
|
||||
const removeMaximizedListener = ipcRenderer.on('main:window-maximized', () => {
|
||||
setIsMaximized(true);
|
||||
});
|
||||
|
||||
const removeUnmaximizedListener = ipcRenderer.on('main:window-unmaximized', () => {
|
||||
setIsMaximized(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
removeMaximizedListener();
|
||||
removeUnmaximizedListener();
|
||||
};
|
||||
}, [showWindowControls]);
|
||||
|
||||
const handleMinimize = useCallback(() => {
|
||||
window.ipcRenderer?.send('renderer:window-minimize');
|
||||
}, []);
|
||||
|
||||
const handleMaximize = useCallback(() => {
|
||||
window.ipcRenderer?.send('renderer:window-maximize');
|
||||
// State will be updated via IPC events from main process (main:window-maximized/main:window-unmaximized)
|
||||
}, []);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
window.ipcRenderer?.send('renderer:window-close');
|
||||
}, []);
|
||||
|
||||
// Get workspace info
|
||||
const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces);
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
@@ -117,12 +55,11 @@ const AppTitleBar = () => {
|
||||
}, [workspaces, preferences]);
|
||||
|
||||
const [createWorkspaceModalOpen, setCreateWorkspaceModalOpen] = useState(false);
|
||||
const [importWorkspaceModalOpen, setImportWorkspaceModalOpen] = useState(false);
|
||||
|
||||
const WorkspaceName = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="workspace-name-container" {...props}>
|
||||
<span data-testid="workspace-name" className={classNames('workspace-name', { 'italic text-muted': !activeWorkspace?.name })}>{getWorkspaceDisplayName(activeWorkspace?.name)}</span>
|
||||
<span className="workspace-name">{toTitleCase(activeWorkspace?.name) || 'Default Workspace'}</span>
|
||||
<IconChevronDown size={14} stroke={1.5} className="chevron-icon" />
|
||||
</div>
|
||||
);
|
||||
@@ -134,7 +71,7 @@ const AppTitleBar = () => {
|
||||
|
||||
const handleWorkspaceSwitch = (workspaceUid) => {
|
||||
dispatch(switchWorkspace(workspaceUid));
|
||||
toast.success(`Switched to ${getWorkspaceDisplayName(workspaces.find((w) => w.uid === workspaceUid)?.name)}`);
|
||||
toast.success(`Switched to ${workspaces.find((w) => w.uid === workspaceUid)?.name}`);
|
||||
};
|
||||
|
||||
const handleOpenWorkspace = async () => {
|
||||
@@ -150,14 +87,6 @@ const AppTitleBar = () => {
|
||||
setCreateWorkspaceModalOpen(true);
|
||||
};
|
||||
|
||||
const handleManageWorkspaces = () => {
|
||||
dispatch(showManageWorkspacePage());
|
||||
};
|
||||
|
||||
const handleImportWorkspace = () => {
|
||||
setImportWorkspaceModalOpen(true);
|
||||
};
|
||||
|
||||
const handlePinWorkspace = useCallback((workspaceUid, e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -165,6 +94,8 @@ const AppTitleBar = () => {
|
||||
dispatch(savePreferences(newPreferences));
|
||||
}, [dispatch, preferences]);
|
||||
|
||||
const orientation = preferences?.layout?.responsePaneOrientation || 'horizontal';
|
||||
|
||||
const handleToggleSidebar = () => {
|
||||
dispatch(toggleSidebarCollapse());
|
||||
};
|
||||
@@ -177,6 +108,18 @@ const AppTitleBar = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleVerticalLayout = () => {
|
||||
const newOrientation = orientation === 'horizontal' ? 'vertical' : 'horizontal';
|
||||
const updatedPreferences = {
|
||||
...preferences,
|
||||
layout: {
|
||||
...preferences?.layout || {},
|
||||
responsePaneOrientation: newOrientation
|
||||
}
|
||||
};
|
||||
dispatch(savePreferences(updatedPreferences));
|
||||
};
|
||||
|
||||
// Build workspace menu items
|
||||
const workspaceMenuItems = useMemo(() => {
|
||||
const items = sortedWorkspaces.map((workspace) => {
|
||||
@@ -185,7 +128,7 @@ const AppTitleBar = () => {
|
||||
|
||||
return {
|
||||
id: workspace.uid,
|
||||
label: getWorkspaceDisplayName(workspace.name),
|
||||
label: toTitleCase(workspace.name),
|
||||
onClick: () => handleWorkspaceSwitch(workspace.uid),
|
||||
className: `workspace-item ${isActive ? 'active' : ''}`,
|
||||
rightSection: (
|
||||
@@ -197,7 +140,11 @@ const AppTitleBar = () => {
|
||||
label={isPinned ? 'Unpin workspace' : 'Pin workspace'}
|
||||
size="sm"
|
||||
>
|
||||
{isPinned ? <IconPinned size={14} stroke={1.5} /> : <IconPin size={14} stroke={1.5} />}
|
||||
{isPinned ? (
|
||||
<IconPinned size={14} stroke={1.5} />
|
||||
) : (
|
||||
<IconPin size={14} stroke={1.5} />
|
||||
)}
|
||||
</ActionIcon>
|
||||
)}
|
||||
{isActive && <IconCheck size={16} stroke={1.5} className="check-icon" />}
|
||||
@@ -220,18 +167,6 @@ const AppTitleBar = () => {
|
||||
leftSection: IconFolder,
|
||||
label: 'Open workspace',
|
||||
onClick: handleOpenWorkspace
|
||||
},
|
||||
{
|
||||
id: 'import-workspace',
|
||||
leftSection: IconDownload,
|
||||
label: 'Import workspace',
|
||||
onClick: handleImportWorkspace
|
||||
},
|
||||
{
|
||||
id: 'manage-workspaces',
|
||||
leftSection: IconSettings,
|
||||
label: 'Manage workspaces',
|
||||
onClick: handleManageWorkspaces
|
||||
}
|
||||
);
|
||||
|
||||
@@ -239,19 +174,20 @@ const AppTitleBar = () => {
|
||||
}, [sortedWorkspaces, activeWorkspaceUid, preferences, handlePinWorkspace]);
|
||||
|
||||
return (
|
||||
<StyledWrapper className={`app-titlebar ${osClass} ${isFullScreen ? 'fullscreen' : ''}`}>
|
||||
<StyledWrapper className={`app-titlebar ${isFullScreen ? 'fullscreen' : ''}`}>
|
||||
{createWorkspaceModalOpen && (
|
||||
<CreateWorkspace onClose={() => setCreateWorkspaceModalOpen(false)} />
|
||||
)}
|
||||
{importWorkspaceModalOpen && (
|
||||
<ImportWorkspace onClose={() => setImportWorkspaceModalOpen(false)} />
|
||||
)}
|
||||
|
||||
<div className="titlebar-content">
|
||||
{/* Left section: Home + Workspace */}
|
||||
<div className="titlebar-left">
|
||||
{showWindowControls && <AppMenu />}
|
||||
|
||||
<ActionIcon onClick={handleHomeClick} label="Home" size="lg" className="home-button">
|
||||
<ActionIcon
|
||||
onClick={handleHomeClick}
|
||||
label="Home"
|
||||
size="lg"
|
||||
className="home-button"
|
||||
>
|
||||
<IconHome size={16} stroke={1.5} />
|
||||
</ActionIcon>
|
||||
|
||||
@@ -274,55 +210,39 @@ const AppTitleBar = () => {
|
||||
|
||||
{/* Right section: Action buttons */}
|
||||
<div className="titlebar-right">
|
||||
<div className="titlebar-actions">
|
||||
{/* Toggle sidebar */}
|
||||
<ActionIcon
|
||||
onClick={handleToggleSidebar}
|
||||
label={sidebarCollapsed ? 'Show sidebar' : 'Hide sidebar'}
|
||||
size="lg"
|
||||
data-testid="toggle-sidebar-button"
|
||||
>
|
||||
<IconSidebarToggle collapsed={sidebarCollapsed} size={16} strokeWidth={1.5} />
|
||||
</ActionIcon>
|
||||
{/* Toggle sidebar */}
|
||||
<ActionIcon
|
||||
onClick={handleToggleSidebar}
|
||||
label={sidebarCollapsed ? 'Show sidebar' : 'Hide sidebar'}
|
||||
size="lg"
|
||||
data-testid="toggle-sidebar-button"
|
||||
>
|
||||
<IconSidebarToggle collapsed={sidebarCollapsed} size={16} strokeWidth={1.5} />
|
||||
</ActionIcon>
|
||||
|
||||
{/* Toggle devtools */}
|
||||
<ActionIcon
|
||||
onClick={handleToggleDevtools}
|
||||
label={isConsoleOpen ? 'Hide devtools' : 'Show devtools'}
|
||||
size="lg"
|
||||
data-testid="toggle-devtools-button"
|
||||
>
|
||||
<IconBottombarToggle collapsed={!isConsoleOpen} size={16} strokeWidth={1.5} />
|
||||
</ActionIcon>
|
||||
{/* Toggle devtools */}
|
||||
<ActionIcon
|
||||
onClick={handleToggleDevtools}
|
||||
label={isConsoleOpen ? 'Hide devtools' : 'Show devtools'}
|
||||
size="lg"
|
||||
data-testid="toggle-devtools-button"
|
||||
>
|
||||
<IconBottombarToggle collapsed={!isConsoleOpen} size={16} strokeWidth={1.5} />
|
||||
</ActionIcon>
|
||||
|
||||
<ResponseLayoutToggle />
|
||||
</div>
|
||||
|
||||
{showWindowControls && (
|
||||
<div className="window-controls">
|
||||
<button
|
||||
className="window-control-btn minimize"
|
||||
onClick={handleMinimize}
|
||||
aria-label="Minimize"
|
||||
>
|
||||
<IconMinus size={16} stroke={1} />
|
||||
</button>
|
||||
<button
|
||||
className="window-control-btn maximize"
|
||||
onClick={handleMaximize}
|
||||
aria-label={isMaximized ? 'Restore' : 'Maximize'}
|
||||
>
|
||||
{isMaximized ? <IconCopy size={14} stroke={1} /> : <IconSquare size={14} stroke={1} />}
|
||||
</button>
|
||||
<button
|
||||
className="window-control-btn close"
|
||||
onClick={handleClose}
|
||||
aria-label="Close"
|
||||
>
|
||||
<IconX size={16} stroke={1} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{/* Toggle vertical layout */}
|
||||
<ActionIcon
|
||||
onClick={handleToggleVerticalLayout}
|
||||
label={orientation === 'horizontal' ? 'Switch to vertical layout' : 'Switch to horizontal layout'}
|
||||
size="lg"
|
||||
data-testid="toggle-vertical-layout-button"
|
||||
>
|
||||
{orientation === 'horizontal' ? (
|
||||
<IconLayoutColumns size={16} stroke={1.5} />
|
||||
) : (
|
||||
<IconLayoutRows size={16} stroke={1.5} />
|
||||
)}
|
||||
</ActionIcon>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
|
||||
.body-mode-selector {
|
||||
background: transparent;
|
||||
border-radius: 3px;
|
||||
|
||||
.dropdown-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
padding-left: 1.5rem !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.label-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
|
||||
.selected-body-mode {
|
||||
color: ${(props) => props.theme.primary.text};
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.caret {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
fill: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,33 +1,17 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { IconCaretDown, IconForms, IconBraces, IconCode, IconFileText, IconDatabase, IconFile, IconX } from '@tabler/icons';
|
||||
import MenuDropdown from 'ui/MenuDropdown';
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import { IconCaretDown } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { humanizeRequestBodyMode } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const DEFAULT_MODES = [
|
||||
{
|
||||
name: 'Form',
|
||||
options: [
|
||||
{ id: 'multipartForm', label: 'Multipart Form', leftSection: IconForms },
|
||||
{ id: 'formUrlEncoded', label: 'Form URL Encoded', leftSection: IconForms }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Raw',
|
||||
options: [
|
||||
{ id: 'json', label: 'JSON', leftSection: IconBraces },
|
||||
{ id: 'xml', label: 'XML', leftSection: IconCode },
|
||||
{ id: 'text', label: 'TEXT', leftSection: IconFileText },
|
||||
{ id: 'sparql', label: 'SPARQL', leftSection: IconDatabase }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Other',
|
||||
options: [
|
||||
{ id: 'file', label: 'File / Binary', leftSection: IconFile },
|
||||
{ id: 'none', label: 'No Body', leftSection: IconX }
|
||||
]
|
||||
}
|
||||
{ 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 = ({
|
||||
@@ -37,39 +21,62 @@ const BodyModeSelector = ({
|
||||
disabled = false,
|
||||
className = '',
|
||||
wrapperClassName = '',
|
||||
showCategories = true,
|
||||
placement = 'bottom-end'
|
||||
}) => {
|
||||
// Add onClick handlers to mode options
|
||||
const menuItems = useMemo(() => {
|
||||
return modes.map((group) => ({
|
||||
...group,
|
||||
options: group.options.map((option) => ({
|
||||
...option,
|
||||
onClick: () => onModeChange(option.id)
|
||||
}))
|
||||
}));
|
||||
}, [modes, onModeChange]);
|
||||
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 (
|
||||
<StyledWrapper className={wrapperClassName}>
|
||||
<div className={`inline-flex items-center body-mode-selector ${disabled ? 'cursor-default' : 'cursor-pointer'}`}>
|
||||
<MenuDropdown
|
||||
items={menuItems}
|
||||
placement={placement}
|
||||
disabled={disabled}
|
||||
className={className}
|
||||
selectedItemId={currentMode}
|
||||
showGroupDividers={false}
|
||||
groupStyle="select"
|
||||
>
|
||||
<div 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>
|
||||
</MenuDropdown>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useSelector } from 'react-redux';
|
||||
@@ -22,8 +21,7 @@ const BulkEditor = ({ params, onChange, onToggle, onSave, onRun }) => {
|
||||
<CodeEditor
|
||||
mode="text/plain"
|
||||
theme={displayedTheme}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
font={preferences.codeFont || 'default'}
|
||||
value={parsedParams}
|
||||
onEdit={handleEdit}
|
||||
onSave={onSave}
|
||||
|
||||
@@ -36,12 +36,12 @@ const StyledWrapper = styled.div`
|
||||
|
||||
/* Style line numbers when there's a lint issue */
|
||||
.CodeMirror-lint-line-error .CodeMirror-linenumber {
|
||||
color: ${(props) => props.theme.colors.text.danger} !important;
|
||||
color: #d32f2f !important;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-line-warning .CodeMirror-linenumber {
|
||||
color: ${(props) => props.theme.colors.text.warning} !important;
|
||||
color: #f57c00 !important;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ const StyledWrapper = styled.div`
|
||||
span.cm-atom {
|
||||
color: ${(props) => props.theme.codemirror.tokens.atom} !important;
|
||||
}
|
||||
span.cm-variable, span.cm-variable-2 {
|
||||
span.cm-variable {
|
||||
color: ${(props) => props.theme.codemirror.tokens.variable} !important;
|
||||
}
|
||||
span.cm-keyword {
|
||||
@@ -128,20 +128,14 @@ const StyledWrapper = styled.div`
|
||||
span.cm-operator {
|
||||
color: ${(props) => props.theme.codemirror.tokens.operator} !important;
|
||||
}
|
||||
span.cm-tag {
|
||||
color: ${(props) => props.theme.codemirror.tokens.tag} !important;
|
||||
}
|
||||
span.cm-tag.cm-bracket {
|
||||
color: ${(props) => props.theme.codemirror.tokens.tagBracket} !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Variable validation colors */
|
||||
.cm-variable-valid {
|
||||
color: ${(props) => props.theme.codemirror.variable.valid} !important;
|
||||
color: #5fad89 !important; /* Soft sage */
|
||||
}
|
||||
.cm-variable-invalid {
|
||||
color: ${(props) => props.theme.codemirror.variable.invalid} !important;
|
||||
color: #d17b7b !important; /* Soft coral */
|
||||
}
|
||||
|
||||
.CodeMirror-search-hint {
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, { createRef } from 'react';
|
||||
import React from 'react';
|
||||
import { isEqual, escapeRegExp } from 'lodash';
|
||||
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
|
||||
import { setupAutoComplete, showRootHints } from 'utils/codemirror/autocomplete';
|
||||
import { setupAutoComplete } from 'utils/codemirror/autocomplete';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import * as jsonlint from '@prantlf/jsonlint';
|
||||
import { JSHINT } from 'jshint';
|
||||
@@ -16,7 +16,7 @@ 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/index';
|
||||
import CodeMirrorSearch from 'components/CodeMirrorSearch';
|
||||
|
||||
const CodeMirror = require('codemirror');
|
||||
window.jsonlint = jsonlint;
|
||||
@@ -34,7 +34,6 @@ export default class CodeEditor extends React.Component {
|
||||
this.cachedValue = props.value || '';
|
||||
this.variables = {};
|
||||
this.searchResultsCountElementId = 'search-results-count';
|
||||
this.searchBarRef = createRef();
|
||||
|
||||
this.lintOptions = {
|
||||
esversion: 11,
|
||||
@@ -95,14 +94,14 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
},
|
||||
'Cmd-F': (cm) => {
|
||||
this.setState({ searchBarVisible: true }, () => {
|
||||
this.searchBarRef.current?.focus();
|
||||
});
|
||||
if (!this.state.searchBarVisible) {
|
||||
this.setState({ searchBarVisible: true });
|
||||
}
|
||||
},
|
||||
'Ctrl-F': (cm) => {
|
||||
this.setState({ searchBarVisible: true }, () => {
|
||||
this.searchBarRef.current?.focus();
|
||||
});
|
||||
if (!this.state.searchBarVisible) {
|
||||
this.setState({ searchBarVisible: true });
|
||||
}
|
||||
},
|
||||
'Cmd-H': 'replace',
|
||||
'Ctrl-H': 'replace',
|
||||
@@ -112,12 +111,8 @@ export default class CodeEditor extends React.Component {
|
||||
: cm.replaceSelection(' ', 'end');
|
||||
},
|
||||
'Shift-Tab': 'indentLess',
|
||||
'Ctrl-Space': (cm) => {
|
||||
showRootHints(cm, this.props.showHintsFor);
|
||||
},
|
||||
'Cmd-Space': (cm) => {
|
||||
showRootHints(cm, this.props.showHintsFor);
|
||||
},
|
||||
'Ctrl-Space': 'autocomplete',
|
||||
'Cmd-Space': 'autocomplete',
|
||||
'Ctrl-Y': 'foldAll',
|
||||
'Cmd-Y': 'foldAll',
|
||||
'Ctrl-I': 'unfoldAll',
|
||||
@@ -197,6 +192,7 @@ export default class CodeEditor extends React.Component {
|
||||
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();
|
||||
|
||||
@@ -233,17 +229,8 @@ export default class CodeEditor extends React.Component {
|
||||
CodeMirror.signal(this.editor, 'change', this.editor);
|
||||
}
|
||||
if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) {
|
||||
// TODO: temporary fix for keeping cursor state when auto save and new line insertion collide PR#7098
|
||||
const nextValue = this.props.value ?? '';
|
||||
const currentValue = this.editor.getValue();
|
||||
if (this.editor.hasFocus?.() && currentValue !== nextValue) {
|
||||
this.cachedValue = currentValue;
|
||||
} else {
|
||||
const cursor = this.editor.getCursor();
|
||||
this.cachedValue = nextValue;
|
||||
this.editor.setValue(nextValue);
|
||||
this.editor.setCursor(cursor);
|
||||
}
|
||||
this.cachedValue = this.props.value;
|
||||
this.editor.setValue(this.props.value);
|
||||
}
|
||||
|
||||
if (this.editor) {
|
||||
@@ -288,19 +275,13 @@ export default class CodeEditor extends React.Component {
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.editor) {
|
||||
if (this.props.onScroll) {
|
||||
this.props.onScroll(this.editor);
|
||||
}
|
||||
|
||||
this.editor?._destroyLinkAware?.();
|
||||
this.editor.off('change', this._onEdit);
|
||||
this.editor.off('scroll', this.onScroll);
|
||||
|
||||
// Clean up lint error tooltip
|
||||
this.cleanupLintErrorTooltip?.();
|
||||
|
||||
const wrapper = this.editor.getWrapperElement();
|
||||
wrapper?.parentNode?.removeChild(wrapper);
|
||||
|
||||
this.editor = null;
|
||||
}
|
||||
}
|
||||
@@ -317,10 +298,6 @@ export default class CodeEditor extends React.Component {
|
||||
fontSize={this.props.fontSize}
|
||||
>
|
||||
<CodeMirrorSearch
|
||||
ref={(node) => {
|
||||
if (!node) return;
|
||||
this.searchBarRef.current = node;
|
||||
}}
|
||||
visible={this.state.searchBarVisible}
|
||||
editor={this.editor}
|
||||
onClose={() => this.setState({ searchBarVisible: false })}
|
||||
@@ -348,6 +325,8 @@ export default class CodeEditor extends React.Component {
|
||||
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);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import styled from 'styled-components';
|
||||
import { rgba } from 'polished';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.bruno-search-bar {
|
||||
@@ -10,15 +9,15 @@ const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
gap: 0;
|
||||
padding: 1px 3px;
|
||||
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;
|
||||
min-height: 22px;
|
||||
background: ${(props) => props.theme.background.base};
|
||||
color: ${(props) => props.theme.text.base};
|
||||
border: solid 1px ${(props) => props.theme.border.border2};
|
||||
border-radius: ${(props) => props.theme.border.radius.sm};
|
||||
}
|
||||
|
||||
.bruno-search-bar input {
|
||||
@@ -39,7 +38,7 @@ const StyledWrapper = styled.div`
|
||||
padding: 0 1px;
|
||||
margin: 0 1px;
|
||||
cursor: pointer;
|
||||
color: ${(props) => props.theme.colors.text.subtext1};
|
||||
color: #aaa;
|
||||
border-radius: 3px;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
@@ -52,14 +51,27 @@ const StyledWrapper = styled.div`
|
||||
min-width: 28px;
|
||||
text-align: center;
|
||||
font-size: ${(props) => props.theme.font.size.xs};
|
||||
color: ${(props) => props.theme.colors.text.subtext1};
|
||||
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: ${(props) => props.theme.colors.text.subtext2};
|
||||
color: inherit;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
@@ -80,9 +92,7 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
.searchbar-icon-btn.active {
|
||||
color: ${(props) => props.theme.brand};
|
||||
background-color: ${(props) => rgba(props.theme.brand, 0.1)};
|
||||
font-weight: 500;
|
||||
color: #f39c12 !important;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useRef, useCallback, useMemo, forwardRef, useImperativeHandle } from 'react';
|
||||
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';
|
||||
@@ -8,7 +8,7 @@ function escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');
|
||||
}
|
||||
|
||||
const CodeMirrorSearch = forwardRef(({ visible, editor, onClose }, ref) => {
|
||||
const CodeMirrorSearch = ({ visible, editor, onClose }) => {
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [regex, setRegex] = useState(false);
|
||||
const [caseSensitive, setCaseSensitive] = useState(false);
|
||||
@@ -19,7 +19,6 @@ const CodeMirrorSearch = forwardRef(({ visible, editor, onClose }, ref) => {
|
||||
const searchMarks = useRef([]);
|
||||
const searchLineHighlight = useRef(null);
|
||||
const searchMatches = useRef([]);
|
||||
const inputRef = useRef(null);
|
||||
|
||||
const debouncedSearchText = useDebounce(searchText, 150);
|
||||
|
||||
@@ -107,14 +106,6 @@ const CodeMirrorSearch = forwardRef(({ visible, editor, onClose }, ref) => {
|
||||
}
|
||||
}, [debouncedSearchText, regex, caseSensitive, wholeWord, editor, memoizedMatches]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
doSearch(0, debouncedSearchText);
|
||||
}, [debouncedSearchText, doSearch]);
|
||||
@@ -175,9 +166,8 @@ const CodeMirrorSearch = forwardRef(({ visible, editor, onClose }, ref) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="bruno-search-bar">
|
||||
<div className="bruno-search-bar compact">
|
||||
<input
|
||||
ref={inputRef}
|
||||
autoFocus
|
||||
type="text"
|
||||
value={searchText}
|
||||
@@ -206,6 +196,6 @@ const CodeMirrorSearch = forwardRef(({ visible, editor, onClose }, ref) => {
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export default CodeMirrorSearch;
|
||||
|
||||
@@ -2,8 +2,7 @@ import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.colors.text.subtext1};
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
}
|
||||
|
||||
.single-line-editor-wrapper {
|
||||
@@ -14,8 +13,7 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
.auth-placement-selector {
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
padding: 0.2rem 0px;
|
||||
padding: 0.5rem 0px;
|
||||
border-radius: 3px;
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
@@ -39,6 +37,7 @@ const Wrapper = styled.div`
|
||||
|
||||
.auth-type-label {
|
||||
width: fit-content;
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
justify-content: space-between;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
@@ -57,31 +57,29 @@ const ApiKeyAuth = ({ collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<label className="block mb-1">Key</label>
|
||||
<div className="single-line-editor-wrapper mb-3">
|
||||
<label className="block font-medium mb-2">Key</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<SingleLineEditor
|
||||
value={apikeyAuth.key || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleAuthChange('key', val)}
|
||||
collection={collection}
|
||||
isCompact
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block mb-1">Value</label>
|
||||
<div className="single-line-editor-wrapper mb-3">
|
||||
<label className="block font-medium mb-2">Value</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<SingleLineEditor
|
||||
value={apikeyAuth.value || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleAuthChange('value', val)}
|
||||
collection={collection}
|
||||
isCompact
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block mb-1">Add To</label>
|
||||
<label className="block font-medium mb-2">Add To</label>
|
||||
<div className="inline-flex items-center cursor-pointer auth-placement-selector w-fit">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
|
||||
<div
|
||||
|
||||
@@ -7,13 +7,21 @@ const Wrapper = styled.div`
|
||||
background: transparent;
|
||||
|
||||
.auth-mode-label {
|
||||
color: ${(props) => props.theme.primary.text};
|
||||
|
||||
.caret {
|
||||
color: rgb(140, 140, 140);
|
||||
fill: rgb(140, 140, 140);
|
||||
}
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
|
||||
.label-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.caret {
|
||||
color: rgb(140, 140, 140);
|
||||
fill: rgb(140 140 140);
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { IconCaretDown } from '@tabler/icons';
|
||||
import MenuDropdown from 'ui/MenuDropdown';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { updateCollectionAuthMode } from 'providers/ReduxStore/slices/collections';
|
||||
import { humanizeRequestAuthMode } from 'utils/collections';
|
||||
@@ -9,77 +9,113 @@ import StyledWrapper from './StyledWrapper';
|
||||
|
||||
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 onModeChange = useCallback((value) => {
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-center auth-mode-label select-none">
|
||||
{humanizeRequestAuthMode(authMode)} <IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const onModeChange = (value) => {
|
||||
dispatch(
|
||||
updateCollectionAuthMode({
|
||||
collectionUid: collection.uid,
|
||||
mode: value
|
||||
})
|
||||
);
|
||||
}, [dispatch, collection.uid]);
|
||||
|
||||
const menuItems = useMemo(() => [
|
||||
{
|
||||
id: 'awsv4',
|
||||
label: 'AWS Sig v4',
|
||||
onClick: () => onModeChange('awsv4')
|
||||
},
|
||||
{
|
||||
id: 'basic',
|
||||
label: 'Basic Auth',
|
||||
onClick: () => onModeChange('basic')
|
||||
},
|
||||
{
|
||||
id: 'wsse',
|
||||
label: 'WSSE Auth',
|
||||
onClick: () => onModeChange('wsse')
|
||||
},
|
||||
{
|
||||
id: 'bearer',
|
||||
label: 'Bearer Token',
|
||||
onClick: () => onModeChange('bearer')
|
||||
},
|
||||
{
|
||||
id: 'digest',
|
||||
label: 'Digest Auth',
|
||||
onClick: () => onModeChange('digest')
|
||||
},
|
||||
{
|
||||
id: 'ntlm',
|
||||
label: 'NTLM Auth',
|
||||
onClick: () => onModeChange('ntlm')
|
||||
},
|
||||
{
|
||||
id: 'oauth2',
|
||||
label: 'OAuth 2.0',
|
||||
onClick: () => onModeChange('oauth2')
|
||||
},
|
||||
{
|
||||
id: 'apikey',
|
||||
label: 'API Key',
|
||||
onClick: () => onModeChange('apikey')
|
||||
},
|
||||
{
|
||||
id: 'none',
|
||||
label: 'No Auth',
|
||||
onClick: () => onModeChange('none')
|
||||
}
|
||||
], [onModeChange]);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="inline-flex items-center cursor-pointer auth-mode-selector">
|
||||
<MenuDropdown
|
||||
items={menuItems}
|
||||
placement="bottom-end"
|
||||
selectedItemId={authMode}
|
||||
>
|
||||
<div className="flex items-center justify-center auth-mode-label select-none">
|
||||
{humanizeRequestAuthMode(authMode)} <IconCaretDown className="caret ml-1" size={14} strokeWidth={2} />
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('awsv4');
|
||||
}}
|
||||
>
|
||||
AWS Sig v4
|
||||
</div>
|
||||
</MenuDropdown>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('basic');
|
||||
}}
|
||||
>
|
||||
Basic Auth
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('wsse');
|
||||
}}
|
||||
>
|
||||
WSSE Auth
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('bearer');
|
||||
}}
|
||||
>
|
||||
Bearer Token
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('digest');
|
||||
}}
|
||||
>
|
||||
Digest Auth
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('ntlm');
|
||||
}}
|
||||
>
|
||||
NTLM Auth
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('oauth2');
|
||||
}}
|
||||
>
|
||||
OAuth 2.0
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('apikey');
|
||||
}}
|
||||
>
|
||||
API Key
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('none');
|
||||
}}
|
||||
>
|
||||
No Auth
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -2,8 +2,7 @@ import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.colors.text.subtext1};
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
}
|
||||
|
||||
.single-line-editor-wrapper {
|
||||
|
||||
@@ -123,20 +123,19 @@ const AwsV4Auth = ({ collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<label className="block mb-1">Access Key ID</label>
|
||||
<div className="single-line-editor-wrapper mb-3">
|
||||
<label className="block font-medium mb-2">Access Key ID</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.accessKeyId || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleAccessKeyIdChange(val)}
|
||||
collection={collection}
|
||||
isCompact
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block mb-1">Secret Access Key</label>
|
||||
<div className="single-line-editor-wrapper mb-3 flex items-center">
|
||||
<label className="block font-medium mb-2">Secret Access Key</label>
|
||||
<div className="single-line-editor-wrapper mb-2 flex items-center">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.secretAccessKey || ''}
|
||||
theme={storedTheme}
|
||||
@@ -144,56 +143,51 @@ const AwsV4Auth = ({ collection }) => {
|
||||
onChange={(val) => handleSecretAccessKeyChange(val)}
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
isCompact
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="awsv4-secret-access-key" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
|
||||
<label className="block mb-1">Session Token</label>
|
||||
<div className="single-line-editor-wrapper mb-3">
|
||||
<label className="block font-medium mb-2">Session Token</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.sessionToken || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleSessionTokenChange(val)}
|
||||
collection={collection}
|
||||
isCompact
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block mb-1">Service</label>
|
||||
<div className="single-line-editor-wrapper mb-3">
|
||||
<label className="block font-medium mb-2">Service</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.service || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleServiceChange(val)}
|
||||
collection={collection}
|
||||
isCompact
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block mb-1">Region</label>
|
||||
<div className="single-line-editor-wrapper mb-3">
|
||||
<label className="block font-medium mb-2">Region</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.region || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleRegionChange(val)}
|
||||
collection={collection}
|
||||
isCompact
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block mb-1">Profile Name</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<label className="block font-medium mb-2">Profile Name</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.profileName || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleProfileNameChange(val)}
|
||||
collection={collection}
|
||||
isCompact
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -2,8 +2,7 @@ import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.colors.text.subtext1};
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
}
|
||||
|
||||
.single-line-editor-wrapper {
|
||||
|
||||
@@ -47,19 +47,18 @@ const BasicAuth = ({ collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<label className="block mb-1">Username</label>
|
||||
<div className="single-line-editor-wrapper mb-3">
|
||||
<label className="block font-medium mb-2">Username</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<SingleLineEditor
|
||||
value={basicAuth.username || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleUsernameChange(val)}
|
||||
collection={collection}
|
||||
isCompact
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block mb-1">Password</label>
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={basicAuth.password || ''}
|
||||
@@ -68,7 +67,6 @@ const BasicAuth = ({ collection }) => {
|
||||
onChange={(val) => handlePasswordChange(val)}
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
isCompact
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="basic-password" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
|
||||
@@ -2,8 +2,7 @@ import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.colors.text.subtext1};
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
}
|
||||
|
||||
.single-line-editor-wrapper {
|
||||
|
||||
@@ -33,7 +33,7 @@ const BearerAuth = ({ collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<label className="block mb-1">Token</label>
|
||||
<label className="block font-medium mb-2">Token</label>
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={bearerToken}
|
||||
@@ -42,7 +42,6 @@ const BearerAuth = ({ collection }) => {
|
||||
onChange={(val) => handleTokenChange(val)}
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
isCompact
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="bearer-token" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
|
||||
@@ -2,8 +2,7 @@ import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.colors.text.subtext1};
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
}
|
||||
|
||||
.single-line-editor-wrapper {
|
||||
|
||||
@@ -47,19 +47,18 @@ const DigestAuth = ({ collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<label className="block mb-1">Username</label>
|
||||
<div className="single-line-editor-wrapper mb-3">
|
||||
<label className="block font-medium mb-2">Username</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<SingleLineEditor
|
||||
value={digestAuth.username || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleUsernameChange(val)}
|
||||
collection={collection}
|
||||
isCompact
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block mb-1">Password</label>
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={digestAuth.password || ''}
|
||||
@@ -68,7 +67,6 @@ const DigestAuth = ({ collection }) => {
|
||||
onChange={(val) => handlePasswordChange(val)}
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
isCompact
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="digest-password" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
|
||||
@@ -2,8 +2,7 @@ import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.colors.text.subtext1};
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
}
|
||||
|
||||
.single-line-editor-wrapper {
|
||||
|
||||
@@ -64,20 +64,19 @@ const NTLMAuth = ({ collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<label className="block mb-1">Username</label>
|
||||
<div className="single-line-editor-wrapper mb-3">
|
||||
<label className="block font-medium mb-2">Username</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<SingleLineEditor
|
||||
value={ntlmAuth.username || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleUsernameChange(val)}
|
||||
collection={collection}
|
||||
isCompact
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block mb-1">Password</label>
|
||||
<div className="single-line-editor-wrapper mb-3 flex items-center">
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={ntlmAuth.password || ''}
|
||||
theme={storedTheme}
|
||||
@@ -85,12 +84,11 @@ const NTLMAuth = ({ collection }) => {
|
||||
onChange={(val) => handlePasswordChange(val)}
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
isCompact
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="ntlm-password" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
|
||||
<label className="block mb-1">Domain</label>
|
||||
<label className="block font-medium mb-2">Domain</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={ntlmAuth.domain || ''}
|
||||
@@ -98,7 +96,6 @@ const NTLMAuth = ({ collection }) => {
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleDomainChange(val)}
|
||||
collection={collection}
|
||||
isCompact
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -2,8 +2,7 @@ import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.colors.text.subtext1};
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
}
|
||||
|
||||
.single-line-editor-wrapper {
|
||||
|
||||
@@ -47,19 +47,18 @@ const WsseAuth = ({ collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<label className="block mb-1">Username</label>
|
||||
<div className="single-line-editor-wrapper mb-3">
|
||||
<label className="block font-medium mb-2">Username</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<SingleLineEditor
|
||||
value={wsseAuth.username || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleUserChange(val)}
|
||||
collection={collection}
|
||||
isCompact
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block mb-1">Password</label>
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={wsseAuth.password || ''}
|
||||
@@ -68,7 +67,6 @@ const WsseAuth = ({ collection }) => {
|
||||
onChange={(val) => handlePasswordChange(val)}
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
isCompact
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="wsse-password" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,6 @@ import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import OAuth2 from './OAuth2';
|
||||
import NTLMAuth from './NTLMAuth';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const Auth = ({ collection }) => {
|
||||
const authMode = collection.draft?.root ? get(collection, 'draft.root.request.auth.mode') : get(collection, 'root.request.auth.mode');
|
||||
@@ -60,9 +59,9 @@ const Auth = ({ collection }) => {
|
||||
</div>
|
||||
{getAuthView()}
|
||||
<div className="mt-6">
|
||||
<Button type="submit" size="sm" onClick={handleSave}>
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
Save
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -30,11 +30,11 @@ const StyledWrapper = styled.div`
|
||||
box-shadow: none;
|
||||
transition: border-color ease-in-out 0.1s;
|
||||
border-radius: 3px;
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
border: 1px solid ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.modal.input.bg};
|
||||
border: 1px solid ${(props) => props.theme.modal.input.border};
|
||||
|
||||
&:focus {
|
||||
border: solid 1px ${(props) => props.theme.input.focusBorder} !important;
|
||||
border: solid 1px ${(props) => props.theme.modal.input.focusBorder} !important;
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
@@ -48,37 +48,32 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
.protocol-https,
|
||||
.protocol-grpcs,
|
||||
.protocol-wss {
|
||||
.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 9s infinite;
|
||||
animation: slideUpDown 6s infinite;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.protocol-grpcs {
|
||||
animation: slideUpDown 9s infinite 3s;
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
.protocol-wss {
|
||||
animation: slideUpDown 9s infinite 6s;
|
||||
animation: slideUpDown 6s infinite 3s;
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
@keyframes slideUpDown {
|
||||
0%, 30% {
|
||||
0%, 45% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
33.33%, 97% {
|
||||
50%, 95% {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
100% {
|
||||
|
||||
@@ -13,7 +13,6 @@ 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';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const ClientCertSettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -181,7 +180,6 @@ const ClientCertSettings = ({ collection }) => {
|
||||
<span className="protocol-placeholder">
|
||||
<span className="protocol-https">https://</span>
|
||||
<span className="protocol-grpcs">grpcs://</span>
|
||||
<span className="protocol-wss">wss://</span>
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
@@ -375,13 +373,13 @@ const ClientCertSettings = ({ collection }) => {
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-6 flex flex-row gap-2 items-center">
|
||||
<Button type="submit" size="sm" data-testid="add-client-cert">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" data-testid="add-client-cert">
|
||||
Add
|
||||
</Button>
|
||||
</button>
|
||||
<div className="h-4 border-l border-gray-600"></div>
|
||||
<Button type="button" size="sm" onClick={handleSave}>
|
||||
<button type="button" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
Save
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -9,8 +9,6 @@ import Markdown from 'components/MarkDown';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { IconEdit, IconX, IconFileText } from '@tabler/icons';
|
||||
import Button from 'ui/Button/index';
|
||||
import ActionIcon from 'ui/ActionIcon/index';
|
||||
|
||||
const Docs = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -57,17 +55,17 @@ const Docs = ({ collection }) => {
|
||||
<div className="flex flex-row gap-2 items-center justify-center">
|
||||
{isEditing ? (
|
||||
<>
|
||||
<Button type="button" color="secondary" onClick={handleDiscardChanges}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="button" onClick={onSave}>
|
||||
<div className="editing-mode" role="tab" onClick={handleDiscardChanges}>
|
||||
<IconX className="cursor-pointer" size={20} strokeWidth={1.5} />
|
||||
</div>
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={onSave}>
|
||||
Save
|
||||
</Button>
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<ActionIcon className="editing-mode" onClick={toggleViewMode}>
|
||||
<IconEdit className="cursor-pointer" size={16} strokeWidth={1.5} />
|
||||
</ActionIcon>
|
||||
<div className="editing-mode" role="tab" onClick={toggleViewMode}>
|
||||
<IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,8 +10,6 @@ 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';
|
||||
import Button from 'ui/Button';
|
||||
import { headerNameRegex, headerValueRegex } from 'utils/common/regex';
|
||||
|
||||
const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
|
||||
|
||||
@@ -33,22 +31,6 @@ const Headers = ({ collection }) => {
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
const getRowError = useCallback((row, index, key) => {
|
||||
if (key === 'name') {
|
||||
if (!row.name || row.name.trim() === '') return null;
|
||||
if (!headerNameRegex.test(row.name)) {
|
||||
return 'Header name cannot contain spaces or newlines';
|
||||
}
|
||||
}
|
||||
if (key === 'value') {
|
||||
if (!row.value) return null;
|
||||
if (!headerValueRegex.test(row.value)) {
|
||||
return 'Header value cannot contain newlines';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
key: 'name',
|
||||
@@ -56,7 +38,7 @@ const Headers = ({ collection }) => {
|
||||
isKeyField: true,
|
||||
placeholder: 'Name',
|
||||
width: '30%',
|
||||
render: ({ value, onChange }) => (
|
||||
render: ({ row, value, onChange, isLastEmptyRow }) => (
|
||||
<SingleLineEditor
|
||||
value={value || ''}
|
||||
theme={storedTheme}
|
||||
@@ -64,7 +46,7 @@ const Headers = ({ collection }) => {
|
||||
onChange={(newValue) => onChange(newValue.replace(/[\r\n]/g, ''))}
|
||||
autocomplete={headerAutoCompleteList}
|
||||
collection={collection}
|
||||
placeholder={!value ? 'Name' : ''}
|
||||
placeholder={isLastEmptyRow ? 'Name' : ''}
|
||||
/>
|
||||
)
|
||||
},
|
||||
@@ -72,7 +54,7 @@ const Headers = ({ collection }) => {
|
||||
key: 'value',
|
||||
name: 'Value',
|
||||
placeholder: 'Value',
|
||||
render: ({ value, onChange }) => (
|
||||
render: ({ row, value, onChange, isLastEmptyRow }) => (
|
||||
<SingleLineEditor
|
||||
value={value || ''}
|
||||
theme={storedTheme}
|
||||
@@ -80,7 +62,7 @@ const Headers = ({ collection }) => {
|
||||
onChange={onChange}
|
||||
collection={collection}
|
||||
autocomplete={MimeTypes}
|
||||
placeholder={!value ? 'Value' : ''}
|
||||
placeholder={isLastEmptyRow ? 'Value' : ''}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -118,7 +100,6 @@ const Headers = ({ collection }) => {
|
||||
rows={headers}
|
||||
onChange={handleHeadersChange}
|
||||
defaultRow={defaultRow}
|
||||
getRowError={getRowError}
|
||||
/>
|
||||
<div className="flex justify-end mt-2">
|
||||
<button className="text-link select-none" onClick={toggleBulkEditMode}>
|
||||
@@ -126,9 +107,9 @@ const Headers = ({ collection }) => {
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<Button type="submit" size="sm" onClick={handleSave}>
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
Save
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
import { rgba } from 'polished';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.icon-box {
|
||||
&.location {
|
||||
background-color: ${(props) => rgba(props.theme.textLink, 0.08)};
|
||||
border: 1px solid ${(props) => rgba(props.theme.textLink, 0.09)};
|
||||
|
||||
svg {
|
||||
color: ${(props) => props.theme.textLink};
|
||||
}
|
||||
}
|
||||
|
||||
&.environments {
|
||||
background-color: ${(props) => rgba(props.theme.colors.text.green, 0.08)};
|
||||
border: 1px solid ${(props) => rgba(props.theme.colors.text.green, 0.09)};
|
||||
|
||||
svg {
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
}
|
||||
}
|
||||
|
||||
&.requests {
|
||||
background-color: ${(props) => rgba(props.theme.colors.text.purple, 0.08)};
|
||||
border: 1px solid ${(props) => rgba(props.theme.colors.text.purple, 0.09)};
|
||||
|
||||
svg {
|
||||
color: ${(props) => props.theme.colors.text.purple};
|
||||
}
|
||||
}
|
||||
|
||||
&.share {
|
||||
background-color: ${(props) => rgba(props.theme.textLink, 0.08)};
|
||||
border: 1px solid ${(props) => rgba(props.theme.textLink, 0.09)};
|
||||
|
||||
svg {
|
||||
color: ${(props) => props.theme.textLink};
|
||||
}
|
||||
}
|
||||
|
||||
&.generate-docs {
|
||||
background-color: ${(props) => rgba(props.theme.accents.primary, 0.08)};
|
||||
border: 1px solid ${(props) => rgba(props.theme.accents.primary, 0.09)};
|
||||
|
||||
svg {
|
||||
color: ${(props) => props.theme.accents.primary};
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,13 +1,11 @@
|
||||
import React from 'react';
|
||||
import { getTotalRequestCountInCollection } from 'utils/collections/';
|
||||
import { IconFolder, IconWorld, IconApi, IconShare, IconBook } from '@tabler/icons';
|
||||
import { IconBox, IconFolder, IconWorld, IconApi, IconShare } from '@tabler/icons';
|
||||
import { areItemsLoading, getItemsLoadStats } from 'utils/collections/index';
|
||||
import { useState } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import ShareCollection from 'components/ShareCollection/index';
|
||||
import GenerateDocumentation from 'components/Sidebar/Collections/Collection/GenerateDocumentation';
|
||||
import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { updateEnvironmentSettingsModalVisibility, updateGlobalEnvironmentSettingsModalVisibility } from 'providers/ReduxStore/slices/app';
|
||||
|
||||
const Info = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -16,7 +14,6 @@ const Info = ({ collection }) => {
|
||||
const isCollectionLoading = areItemsLoading(collection);
|
||||
const { loading: itemsLoadingCount, total: totalItems } = getItemsLoadStats(collection);
|
||||
const [showShareCollectionModal, toggleShowShareCollectionModal] = useState(false);
|
||||
const [showGenerateDocumentationModal, setShowGenerateDocumentationModal] = useState(false);
|
||||
|
||||
const globalEnvironments = useSelector((state) => state.globalEnvironments.globalEnvironments);
|
||||
|
||||
@@ -28,17 +25,17 @@ const Info = ({ collection }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col h-fit">
|
||||
<div className="w-full flex flex-col h-fit">
|
||||
<div className="rounded-lg py-6">
|
||||
<div className="grid gap-5">
|
||||
{/* Location Row */}
|
||||
<div className="flex items-start">
|
||||
<div className="icon-box location flex-shrink-0 p-3 rounded-lg">
|
||||
<IconFolder className="w-5 h-5" stroke={1.5} />
|
||||
<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">
|
||||
<div className="mt-1 text-muted break-all text-xs">
|
||||
{collection.pathname}
|
||||
</div>
|
||||
</div>
|
||||
@@ -46,39 +43,25 @@ const Info = ({ collection }) => {
|
||||
|
||||
{/* Environments Row */}
|
||||
<div className="flex items-start">
|
||||
<div className="icon-box environments flex-shrink-0 p-3 rounded-lg">
|
||||
<IconWorld className="w-5 h-5" stroke={1.5} />
|
||||
<div className="flex-shrink-0 p-3 bg-green-50 dark:bg-green-900/20 rounded-lg">
|
||||
<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="font-medium text-sm">Environments</div>
|
||||
<div className="mt-1 flex flex-col gap-1">
|
||||
<button
|
||||
type="button"
|
||||
className="text-link cursor-pointer hover:underline text-left bg-transparent"
|
||||
className="text-sm text-link cursor-pointer hover:underline text-left bg-transparent"
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: `${collection.uid}-environment-settings`,
|
||||
collectionUid: collection.uid,
|
||||
type: 'environment-settings'
|
||||
})
|
||||
);
|
||||
dispatch(updateEnvironmentSettingsModalVisibility(true));
|
||||
}}
|
||||
>
|
||||
{collectionEnvironmentCount} collection environment{collectionEnvironmentCount !== 1 ? 's' : ''}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="text-link cursor-pointer hover:underline text-left bg-transparent"
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: `${collection.uid}-global-environment-settings`,
|
||||
collectionUid: collection.uid,
|
||||
type: 'global-environment-settings'
|
||||
})
|
||||
);
|
||||
}}
|
||||
className="text-sm text-link cursor-pointer hover:underline text-left bg-transparent"
|
||||
onClick={() => dispatch(updateGlobalEnvironmentSettingsModalVisibility(true))}
|
||||
>
|
||||
{globalEnvironmentCount} global environment{globalEnvironmentCount !== 1 ? 's' : ''}
|
||||
</button>
|
||||
@@ -88,12 +71,12 @@ const Info = ({ collection }) => {
|
||||
|
||||
{/* Requests Row */}
|
||||
<div className="flex items-start">
|
||||
<div className="icon-box requests flex-shrink-0 p-3 rounded-lg">
|
||||
<IconApi className="w-5 h-5" stroke={1.5} />
|
||||
<div className="flex-shrink-0 p-3 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
|
||||
<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">
|
||||
<div className="mt-1 text-muted text-xs">
|
||||
{
|
||||
isCollectionLoading ? `${totalItems - itemsLoadingCount} out of ${totalItems} requests in the collection loaded` : `${totalRequestsInCollection} request${totalRequestsInCollection !== 1 ? 's' : ''} in collection`
|
||||
}
|
||||
@@ -102,33 +85,20 @@ const Info = ({ collection }) => {
|
||||
</div>
|
||||
|
||||
<div className="flex items-start group cursor-pointer" onClick={handleToggleShowShareCollectionModal(true)}>
|
||||
<div className="icon-box share flex-shrink-0 p-3 rounded-lg">
|
||||
<IconShare className="w-5 h-5" stroke={1.5} />
|
||||
<div className="flex-shrink-0 p-3 bg-indigo-50 dark:bg-indigo-900/20 rounded-lg">
|
||||
<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">
|
||||
<div className="group-hover:underline text-link text-xs">
|
||||
Share Collection
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{showShareCollectionModal && <ShareCollection collectionUid={collection.uid} onClose={handleToggleShowShareCollectionModal(false)} />}
|
||||
|
||||
<div className="flex items-start group cursor-pointer" onClick={() => setShowGenerateDocumentationModal(true)}>
|
||||
<div className="icon-box generate-docs flex-shrink-0 p-3 rounded-lg">
|
||||
<IconBook className="w-5 h-5" stroke={1.5} />
|
||||
</div>
|
||||
<div className="ml-4 h-full flex flex-col justify-start">
|
||||
<div className="font-medium h-fit my-auto">Documentation</div>
|
||||
<div className="group-hover:underline text-link">
|
||||
Generate Docs
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{showGenerateDocumentationModal && <GenerateDocumentation collectionUid={collection.uid} onClose={() => setShowGenerateDocumentationModal(false)} />}
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,29 +1,22 @@
|
||||
import styled from 'styled-components';
|
||||
import { rgba } from 'polished';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
&.card {
|
||||
background-color: ${(props) => props.theme.requestTabPanel.card.bg};
|
||||
|
||||
.title {
|
||||
border-top: 1px solid ${(props) => props.theme.table.border};
|
||||
border-left: 1px solid ${(props) => props.theme.table.border};
|
||||
border-right: 1px solid ${(props) => props.theme.table.border};
|
||||
border-top: 1px solid ${(props) => props.theme.requestTabPanel.cardTable.border};
|
||||
border-left: 1px solid ${(props) => props.theme.requestTabPanel.cardTable.border};
|
||||
border-right: 1px solid ${(props) => props.theme.requestTabPanel.cardTable.border};
|
||||
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
|
||||
background-color: ${(props) => props.theme.status.warning.background};
|
||||
}
|
||||
|
||||
.warning-icon {
|
||||
color: ${(props) => props.theme.status.warning.text};
|
||||
}
|
||||
|
||||
.table {
|
||||
thead {
|
||||
color: ${(props) => props.theme.table.thead.color} !important;
|
||||
background: ${(props) => props.theme.sidebar.bg};
|
||||
background-color: ${(props) => props.theme.requestTabPanel.cardTable.table.thead.bg};
|
||||
color: ${(props) => props.theme.requestTabPanel.cardTable.table.thead.color};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||
import { isItemARequest, itemIsOpenedInTabs } from 'utils/tabs/index';
|
||||
import { getDefaultRequestPaneTab } from 'utils/collections/index';
|
||||
import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { hideHomePage } from 'providers/ReduxStore/slices/app';
|
||||
|
||||
const RequestsNotLoaded = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -20,6 +21,7 @@ const RequestsNotLoaded = ({ collection }) => {
|
||||
const handleRequestClick = (item) => (e) => {
|
||||
e.preventDefault();
|
||||
if (isItemARequest(item)) {
|
||||
dispatch(hideHomePage());
|
||||
if (itemIsOpenedInTabs(item, tabs)) {
|
||||
dispatch(
|
||||
focusTab({
|
||||
@@ -41,8 +43,8 @@ const RequestsNotLoaded = ({ collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full card my-2">
|
||||
<div className="flex items-center gap-2 px-3 py-2 title">
|
||||
<IconAlertTriangle size={16} className="warning-icon" />
|
||||
<div className="flex items-center gap-2 px-3 py-2 title bg-yellow-50 dark:bg-yellow-900/20">
|
||||
<IconAlertTriangle size={16} className="text-yellow-500" />
|
||||
<span className="font-medium">Following requests were not loaded</span>
|
||||
</div>
|
||||
<table className="w-full border-collapse">
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
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.input.bg};
|
||||
border: 1px solid ${(props) => props.theme.input.border};
|
||||
|
||||
&:focus {
|
||||
border: solid 1px ${(props) => props.theme.input.focusBorder} !important;
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,135 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { updateCollectionPresets } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { get } from 'lodash';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const PresetsSettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const initialPresets = { requestType: 'http', requestUrl: '' };
|
||||
|
||||
// Get presets from draft.brunoConfig if it exists, otherwise from brunoConfig
|
||||
const currentPresets = collection.draft?.brunoConfig
|
||||
? get(collection, 'draft.brunoConfig.presets', initialPresets)
|
||||
: get(collection, 'brunoConfig.presets', initialPresets);
|
||||
|
||||
// Helper to update presets config
|
||||
const updatePresets = (updates) => {
|
||||
const updatedPresets = { ...currentPresets, ...updates };
|
||||
dispatch(updateCollectionPresets({
|
||||
collectionUid: collection.uid,
|
||||
presets: updatedPresets
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
const handleRequestTypeChange = (e) => {
|
||||
updatePresets({ requestType: e.target.value });
|
||||
};
|
||||
|
||||
const handleRequestUrlChange = (e) => {
|
||||
updatePresets({ requestUrl: e.target.value });
|
||||
};
|
||||
|
||||
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>
|
||||
<div className="bruno-form">
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label flex items-center" htmlFor="http">
|
||||
Request Type
|
||||
</label>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
id="http"
|
||||
className="cursor-pointer"
|
||||
type="radio"
|
||||
name="requestType"
|
||||
onChange={handleRequestTypeChange}
|
||||
value="http"
|
||||
checked={(currentPresets.requestType || 'http') === '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={handleRequestTypeChange}
|
||||
value="graphql"
|
||||
checked={(currentPresets.requestType || 'http') === 'graphql'}
|
||||
/>
|
||||
<label htmlFor="graphql" className="ml-1 cursor-pointer select-none">
|
||||
GraphQL
|
||||
</label>
|
||||
|
||||
<input
|
||||
id="grpc"
|
||||
className="ml-4 cursor-pointer"
|
||||
type="radio"
|
||||
name="requestType"
|
||||
onChange={handleRequestTypeChange}
|
||||
value="grpc"
|
||||
checked={(currentPresets.requestType || 'http') === 'grpc'}
|
||||
/>
|
||||
<label htmlFor="grpc" className="ml-1 cursor-pointer select-none">
|
||||
gRPC
|
||||
</label>
|
||||
|
||||
<input
|
||||
id="ws"
|
||||
className="ml-4 cursor-pointer"
|
||||
type="radio"
|
||||
name="requestType"
|
||||
onChange={handleRequestTypeChange}
|
||||
value="ws"
|
||||
checked={(currentPresets.requestType || 'http') === 'ws'}
|
||||
/>
|
||||
<label htmlFor="ws" className="ml-1 cursor-pointer select-none">
|
||||
WebSocket
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="request-url">
|
||||
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={handleRequestUrlChange}
|
||||
value={currentPresets.requestUrl || ''}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<Button type="button" size="sm" onClick={handleSave}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default PresetsSettings;
|
||||
@@ -8,151 +8,6 @@ const StyledWrapper = styled.div`
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
}
|
||||
|
||||
/* Section labels */
|
||||
label {
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
|
||||
/* Tooltip icon */
|
||||
.tooltip-icon {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Error messages */
|
||||
.error-message {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
background-color: ${(props) => props.theme.bg};
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
||||
thead {
|
||||
th {
|
||||
text-align: left;
|
||||
font-size: ${(props) => props.theme.font.size.xs};
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: ${(props) => props.theme.table.thead.color};
|
||||
border: 1px solid ${(props) => props.theme.table.border};
|
||||
padding: 0.5rem 0.75rem;
|
||||
|
||||
&.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
td {
|
||||
border: 1px solid ${(props) => props.theme.table.border};
|
||||
padding: 0.5rem 0.75rem;
|
||||
|
||||
&.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* File/Directory icons */
|
||||
.file-icon,
|
||||
.folder-icon {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
/* File/Directory names */
|
||||
.file-name,
|
||||
.directory-name {
|
||||
font-weight: 500;
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
|
||||
/* Path text */
|
||||
.path-text {
|
||||
font-size: ${(props) => props.theme.font.size.xs};
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.empty-state {
|
||||
.empty-icon {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
}
|
||||
|
||||
/* Invalid file indicator */
|
||||
.invalid-indicator {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
|
||||
/* Action buttons */
|
||||
.action-button {
|
||||
padding: 0.25rem;
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
transition: all 0.2s;
|
||||
|
||||
&.replace-button {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
|
||||
&:hover {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
background-color: ${(props) => props.theme.colors.bg.danger}20;
|
||||
}
|
||||
}
|
||||
|
||||
&.remove-button {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
|
||||
&:hover {
|
||||
color: ${(props) => props.theme.text};
|
||||
background-color: ${(props) => props.theme.dropdown.hoverBg};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Checkbox */
|
||||
input[type='checkbox'] {
|
||||
cursor: pointer;
|
||||
accent-color: ${(props) => props.theme.colors.accent};
|
||||
border-color: ${(props) => props.theme.table.border};
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: ${(props) => props.theme.primary.solid};
|
||||
}
|
||||
}
|
||||
|
||||
/* Add button */
|
||||
.btn-add-param {
|
||||
color: ${(props) => props.theme.textLink};
|
||||
padding-right: 0.5rem;
|
||||
padding-top: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
margin-top: 0.5rem;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: ${(props) => props.theme.primary.solid};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -12,7 +12,6 @@ import { getBasename } from 'utils/common/path';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import useProtoFileManagement from '../../../hooks/useProtoFileManagement';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const ProtobufSettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -113,12 +112,12 @@ const ProtobufSettings = ({ collection }) => {
|
||||
<div className="mb-6" data-testid="protobuf-proto-files-section">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center">
|
||||
<label className="flex items-center" htmlFor="protoFiles">
|
||||
<label className="font-medium flex items-center" htmlFor="protoFiles">
|
||||
Proto Files (
|
||||
{protoFiles.length}
|
||||
)
|
||||
<span id="proto-files-tooltip" className="ml-2">
|
||||
<IconAlertCircle size={16} className="tooltip-icon" />
|
||||
<IconAlertCircle size={16} className="text-gray-500 cursor-pointer" />
|
||||
</span>
|
||||
<Tooltip
|
||||
anchorId="proto-files-tooltip"
|
||||
@@ -131,7 +130,7 @@ const ProtobufSettings = ({ collection }) => {
|
||||
|
||||
<div>
|
||||
{protoFiles.some((file) => !file.exists) && (
|
||||
<div className="error-message text-xs mb-2 flex items-center p-2" data-testid="protobuf-invalid-files-message">
|
||||
<div className="text-xs text-red-600 dark:text-red-400 mb-2 flex items-center p-2 rounded" data-testid="protobuf-invalid-files-message">
|
||||
<IconAlertCircle size={14} className="mr-1" />
|
||||
Some proto files cannot be found. Use the replace option to update their locations.
|
||||
</div>
|
||||
@@ -140,13 +139,13 @@ const ProtobufSettings = ({ collection }) => {
|
||||
<table className="w-full border-collapse" data-testid="protobuf-proto-files-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<th className="text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
File
|
||||
</th>
|
||||
<th>
|
||||
<th className="text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
Path
|
||||
</th>
|
||||
<th className="text-right">
|
||||
<th className="text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
@@ -154,10 +153,10 @@ const ProtobufSettings = ({ collection }) => {
|
||||
<tbody>
|
||||
{protoFiles.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan="3" className="text-center">
|
||||
<div className="empty-state flex flex-col items-center">
|
||||
<IconFile size={24} className="empty-icon mb-2" />
|
||||
<span className="empty-text">No proto files added</span>
|
||||
<td colSpan="3" className="border border-gray-200 dark:border-gray-700 px-3 py-8 text-center">
|
||||
<div className="flex flex-col items-center">
|
||||
<IconFile size={24} className="text-gray-400 mb-2" />
|
||||
<span className="text-gray-500 dark:text-gray-400">No proto files added</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -167,27 +166,27 @@ const ProtobufSettings = ({ collection }) => {
|
||||
|
||||
return (
|
||||
<tr key={index}>
|
||||
<td>
|
||||
<td className="border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
<div className="flex items-center">
|
||||
<IconFile size={16} className="file-icon mr-2" />
|
||||
<span className="file-name" data-testid="protobuf-proto-file-name">
|
||||
<IconFile size={16} className="text-gray-500 dark:text-gray-400 mr-2" />
|
||||
<span className="font-medium text-gray-900 dark:text-gray-100" data-testid="protobuf-proto-file-name">
|
||||
{getBasename(collection.pathname, file.path)}
|
||||
</span>
|
||||
{!isValid && <IconAlertCircle size={12} className="invalid-indicator ml-2" />}
|
||||
{!isValid && <IconAlertCircle size={12} className="text-red-600 dark:text-red-400 ml-2" />}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="path-text">
|
||||
<td className="border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400 font-mono">
|
||||
{file.path}
|
||||
</div>
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<td className="border border-gray-200 dark:border-gray-700 px-3 py-2 text-right">
|
||||
<div className="flex items-center justify-end space-x-1">
|
||||
{!isValid && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleReplaceProtoFile(index)}
|
||||
className="action-button replace-button"
|
||||
className="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300 p-1 rounded"
|
||||
title="Replace file"
|
||||
>
|
||||
<IconFileImport size={14} />
|
||||
@@ -196,7 +195,7 @@ const ProtobufSettings = ({ collection }) => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveProtoFile(index)}
|
||||
className="action-button remove-button"
|
||||
className="text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300 p-1 rounded"
|
||||
title="Remove file"
|
||||
data-testid="protobuf-remove-file-button"
|
||||
>
|
||||
@@ -220,12 +219,12 @@ const ProtobufSettings = ({ collection }) => {
|
||||
<div className="mb-6" data-testid="protobuf-import-paths-section">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center">
|
||||
<label className="flex items-center" htmlFor="importPaths">
|
||||
<label className="font-medium flex items-center" htmlFor="importPaths">
|
||||
Import Paths (
|
||||
{importPaths.length}
|
||||
)
|
||||
<span id="import-paths-tooltip" className="ml-2">
|
||||
<IconAlertCircle size={16} className="tooltip-icon" />
|
||||
<IconAlertCircle size={16} className="text-gray-500 cursor-pointer" />
|
||||
</span>
|
||||
<Tooltip
|
||||
anchorId="import-paths-tooltip"
|
||||
@@ -238,7 +237,7 @@ const ProtobufSettings = ({ collection }) => {
|
||||
|
||||
<div>
|
||||
{importPaths.some((path) => !path.exists) && (
|
||||
<div className="error-message text-xs mb-2 flex items-center p-2" data-testid="protobuf-invalid-import-paths-message">
|
||||
<div className="text-xs text-red-600 dark:text-red-400 mb-2 flex items-center p-2 rounded" data-testid="protobuf-invalid-import-paths-message">
|
||||
<IconAlertCircle size={14} className="mr-1" />
|
||||
Some import paths cannot be found at their specified locations.
|
||||
</div>
|
||||
@@ -247,15 +246,15 @@ const ProtobufSettings = ({ collection }) => {
|
||||
<table className="w-full border-collapse" data-testid="protobuf-import-paths-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<th className="text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
</th>
|
||||
<th>
|
||||
<th className="text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
Directory
|
||||
</th>
|
||||
<th>
|
||||
<th className="text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
Path
|
||||
</th>
|
||||
<th className="text-right">
|
||||
<th className="text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
@@ -263,10 +262,10 @@ const ProtobufSettings = ({ collection }) => {
|
||||
<tbody>
|
||||
{importPaths.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan="4" className="text-center">
|
||||
<div className="empty-state flex flex-col items-center">
|
||||
<IconFolder size={24} className="empty-icon mb-2" />
|
||||
<span className="empty-text">No import paths added</span>
|
||||
<td colSpan="4" className="border border-gray-200 dark:border-gray-700 px-3 py-8 text-center">
|
||||
<div className="flex flex-col items-center">
|
||||
<IconFolder size={24} className="text-gray-400 mb-2" />
|
||||
<span className="text-gray-500 dark:text-gray-400">No import paths added</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -276,37 +275,37 @@ const ProtobufSettings = ({ collection }) => {
|
||||
|
||||
return (
|
||||
<tr key={index}>
|
||||
<td>
|
||||
<td className="border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={importPath.enabled}
|
||||
onChange={() => handleToggleImportPath(index)}
|
||||
className="h-4 w-4"
|
||||
className="h-4 w-4 text-gray-600 focus:ring-gray-500 border-gray-300 dark:border-gray-600 rounded"
|
||||
title={importPath.enabled ? 'Disable this import path' : 'Enable this import path'}
|
||||
data-testid="protobuf-import-path-checkbox"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<td className="border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
<div className="flex items-center">
|
||||
<IconFolder size={16} className="folder-icon mr-2" />
|
||||
<span className="directory-name">
|
||||
<IconFolder size={16} className="text-gray-500 dark:text-gray-400 mr-2" />
|
||||
<span className="font-medium text-gray-900 dark:text-gray-100">
|
||||
{getBasename(collection.pathname, importPath.path)}
|
||||
</span>
|
||||
{!isValid && <IconAlertCircle size={12} className="invalid-indicator ml-2" />}
|
||||
{!isValid && <IconAlertCircle size={12} className="text-red-600 dark:text-red-400 ml-2" />}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="path-text">
|
||||
<td className="border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400 font-mono">
|
||||
{importPath.path}
|
||||
</div>
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<td className="border border-gray-200 dark:border-gray-700 px-3 py-2 text-right">
|
||||
<div className="flex items-center justify-end space-x-1">
|
||||
{!isValid && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleReplaceImportPath(index)}
|
||||
className="action-button replace-button"
|
||||
className="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300 p-1 rounded"
|
||||
title="Replace directory"
|
||||
>
|
||||
<IconFileImport size={14} />
|
||||
@@ -315,7 +314,7 @@ const ProtobufSettings = ({ collection }) => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveImportPath(index)}
|
||||
className="action-button remove-button"
|
||||
className="text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300 p-1 rounded"
|
||||
title="Remove import path"
|
||||
data-testid="protobuf-remove-import-path-button"
|
||||
>
|
||||
@@ -336,9 +335,9 @@ const ProtobufSettings = ({ collection }) => {
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<Button type="button" size="sm" onClick={handleSave}>
|
||||
<button type="button" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
Save
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -14,11 +14,11 @@ const StyledWrapper = styled.div`
|
||||
box-shadow: none;
|
||||
transition: border-color ease-in-out 0.1s;
|
||||
border-radius: 3px;
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
border: 1px solid ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.modal.input.bg};
|
||||
border: 1px solid ${(props) => props.theme.modal.input.border};
|
||||
|
||||
&:focus {
|
||||
border: solid 1px ${(props) => props.theme.input.focusBorder} !important;
|
||||
border: solid 1px ${(props) => props.theme.modal.input.focusBorder} !important;
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,23 +8,10 @@ import { updateCollectionProxy } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { get } from 'lodash';
|
||||
import toast from 'react-hot-toast';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const ProxySettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const initialProxyConfig = {
|
||||
inherit: true,
|
||||
config: {
|
||||
protocol: 'http',
|
||||
hostname: '',
|
||||
port: '',
|
||||
auth: {
|
||||
username: '',
|
||||
password: ''
|
||||
},
|
||||
bypassProxy: ''
|
||||
}
|
||||
};
|
||||
const initialProxyConfig = { enabled: 'global', protocol: 'http', hostname: '', port: '', auth: { enabled: false, username: '', password: '' }, bypassProxy: '' };
|
||||
|
||||
// Get proxy from draft.brunoConfig if it exists, otherwise from brunoConfig
|
||||
const currentProxyConfig = collection.draft?.brunoConfig
|
||||
@@ -94,57 +81,34 @@ const ProxySettings = ({ collection }) => {
|
||||
|
||||
const handleEnabledChange = (e) => {
|
||||
const value = e.target.value;
|
||||
// Map UI values to new format
|
||||
if (value === 'inherit') {
|
||||
updateProxy({ disabled: false, inherit: true });
|
||||
} else if (value === 'true') {
|
||||
updateProxy({ disabled: false, inherit: false });
|
||||
} else {
|
||||
updateProxy({ disabled: true, inherit: false });
|
||||
}
|
||||
// Convert string to boolean or keep as 'global'
|
||||
const enabled = value === 'true' ? true : value === 'false' ? false : 'global';
|
||||
updateProxy({ enabled });
|
||||
};
|
||||
|
||||
const handleProtocolChange = (e) => {
|
||||
updateProxy({
|
||||
config: {
|
||||
...currentProxyConfig.config,
|
||||
protocol: e.target.value
|
||||
}
|
||||
});
|
||||
updateProxy({ protocol: e.target.value });
|
||||
};
|
||||
|
||||
const handleHostnameChange = (e) => {
|
||||
const hostname = e.target.value;
|
||||
if (validateHostnameOnChange(hostname)) {
|
||||
updateProxy({
|
||||
config: {
|
||||
...currentProxyConfig.config,
|
||||
hostname
|
||||
}
|
||||
});
|
||||
updateProxy({ hostname });
|
||||
}
|
||||
};
|
||||
|
||||
const handlePortChange = (e) => {
|
||||
const port = e.target.value ? Number(e.target.value) : '';
|
||||
if (validatePortOnChange(port)) {
|
||||
updateProxy({
|
||||
config: {
|
||||
...currentProxyConfig.config,
|
||||
port
|
||||
}
|
||||
});
|
||||
updateProxy({ port });
|
||||
}
|
||||
};
|
||||
|
||||
const handleAuthEnabledChange = (e) => {
|
||||
updateProxy({
|
||||
config: {
|
||||
...currentProxyConfig.config,
|
||||
auth: {
|
||||
...currentProxyConfig.config.auth,
|
||||
disabled: !e.target.checked
|
||||
}
|
||||
auth: {
|
||||
...currentProxyConfig.auth,
|
||||
enabled: e.target.checked
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -153,12 +117,9 @@ const ProxySettings = ({ collection }) => {
|
||||
const username = e.target.value;
|
||||
if (validateAuthUsernameOnChange(username)) {
|
||||
updateProxy({
|
||||
config: {
|
||||
...currentProxyConfig.config,
|
||||
auth: {
|
||||
...currentProxyConfig.config.auth,
|
||||
username
|
||||
}
|
||||
auth: {
|
||||
...currentProxyConfig.auth,
|
||||
username
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -168,12 +129,9 @@ const ProxySettings = ({ collection }) => {
|
||||
const password = e.target.value;
|
||||
if (validateAuthPasswordOnChange(password)) {
|
||||
updateProxy({
|
||||
config: {
|
||||
...currentProxyConfig.config,
|
||||
auth: {
|
||||
...currentProxyConfig.config.auth,
|
||||
password
|
||||
}
|
||||
auth: {
|
||||
...currentProxyConfig.auth,
|
||||
password
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -182,19 +140,11 @@ const ProxySettings = ({ collection }) => {
|
||||
const handleBypassProxyChange = (e) => {
|
||||
const bypassProxy = e.target.value;
|
||||
if (validateBypassProxyOnChange(bypassProxy)) {
|
||||
updateProxy({
|
||||
config: {
|
||||
...currentProxyConfig.config,
|
||||
bypassProxy
|
||||
}
|
||||
});
|
||||
updateProxy({ bypassProxy });
|
||||
}
|
||||
};
|
||||
|
||||
// Map new format to UI values
|
||||
const disabled = currentProxyConfig.disabled || false;
|
||||
const inherit = currentProxyConfig.inherit !== undefined ? currentProxyConfig.inherit : true;
|
||||
const enabledValue = disabled ? 'false' : (inherit ? 'inherit' : 'true');
|
||||
const enabledValue = currentProxyConfig.enabled === true ? 'true' : currentProxyConfig.enabled === false ? 'false' : 'global';
|
||||
|
||||
return (
|
||||
<StyledWrapper className="h-full w-full">
|
||||
@@ -206,9 +156,9 @@ const ProxySettings = ({ collection }) => {
|
||||
<InfoTip infotipId="request-var">
|
||||
<div>
|
||||
<ul>
|
||||
<li><span style={{ width: '50px', display: 'inline-block' }}>inherit</span> - inherit from global preferences</li>
|
||||
<li><span style={{ width: '50px', display: 'inline-block' }}>enabled</span> - use collection-specific proxy config</li>
|
||||
<li><span style={{ width: '50px', display: 'inline-block' }}>disabled</span> - disable proxy for this collection</li>
|
||||
<li><span style={{ width: '50px', display: 'inline-block' }}>global</span> - use global proxy config</li>
|
||||
<li><span style={{ width: '50px', display: 'inline-block' }}>enabled</span> - use collection proxy config</li>
|
||||
<li><span style={{ width: '50px', display: 'inline-block' }}>disable</span> - disable proxy</li>
|
||||
</ul>
|
||||
</div>
|
||||
</InfoTip>
|
||||
@@ -218,12 +168,12 @@ const ProxySettings = ({ collection }) => {
|
||||
<input
|
||||
type="radio"
|
||||
name="enabled"
|
||||
value="inherit"
|
||||
checked={enabledValue === 'inherit'}
|
||||
value="global"
|
||||
checked={enabledValue === 'global'}
|
||||
onChange={handleEnabledChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
inherit
|
||||
global
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
@@ -249,172 +199,168 @@ const ProxySettings = ({ collection }) => {
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{enabledValue === 'true' && (
|
||||
<>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="protocol">
|
||||
Protocol
|
||||
</label>
|
||||
<div className="flex items-center">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="http"
|
||||
checked={(currentProxyConfig.config?.protocol || 'http') === 'http'}
|
||||
onChange={handleProtocolChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
HTTP
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="https"
|
||||
checked={(currentProxyConfig.config?.protocol || 'http') === 'https'}
|
||||
onChange={handleProtocolChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
HTTPS
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="socks4"
|
||||
checked={(currentProxyConfig.config?.protocol || 'http') === 'socks4'}
|
||||
onChange={handleProtocolChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
SOCKS4
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="socks5"
|
||||
checked={(currentProxyConfig.config?.protocol || 'http') === 'socks5'}
|
||||
onChange={handleProtocolChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
SOCKS5
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="hostname">
|
||||
Hostname
|
||||
</label>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="protocol">
|
||||
Protocol
|
||||
</label>
|
||||
<div className="flex items-center">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
id="hostname"
|
||||
type="text"
|
||||
name="hostname"
|
||||
className="block textbox"
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="http"
|
||||
checked={(currentProxyConfig.protocol || 'http') === 'http'}
|
||||
onChange={handleProtocolChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
HTTP
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="https"
|
||||
checked={(currentProxyConfig.protocol || 'http') === 'https'}
|
||||
onChange={handleProtocolChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
HTTPS
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="socks4"
|
||||
checked={(currentProxyConfig.protocol || 'http') === 'socks4'}
|
||||
onChange={handleProtocolChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
SOCKS4
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="socks5"
|
||||
checked={(currentProxyConfig.protocol || 'http') === 'socks5'}
|
||||
onChange={handleProtocolChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
SOCKS5
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="hostname">
|
||||
Hostname
|
||||
</label>
|
||||
<input
|
||||
id="hostname"
|
||||
type="text"
|
||||
name="hostname"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={handleHostnameChange}
|
||||
value={currentProxyConfig.hostname || ''}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="port">
|
||||
Port
|
||||
</label>
|
||||
<input
|
||||
id="port"
|
||||
type="number"
|
||||
name="port"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={handlePortChange}
|
||||
value={currentProxyConfig.port || ''}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="auth.enabled">
|
||||
Auth
|
||||
</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="auth.enabled"
|
||||
checked={currentProxyConfig.auth?.enabled || false}
|
||||
onChange={handleAuthEnabledChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="auth.username">
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
id="auth.username"
|
||||
type="text"
|
||||
name="auth.username"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={currentProxyConfig.auth?.username || ''}
|
||||
onChange={handleAuthUsernameChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="auth.password">
|
||||
Password
|
||||
</label>
|
||||
<div className="textbox flex flex-row items-center w-[13.2rem] h-[1.70rem] relative">
|
||||
<input
|
||||
id="auth.password"
|
||||
type={passwordVisible ? 'text' : 'password'}
|
||||
name="auth.password"
|
||||
className="outline-none bg-transparent w-[10.5rem]"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={handleHostnameChange}
|
||||
value={currentProxyConfig.config?.hostname || ''}
|
||||
value={currentProxyConfig.auth?.password || ''}
|
||||
onChange={handleAuthPasswordChange}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm absolute right-0"
|
||||
onClick={() => setPasswordVisible(!passwordVisible)}
|
||||
>
|
||||
{passwordVisible ? <IconEyeOff size={18} strokeWidth={1.5} /> : <IconEye size={18} strokeWidth={1.5} />}
|
||||
</button>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="port">
|
||||
Port
|
||||
</label>
|
||||
<input
|
||||
id="port"
|
||||
type="number"
|
||||
name="port"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={handlePortChange}
|
||||
value={currentProxyConfig.config?.port || ''}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="auth.disabled">
|
||||
Auth
|
||||
</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="auth.disabled"
|
||||
checked={!currentProxyConfig.config?.auth?.disabled}
|
||||
onChange={handleAuthEnabledChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="auth.username">
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
id="auth.username"
|
||||
type="text"
|
||||
name="auth.username"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={currentProxyConfig.config?.auth?.username || ''}
|
||||
onChange={handleAuthUsernameChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="auth.password">
|
||||
Password
|
||||
</label>
|
||||
<div className="textbox flex flex-row items-center w-[13.2rem] h-[1.70rem] relative">
|
||||
<input
|
||||
id="auth.password"
|
||||
type={passwordVisible ? 'text' : 'password'}
|
||||
name="auth.password"
|
||||
className="outline-none bg-transparent w-[10.5rem]"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={currentProxyConfig.config?.auth?.password || ''}
|
||||
onChange={handleAuthPasswordChange}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm absolute right-0"
|
||||
onClick={() => setPasswordVisible(!passwordVisible)}
|
||||
>
|
||||
{passwordVisible ? <IconEyeOff size={18} strokeWidth={1.5} /> : <IconEye size={18} strokeWidth={1.5} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="bypassProxy">
|
||||
Proxy Bypass
|
||||
</label>
|
||||
<input
|
||||
id="bypassProxy"
|
||||
type="text"
|
||||
name="bypassProxy"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={handleBypassProxyChange}
|
||||
value={currentProxyConfig.config?.bypassProxy || ''}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="bypassProxy">
|
||||
Proxy Bypass
|
||||
</label>
|
||||
<input
|
||||
id="bypassProxy"
|
||||
type="text"
|
||||
name="bypassProxy"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={handleBypassProxyChange}
|
||||
value={currentProxyConfig.bypassProxy || ''}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<Button type="submit" size="sm" onClick={handleSave}>
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
Save
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -8,7 +8,7 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
div.title {
|
||||
color: ${(props) => props.theme.colors.text.subtext0};
|
||||
color: var(--color-tab-inactive);
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -6,38 +6,19 @@ import { updateCollectionRequestScript, updateCollectionResponseScript } from 'p
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from 'components/Tabs';
|
||||
import StatusDot from 'components/StatusDot';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const Script = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const [activeTab, setActiveTab] = useState('pre-request');
|
||||
const preRequestEditorRef = useRef(null);
|
||||
const postResponseEditorRef = useRef(null);
|
||||
const requestScript = collection.draft?.root ? get(collection, 'draft.root.request.script.req', '') : get(collection, 'root.request.script.req', '');
|
||||
const responseScript = collection.draft?.root ? get(collection, 'draft.root.request.script.res', '') : get(collection, 'root.request.script.res', '');
|
||||
|
||||
// Default to post-response if pre-request script is empty
|
||||
const getInitialTab = () => {
|
||||
const hasPreRequestScript = requestScript && requestScript.trim().length > 0;
|
||||
return hasPreRequestScript ? 'pre-request' : 'post-response';
|
||||
};
|
||||
|
||||
const [activeTab, setActiveTab] = useState(getInitialTab);
|
||||
const prevCollectionUidRef = useRef(collection.uid);
|
||||
|
||||
const { displayedTheme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
|
||||
// Update active tab only when switching to a different collection
|
||||
useEffect(() => {
|
||||
if (prevCollectionUidRef.current !== collection.uid) {
|
||||
prevCollectionUidRef.current = collection.uid;
|
||||
const hasPreRequestScript = requestScript && requestScript.trim().length > 0;
|
||||
setActiveTab(hasPreRequestScript ? 'pre-request' : 'post-response');
|
||||
}
|
||||
}, [collection.uid, requestScript]);
|
||||
|
||||
// Refresh CodeMirror when tab becomes visible
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
@@ -74,21 +55,15 @@ const Script = ({ collection }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col h-full">
|
||||
<StyledWrapper className="w-full flex flex-col h-full pt-4">
|
||||
<div className="text-xs mb-4 text-muted">
|
||||
Write pre and post-request scripts that will run before and after any request in this collection is sent.
|
||||
</div>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="pre-request">
|
||||
Pre Request
|
||||
{requestScript && requestScript.trim().length > 0 && <StatusDot />}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="post-response">
|
||||
Post Response
|
||||
{responseScript && responseScript.trim().length > 0 && <StatusDot />}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="pre-request">Pre Request</TabsTrigger>
|
||||
<TabsTrigger value="post-response">Post Response</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="pre-request" className="mt-2">
|
||||
@@ -123,9 +98,9 @@ const Script = ({ collection }) => {
|
||||
</Tabs>
|
||||
|
||||
<div className="mt-12">
|
||||
<Button type="submit" size="sm" onClick={handleSave}>
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
Save
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@ const StyledWrapper = styled.div`
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: ${(props) => props.theme.tabs.marginRight};
|
||||
color: ${(props) => props.theme.colors.text.subtext0};
|
||||
color: var(--color-tab-inactive);
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
@@ -19,10 +19,6 @@ const StyledWrapper = styled.div`
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: ${(props) => props.theme.tabs.active.color} !important;
|
||||
}
|
||||
|
||||
&.active {
|
||||
font-weight: ${(props) => props.theme.tabs.active.fontWeight} !important;
|
||||
color: ${(props) => props.theme.tabs.active.color} !important;
|
||||
@@ -44,11 +40,6 @@ const StyledWrapper = styled.div`
|
||||
.muted {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
input[type='radio'] {
|
||||
cursor: pointer;
|
||||
accent-color: ${(props) => props.theme.primary.solid};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -6,7 +6,6 @@ import { updateCollectionTests } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const Tests = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -42,9 +41,9 @@ const Tests = ({ collection }) => {
|
||||
/>
|
||||
|
||||
<div className="mt-6">
|
||||
<Button type="submit" size="sm" onClick={handleSave}>
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
Save
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ const StyledWrapper = styled.div`
|
||||
max-width: 800px;
|
||||
|
||||
div.title {
|
||||
color: ${(props) => props.theme.colors.text.subtext0};
|
||||
color: var(--color-tab-inactive);
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -46,14 +46,14 @@ const VarsTable = ({ collection, vars, varType }) => {
|
||||
</div>
|
||||
),
|
||||
placeholder: varType === 'request' ? 'Value' : 'Expr',
|
||||
render: ({ value, onChange }) => (
|
||||
render: ({ row, value, onChange, isLastEmptyRow }) => (
|
||||
<MultiLineEditor
|
||||
value={value || ''}
|
||||
theme={storedTheme}
|
||||
onSave={onSave}
|
||||
onChange={onChange}
|
||||
collection={collection}
|
||||
placeholder={!value ? (varType === 'request' ? 'Value' : 'Expr') : ''}
|
||||
placeholder={isLastEmptyRow ? (varType === 'request' ? 'Value' : 'Expr') : ''}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import VarsTable from './VarsTable';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const Vars = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -14,18 +13,18 @@ const Vars = ({ collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col">
|
||||
<div className="flex-1">
|
||||
<div className="mb-3 title text-xs">Pre Request</div>
|
||||
<div className="flex-1 mt-2">
|
||||
<div className="mb-1 title text-xs">Pre Request</div>
|
||||
<VarsTable collection={collection} vars={requestVars} varType="request" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="mt-3 mb-3 title text-xs">Post Response</div>
|
||||
<div className="mt-1 mb-1 title text-xs">Post Response</div>
|
||||
<VarsTable collection={collection} vars={responseVars} varType="response" />
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<Button type="submit" size="sm" onClick={handleSave}>
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
Save
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -9,7 +9,6 @@ import Headers from './Headers';
|
||||
import Auth from './Auth';
|
||||
import Script from './Script';
|
||||
import Test from './Tests';
|
||||
import Presets from './Presets';
|
||||
import Protobuf from './Protobuf';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Vars from './Vars/index';
|
||||
@@ -59,8 +58,6 @@ const CollectionSettings = ({ collection }) => {
|
||||
const protobufConfig = collection.draft?.brunoConfig
|
||||
? get(collection, 'draft.brunoConfig.protobuf', {})
|
||||
: get(collection, 'brunoConfig.protobuf', {});
|
||||
const presets = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.presets', {}) : get(collection, 'brunoConfig.presets', {});
|
||||
const hasPresets = presets && presets.requestUrl !== '';
|
||||
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
@@ -82,9 +79,6 @@ const CollectionSettings = ({ collection }) => {
|
||||
case 'tests': {
|
||||
return <Test collection={collection} />;
|
||||
}
|
||||
case 'presets': {
|
||||
return <Presets collection={collection} />;
|
||||
}
|
||||
case 'proxy': {
|
||||
return <ProxySettings collection={collection} />;
|
||||
}
|
||||
@@ -129,10 +123,6 @@ const CollectionSettings = ({ collection }) => {
|
||||
Tests
|
||||
{hasTests && <StatusDot />}
|
||||
</div>
|
||||
<div className={getTabClassname('presets')} role="tab" onClick={() => setTab('presets')}>
|
||||
Presets
|
||||
{hasPresets && <StatusDot />}
|
||||
</div>
|
||||
<div className={getTabClassname('proxy')} role="tab" onClick={() => setTab('proxy')}>
|
||||
Proxy
|
||||
{Object.keys(proxyConfig).length > 0 && proxyEnabled && <StatusDot />}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
|
||||
const ColorBadge = ({ color, size = 10 }) => {
|
||||
const sizeValue = typeof size === 'string' ? size : `${size}px`;
|
||||
const { theme } = useTheme();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
width: sizeValue,
|
||||
height: sizeValue,
|
||||
backgroundColor: color || 'transparent'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ColorBadge;
|
||||
@@ -1,7 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,164 +0,0 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { IconBan, IconBrush } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import ColorBadge from 'components/ColorBadge';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { parseToRgb, toColorString } from 'polished';
|
||||
import ColorRangePicker from 'components/ColorRange/index';
|
||||
|
||||
const PRESET_COLORS = [
|
||||
'#CE4F3B',
|
||||
'#2E8A54',
|
||||
'#346AB2',
|
||||
'#C77A0F',
|
||||
'#B83D7F',
|
||||
'#8D44B2'
|
||||
];
|
||||
|
||||
const COLOR_RANGE_SEQUENCE = ['#D85D43', '#F4BB74', '#61DCB1', '#7EBDF2', '#D48ADE', '#B491E5'];
|
||||
|
||||
/**
|
||||
* @param {string} hex
|
||||
* @returns {red:string,green:string,blue:string}
|
||||
*/
|
||||
const hexToRgb = (hex) => {
|
||||
try {
|
||||
return parseToRgb(hex);
|
||||
} catch (err) {
|
||||
return { red: 0, green: 0, blue: 0 };
|
||||
}
|
||||
};
|
||||
|
||||
const rgbToHex = (r, g, b) => {
|
||||
return toColorString({ red: Math.round(r), green: Math.round(g), blue: Math.round(b) });
|
||||
};
|
||||
|
||||
const interpolateColor = (position) => {
|
||||
const numColors = COLOR_RANGE_SEQUENCE.length;
|
||||
const scaledPos = (position / 100) * (numColors - 1);
|
||||
const index = Math.floor(scaledPos);
|
||||
const fraction = scaledPos - index;
|
||||
|
||||
if (index >= numColors - 1) {
|
||||
return COLOR_RANGE_SEQUENCE[numColors - 1];
|
||||
}
|
||||
|
||||
const color1 = hexToRgb(COLOR_RANGE_SEQUENCE[index]);
|
||||
const color2 = hexToRgb(COLOR_RANGE_SEQUENCE[index + 1]);
|
||||
|
||||
const r = color1.red + (color2.red - color1.red) * fraction;
|
||||
const g = color1.green + (color2.green - color1.green) * fraction;
|
||||
const b = color1.blue + (color2.blue - color1.blue) * fraction;
|
||||
|
||||
return rgbToHex(r, g, b);
|
||||
};
|
||||
|
||||
const findClosestPosition = (hex) => {
|
||||
if (!hex) return 0;
|
||||
const target = hexToRgb(hex);
|
||||
let closestPos = 0;
|
||||
let minDistance = Infinity;
|
||||
|
||||
for (let pos = 0; pos <= 100; pos++) {
|
||||
const color = hexToRgb(interpolateColor(pos));
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(target.red - color.red, 2) + Math.pow(target.green - color.green, 2) + Math.pow(target.blue - color.blue, 2)
|
||||
);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
closestPos = pos;
|
||||
}
|
||||
}
|
||||
return closestPos;
|
||||
};
|
||||
|
||||
const ColorPickerIcon = ({ color }) => {
|
||||
if (color) {
|
||||
return <ColorBadge color={color} size={8} />;
|
||||
}
|
||||
return <IconBrush size={14} strokeWidth={1.5} className="opacity-70" />;
|
||||
};
|
||||
|
||||
const ColorPicker = ({ color, onChange, icon }) => {
|
||||
const [sliderPosition, setSliderPosition] = useState(() =>
|
||||
color && !PRESET_COLORS.includes(color) ? findClosestPosition(color) : 0
|
||||
);
|
||||
const [customColor, setCustomColor] = useState(() =>
|
||||
color && !PRESET_COLORS.includes(color) ? color : COLOR_RANGE_SEQUENCE[0]
|
||||
);
|
||||
const pendingColorRef = useRef(customColor);
|
||||
|
||||
const handleColorSelect = (selectedColor) => {
|
||||
onChange(selectedColor);
|
||||
};
|
||||
|
||||
const handleSliderChange = (e) => {
|
||||
const newPosition = parseInt(e.target.value, 10);
|
||||
setSliderPosition(newPosition);
|
||||
const newColor = interpolateColor(newPosition);
|
||||
setCustomColor(newColor);
|
||||
pendingColorRef.current = newColor;
|
||||
};
|
||||
|
||||
const handleSliderEnd = () => {
|
||||
onChange(pendingColorRef.current);
|
||||
};
|
||||
|
||||
const defaultIcon = (
|
||||
<div className="cursor-pointer flex items-center" title="Change color">
|
||||
<ColorPickerIcon color={color} />
|
||||
</div>
|
||||
);
|
||||
|
||||
const colorPickerContent = (
|
||||
<StyledWrapper>
|
||||
<div className="p-2">
|
||||
<div className="flex flex-wrap gap-1.5 justify-between items-center">
|
||||
<div
|
||||
className="w-5 h-5 cursor-pointer flex items-center justify-center transition-transform duration-100 hover:scale-110"
|
||||
onClick={() => handleColorSelect(null)}
|
||||
title="No color"
|
||||
>
|
||||
<IconBan size={20} strokeWidth={1.5} />
|
||||
</div>
|
||||
{PRESET_COLORS.map((presetColor, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`w-5 h-5 rounded cursor-pointer flex items-center justify-center transition-transform duration-100 hover:scale-110 border-2 border-transparent
|
||||
${color === presetColor ? 'border-solid !border-current' : ''}
|
||||
`}
|
||||
style={{ backgroundColor: presetColor }}
|
||||
onClick={() => handleColorSelect(presetColor)}
|
||||
title={presetColor}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 mt-2 pt-0.5">
|
||||
<div
|
||||
className="w-5 h-5 rounded-full flex-shrink-0 cursor-pointer"
|
||||
style={{ backgroundColor: customColor }}
|
||||
onClick={() => handleColorSelect(customColor)}
|
||||
title="Custom color"
|
||||
/>
|
||||
<ColorRangePicker
|
||||
className="flex-1 flex"
|
||||
value={sliderPosition}
|
||||
onChange={handleSliderChange}
|
||||
onMouseUp={handleSliderEnd}
|
||||
selectedColor={customColor}
|
||||
colorRange={COLOR_RANGE_SEQUENCE}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown icon={icon || defaultIcon} placement="bottom-start">
|
||||
{colorPickerContent}
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default ColorPicker;
|
||||
@@ -1,46 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.hue-slider {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.hue-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
background: ${(props) => props.color ?? props.theme.bg};
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
|
||||
.hue-slider::-webkit-slider-thumb:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.hue-slider::-moz-range-thumb {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
background: ${(props) => props.color ?? props.theme.bg};
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
|
||||
.hue-slider::-moz-range-thumb:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,23 +0,0 @@
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const ColorRangePicker = ({ selectedColor, className, value, onChange, colorRange, ...props }) => {
|
||||
return (
|
||||
<StyledWrapper color={selectedColor} className={className}>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
className="hue-slider"
|
||||
style={{
|
||||
background: `linear-gradient(to right, ${colorRange.join(',')})`
|
||||
}}
|
||||
title="Adjust color"
|
||||
{...props}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ColorRangePicker;
|
||||
@@ -1,26 +1,5 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
/* Info icon */
|
||||
.info-icon {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
/* Required field asterisk */
|
||||
.required-asterisk {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
|
||||
/* Error messages */
|
||||
.error-message {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
|
||||
/* Checkbox */
|
||||
input[type='checkbox'] {
|
||||
cursor: pointer;
|
||||
accent-color: ${(props) => props.theme.primary.solid};
|
||||
}
|
||||
`;
|
||||
const StyledWrapper = styled.div``;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -11,7 +11,6 @@ import moment from 'moment';
|
||||
import 'moment-timezone';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { isEmpty } from 'lodash';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const removeEmptyValues = (obj) => {
|
||||
return Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== null && value !== undefined));
|
||||
@@ -226,147 +225,145 @@ const ModifyCookieModal = ({ onClose, domain, cookie }) => {
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<StyledWrapper>
|
||||
<form onSubmit={(e) => e.preventDefault()} className="px-2">
|
||||
{isRawMode ? (
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<label className="block">Set-Cookie String</label>
|
||||
<IconInfoCircle id="cookie-raw-info" size={16} strokeWidth={1.5} className="info-icon" />
|
||||
<Tooltip
|
||||
anchorId="cookie-raw-info"
|
||||
className="tooltip-mod"
|
||||
html="Key, Path, and Domain are immutable properties and cannot be modified for existing cookies"
|
||||
/>
|
||||
</div>
|
||||
<textarea
|
||||
value={cookieString}
|
||||
onChange={(e) => setCookieString(e.target.value)}
|
||||
className="block textbox w-full h-24"
|
||||
placeholder="key=value; key2=value2"
|
||||
<form onSubmit={(e) => e.preventDefault()} className="px-2">
|
||||
{isRawMode ? (
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<label className="block">Set-Cookie String</label>
|
||||
<IconInfoCircle id="cookie-raw-info" size={16} strokeWidth={1.5} className="text-gray-400" />
|
||||
<Tooltip
|
||||
anchorId="cookie-raw-info"
|
||||
className="tooltip-mod"
|
||||
html="Key, Path, and Domain are immutable properties and cannot be modified for existing cookies"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block mb-1">
|
||||
Domain<span className="required-asterisk">*</span>{' '}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="domain"
|
||||
// Auto-focus if its add-new i.e. when domain prop is empty
|
||||
autoFocus={!domain && !formik.values.domain}
|
||||
value={formik.values.domain}
|
||||
onChange={formik.handleChange}
|
||||
className="block textbox non-passphrase-input w-full disabled:opacity-50"
|
||||
disabled={!!cookie}
|
||||
/>
|
||||
{formik.touched.domain && formik.errors.domain && (
|
||||
<div className="error-message mt-1">{formik.errors.domain}</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-1">Path</label>
|
||||
<input
|
||||
type="text"
|
||||
name="path"
|
||||
value={formik.values.path}
|
||||
onChange={formik.handleChange}
|
||||
className="block textbox non-passphrase-input w-full disabled:opacity-50"
|
||||
disabled={!!cookie}
|
||||
/>
|
||||
{formik.touched.path && formik.errors.path && (
|
||||
<div className="error-message mt-1">{formik.errors.path}</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-1">
|
||||
Key<span className="required-asterisk">*</span>{' '}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="key"
|
||||
// Auto focus when add-for-domain i.e. if domain is already prefilled
|
||||
autoFocus={!!domain && !formik.values.key}
|
||||
value={formik.values.key}
|
||||
onChange={formik.handleChange}
|
||||
className="block textbox non-passphrase-input w-full disabled:opacity-50"
|
||||
disabled={!!cookie}
|
||||
/>
|
||||
{formik.touched.key && formik.errors.key && (
|
||||
<div className="error-message mt-1">{formik.errors.key}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block mb-1">
|
||||
Value<span className="required-asterisk">*</span>{' '}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="value"
|
||||
// Auto-focus when its in edit mode i.e. cookie prop is present
|
||||
autoFocus={!!cookie}
|
||||
value={formik.values.value}
|
||||
onChange={formik.handleChange}
|
||||
className="block textbox non-passphrase-input w-full"
|
||||
/>
|
||||
{formik.touched.value && formik.errors.value && (
|
||||
<div className="error-message mt-1">{formik.errors.value}</div>
|
||||
)}
|
||||
</div>
|
||||
<textarea
|
||||
value={cookieString}
|
||||
onChange={(e) => setCookieString(e.target.value)}
|
||||
className="block textbox w-full h-24"
|
||||
placeholder="key=value; key2=value2"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block mb-1">
|
||||
Domain<span className="text-red-600">*</span>{' '}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="domain"
|
||||
// Auto-focus if its add-new i.e. when domain prop is empty
|
||||
autoFocus={!domain && !formik.values.domain}
|
||||
value={formik.values.domain}
|
||||
onChange={formik.handleChange}
|
||||
className="block textbox non-passphrase-input w-full disabled:opacity-50"
|
||||
disabled={!!cookie}
|
||||
/>
|
||||
{formik.touched.domain && formik.errors.domain && (
|
||||
<div className="text-red-500 mt-1">{formik.errors.domain}</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-1">Path</label>
|
||||
<input
|
||||
type="text"
|
||||
name="path"
|
||||
value={formik.values.path}
|
||||
onChange={formik.handleChange}
|
||||
className="block textbox non-passphrase-input w-full disabled:opacity-50"
|
||||
disabled={!!cookie}
|
||||
/>
|
||||
{formik.touched.path && formik.errors.path && (
|
||||
<div className="text-red-500 mt-1">{formik.errors.path}</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-1">
|
||||
Key<span className="text-red-600">*</span>{' '}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="key"
|
||||
// Auto focus when add-for-domain i.e. if domain is already prefilled
|
||||
autoFocus={!!domain && !formik.values.key}
|
||||
value={formik.values.key}
|
||||
onChange={formik.handleChange}
|
||||
className="block textbox non-passphrase-input w-full disabled:opacity-50"
|
||||
disabled={!!cookie}
|
||||
/>
|
||||
{formik.touched.key && formik.errors.key && (
|
||||
<div className="text-red-500 mt-1">{formik.errors.key}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Date Picker */}
|
||||
<div className="w-full flex items-end">
|
||||
<div>
|
||||
<label className="block mb-1">Expiration ({moment.tz.guess()})</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
name="expires"
|
||||
value={formik.values.expires}
|
||||
onChange={(e) => {
|
||||
formik.handleChange(e);
|
||||
}}
|
||||
className="block textbox non-passphrase-input w-full"
|
||||
min={moment().format(moment.HTML5_FMT.DATETIME_LOCAL)}
|
||||
/>
|
||||
{formik.touched.expires && formik.errors.expires && (
|
||||
<div className="error-message mt-1">{formik.errors.expires}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Checkboxes */}
|
||||
<div className="flex space-x-4 ml-auto">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="secure"
|
||||
checked={formik.values.secure}
|
||||
onChange={formik.handleChange}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span>Secure</span>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="httpOnly"
|
||||
checked={formik.values.httpOnly}
|
||||
onChange={formik.handleChange}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span>HTTP Only</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-1">
|
||||
Value<span className="text-red-600">*</span>{' '}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="value"
|
||||
// Auto-focus when its in edit mode i.e. cookie prop is present
|
||||
autoFocus={!!cookie}
|
||||
value={formik.values.value}
|
||||
onChange={formik.handleChange}
|
||||
className="block textbox non-passphrase-input w-full"
|
||||
/>
|
||||
{formik.touched.value && formik.errors.value && (
|
||||
<div className="text-red-500 mt-1">{formik.errors.value}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</StyledWrapper>
|
||||
|
||||
{/* Date Picker */}
|
||||
<div className="w-full flex items-end">
|
||||
<div>
|
||||
<label className="block mb-1">Expiration ({moment.tz.guess()})</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
name="expires"
|
||||
value={formik.values.expires}
|
||||
onChange={(e) => {
|
||||
formik.handleChange(e);
|
||||
}}
|
||||
className="block textbox non-passphrase-input w-full"
|
||||
min={moment().format(moment.HTML5_FMT.DATETIME_LOCAL)}
|
||||
/>
|
||||
{formik.touched.expires && formik.errors.expires && (
|
||||
<div className="text-red-500 mt-1">{formik.errors.expires}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Checkboxes */}
|
||||
<div className="flex space-x-4 ml-auto">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="secure"
|
||||
checked={formik.values.secure}
|
||||
onChange={formik.handleChange}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span>Secure</span>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="httpOnly"
|
||||
checked={formik.values.httpOnly}
|
||||
onChange={formik.handleChange}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span>HTTP Only</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -28,11 +28,11 @@ const Wrapper = styled.div`
|
||||
box-shadow: none;
|
||||
transition: border-color ease-in-out 0.1s;
|
||||
border-radius: 3px;
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
border: 1px solid ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.modal.input.bg};
|
||||
border: 1px solid ${(props) => props.theme.modal.input.border};
|
||||
|
||||
&:focus {
|
||||
border: solid 1px ${(props) => props.theme.input.focusBorder} !important;
|
||||
border: solid 1px ${(props) => props.theme.modal.input.focusBorder} !important;
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
@@ -70,92 +70,6 @@ const Wrapper = styled.div`
|
||||
background-size: 100% 30px, 100% 30px, 100% 10px, 100% 10px;
|
||||
background-attachment: local, local, scroll, scroll;
|
||||
}
|
||||
|
||||
/* Warning icon */
|
||||
.warning-icon {
|
||||
color: ${(props) => props.theme.colors.text.warning};
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.empty-icon {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
/* Domain count text */
|
||||
.domain-count {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
/* Action buttons */
|
||||
.action-button {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
transition: color 0.2s;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
}
|
||||
|
||||
.action-button-danger {
|
||||
color: ${(props) => props.theme.text};
|
||||
transition: color 0.2s;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
}
|
||||
|
||||
/* Table styles */
|
||||
table {
|
||||
thead {
|
||||
tr {
|
||||
border-bottom: 1px solid ${(props) => props.theme.table.border};
|
||||
color: ${(props) => props.theme.table.thead.color};
|
||||
|
||||
th {
|
||||
color: ${(props) => props.theme.table.thead.color};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
border-bottom: 1px solid ${(props) => props.theme.table.border};
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Edit button */
|
||||
.edit-button {
|
||||
color: ${(props) => props.theme.text};
|
||||
transition: color 0.2s;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
}
|
||||
|
||||
/* Delete button */
|
||||
.delete-button {
|
||||
color: ${(props) => props.theme.text};
|
||||
transition: color 0.2s;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
|
||||
@@ -9,12 +9,11 @@ import ModifyCookieModal from 'components/Cookies/ModifyCookieModal/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import moment from 'moment';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const ClearDomainCookiesModal = ({ onClose, domain, onClear }) => (
|
||||
<Modal onClose={onClose} handleCancel={onClose} title="Clear Domain Cookies" hideFooter={true}>
|
||||
<div className="flex items-center font-normal">
|
||||
<IconAlertTriangle size={32} strokeWidth={1.5} className="warning-icon" />
|
||||
<IconAlertTriangle size={32} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<h1 className="ml-2 text-lg font-medium">Hold on..</h1>
|
||||
</div>
|
||||
<div className="font-normal mt-4">
|
||||
@@ -23,14 +22,14 @@ const ClearDomainCookiesModal = ({ onClose, domain, onClear }) => (
|
||||
|
||||
<div className="flex justify-between mt-6">
|
||||
<div>
|
||||
<Button color="secondary" variant="ghost" onClick={onClose}>
|
||||
<button className="btn btn-sm btn-close" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<Button color="danger" onClick={onClear}>
|
||||
<button className="btn btn-sm btn-danger" onClick={onClear}>
|
||||
Clear All
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
@@ -39,7 +38,7 @@ const ClearDomainCookiesModal = ({ onClose, domain, onClear }) => (
|
||||
const DeleteCookieModal = ({ onClose, cookieName, onDelete }) => (
|
||||
<Modal onClose={onClose} handleCancel={onClose} title="Delete Cookie" hideFooter={true}>
|
||||
<div className="flex items-center font-normal">
|
||||
<IconAlertTriangle size={32} strokeWidth={1.5} className="warning-icon" />
|
||||
<IconAlertTriangle size={32} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<h1 className="ml-2 text-lg font-medium">Hold on..</h1>
|
||||
</div>
|
||||
<div className="font-normal mt-4">
|
||||
@@ -48,14 +47,14 @@ const DeleteCookieModal = ({ onClose, cookieName, onDelete }) => (
|
||||
|
||||
<div className="flex justify-between mt-6">
|
||||
<div>
|
||||
<Button color="secondary" variant="ghost" onClick={onClose}>
|
||||
<button className="btn btn-sm btn-close" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<Button color="danger" onClick={onDelete}>
|
||||
<button className="btn btn-sm btn-danger" onClick={onDelete}>
|
||||
Delete
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
@@ -138,20 +137,18 @@ const CollectionProperties = ({ onClose }) => {
|
||||
value={searchText || ''}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
className="block textbox non-passphrase-input ml-auto font-normal"
|
||||
autoFocus
|
||||
/>
|
||||
<Button
|
||||
<button
|
||||
type="submit"
|
||||
size="sm"
|
||||
className="mx-4"
|
||||
icon={<IconCirclePlus strokeWidth={1.5} size={16} />}
|
||||
className="submit btn btn-sm btn-secondary flex items-center gap-1 mx-4 font-medium"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleAddCookie();
|
||||
}}
|
||||
>
|
||||
<IconCirclePlus strokeWidth={1.5} size={16} />
|
||||
<span>Add Cookie</span>
|
||||
</Button>
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
) : null}
|
||||
>
|
||||
@@ -159,28 +156,27 @@ const CollectionProperties = ({ onClose }) => {
|
||||
{!cookies || !cookies.length ? (
|
||||
// No cookies found
|
||||
<div className="flex items-center justify-center flex-col">
|
||||
<IconCookieOff size={48} strokeWidth={1.5} className="empty-icon" />
|
||||
<IconCookieOff size={48} strokeWidth={1.5} className="text-gray-500" />
|
||||
<h2 className="text-lg font-medium mt-4">No cookies found</h2>
|
||||
<p className="empty-text mt-2">Add cookies to get started</p>
|
||||
<Button
|
||||
<p className="text-gray-500 mt-2">Add cookies to get started</p>
|
||||
<button
|
||||
type="submit"
|
||||
size="sm"
|
||||
className="mt-8"
|
||||
icon={<IconCirclePlus strokeWidth={1.5} size={16} />}
|
||||
className="submit btn btn-sm btn-secondary flex items-center gap-1 mt-8"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleAddCookie();
|
||||
}}
|
||||
>
|
||||
Add Cookie
|
||||
</Button>
|
||||
<IconCirclePlus strokeWidth={1.5} size={16} />
|
||||
<span>Add Cookie</span>
|
||||
</button>
|
||||
</div>
|
||||
) : cookies.length && !filteredCookies.length ? (
|
||||
// No search results
|
||||
<div className="flex items-center justify-center flex-col">
|
||||
<IconSearch size={48} />
|
||||
<h2 className="text-lg font-medium mt-4">No search results</h2>
|
||||
<p className="empty-text mt-2">Try a different search term</p>
|
||||
<p className="text-gray-500 mt-2">Try a different search term</p>
|
||||
</div>
|
||||
) : (
|
||||
// Show cookies list
|
||||
@@ -191,14 +187,14 @@ const CollectionProperties = ({ onClose }) => {
|
||||
<Accordion.Header index={i} className="flex items-center">
|
||||
<div className="flex items-center">
|
||||
<span>{domainWithCookies.domain}</span>
|
||||
<span className="domain-count ml-2 text-xs">
|
||||
<span className="ml-2 text-xs dark:text-gray-300 text-gray-500">
|
||||
({domainWithCookies.cookies.length}{' '}
|
||||
{domainWithCookies.cookies.length === 1 ? 'cookie' : 'cookies'})
|
||||
</span>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<button
|
||||
type="submit"
|
||||
className="action-button flex items-center gap-1"
|
||||
className="flex items-center gap-1 text-gray-500 hover:text-gray-950 dark:text-white dark:hover:text-gray-300"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleAddCookie(domainWithCookies.domain);
|
||||
@@ -211,7 +207,7 @@ const CollectionProperties = ({ onClose }) => {
|
||||
e.stopPropagation();
|
||||
handleClearDomainCookies(domainWithCookies.domain);
|
||||
}}
|
||||
className="action-button-danger mr-2"
|
||||
className="text-gray-950 dark:text-white dark:hover:hover:text-red-600 hover:text-red-600 mr-2"
|
||||
>
|
||||
<IconTrash strokeWidth={1.5} size={16} />
|
||||
</button>
|
||||
@@ -222,7 +218,7 @@ const CollectionProperties = ({ onClose }) => {
|
||||
<div className="flex items-center justify-between">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="text-left">
|
||||
<tr className="text-left border-b border-gray-200 dark:border-neutral-600 text-gray-700 dark:text-gray-300">
|
||||
<th className="py-2 px-4 font-medium w-32">Name</th>
|
||||
<th className="py-2 px-4 font-medium w-52">Value</th>
|
||||
<th className="py-2 px-4 font-medium">Path</th>
|
||||
@@ -234,7 +230,7 @@ const CollectionProperties = ({ onClose }) => {
|
||||
</thead>
|
||||
<tbody>
|
||||
{domainWithCookies.cookies.map((cookie) => (
|
||||
<tr key={cookie.key}>
|
||||
<tr key={cookie.key} className="border-b border-gray-200 dark:border-neutral-600 last:border-none">
|
||||
<td className="py-2 px-4 truncate">
|
||||
<span id={`cookie-key-${cookie.key}`}>{cookie.key}</span>
|
||||
<Tooltip
|
||||
@@ -275,7 +271,8 @@ const CollectionProperties = ({ onClose }) => {
|
||||
e.stopPropagation();
|
||||
handleEditCookie(domainWithCookies.domain, cookie);
|
||||
}}
|
||||
className="edit-button"
|
||||
className="text-gray-700 hover:text-gray-950
|
||||
dark:text-white dark:hover:text-gray-300"
|
||||
>
|
||||
<IconEdit strokeWidth={1.5} size={16} />
|
||||
</button>
|
||||
@@ -284,7 +281,7 @@ const CollectionProperties = ({ onClose }) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteCookie(domainWithCookies.domain, cookie.path, cookie.key);
|
||||
}}
|
||||
className="delete-button"
|
||||
className="text-gray-950 dark:text-white dark:hover:hover:text-red-600 hover:text-red-600"
|
||||
>
|
||||
<IconTrash strokeWidth={1.5} size={16} />
|
||||
</button>
|
||||
|
||||
@@ -1,246 +0,0 @@
|
||||
import React, { useState, useRef, useCallback, useMemo } from 'react';
|
||||
import { IconPlus, IconApi, IconBrandGraphql, IconPlugConnected, IconCode } from '@tabler/icons';
|
||||
import ActionIcon from 'ui/ActionIcon/index';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { newHttpRequest, newGrpcRequest, newWsRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { sanitizeName } from 'utils/common/regex';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { flattenItems, isItemARequest, isItemTransientRequest } from 'utils/collections';
|
||||
import filter from 'lodash/filter';
|
||||
import { get } from 'lodash';
|
||||
import { formatIpcError } from 'utils/common/error';
|
||||
|
||||
const REQUEST_TYPE = {
|
||||
HTTP: 'http',
|
||||
GRAPHQL: 'graphql',
|
||||
GRPC: 'grpc',
|
||||
WEBSOCKET: 'websocket'
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a request name for transient requests in the pattern "Untitled {Count}"
|
||||
* @param {Object} collection - The collection object
|
||||
* @returns {string} A request name like "Untitled 1", "Untitled 2", etc.
|
||||
*/
|
||||
const generateTransientRequestName = (collection) => {
|
||||
if (!collection || !collection.items) {
|
||||
return 'Untitled 1';
|
||||
}
|
||||
const allItems = flattenItems(collection.items);
|
||||
const transientRequests = filter(allItems, (item) => {
|
||||
return isItemTransientRequest(item);
|
||||
});
|
||||
|
||||
// Find the highest "Untitled X" number among transient requests
|
||||
let maxNumber = 0;
|
||||
transientRequests.forEach((item) => {
|
||||
const match = item.name?.match(/^Untitled (\d+)$/);
|
||||
if (match) {
|
||||
const number = parseInt(match[1], 10);
|
||||
if (number > maxNumber) {
|
||||
maxNumber = number;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Increment from the highest number found, or start at 1 if none found
|
||||
const count = maxNumber + 1;
|
||||
|
||||
return `Untitled ${count}`;
|
||||
};
|
||||
|
||||
const CreateTransientRequest = ({ collectionUid }) => {
|
||||
const [dropdownVisible, setDropdownVisible] = useState(false);
|
||||
const dropdownTippyRef = useRef();
|
||||
const dispatch = useDispatch();
|
||||
const collections = useSelector((state) => state.collections.collections);
|
||||
|
||||
const collection = useMemo(() => {
|
||||
return collections?.find((c) => c.uid === collectionUid);
|
||||
}, [collections, collectionUid]);
|
||||
|
||||
const collectionPresets = useMemo(() => {
|
||||
return get(collection, collection?.draft?.brunoConfig ? 'draft.brunoConfig.presets' : 'brunoConfig.presets', {
|
||||
requestType: 'http',
|
||||
requestUrl: ''
|
||||
});
|
||||
}, [collection]);
|
||||
|
||||
const onDropdownCreate = (ref) => {
|
||||
dropdownTippyRef.current = ref;
|
||||
if (ref) {
|
||||
ref.setProps({
|
||||
onHide: () => {
|
||||
setDropdownVisible(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleLeftClick = () => {
|
||||
handleItemClick(collectionPresets.requestType);
|
||||
};
|
||||
|
||||
const handleRightClick = (e) => {
|
||||
e.preventDefault();
|
||||
setDropdownVisible(true);
|
||||
};
|
||||
|
||||
const handleCreateHttpRequest = useCallback(() => {
|
||||
if (!collection) return;
|
||||
|
||||
const uniqueName = generateTransientRequestName(collection);
|
||||
const filename = sanitizeName(uniqueName);
|
||||
|
||||
dispatch(
|
||||
newHttpRequest({
|
||||
requestName: uniqueName,
|
||||
filename: filename,
|
||||
requestType: 'http-request',
|
||||
requestUrl: collectionPresets.requestUrl,
|
||||
requestMethod: 'GET',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: null,
|
||||
isTransient: true
|
||||
})
|
||||
).catch((err) => toast.error(formatIpcError(err) || 'An error occurred while adding the request'));
|
||||
}, [dispatch, collection, collectionPresets.requestUrl]);
|
||||
|
||||
const handleCreateGraphQLRequest = useCallback(() => {
|
||||
if (!collection) return;
|
||||
|
||||
const uniqueName = generateTransientRequestName(collection);
|
||||
const filename = sanitizeName(uniqueName);
|
||||
|
||||
dispatch(
|
||||
newHttpRequest({
|
||||
requestName: uniqueName,
|
||||
filename: filename,
|
||||
requestType: 'graphql-request',
|
||||
requestUrl: collectionPresets.requestUrl,
|
||||
requestMethod: 'POST',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: null,
|
||||
isTransient: true,
|
||||
body: {
|
||||
mode: 'graphql',
|
||||
graphql: {
|
||||
query: '',
|
||||
variables: ''
|
||||
}
|
||||
}
|
||||
})
|
||||
).catch((err) => toast.error(formatIpcError(err) || 'An error occurred while adding the request'));
|
||||
}, [dispatch, collection, collectionPresets.requestUrl]);
|
||||
|
||||
const handleCreateWebSocketRequest = useCallback(() => {
|
||||
if (!collection) return;
|
||||
|
||||
const uniqueName = generateTransientRequestName(collection);
|
||||
const filename = sanitizeName(uniqueName);
|
||||
|
||||
dispatch(
|
||||
newWsRequest({
|
||||
requestName: uniqueName,
|
||||
filename: filename,
|
||||
requestUrl: collectionPresets.requestUrl,
|
||||
requestMethod: 'ws',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: null,
|
||||
isTransient: true
|
||||
})
|
||||
).catch((err) => toast.error(formatIpcError(err) || 'An error occurred while adding the request'));
|
||||
}, [dispatch, collection, collectionPresets.requestUrl]);
|
||||
|
||||
const handleCreateGrpcRequest = useCallback(() => {
|
||||
if (!collection) return;
|
||||
|
||||
const uniqueName = generateTransientRequestName(collection);
|
||||
const filename = sanitizeName(uniqueName);
|
||||
|
||||
dispatch(
|
||||
newGrpcRequest({
|
||||
requestName: uniqueName,
|
||||
filename: filename,
|
||||
requestUrl: collectionPresets.requestUrl,
|
||||
collectionUid: collection.uid,
|
||||
itemUid: null,
|
||||
isTransient: true
|
||||
})
|
||||
).catch((err) => toast.error(formatIpcError(err) || 'An error occurred while adding the request'));
|
||||
}, [dispatch, collection, collectionPresets.requestUrl]);
|
||||
|
||||
const handleItemClick = (type) => {
|
||||
if (dropdownTippyRef.current) {
|
||||
dropdownTippyRef.current.hide();
|
||||
}
|
||||
switch (type) {
|
||||
case REQUEST_TYPE.HTTP:
|
||||
handleCreateHttpRequest();
|
||||
break;
|
||||
case REQUEST_TYPE.GRAPHQL:
|
||||
handleCreateGraphQLRequest();
|
||||
break;
|
||||
case REQUEST_TYPE.GRPC:
|
||||
handleCreateGrpcRequest();
|
||||
break;
|
||||
case REQUEST_TYPE.WEBSOCKET:
|
||||
handleCreateWebSocketRequest();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
if (!collection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const IconButton = (
|
||||
<ActionIcon
|
||||
onClick={handleLeftClick}
|
||||
onContextMenu={handleRightClick}
|
||||
aria-label="New Transient Request"
|
||||
size="lg"
|
||||
style={{ marginBottom: '3px' }}
|
||||
>
|
||||
<IconPlus size={18} strokeWidth={1.5} />
|
||||
</ActionIcon>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
icon={IconButton}
|
||||
visible={dropdownVisible}
|
||||
onCreate={onDropdownCreate}
|
||||
onClickOutside={() => setDropdownVisible(false)}
|
||||
placement="bottom-end"
|
||||
>
|
||||
<div className="dropdown-item" onClick={() => handleItemClick(REQUEST_TYPE.HTTP)}>
|
||||
<div className="dropdown-icon">
|
||||
<IconApi size={16} strokeWidth={2} />
|
||||
</div>
|
||||
<div className="dropdown-label">HTTP</div>
|
||||
</div>
|
||||
<div className="dropdown-item" onClick={() => handleItemClick(REQUEST_TYPE.GRAPHQL)}>
|
||||
<div className="dropdown-icon">
|
||||
<IconBrandGraphql size={16} strokeWidth={2} />
|
||||
</div>
|
||||
<div className="dropdown-label">GraphQL</div>
|
||||
</div>
|
||||
<div className="dropdown-item" onClick={() => handleItemClick(REQUEST_TYPE.GRPC)}>
|
||||
<div className="dropdown-icon">
|
||||
<IconCode size={16} strokeWidth={2} />
|
||||
</div>
|
||||
<div className="dropdown-label">gRPC</div>
|
||||
</div>
|
||||
<div className="dropdown-item" onClick={() => handleItemClick(REQUEST_TYPE.WEBSOCKET)}>
|
||||
<div className="dropdown-icon">
|
||||
<IconPlugConnected size={16} strokeWidth={2} />
|
||||
</div>
|
||||
<div className="dropdown-label">WebSocket</div>
|
||||
</div>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateTransientRequest;
|
||||
@@ -1,19 +1,26 @@
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import MenuDropdown from 'ui/MenuDropdown';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { newHttpRequest, newGrpcRequest, newWsRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { generateUniqueRequestName } from 'utils/collections';
|
||||
import { sanitizeName } from 'utils/common/regex';
|
||||
import toast from 'react-hot-toast';
|
||||
import { IconApi, IconBrandGraphql, IconPlugConnected, IconCode, IconPlus } from '@tabler/icons';
|
||||
import ActionIcon from 'ui/ActionIcon';
|
||||
|
||||
const CreateUntitledRequest = ({ collectionUid, itemUid = null, onRequestCreated, placement = 'bottom' }) => {
|
||||
const dispatch = useDispatch();
|
||||
const collections = useSelector((state) => state.collections.collections);
|
||||
const collection = collections?.find((c) => c.uid === collectionUid);
|
||||
const dropdownTippyRef = useRef();
|
||||
|
||||
const handleCreateHttpRequest = useCallback(async () => {
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
if (!collection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleCreateHttpRequest = async () => {
|
||||
dropdownTippyRef.current?.hide();
|
||||
const uniqueName = await generateUniqueRequestName(collection, 'Untitled', itemUid);
|
||||
const filename = sanitizeName(uniqueName);
|
||||
|
||||
@@ -33,9 +40,10 @@ const CreateUntitledRequest = ({ collectionUid, itemUid = null, onRequestCreated
|
||||
onRequestCreated?.();
|
||||
})
|
||||
.catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));
|
||||
}, [dispatch, collection, itemUid, onRequestCreated]);
|
||||
};
|
||||
|
||||
const handleCreateGraphQLRequest = useCallback(async () => {
|
||||
const handleCreateGraphQLRequest = async () => {
|
||||
dropdownTippyRef.current?.hide();
|
||||
const uniqueName = await generateUniqueRequestName(collection, 'Untitled', itemUid);
|
||||
const filename = sanitizeName(uniqueName);
|
||||
|
||||
@@ -62,9 +70,10 @@ const CreateUntitledRequest = ({ collectionUid, itemUid = null, onRequestCreated
|
||||
onRequestCreated?.();
|
||||
})
|
||||
.catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));
|
||||
}, [dispatch, collection, itemUid, onRequestCreated]);
|
||||
};
|
||||
|
||||
const handleCreateWebSocketRequest = useCallback(async () => {
|
||||
const handleCreateWebSocketRequest = async () => {
|
||||
dropdownTippyRef.current?.hide();
|
||||
const uniqueName = await generateUniqueRequestName(collection, 'Untitled', itemUid);
|
||||
const filename = sanitizeName(uniqueName);
|
||||
|
||||
@@ -83,9 +92,10 @@ const CreateUntitledRequest = ({ collectionUid, itemUid = null, onRequestCreated
|
||||
onRequestCreated?.();
|
||||
})
|
||||
.catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));
|
||||
}, [dispatch, collection, itemUid, onRequestCreated]);
|
||||
};
|
||||
|
||||
const handleCreateGrpcRequest = useCallback(async () => {
|
||||
const handleCreateGrpcRequest = async () => {
|
||||
dropdownTippyRef.current?.hide();
|
||||
const uniqueName = await generateUniqueRequestName(collection, 'Untitled', itemUid);
|
||||
const filename = sanitizeName(uniqueName);
|
||||
|
||||
@@ -103,49 +113,59 @@ const CreateUntitledRequest = ({ collectionUid, itemUid = null, onRequestCreated
|
||||
onRequestCreated?.();
|
||||
})
|
||||
.catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));
|
||||
}, [dispatch, collection, itemUid, onRequestCreated]);
|
||||
|
||||
const menuItems = useMemo(() => [
|
||||
{
|
||||
id: 'http',
|
||||
label: 'HTTP',
|
||||
leftSection: <IconApi size={16} strokeWidth={2} />,
|
||||
onClick: handleCreateHttpRequest
|
||||
},
|
||||
{
|
||||
id: 'graphql',
|
||||
label: 'GraphQL',
|
||||
leftSection: <IconBrandGraphql size={16} strokeWidth={2} />,
|
||||
onClick: handleCreateGraphQLRequest
|
||||
},
|
||||
{
|
||||
id: 'websocket',
|
||||
label: 'WebSocket',
|
||||
leftSection: <IconPlugConnected size={16} strokeWidth={2} />,
|
||||
onClick: handleCreateWebSocketRequest
|
||||
},
|
||||
{
|
||||
id: 'grpc',
|
||||
label: 'gRPC',
|
||||
leftSection: <IconCode size={16} strokeWidth={2} />,
|
||||
onClick: handleCreateGrpcRequest
|
||||
}
|
||||
], [handleCreateHttpRequest, handleCreateGraphQLRequest, handleCreateWebSocketRequest, handleCreateGrpcRequest]);
|
||||
|
||||
if (!collection) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<MenuDropdown
|
||||
items={menuItems}
|
||||
placement={placement}
|
||||
autoFocusFirstOption={true}
|
||||
>
|
||||
<ActionIcon size="sm">
|
||||
<IconPlus size={16} strokeWidth={2} />
|
||||
</ActionIcon>
|
||||
</MenuDropdown>
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<IconPlus size={16} strokeWidth={2} />} placement={placement}>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleCreateHttpRequest();
|
||||
}}
|
||||
>
|
||||
<span className="dropdown-icon">
|
||||
<IconApi size={16} strokeWidth={2} />
|
||||
</span>
|
||||
HTTP
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleCreateGraphQLRequest();
|
||||
}}
|
||||
>
|
||||
<span className="dropdown-icon">
|
||||
<IconBrandGraphql size={16} strokeWidth={2} />
|
||||
</span>
|
||||
GraphQL
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleCreateWebSocketRequest();
|
||||
}}
|
||||
>
|
||||
<span className="dropdown-icon">
|
||||
<IconPlugConnected size={16} strokeWidth={2} />
|
||||
</span>
|
||||
WebSocket
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleCreateGrpcRequest();
|
||||
}}
|
||||
>
|
||||
<span className="dropdown-icon">
|
||||
<IconCode size={16} strokeWidth={2} />
|
||||
</span>
|
||||
gRPC
|
||||
</div>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user