mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-03 01:18:32 +00:00
Compare commits
217 Commits
feat/updat
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51312a3148 | ||
|
|
6e6804055d | ||
|
|
5904c36cdb | ||
|
|
8c997c46af | ||
|
|
700e25a1d5 | ||
|
|
c9059c9905 | ||
|
|
416b693afc | ||
|
|
bafb235e72 | ||
|
|
06dd5c14d5 | ||
|
|
0f0c2b5912 | ||
|
|
6664295b2b | ||
|
|
679cb91549 | ||
|
|
b9d9a27599 | ||
|
|
1fc703e4e3 | ||
|
|
89a0494e7e | ||
|
|
04806144a5 | ||
|
|
0c3d20b198 | ||
|
|
3ddf8e2a8b | ||
|
|
f10422cca6 | ||
|
|
ba166561cc | ||
|
|
3112380289 | ||
|
|
559946bcce | ||
|
|
e1c01ebe18 | ||
|
|
eb5dc12b43 | ||
|
|
a04ff3e819 | ||
|
|
5a6714f085 | ||
|
|
214e1434e5 | ||
|
|
3996b86bcb | ||
|
|
8fface4bbe | ||
|
|
b268aa9f98 | ||
|
|
df3ff5e48a | ||
|
|
6ca5c71f7a | ||
|
|
ca4d0dd40b | ||
|
|
4b724ebd85 | ||
|
|
b3a66e9c3c | ||
|
|
4f327b7b77 | ||
|
|
579cda1d1a | ||
|
|
61199fb966 | ||
|
|
79daf7700f | ||
|
|
6e34fbd0ce | ||
|
|
1fcf9ecc32 | ||
|
|
fec407e2eb | ||
|
|
5044241d17 | ||
|
|
584344ac47 | ||
|
|
d975d0b642 | ||
|
|
af6908e9c0 | ||
|
|
21673f46de | ||
|
|
51276beaf1 | ||
|
|
7661af34c8 | ||
|
|
01b87ee71c | ||
|
|
9e1c58ab6f | ||
|
|
0fb605a684 | ||
|
|
5fd3948028 | ||
|
|
3e92c44a5a | ||
|
|
67c1d39e60 | ||
|
|
2288121f70 | ||
|
|
a22eb43a27 | ||
|
|
27b7fa81f2 | ||
|
|
1f571267b0 | ||
|
|
0b79ce9095 | ||
|
|
75e17610f0 | ||
|
|
acf576872c | ||
|
|
c94785f521 | ||
|
|
154c45d87d | ||
|
|
0bf169562b | ||
|
|
967b073ded | ||
|
|
725dfeacac | ||
|
|
923d26ce56 | ||
|
|
7e258003d5 | ||
|
|
7689288763 | ||
|
|
81faa57808 | ||
|
|
bac9616de4 | ||
|
|
9ab1ed3d90 | ||
|
|
408c9d4a4e | ||
|
|
ebafdd813c | ||
|
|
6642f4d0b0 | ||
|
|
4f75474c87 | ||
|
|
e5b7aa5ab4 | ||
|
|
875df38501 | ||
|
|
a724f010ff | ||
|
|
f9423d1238 | ||
|
|
51e36519f7 | ||
|
|
bd0894ede0 | ||
|
|
b1e6a707bf | ||
|
|
c51381888a | ||
|
|
8b1b18cc39 | ||
|
|
707ed63be6 | ||
|
|
bc0bb64400 | ||
|
|
4c110900c1 | ||
|
|
65ed6d3cfb | ||
|
|
7b28b05bc1 | ||
|
|
b0f27d01b9 | ||
|
|
3351bf990a | ||
|
|
07fff423bb | ||
|
|
36d10ab480 | ||
|
|
c918c679d7 | ||
|
|
7e3386b1b8 | ||
|
|
f4162e1ce6 | ||
|
|
e6a48a73bf | ||
|
|
fceb99edc2 | ||
|
|
ebc105d42e | ||
|
|
32d56f6942 | ||
|
|
e4a1fca3b1 | ||
|
|
59ff9bdafb | ||
|
|
071ee9ab2e | ||
|
|
176646f983 | ||
|
|
d76a574c51 | ||
|
|
734ee16fe1 | ||
|
|
33594bdcec | ||
|
|
2acfe60a5f | ||
|
|
aecaab84dd | ||
|
|
45264bfcc5 | ||
|
|
b01b8d7bc4 | ||
|
|
58a38ac5a1 | ||
|
|
7328988e59 | ||
|
|
39a6fc837d | ||
|
|
5b1b1b5541 | ||
|
|
578fa72dc8 | ||
|
|
4708e8e589 | ||
|
|
2a9386ef6b | ||
|
|
9483dbf4af | ||
|
|
0b436e2c9f | ||
|
|
9005e17eb5 | ||
|
|
efe94d9c90 | ||
|
|
848825f16a | ||
|
|
4d60425a05 | ||
|
|
c03fe301f8 | ||
|
|
c8371410c2 | ||
|
|
dcbf38bf61 | ||
|
|
a57ecde1d0 | ||
|
|
791843174e | ||
|
|
1174f22d88 | ||
|
|
8300abe086 | ||
|
|
a3809ce4b9 | ||
|
|
adb46110dd | ||
|
|
7cc4c0993e | ||
|
|
1030d02ac7 | ||
|
|
d616be7271 | ||
|
|
afd49d146f | ||
|
|
97e43c4489 | ||
|
|
f9af22d586 | ||
|
|
8590bacd79 | ||
|
|
a7d1a349e3 | ||
|
|
d03d8f01a1 | ||
|
|
97c700beba | ||
|
|
b6a27bc66c | ||
|
|
76a2889206 | ||
|
|
d506c37516 | ||
|
|
0c4ad0ed60 | ||
|
|
30dbe34e2e | ||
|
|
c83c055654 | ||
|
|
33f47ca5e4 | ||
|
|
475707848e | ||
|
|
2d76c444f6 | ||
|
|
33361ba659 | ||
|
|
1e3a0d9af3 | ||
|
|
2a5ec854cc | ||
|
|
d758645144 | ||
|
|
902d9ff968 | ||
|
|
781def844d | ||
|
|
15065eb2f1 | ||
|
|
481fa4ed12 | ||
|
|
a35b455041 | ||
|
|
bfc8968e24 | ||
|
|
f90f256f5f | ||
|
|
4253080f6b | ||
|
|
7a83641436 | ||
|
|
7105e4e8ed | ||
|
|
849465d62a | ||
|
|
058d2e0e61 | ||
|
|
c03c5eb927 | ||
|
|
156e798f90 | ||
|
|
a1a90d19e8 | ||
|
|
ec40d7fc85 | ||
|
|
5fda0866f8 | ||
|
|
f611c05fe8 | ||
|
|
b3764e1703 | ||
|
|
f961e692ee | ||
|
|
a7237c2e41 | ||
|
|
3ccaf29ddd | ||
|
|
002a5d16eb | ||
|
|
877b4dcf3a | ||
|
|
b9856f8c64 | ||
|
|
e63bac6ce4 | ||
|
|
23e809e827 | ||
|
|
1a4a30c8f2 | ||
|
|
2c973bbd35 | ||
|
|
8e74fa6233 | ||
|
|
1ec8f55a9e | ||
|
|
1ae05dfb0e | ||
|
|
72f186b38a | ||
|
|
ea1002c7a0 | ||
|
|
89ed1da4de | ||
|
|
4d519df8bc | ||
|
|
71413b9154 | ||
|
|
ce9773b7c9 | ||
|
|
8a394cdafc | ||
|
|
ddc88b3b05 | ||
|
|
746a49faed | ||
|
|
2827a6f133 | ||
|
|
1ed9d61ee8 | ||
|
|
ac0b69787d | ||
|
|
ab04850367 | ||
|
|
1c9db0886d | ||
|
|
b75c9fdd6d | ||
|
|
27dff7567c | ||
|
|
8fa8ae5fed | ||
|
|
0848393319 | ||
|
|
76f8bce9ac | ||
|
|
676f8223ec | ||
|
|
36b0a90de3 | ||
|
|
d7cef7aa4e | ||
|
|
5dad137631 | ||
|
|
8b1d59fa74 | ||
|
|
3a6f2f26ee | ||
|
|
6f71717105 | ||
|
|
2b28d37c74 |
70
.github/scripts/comment-on-flaky-tests.js
vendored
Normal file
70
.github/scripts/comment-on-flaky-tests.js
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
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
Normal file
78
.github/scripts/detect-flaky-tests.js
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
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
Normal file
119
.github/workflows/flaky-test-detector.yml
vendored
Normal file
@@ -0,0 +1,119 @@
|
||||
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@v8
|
||||
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
|
||||
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -58,6 +58,8 @@ 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
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -48,12 +48,14 @@ yarn-error.log*
|
||||
bruno.iml
|
||||
.idea
|
||||
.vscode
|
||||
.cursor
|
||||
|
||||
# Playwright
|
||||
/blob-report/
|
||||
|
||||
# Development plan files
|
||||
CLAUDE.md
|
||||
AGENTS.md
|
||||
*.plan.md
|
||||
|
||||
# packages dist
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"printWidth": 120
|
||||
}
|
||||
@@ -66,7 +66,16 @@ 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.
|
||||
- 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
|
||||
|
||||
|
||||
## Readability and Abstractions
|
||||
|
||||
|
||||
BIN
assets/images/landing-2-dark.png
Normal file
BIN
assets/images/landing-2-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 346 KiB |
BIN
assets/images/landing-2-light.png
Normal file
BIN
assets/images/landing-2-light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 347 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 813 KiB After Width: | Height: | Size: 584 KiB |
@@ -3,7 +3,7 @@
|
||||
|
||||
### برونو - بيئة تطوير مفتوحة المصدر لاستكشاف واختبار واجهات برمجة التطبيقات (APIs).
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](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%bruno)
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](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%bruno)
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](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%bruno)
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](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%bruno)
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](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%bruno)
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](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%bruno)
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](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%bruno)
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](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%bruno)
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](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%bruno)
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](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%bruno)
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](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%bruno)
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](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%bruno)
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](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%bruno)
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](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%bruno)
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](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%bruno)
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](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%bruno)
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](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%bruno)
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](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%bruno)
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
569
package-lock.json
generated
569
package-lock.json
generated
@@ -29,7 +29,7 @@
|
||||
"@eslint/compat": "^1.3.2",
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@jest/globals": "^29.2.0",
|
||||
"@opencollection/types": "~0.5.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",
|
||||
@@ -42,6 +42,7 @@
|
||||
"@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",
|
||||
@@ -3771,6 +3772,13 @@
|
||||
"integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@epic-web/invariant": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz",
|
||||
"integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
||||
@@ -6100,9 +6108,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@opencollection/types": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@opencollection/types/-/types-0.5.0.tgz",
|
||||
"integrity": "sha512-9rpu5agMrMLcMVU2UgyV+PYV3Zf/sHBJDHMQoq8XiMEUH8lt9f7yGtlerm/9dS3SHMpGX4A8ik0OFtc0dX4r1Q==",
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@opencollection/types/-/types-0.7.0.tgz",
|
||||
"integrity": "sha512-CSwdaHNPa2bNNBAOy++t6W9gBTExUJZW3aPkWyhAjasusThbvjymD/0uCLR50gCXSs0ezv61jsd19m9x+2DMtQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -6458,56 +6466,6 @@
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@postman/form-data": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@postman/form-data/-/form-data-3.1.1.tgz",
|
||||
"integrity": "sha512-vjh8Q2a8S6UCm/KKs31XFJqEEgmbjBmpPNVV2eVav6905wyFAwaUOBGA1NPBI4ERH9MMZc6w0umFgM6WbEPMdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/@postman/tough-cookie": {
|
||||
"version": "4.1.3-postman.1",
|
||||
"resolved": "https://registry.npmjs.org/@postman/tough-cookie/-/tough-cookie-4.1.3-postman.1.tgz",
|
||||
"integrity": "sha512-txpgUqZOnWYnUHZpHjkfb0IwVH4qJmyq77pPnJLlfhMtdCLMFTEeQHlzQiK906aaNCe4NEB5fGJHo9uzGbFMeA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"psl": "^1.1.33",
|
||||
"punycode": "^2.1.1",
|
||||
"universalify": "^0.2.0",
|
||||
"url-parse": "^1.5.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@postman/tough-cookie/node_modules/universalify": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
|
||||
"integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@postman/tunnel-agent": {
|
||||
"version": "0.6.4",
|
||||
"resolved": "https://registry.npmjs.org/@postman/tunnel-agent/-/tunnel-agent-0.6.4.tgz",
|
||||
"integrity": "sha512-CJJlq8V7rNKhAw4sBfjixKpJW00SHqebqNUQKxMoepgeWZIbdPcD+rguRcivGhS4N12PymDcKgUgSD4rVC+RjQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@prantlf/jsonlint": {
|
||||
"version": "16.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@prantlf/jsonlint/-/jsonlint-16.0.0.tgz",
|
||||
@@ -11429,15 +11387,6 @@
|
||||
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asn1": {
|
||||
"version": "0.2.6",
|
||||
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
|
||||
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": "~2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/asn1.js": {
|
||||
"version": "4.10.1",
|
||||
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
|
||||
@@ -11476,6 +11425,7 @@
|
||||
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
|
||||
"integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
@@ -11626,15 +11576,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-sign2": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
|
||||
"integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/aws4": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz",
|
||||
@@ -12028,15 +11969,6 @@
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bcrypt-pbkdf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
||||
"integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"tweetnacl": "^0.14.3"
|
||||
}
|
||||
},
|
||||
"node_modules/big.js": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
|
||||
@@ -12229,15 +12161,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/brotli": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz",
|
||||
"integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/browserify-aes": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
|
||||
@@ -12750,12 +12673,6 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/caseless": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
|
||||
"integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/chai": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz",
|
||||
@@ -13963,22 +13880,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cross-env": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
|
||||
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz",
|
||||
"integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.1"
|
||||
"@epic-web/invariant": "^1.0.0",
|
||||
"cross-spawn": "^7.0.6"
|
||||
},
|
||||
"bin": {
|
||||
"cross-env": "src/bin/cross-env.js",
|
||||
"cross-env-shell": "src/bin/cross-env-shell.js"
|
||||
"cross-env": "dist/bin/cross-env.js",
|
||||
"cross-env-shell": "dist/bin/cross-env-shell.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.14",
|
||||
"npm": ">=6",
|
||||
"yarn": ">=1"
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-fetch": {
|
||||
@@ -14375,18 +14291,6 @@
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dashdash": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
|
||||
"integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"assert-plus": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/data-urls": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz",
|
||||
@@ -15110,22 +15014,6 @@
|
||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ecc-jsbn": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
|
||||
"integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jsbn": "~0.1.0",
|
||||
"safer-buffer": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ecc-jsbn/node_modules/jsbn": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
|
||||
"integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ecdsa-sig-formatter": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||
@@ -16424,12 +16312,6 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/extract-files": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/extract-files/-/extract-files-9.0.0.tgz",
|
||||
@@ -16555,6 +16437,7 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-levenshtein": {
|
||||
@@ -17037,15 +16920,6 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/forever-agent": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
|
||||
"integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/fork-ts-checker-webpack-plugin": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.1.0.tgz",
|
||||
@@ -17524,15 +17398,6 @@
|
||||
"node": ">=6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/getpass": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
|
||||
"integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"assert-plus": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/github-markdown-css": {
|
||||
"version": "5.8.1",
|
||||
"resolved": "https://registry.npmjs.org/github-markdown-css/-/github-markdown-css-5.8.1.tgz",
|
||||
@@ -17815,57 +17680,12 @@
|
||||
"@grpc/grpc-js": "^1.12.6"
|
||||
}
|
||||
},
|
||||
"node_modules/har-schema": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
|
||||
"integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/har-validator": {
|
||||
"version": "5.1.5",
|
||||
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz",
|
||||
"integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==",
|
||||
"deprecated": "this library is no longer supported",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "^6.12.3",
|
||||
"har-schema": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/har-validator-compiled": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/har-validator-compiled/-/har-validator-compiled-1.0.0.tgz",
|
||||
"integrity": "sha512-dher7nFSx+Ef6OoqVveLClh8itAR3vd8Qx70Lh/hEgP1iGeARAolbci7Y8JBrHIYgFCT6xRdvvL16AR9Zh07Dw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/har-validator/node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"json-schema-traverse": "^0.4.1",
|
||||
"uri-js": "^4.2.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/har-validator/node_modules/json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
@@ -18338,20 +18158,6 @@
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/http-signature": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz",
|
||||
"integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"assert-plus": "^1.0.0",
|
||||
"jsprim": "^2.0.2",
|
||||
"sshpk": "^1.14.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/http2-wrapper": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
|
||||
@@ -19168,12 +18974,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-typedarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
|
||||
"integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-valid-path": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-valid-path/-/is-valid-path-0.1.1.tgz",
|
||||
@@ -19236,12 +19036,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/isstream": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
|
||||
"integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/istanbul-lib-coverage": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
|
||||
@@ -20822,12 +20616,6 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/json-schema": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
|
||||
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
|
||||
"license": "(AFL-2.1 OR BSD-3-Clause)"
|
||||
},
|
||||
"node_modules/json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
@@ -20851,7 +20639,9 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
||||
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
|
||||
"license": "ISC"
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "2.2.3",
|
||||
@@ -20892,44 +20682,6 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/jsprim": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz",
|
||||
"integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==",
|
||||
"engines": [
|
||||
"node >=0.6.0"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"assert-plus": "1.0.0",
|
||||
"extsprintf": "1.3.0",
|
||||
"json-schema": "0.4.0",
|
||||
"verror": "1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jsprim/node_modules/extsprintf": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
|
||||
"integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==",
|
||||
"engines": [
|
||||
"node >=0.6.0"
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsprim/node_modules/verror": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
|
||||
"integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==",
|
||||
"engines": [
|
||||
"node >=0.6.0"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"assert-plus": "^1.0.0",
|
||||
"core-util-is": "1.0.2",
|
||||
"extsprintf": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jszip": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
||||
@@ -22030,15 +21782,6 @@
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mustache": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
|
||||
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mustache": "bin/mustache"
|
||||
}
|
||||
},
|
||||
"node_modules/mz": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
|
||||
@@ -22259,44 +22002,6 @@
|
||||
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-vault": {
|
||||
"version": "0.10.2",
|
||||
"resolved": "https://registry.npmjs.org/node-vault/-/node-vault-0.10.2.tgz",
|
||||
"integrity": "sha512-//uc9/YImE7Dx0QHdwMiAzLaOumiKUnOUP8DymgtkZ8nsq6/V2LKvEu6kw91Lcruw8lWUfj4DO7CIXNPRWBuuA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.4",
|
||||
"mustache": "^4.2.0",
|
||||
"postman-request": "^2.88.1-postman.33",
|
||||
"tv4": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-vault/node_modules/debug": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/node-vault/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
@@ -22367,15 +22072,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/oauth-sign": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
|
||||
"integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
@@ -23090,12 +22786,6 @@
|
||||
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/performance-now": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -24075,57 +23765,6 @@
|
||||
"node": ">=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postman-request": {
|
||||
"version": "2.88.1-postman.40",
|
||||
"resolved": "https://registry.npmjs.org/postman-request/-/postman-request-2.88.1-postman.40.tgz",
|
||||
"integrity": "sha512-uE4AiIqhjtHKp4pj9ei7fkdfNXEX9IqDBlK1plGAQne6y79UUlrTdtYLhwXoO0AMOvqyl9Ar+BU6Eo6P/MPgfg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@postman/form-data": "~3.1.1",
|
||||
"@postman/tough-cookie": "~4.1.3-postman.1",
|
||||
"@postman/tunnel-agent": "^0.6.4",
|
||||
"aws-sign2": "~0.7.0",
|
||||
"aws4": "^1.12.0",
|
||||
"brotli": "^1.3.3",
|
||||
"caseless": "~0.12.0",
|
||||
"combined-stream": "~1.0.6",
|
||||
"extend": "~3.0.2",
|
||||
"forever-agent": "~0.6.1",
|
||||
"har-validator": "~5.1.3",
|
||||
"http-signature": "~1.3.1",
|
||||
"is-typedarray": "~1.0.0",
|
||||
"isstream": "~0.1.2",
|
||||
"json-stringify-safe": "~5.0.1",
|
||||
"mime-types": "^2.1.35",
|
||||
"oauth-sign": "~0.9.0",
|
||||
"performance-now": "^2.1.0",
|
||||
"qs": "~6.5.3",
|
||||
"safe-buffer": "^5.1.2",
|
||||
"stream-length": "^1.0.2",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
}
|
||||
},
|
||||
"node_modules/postman-request/node_modules/qs": {
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz",
|
||||
"integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/postman-request/node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
@@ -24427,6 +24066,7 @@
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
|
||||
"integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"punycode": "^2.3.1"
|
||||
@@ -24471,6 +24111,7 @@
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -27386,37 +27027,6 @@
|
||||
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/sshpk": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz",
|
||||
"integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asn1": "~0.2.3",
|
||||
"assert-plus": "^1.0.0",
|
||||
"bcrypt-pbkdf": "^1.0.0",
|
||||
"dashdash": "^1.12.0",
|
||||
"ecc-jsbn": "~0.1.1",
|
||||
"getpass": "^0.1.1",
|
||||
"jsbn": "~0.1.0",
|
||||
"safer-buffer": "^2.0.2",
|
||||
"tweetnacl": "~0.14.0"
|
||||
},
|
||||
"bin": {
|
||||
"sshpk-conv": "bin/sshpk-conv",
|
||||
"sshpk-sign": "bin/sshpk-sign",
|
||||
"sshpk-verify": "bin/sshpk-verify"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sshpk/node_modules/jsbn": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
|
||||
"integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/stable": {
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
|
||||
@@ -27587,21 +27197,6 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/stream-length": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/stream-length/-/stream-length-1.0.2.tgz",
|
||||
"integrity": "sha512-aI+qKFiwoDV4rsXiS7WRoCt+v2RX1nUj17+KJC5r2gfh5xoSJIfP6Y3Do/HtvesFcTSWthIuJ3l1cvKQY/+nZg==",
|
||||
"license": "WTFPL",
|
||||
"dependencies": {
|
||||
"bluebird": "^2.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/stream-length/node_modules/bluebird": {
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz",
|
||||
"integrity": "sha512-UfFSr22dmHPQqPP9XWHRhq+gWnHCYguQGkXQlbyPtW5qTnhFWA8/iXg765tH0cAjy7l/zPJ1aBTO0g5XgA7kvQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/streamsearch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
|
||||
@@ -29184,12 +28779,6 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tweetnacl": {
|
||||
"version": "0.14.5",
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
||||
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
@@ -29454,6 +29043,7 @@
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
|
||||
"devOptional": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"punycode": "^2.1.0"
|
||||
@@ -30477,7 +30067,7 @@
|
||||
"polished": "^4.3.1",
|
||||
"posthog-node": "4.2.1",
|
||||
"prettier": "^2.7.1",
|
||||
"qs": "^6.11.0",
|
||||
"qs": "^6.14.1",
|
||||
"query-string": "^7.0.1",
|
||||
"react": "19.0.0",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
@@ -30492,6 +30082,7 @@
|
||||
"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",
|
||||
@@ -31863,6 +31454,25 @@
|
||||
"url": "https://opencollective.com/core-js"
|
||||
}
|
||||
},
|
||||
"packages/bruno-app/node_modules/cross-env": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
|
||||
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"cross-env": "src/bin/cross-env.js",
|
||||
"cross-env-shell": "src/bin/cross-env-shell.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.14",
|
||||
"npm": ">=6",
|
||||
"yarn": ">=1"
|
||||
}
|
||||
},
|
||||
"packages/bruno-app/node_modules/electron-to-chromium": {
|
||||
"version": "1.5.157",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.157.tgz",
|
||||
@@ -31932,6 +31542,31 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"packages/bruno-app/node_modules/qs": {
|
||||
"version": "6.14.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
|
||||
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"packages/bruno-app/node_modules/react-virtuoso": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-4.18.1.tgz",
|
||||
"integrity": "sha512-KF474cDwaSb9+SJ380xruBB4P+yGWcVkcu26HtMqYNMTYlYbrNy8vqMkE+GpAApPPufJqgOLMoWMFG/3pJMXUA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": ">=16 || >=17 || >= 18 || >= 19",
|
||||
"react-dom": ">=16 || >=17 || >= 18 || >=19"
|
||||
}
|
||||
},
|
||||
"packages/bruno-app/node_modules/semver": {
|
||||
"version": "7.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
|
||||
@@ -32017,7 +31652,7 @@
|
||||
"iconv-lite": "^0.6.3",
|
||||
"js-yaml": "^4.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
"qs": "^6.11.0",
|
||||
"qs": "^6.14.1",
|
||||
"socks-proxy-agent": "^8.0.2",
|
||||
"xmlbuilder": "^15.1.1",
|
||||
"yargs": "^17.6.2"
|
||||
@@ -33067,6 +32702,21 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"packages/bruno-cli/node_modules/qs": {
|
||||
"version": "6.14.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
|
||||
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"packages/bruno-common": {
|
||||
"name": "@usebruno/common",
|
||||
"version": "0.1.0",
|
||||
@@ -33081,6 +32731,7 @@
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@types/jest": "^29.5.14",
|
||||
"babel-jest": "^29.7.0",
|
||||
"form-data": "^4.0.0",
|
||||
"is-ip": "^5.0.1",
|
||||
"moment": "^2.29.4",
|
||||
"rollup": "3.29.5",
|
||||
@@ -33643,10 +33294,17 @@
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
},
|
||||
"packages/bruno-converters/node_modules/@opencollection/types": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@opencollection/types/-/types-0.5.0.tgz",
|
||||
"integrity": "sha512-9rpu5agMrMLcMVU2UgyV+PYV3Zf/sHBJDHMQoq8XiMEUH8lt9f7yGtlerm/9dS3SHMpGX4A8ik0OFtc0dX4r1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"packages/bruno-converters/node_modules/glob": {
|
||||
"version": "10.4.5",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
||||
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
@@ -33749,7 +33407,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"mime-types": "^2.1.35",
|
||||
"nanoid": "3.3.8",
|
||||
"qs": "^6.11.0",
|
||||
"qs": "^6.14.1",
|
||||
"socks-proxy-agent": "^8.0.2",
|
||||
"tough-cookie": "^6.0.0",
|
||||
"uuid": "^9.0.0",
|
||||
@@ -35235,6 +34893,21 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"packages/bruno-electron/node_modules/qs": {
|
||||
"version": "6.14.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
|
||||
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"packages/bruno-electron/node_modules/semver": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
@@ -35484,7 +35157,6 @@
|
||||
"moment": "^2.29.4",
|
||||
"nanoid": "3.3.8",
|
||||
"node-fetch": "^2.7.0",
|
||||
"node-vault": "^0.10.2",
|
||||
"path": "^0.12.7",
|
||||
"quickjs-emscripten": "^0.29.2",
|
||||
"tv4": "^1.3.0",
|
||||
@@ -35599,7 +35271,10 @@
|
||||
"debug": "^4.4.3",
|
||||
"google-protobuf": "^4.0.0",
|
||||
"grpc-js-reflection-client": "^1.3.0",
|
||||
"http-proxy-agent": "~7.0.2",
|
||||
"https-proxy-agent": "~7.0.6",
|
||||
"is-ip": "^5.0.1",
|
||||
"socks-proxy-agent": "~8.0.5",
|
||||
"system-ca": "^2.0.1",
|
||||
"tough-cookie": "^6.0.0",
|
||||
"ws": "^8.18.3"
|
||||
@@ -35699,9 +35374,7 @@
|
||||
"version": "0.7.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "3.3.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"nanoid": "3.3.8",
|
||||
"yup": "^0.32.11"
|
||||
}
|
||||
},
|
||||
|
||||
14
package.json
14
package.json
@@ -23,7 +23,7 @@
|
||||
"@eslint/compat": "^1.3.2",
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@jest/globals": "^29.2.0",
|
||||
"@opencollection/types": "~0.5.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",
|
||||
@@ -36,6 +36,7 @@
|
||||
"@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",
|
||||
@@ -54,12 +55,11 @@
|
||||
"scripts": {
|
||||
"setup": "node ./scripts/setup.js",
|
||||
"watch:converters": "npm run watch --workspace=packages/bruno-converters",
|
||||
"dev": "concurrently --kill-others \"npm run dev:web\" \"npm run dev:electron\"",
|
||||
"dev": "node ./scripts/dev.js",
|
||||
"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",
|
||||
@@ -78,12 +78,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",
|
||||
"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",
|
||||
"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",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"nano-staged": {
|
||||
@@ -102,4 +102,4 @@
|
||||
"dependencies": {
|
||||
"ajv": "^8.17.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
3
packages/bruno-app/.gitignore
vendored
3
packages/bruno-app/.gitignore
vendored
@@ -34,4 +34,5 @@ yarn-error.log*
|
||||
.next/
|
||||
dist/
|
||||
|
||||
.env
|
||||
.env
|
||||
storybook-static/
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"printWidth": 120
|
||||
}
|
||||
@@ -8,8 +8,6 @@
|
||||
"build": "rsbuild build -m production",
|
||||
"preview": "rsbuild preview",
|
||||
"test": "jest",
|
||||
"test:prettier": "prettier --check \"./src/**/*.{js,jsx,json,ts,tsx}\"",
|
||||
"prettier": "prettier --write \"./src/**/*.{js,jsx,json,ts,tsx}\"",
|
||||
"storybook": "storybook dev -p 6006 --config-dir storybook",
|
||||
"build-storybook": "storybook build --config-dir storybook"
|
||||
},
|
||||
@@ -69,7 +67,7 @@
|
||||
"polished": "^4.3.1",
|
||||
"posthog-node": "4.2.1",
|
||||
"prettier": "^2.7.1",
|
||||
"qs": "^6.11.0",
|
||||
"qs": "^6.14.1",
|
||||
"query-string": "^7.0.1",
|
||||
"react": "19.0.0",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
@@ -84,6 +82,7 @@
|
||||
"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",
|
||||
@@ -131,4 +130,4 @@
|
||||
"form-data": "4.0.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,10 +55,10 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
.cm-variable-valid {
|
||||
color: green;
|
||||
color: ${(props) => props.theme.codemirror.variable.valid};
|
||||
}
|
||||
.cm-variable-invalid {
|
||||
color: red;
|
||||
color: ${(props) => props.theme.codemirror.variable.invalid};
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ const FileEditor = ({ apiSpec }) => {
|
||||
/>
|
||||
<IconDeviceFloppy
|
||||
onClick={onSave}
|
||||
color={hasChanges ? theme.colors.text.yellow : theme.requestTabs.icon.color}
|
||||
color={hasChanges ? theme.draftColor : theme.requestTabs.icon.color}
|
||||
strokeWidth={1.5}
|
||||
size={22}
|
||||
className={`absolute right-0 top-0 m-4 ${
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
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;
|
||||
154
packages/bruno-app/src/components/AppTitleBar/AppMenu/index.js
Normal file
154
packages/bruno-app/src/components/AppTitleBar/AppMenu/index.js
Normal file
@@ -0,0 +1,154 @@
|
||||
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;
|
||||
@@ -210,6 +210,10 @@ const Wrapper = styled.div`
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.app-menu {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
/* Custom window control buttons for Windows - always interactive, above modal overlay */
|
||||
.window-controls {
|
||||
display: flex;
|
||||
|
||||
@@ -17,10 +17,11 @@ 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 { toTitleCase } from 'utils/common/index';
|
||||
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';
|
||||
@@ -29,6 +30,12 @@ const getOsClass = () => {
|
||||
return 'os-other';
|
||||
};
|
||||
|
||||
// Helper to get display name for workspace
|
||||
export const getWorkspaceDisplayName = (name) => {
|
||||
if (!name) return 'Untitled Workspace';
|
||||
return name;
|
||||
};
|
||||
|
||||
const AppTitleBar = () => {
|
||||
const dispatch = useDispatch();
|
||||
const [isFullScreen, setIsFullScreen] = useState(false);
|
||||
@@ -115,7 +122,7 @@ const AppTitleBar = () => {
|
||||
const WorkspaceName = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="workspace-name-container" {...props}>
|
||||
<span className="workspace-name">{toTitleCase(activeWorkspace?.name) || 'Default Workspace'}</span>
|
||||
<span data-testid="workspace-name" className={classNames('workspace-name', { 'italic text-muted': !activeWorkspace?.name })}>{getWorkspaceDisplayName(activeWorkspace?.name)}</span>
|
||||
<IconChevronDown size={14} stroke={1.5} className="chevron-icon" />
|
||||
</div>
|
||||
);
|
||||
@@ -127,7 +134,7 @@ const AppTitleBar = () => {
|
||||
|
||||
const handleWorkspaceSwitch = (workspaceUid) => {
|
||||
dispatch(switchWorkspace(workspaceUid));
|
||||
toast.success(`Switched to ${workspaces.find((w) => w.uid === workspaceUid)?.name}`);
|
||||
toast.success(`Switched to ${getWorkspaceDisplayName(workspaces.find((w) => w.uid === workspaceUid)?.name)}`);
|
||||
};
|
||||
|
||||
const handleOpenWorkspace = async () => {
|
||||
@@ -178,7 +185,7 @@ const AppTitleBar = () => {
|
||||
|
||||
return {
|
||||
id: workspace.uid,
|
||||
label: toTitleCase(workspace.name),
|
||||
label: getWorkspaceDisplayName(workspace.name),
|
||||
onClick: () => handleWorkspaceSwitch(workspace.uid),
|
||||
className: `workspace-item ${isActive ? 'active' : ''}`,
|
||||
rightSection: (
|
||||
@@ -190,11 +197,7 @@ 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" />}
|
||||
@@ -245,14 +248,10 @@ const AppTitleBar = () => {
|
||||
)}
|
||||
|
||||
<div className="titlebar-content">
|
||||
{/* Left section: Home + Workspace */}
|
||||
<div className="titlebar-left">
|
||||
<ActionIcon
|
||||
onClick={handleHomeClick}
|
||||
label="Home"
|
||||
size="lg"
|
||||
className="home-button"
|
||||
>
|
||||
{showWindowControls && <AppMenu />}
|
||||
|
||||
<ActionIcon onClick={handleHomeClick} label="Home" size="lg" className="home-button">
|
||||
<IconHome size={16} stroke={1.5} />
|
||||
</ActionIcon>
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
.selected-body-mode {
|
||||
color: ${(props) => props.theme.brand};
|
||||
color: ${(props) => props.theme.primary.text};
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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';
|
||||
@@ -21,7 +22,8 @@ const BulkEditor = ({ params, onChange, onToggle, onSave, onRun }) => {
|
||||
<CodeEditor
|
||||
mode="text/plain"
|
||||
theme={displayedTheme}
|
||||
font={preferences.codeFont || 'default'}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
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: #d32f2f !important;
|
||||
color: ${(props) => props.theme.colors.text.danger} !important;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-line-warning .CodeMirror-linenumber {
|
||||
color: #f57c00 !important;
|
||||
color: ${(props) => props.theme.colors.text.warning} !important;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@@ -138,10 +138,10 @@ const StyledWrapper = styled.div`
|
||||
|
||||
/* Variable validation colors */
|
||||
.cm-variable-valid {
|
||||
color: #5fad89 !important; /* Soft sage */
|
||||
color: ${(props) => props.theme.codemirror.variable.valid} !important;
|
||||
}
|
||||
.cm-variable-invalid {
|
||||
color: #d17b7b !important; /* Soft coral */
|
||||
color: ${(props) => props.theme.codemirror.variable.invalid} !important;
|
||||
}
|
||||
|
||||
.CodeMirror-search-hint {
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { createRef } from 'react';
|
||||
import { isEqual, escapeRegExp } from 'lodash';
|
||||
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
|
||||
import { setupAutoComplete } from 'utils/codemirror/autocomplete';
|
||||
import { setupAutoComplete, showRootHints } 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';
|
||||
import CodeMirrorSearch from 'components/CodeMirrorSearch/index';
|
||||
|
||||
const CodeMirror = require('codemirror');
|
||||
window.jsonlint = jsonlint;
|
||||
@@ -34,6 +34,7 @@ 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,
|
||||
@@ -94,14 +95,14 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
},
|
||||
'Cmd-F': (cm) => {
|
||||
if (!this.state.searchBarVisible) {
|
||||
this.setState({ searchBarVisible: true });
|
||||
}
|
||||
this.setState({ searchBarVisible: true }, () => {
|
||||
this.searchBarRef.current?.focus();
|
||||
});
|
||||
},
|
||||
'Ctrl-F': (cm) => {
|
||||
if (!this.state.searchBarVisible) {
|
||||
this.setState({ searchBarVisible: true });
|
||||
}
|
||||
this.setState({ searchBarVisible: true }, () => {
|
||||
this.searchBarRef.current?.focus();
|
||||
});
|
||||
},
|
||||
'Cmd-H': 'replace',
|
||||
'Ctrl-H': 'replace',
|
||||
@@ -111,8 +112,12 @@ export default class CodeEditor extends React.Component {
|
||||
: cm.replaceSelection(' ', 'end');
|
||||
},
|
||||
'Shift-Tab': 'indentLess',
|
||||
'Ctrl-Space': 'autocomplete',
|
||||
'Cmd-Space': 'autocomplete',
|
||||
'Ctrl-Space': (cm) => {
|
||||
showRootHints(cm, this.props.showHintsFor);
|
||||
},
|
||||
'Cmd-Space': (cm) => {
|
||||
showRootHints(cm, this.props.showHintsFor);
|
||||
},
|
||||
'Ctrl-Y': 'foldAll',
|
||||
'Cmd-Y': 'foldAll',
|
||||
'Ctrl-I': 'unfoldAll',
|
||||
@@ -305,6 +310,10 @@ 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 })}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
||||
import React, { useState, useEffect, useRef, useCallback, useMemo, forwardRef, useImperativeHandle } 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 = ({ visible, editor, onClose }) => {
|
||||
const CodeMirrorSearch = forwardRef(({ visible, editor, onClose }, ref) => {
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [regex, setRegex] = useState(false);
|
||||
const [caseSensitive, setCaseSensitive] = useState(false);
|
||||
@@ -19,6 +19,7 @@ const CodeMirrorSearch = ({ visible, editor, onClose }) => {
|
||||
const searchMarks = useRef([]);
|
||||
const searchLineHighlight = useRef(null);
|
||||
const searchMatches = useRef([]);
|
||||
const inputRef = useRef(null);
|
||||
|
||||
const debouncedSearchText = useDebounce(searchText, 150);
|
||||
|
||||
@@ -106,6 +107,14 @@ const CodeMirrorSearch = ({ visible, editor, onClose }) => {
|
||||
}
|
||||
}, [debouncedSearchText, regex, caseSensitive, wholeWord, editor, memoizedMatches]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
doSearch(0, debouncedSearchText);
|
||||
}, [debouncedSearchText, doSearch]);
|
||||
@@ -168,6 +177,7 @@ const CodeMirrorSearch = ({ visible, editor, onClose }) => {
|
||||
<StyledWrapper>
|
||||
<div className="bruno-search-bar">
|
||||
<input
|
||||
ref={inputRef}
|
||||
autoFocus
|
||||
type="text"
|
||||
value={searchText}
|
||||
@@ -196,6 +206,6 @@ const CodeMirrorSearch = ({ visible, editor, onClose }) => {
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default CodeMirrorSearch;
|
||||
|
||||
@@ -14,7 +14,8 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
.auth-placement-selector {
|
||||
padding: 0.5rem 0px;
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
padding: 0.2rem 0px;
|
||||
border-radius: 3px;
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
|
||||
@@ -7,7 +7,7 @@ const Wrapper = styled.div`
|
||||
background: transparent;
|
||||
|
||||
.auth-mode-label {
|
||||
color: ${(props) => props.theme.brand};
|
||||
color: ${(props) => props.theme.primary.text};
|
||||
|
||||
.caret {
|
||||
color: rgb(140, 140, 140);
|
||||
|
||||
@@ -9,6 +9,8 @@ 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();
|
||||
@@ -55,17 +57,17 @@ const Docs = ({ collection }) => {
|
||||
<div className="flex flex-row gap-2 items-center justify-center">
|
||||
{isEditing ? (
|
||||
<>
|
||||
<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}>
|
||||
<Button type="button" color="secondary" onClick={handleDiscardChanges}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="button" onClick={onSave}>
|
||||
Save
|
||||
</button>
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<div className="editing-mode" role="tab" onClick={toggleViewMode}>
|
||||
<IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} />
|
||||
</div>
|
||||
<ActionIcon className="editing-mode" onClick={toggleViewMode}>
|
||||
<IconEdit className="cursor-pointer" size={16} strokeWidth={1.5} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ 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);
|
||||
|
||||
@@ -32,6 +33,22 @@ 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',
|
||||
@@ -39,7 +56,7 @@ const Headers = ({ collection }) => {
|
||||
isKeyField: true,
|
||||
placeholder: 'Name',
|
||||
width: '30%',
|
||||
render: ({ row, value, onChange, isLastEmptyRow }) => (
|
||||
render: ({ value, onChange }) => (
|
||||
<SingleLineEditor
|
||||
value={value || ''}
|
||||
theme={storedTheme}
|
||||
@@ -47,7 +64,7 @@ const Headers = ({ collection }) => {
|
||||
onChange={(newValue) => onChange(newValue.replace(/[\r\n]/g, ''))}
|
||||
autocomplete={headerAutoCompleteList}
|
||||
collection={collection}
|
||||
placeholder={isLastEmptyRow ? 'Name' : ''}
|
||||
placeholder={!value ? 'Name' : ''}
|
||||
/>
|
||||
)
|
||||
},
|
||||
@@ -55,7 +72,7 @@ const Headers = ({ collection }) => {
|
||||
key: 'value',
|
||||
name: 'Value',
|
||||
placeholder: 'Value',
|
||||
render: ({ row, value, onChange, isLastEmptyRow }) => (
|
||||
render: ({ value, onChange }) => (
|
||||
<SingleLineEditor
|
||||
value={value || ''}
|
||||
theme={storedTheme}
|
||||
@@ -63,7 +80,7 @@ const Headers = ({ collection }) => {
|
||||
onChange={onChange}
|
||||
collection={collection}
|
||||
autocomplete={MimeTypes}
|
||||
placeholder={isLastEmptyRow ? 'Value' : ''}
|
||||
placeholder={!value ? 'Value' : ''}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -101,6 +118,7 @@ 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}>
|
||||
|
||||
@@ -38,6 +38,15 @@ const StyledWrapper = styled.div`
|
||||
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};
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React from 'react';
|
||||
import { getTotalRequestCountInCollection } from 'utils/collections/';
|
||||
import { IconFolder, IconWorld, IconApi, IconShare } from '@tabler/icons';
|
||||
import { IconFolder, IconWorld, IconApi, IconShare, IconBook } 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';
|
||||
|
||||
@@ -15,6 +16,7 @@ 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);
|
||||
|
||||
@@ -67,7 +69,7 @@ const Info = ({ collection }) => {
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="text-sm text-link cursor-pointer hover:underline text-left bg-transparent"
|
||||
className="text-link cursor-pointer hover:underline text-left bg-transparent"
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
addTab({
|
||||
@@ -111,6 +113,19 @@ const Info = ({ collection }) => {
|
||||
</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>
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
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.border.BORDER0};
|
||||
border-left: 1px solid ${(props) => props.theme.border.BORDER0};
|
||||
border-right: 1px solid ${(props) => props.theme.border.BORDER0};
|
||||
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-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 {
|
||||
|
||||
@@ -41,8 +41,8 @@ const RequestsNotLoaded = ({ collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full card my-2">
|
||||
<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" />
|
||||
<div className="flex items-center gap-2 px-3 py-2 title">
|
||||
<IconAlertTriangle size={16} className="warning-icon" />
|
||||
<span className="font-medium">Following requests were not loaded</span>
|
||||
</div>
|
||||
<table className="w-full border-collapse">
|
||||
|
||||
@@ -36,7 +36,7 @@ const PresetsSettings = ({ collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="h-full w-full">
|
||||
<div className="text-xs mb-4 mt-4 text-muted">
|
||||
<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">
|
||||
|
||||
@@ -8,6 +8,151 @@ 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;
|
||||
|
||||
@@ -113,12 +113,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="font-medium flex items-center" htmlFor="protoFiles">
|
||||
<label className="flex items-center" htmlFor="protoFiles">
|
||||
Proto Files (
|
||||
{protoFiles.length}
|
||||
)
|
||||
<span id="proto-files-tooltip" className="ml-2">
|
||||
<IconAlertCircle size={16} className="text-gray-500 cursor-pointer" />
|
||||
<IconAlertCircle size={16} className="tooltip-icon" />
|
||||
</span>
|
||||
<Tooltip
|
||||
anchorId="proto-files-tooltip"
|
||||
@@ -131,7 +131,7 @@ const ProtobufSettings = ({ collection }) => {
|
||||
|
||||
<div>
|
||||
{protoFiles.some((file) => !file.exists) && (
|
||||
<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">
|
||||
<div className="error-message text-xs mb-2 flex items-center p-2" 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 +140,13 @@ const ProtobufSettings = ({ collection }) => {
|
||||
<table className="w-full border-collapse" data-testid="protobuf-proto-files-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<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>
|
||||
File
|
||||
</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>
|
||||
Path
|
||||
</th>
|
||||
<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">
|
||||
<th className="text-right">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
@@ -154,10 +154,10 @@ const ProtobufSettings = ({ collection }) => {
|
||||
<tbody>
|
||||
{protoFiles.length === 0 ? (
|
||||
<tr>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -167,27 +167,27 @@ const ProtobufSettings = ({ collection }) => {
|
||||
|
||||
return (
|
||||
<tr key={index}>
|
||||
<td className="border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<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">
|
||||
<IconFile size={16} className="file-icon mr-2" />
|
||||
<span className="file-name" data-testid="protobuf-proto-file-name">
|
||||
{getBasename(collection.pathname, file.path)}
|
||||
</span>
|
||||
{!isValid && <IconAlertCircle size={12} className="text-red-600 dark:text-red-400 ml-2" />}
|
||||
{!isValid && <IconAlertCircle size={12} className="invalid-indicator ml-2" />}
|
||||
</div>
|
||||
</td>
|
||||
<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">
|
||||
<td>
|
||||
<div className="path-text">
|
||||
{file.path}
|
||||
</div>
|
||||
</td>
|
||||
<td className="border border-gray-200 dark:border-gray-700 px-3 py-2 text-right">
|
||||
<td className="text-right">
|
||||
<div className="flex items-center justify-end space-x-1">
|
||||
{!isValid && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleReplaceProtoFile(index)}
|
||||
className="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300 p-1 rounded"
|
||||
className="action-button replace-button"
|
||||
title="Replace file"
|
||||
>
|
||||
<IconFileImport size={14} />
|
||||
@@ -196,7 +196,7 @@ const ProtobufSettings = ({ collection }) => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveProtoFile(index)}
|
||||
className="text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300 p-1 rounded"
|
||||
className="action-button remove-button"
|
||||
title="Remove file"
|
||||
data-testid="protobuf-remove-file-button"
|
||||
>
|
||||
@@ -220,12 +220,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="font-medium flex items-center" htmlFor="importPaths">
|
||||
<label className="flex items-center" htmlFor="importPaths">
|
||||
Import Paths (
|
||||
{importPaths.length}
|
||||
)
|
||||
<span id="import-paths-tooltip" className="ml-2">
|
||||
<IconAlertCircle size={16} className="text-gray-500 cursor-pointer" />
|
||||
<IconAlertCircle size={16} className="tooltip-icon" />
|
||||
</span>
|
||||
<Tooltip
|
||||
anchorId="import-paths-tooltip"
|
||||
@@ -238,7 +238,7 @@ const ProtobufSettings = ({ collection }) => {
|
||||
|
||||
<div>
|
||||
{importPaths.some((path) => !path.exists) && (
|
||||
<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">
|
||||
<div className="error-message text-xs mb-2 flex items-center p-2" 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 +247,15 @@ const ProtobufSettings = ({ collection }) => {
|
||||
<table className="w-full border-collapse" data-testid="protobuf-import-paths-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<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">
|
||||
<th>
|
||||
Directory
|
||||
</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>
|
||||
Path
|
||||
</th>
|
||||
<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">
|
||||
<th className="text-right">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
@@ -263,10 +263,10 @@ const ProtobufSettings = ({ collection }) => {
|
||||
<tbody>
|
||||
{importPaths.length === 0 ? (
|
||||
<tr>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -276,37 +276,37 @@ const ProtobufSettings = ({ collection }) => {
|
||||
|
||||
return (
|
||||
<tr key={index}>
|
||||
<td className="border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={importPath.enabled}
|
||||
onChange={() => handleToggleImportPath(index)}
|
||||
className="h-4 w-4 text-gray-600 focus:ring-gray-500 border-gray-300 dark:border-gray-600 rounded"
|
||||
className="h-4 w-4"
|
||||
title={importPath.enabled ? 'Disable this import path' : 'Enable this import path'}
|
||||
data-testid="protobuf-import-path-checkbox"
|
||||
/>
|
||||
</td>
|
||||
<td className="border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<IconFolder size={16} className="text-gray-500 dark:text-gray-400 mr-2" />
|
||||
<span className="font-medium text-gray-900 dark:text-gray-100">
|
||||
<IconFolder size={16} className="folder-icon mr-2" />
|
||||
<span className="directory-name">
|
||||
{getBasename(collection.pathname, importPath.path)}
|
||||
</span>
|
||||
{!isValid && <IconAlertCircle size={12} className="text-red-600 dark:text-red-400 ml-2" />}
|
||||
{!isValid && <IconAlertCircle size={12} className="invalid-indicator ml-2" />}
|
||||
</div>
|
||||
</td>
|
||||
<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">
|
||||
<td>
|
||||
<div className="path-text">
|
||||
{importPath.path}
|
||||
</div>
|
||||
</td>
|
||||
<td className="border border-gray-200 dark:border-gray-700 px-3 py-2 text-right">
|
||||
<td className="text-right">
|
||||
<div className="flex items-center justify-end space-x-1">
|
||||
{!isValid && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleReplaceImportPath(index)}
|
||||
className="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300 p-1 rounded"
|
||||
className="action-button replace-button"
|
||||
title="Replace directory"
|
||||
>
|
||||
<IconFileImport size={14} />
|
||||
@@ -315,7 +315,7 @@ const ProtobufSettings = ({ collection }) => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveImportPath(index)}
|
||||
className="text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300 p-1 rounded"
|
||||
className="action-button remove-button"
|
||||
title="Remove import path"
|
||||
data-testid="protobuf-remove-import-path-button"
|
||||
>
|
||||
|
||||
@@ -6,20 +6,38 @@ 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(() => {
|
||||
@@ -56,15 +74,21 @@ const Script = ({ collection }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col h-full pt-4">
|
||||
<StyledWrapper className="w-full flex flex-col h-full">
|
||||
<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</TabsTrigger>
|
||||
<TabsTrigger value="post-response">Post Response</TabsTrigger>
|
||||
<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>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="pre-request" className="mt-2">
|
||||
|
||||
@@ -19,6 +19,10 @@ 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;
|
||||
@@ -40,6 +44,11 @@ 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;
|
||||
|
||||
@@ -46,14 +46,14 @@ const VarsTable = ({ collection, vars, varType }) => {
|
||||
</div>
|
||||
),
|
||||
placeholder: varType === 'request' ? 'Value' : 'Expr',
|
||||
render: ({ row, value, onChange, isLastEmptyRow }) => (
|
||||
render: ({ value, onChange }) => (
|
||||
<MultiLineEditor
|
||||
value={value || ''}
|
||||
theme={storedTheme}
|
||||
onSave={onSave}
|
||||
onChange={onChange}
|
||||
collection={collection}
|
||||
placeholder={isLastEmptyRow ? (varType === 'request' ? 'Value' : 'Expr') : ''}
|
||||
placeholder={!value ? (varType === 'request' ? 'Value' : 'Expr') : ''}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ const Vars = ({ collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col">
|
||||
<div className="flex-1 mt-2">
|
||||
<div className="flex-1">
|
||||
<div className="mb-3 title text-xs">Pre Request</div>
|
||||
<VarsTable collection={collection} vars={requestVars} varType="request" />
|
||||
</div>
|
||||
|
||||
24
packages/bruno-app/src/components/ColorBadge/index.js
Normal file
24
packages/bruno-app/src/components/ColorBadge/index.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
|
||||
const ColorBadge = ({ color, size = 10, showEmptyBorder = true }) => {
|
||||
const sizeValue = typeof size === 'string' ? size : `${size}px`;
|
||||
const { theme } = useTheme();
|
||||
|
||||
const showBorder = !color && showEmptyBorder;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex-shrink-0 rounded-full"
|
||||
style={{
|
||||
width: sizeValue,
|
||||
height: sizeValue,
|
||||
backgroundColor: color || 'transparent',
|
||||
border: showBorder ? '1px solid' : 'none',
|
||||
borderColor: showBorder ? theme.background.surface1 : 'transparent'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ColorBadge;
|
||||
@@ -0,0 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
164
packages/bruno-app/src/components/ColorPicker/index.js
Normal file
164
packages/bruno-app/src/components/ColorPicker/index.js
Normal file
@@ -0,0 +1,164 @@
|
||||
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-2">
|
||||
<div
|
||||
className="w-4 h-4 rounded-full flex-shrink-0 cursor-pointer"
|
||||
style={{ backgroundColor: customColor }}
|
||||
onClick={() => handleColorSelect(customColor)}
|
||||
title="Custom color"
|
||||
/>
|
||||
<ColorRangePicker
|
||||
className="flex-1"
|
||||
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;
|
||||
@@ -0,0 +1,45 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.hue-slider {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
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;
|
||||
23
packages/bruno-app/src/components/ColorRange/index.js
Normal file
23
packages/bruno-app/src/components/ColorRange/index.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const ColorRangePicker = ({ selectedColor, className, value, onChange, colorRange, ...props }) => {
|
||||
return (
|
||||
<StyledWrapper color={selectedColor}>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
className={`hue-slider ${className}`}
|
||||
style={{
|
||||
background: `linear-gradient(to right, ${colorRange.join(',')})`
|
||||
}}
|
||||
title="Adjust color"
|
||||
{...props}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ColorRangePicker;
|
||||
@@ -1,5 +1,26 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div``;
|
||||
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};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -11,6 +11,7 @@ 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));
|
||||
@@ -225,145 +226,147 @@ const ModifyCookieModal = ({ onClose, domain, cookie }) => {
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<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"
|
||||
<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"
|
||||
/>
|
||||
</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 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>
|
||||
</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>
|
||||
)}
|
||||
{/* 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>
|
||||
</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="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>
|
||||
)}
|
||||
</form>
|
||||
</StyledWrapper>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -70,6 +70,92 @@ 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;
|
||||
|
||||
@@ -14,7 +14,7 @@ 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="text-yellow-600" />
|
||||
<IconAlertTriangle size={32} strokeWidth={1.5} className="warning-icon" />
|
||||
<h1 className="ml-2 text-lg font-medium">Hold on..</h1>
|
||||
</div>
|
||||
<div className="font-normal mt-4">
|
||||
@@ -23,12 +23,12 @@ const ClearDomainCookiesModal = ({ onClose, domain, onClear }) => (
|
||||
|
||||
<div className="flex justify-between mt-6">
|
||||
<div>
|
||||
<Button size="sm" color="secondary" variant="ghost" onClick={onClose}>
|
||||
<Button color="secondary" variant="ghost" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button size="sm" color="danger" onClick={onClear}>
|
||||
<Button color="danger" onClick={onClear}>
|
||||
Clear All
|
||||
</Button>
|
||||
</div>
|
||||
@@ -39,7 +39,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="text-yellow-600" />
|
||||
<IconAlertTriangle size={32} strokeWidth={1.5} className="warning-icon" />
|
||||
<h1 className="ml-2 text-lg font-medium">Hold on..</h1>
|
||||
</div>
|
||||
<div className="font-normal mt-4">
|
||||
@@ -48,12 +48,12 @@ const DeleteCookieModal = ({ onClose, cookieName, onDelete }) => (
|
||||
|
||||
<div className="flex justify-between mt-6">
|
||||
<div>
|
||||
<Button size="sm" color="secondary" variant="ghost" onClick={onClose}>
|
||||
<Button color="secondary" variant="ghost" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button size="sm" color="danger" onClick={onDelete}>
|
||||
<Button color="danger" onClick={onDelete}>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
@@ -159,9 +159,9 @@ 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="text-gray-500" />
|
||||
<IconCookieOff size={48} strokeWidth={1.5} className="empty-icon" />
|
||||
<h2 className="text-lg font-medium mt-4">No cookies found</h2>
|
||||
<p className="text-gray-500 mt-2">Add cookies to get started</p>
|
||||
<p className="empty-text mt-2">Add cookies to get started</p>
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
@@ -180,7 +180,7 @@ const CollectionProperties = ({ onClose }) => {
|
||||
<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="text-gray-500 mt-2">Try a different search term</p>
|
||||
<p className="empty-text mt-2">Try a different search term</p>
|
||||
</div>
|
||||
) : (
|
||||
// Show cookies list
|
||||
@@ -191,14 +191,14 @@ const CollectionProperties = ({ onClose }) => {
|
||||
<Accordion.Header index={i} className="flex items-center">
|
||||
<div className="flex items-center">
|
||||
<span>{domainWithCookies.domain}</span>
|
||||
<span className="ml-2 text-xs dark:text-gray-300 text-gray-500">
|
||||
<span className="domain-count ml-2 text-xs">
|
||||
({domainWithCookies.cookies.length}{' '}
|
||||
{domainWithCookies.cookies.length === 1 ? 'cookie' : 'cookies'})
|
||||
</span>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<button
|
||||
type="submit"
|
||||
className="flex items-center gap-1 text-gray-500 hover:text-gray-950 dark:text-white dark:hover:text-gray-300"
|
||||
className="action-button flex items-center gap-1"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleAddCookie(domainWithCookies.domain);
|
||||
@@ -211,7 +211,7 @@ const CollectionProperties = ({ onClose }) => {
|
||||
e.stopPropagation();
|
||||
handleClearDomainCookies(domainWithCookies.domain);
|
||||
}}
|
||||
className="text-gray-950 dark:text-white dark:hover:hover:text-red-600 hover:text-red-600 mr-2"
|
||||
className="action-button-danger mr-2"
|
||||
>
|
||||
<IconTrash strokeWidth={1.5} size={16} />
|
||||
</button>
|
||||
@@ -222,7 +222,7 @@ const CollectionProperties = ({ onClose }) => {
|
||||
<div className="flex items-center justify-between">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="text-left border-b border-gray-200 dark:border-neutral-600 text-gray-700 dark:text-gray-300">
|
||||
<tr className="text-left">
|
||||
<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 +234,7 @@ const CollectionProperties = ({ onClose }) => {
|
||||
</thead>
|
||||
<tbody>
|
||||
{domainWithCookies.cookies.map((cookie) => (
|
||||
<tr key={cookie.key} className="border-b border-gray-200 dark:border-neutral-600 last:border-none">
|
||||
<tr key={cookie.key}>
|
||||
<td className="py-2 px-4 truncate">
|
||||
<span id={`cookie-key-${cookie.key}`}>{cookie.key}</span>
|
||||
<Tooltip
|
||||
@@ -275,8 +275,7 @@ const CollectionProperties = ({ onClose }) => {
|
||||
e.stopPropagation();
|
||||
handleEditCookie(domainWithCookies.domain, cookie);
|
||||
}}
|
||||
className="text-gray-700 hover:text-gray-950
|
||||
dark:text-white dark:hover:text-gray-300"
|
||||
className="edit-button"
|
||||
>
|
||||
<IconEdit strokeWidth={1.5} size={16} />
|
||||
</button>
|
||||
@@ -285,7 +284,7 @@ const CollectionProperties = ({ onClose }) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteCookie(domainWithCookies.domain, cookie.path, cookie.key);
|
||||
}}
|
||||
className="text-gray-950 dark:text-white dark:hover:hover:text-red-600 hover:text-red-600"
|
||||
className="delete-button"
|
||||
>
|
||||
<IconTrash strokeWidth={1.5} size={16} />
|
||||
</button>
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
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';
|
||||
|
||||
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]);
|
||||
|
||||
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(err ? err.message : '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(err ? err.message : '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(err ? err.message : '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(err ? err.message : '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;
|
||||
@@ -38,20 +38,6 @@ const StyledWrapper = styled.div`
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.control-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: transparent;
|
||||
border: 1px solid ${(props) => props.theme.console.border};
|
||||
border-radius: 4px;
|
||||
color: ${(props) => props.theme.console.buttonColor};
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.debug-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -32,12 +32,6 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
}
|
||||
|
||||
.network-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.network-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
@@ -81,11 +75,10 @@ const StyledWrapper = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 80px 80px 150px 1fr 100px 80px 80px;
|
||||
gap: 12px;
|
||||
padding: 8px 16px;
|
||||
padding: 4px 16px;
|
||||
background: ${(props) => props.theme.console.headerBg};
|
||||
border-bottom: 1px solid ${(props) => props.theme.console.border};
|
||||
font-size: ${(props) => props.theme.font.size.xs};
|
||||
font-weight: 500;
|
||||
font-size: 10px;
|
||||
color: ${(props) => props.theme.console.titleColor};
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
@@ -103,8 +96,7 @@ const StyledWrapper = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 80px 80px 150px 1fr 100px 80px 80px;
|
||||
gap: 12px;
|
||||
padding: 6px 16px;
|
||||
border-bottom: 1px solid ${(props) => props.theme.console.border};
|
||||
padding: 2px 16px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.1s ease;
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
@@ -115,7 +107,8 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: ${(props) => props.theme.console.buttonHoverBg};
|
||||
padding-left: 13px;
|
||||
background: ${(props) => props.theme.console.logHoverBg};
|
||||
border-left: 3px solid ${(props) => props.theme.console.checkboxColor};
|
||||
}
|
||||
}
|
||||
@@ -123,25 +116,19 @@ const StyledWrapper = styled.div`
|
||||
.method-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
justify-content: start;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
min-width: 45px;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
font-weight: 500;
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
}
|
||||
|
||||
.request-domain {
|
||||
color: ${(props) => props.theme.console.messageColor};
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -174,120 +161,6 @@ const StyledWrapper = styled.div`
|
||||
font-size: ${(props) => props.theme.font.size.xs};
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.filter-dropdown {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.filter-dropdown-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 8px;
|
||||
background: transparent;
|
||||
border: 1px solid ${(props) => props.theme.console.border};
|
||||
border-radius: 4px;
|
||||
color: ${(props) => props.theme.console.buttonColor};
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.console.buttonHoverBg};
|
||||
color: ${(props) => props.theme.console.buttonHoverColor};
|
||||
}
|
||||
|
||||
.filter-summary {
|
||||
font-weight: 500;
|
||||
min-width: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-dropdown-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
right: 0;
|
||||
min-width: 200px;
|
||||
max-width: 250px;
|
||||
background: ${(props) => props.theme.console.dropdownBg};
|
||||
border: 1px solid ${(props) => props.theme.console.border};
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.filter-dropdown-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
background: ${(props) => props.theme.console.dropdownHeaderBg};
|
||||
border-bottom: 1px solid ${(props) => props.theme.console.border};
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
font-weight: 500;
|
||||
color: ${(props) => props.theme.console.titleColor};
|
||||
}
|
||||
|
||||
.filter-toggle-all {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: ${(props) => props.theme.console.buttonColor};
|
||||
cursor: pointer;
|
||||
font-size: ${(props) => props.theme.font.size.xs};
|
||||
font-weight: 500;
|
||||
padding: 2px 4px;
|
||||
border-radius: 2px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.console.buttonHoverBg};
|
||||
}
|
||||
}
|
||||
|
||||
.filter-dropdown-options {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.filter-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.console.optionHoverBg};
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
margin: 0 8px 0 0;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
accent-color: ${(props) => props.theme.console.checkboxColor};
|
||||
}
|
||||
}
|
||||
|
||||
.filter-option-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.filter-option-label {
|
||||
color: ${(props) => props.theme.console.optionLabelColor};
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.filter-option-count {
|
||||
color: ${(props) => props.theme.console.optionCountColor};
|
||||
font-size: ${(props) => props.theme.font.size.xs};
|
||||
font-weight: 400;
|
||||
margin-left: auto;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -1,128 +1,33 @@
|
||||
import React, { useState, useRef, useEffect, useMemo } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import {
|
||||
IconFilter,
|
||||
IconChevronDown,
|
||||
IconNetwork
|
||||
} from '@tabler/icons';
|
||||
import {
|
||||
updateNetworkFilter,
|
||||
toggleAllNetworkFilters,
|
||||
setSelectedRequest
|
||||
} from 'providers/ReduxStore/slices/logs';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const MethodBadge = ({ method }) => {
|
||||
const getMethodColor = (method) => {
|
||||
switch (method?.toUpperCase()) {
|
||||
case 'GET': return '#10b981';
|
||||
case 'POST': return '#8b5cf6';
|
||||
case 'PUT': return '#f59e0b';
|
||||
case 'DELETE': return '#ef4444';
|
||||
case 'PATCH': return '#06b6d4';
|
||||
case 'HEAD': return '#6b7280';
|
||||
case 'OPTIONS': return '#84cc16';
|
||||
default: return '#6b7280';
|
||||
}
|
||||
};
|
||||
const methodLower = method?.toLowerCase() || 'get';
|
||||
|
||||
return (
|
||||
<span
|
||||
className="method-badge"
|
||||
style={{ backgroundColor: getMethodColor(method) }}
|
||||
>
|
||||
<span className={`method-badge ${methodLower}`}>
|
||||
{method?.toUpperCase() || 'GET'}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const StatusBadge = ({ status, statusCode }) => {
|
||||
const getStatusColor = (code) => {
|
||||
if (code >= 200 && code < 300) return '#10b981';
|
||||
if (code >= 300 && code < 400) return '#f59e0b';
|
||||
if (code >= 400 && code < 500) return '#ef4444';
|
||||
if (code >= 500) return '#dc2626';
|
||||
return '#6b7280';
|
||||
};
|
||||
|
||||
const displayStatus = statusCode || status;
|
||||
|
||||
return (
|
||||
<span
|
||||
className="status-badge"
|
||||
style={{ color: getStatusColor(statusCode) }}
|
||||
>
|
||||
<span className="status-badge">
|
||||
{displayStatus}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const NetworkFilterDropdown = ({ filters, requestCounts, onFilterToggle, onToggleAll }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef(null);
|
||||
|
||||
const allFiltersEnabled = Object.values(filters).every((f) => f);
|
||||
const activeFilters = Object.entries(filters).filter(([_, enabled]) => enabled);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="filter-dropdown" ref={dropdownRef}>
|
||||
<button
|
||||
className="filter-dropdown-trigger"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
title="Filter requests by method"
|
||||
>
|
||||
<IconFilter size={16} strokeWidth={1.5} />
|
||||
<span className="filter-summary">
|
||||
{activeFilters.length === Object.keys(filters).length ? 'All' : `${activeFilters.length}/${Object.keys(filters).length}`}
|
||||
</span>
|
||||
<IconChevronDown size={14} strokeWidth={1.5} />
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="filter-dropdown-menu right">
|
||||
<div className="filter-dropdown-header">
|
||||
<span>Filter by Method</span>
|
||||
<button
|
||||
className="filter-toggle-all"
|
||||
onClick={() => onToggleAll(!allFiltersEnabled)}
|
||||
>
|
||||
{allFiltersEnabled ? 'Hide All' : 'Show All'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="filter-dropdown-options">
|
||||
{Object.keys(filters).map((method) => (
|
||||
<label key={method} className="filter-option">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={filters[method]}
|
||||
onChange={(e) => onFilterToggle(method, e.target.checked)}
|
||||
/>
|
||||
<div className="filter-option-content">
|
||||
<MethodBadge method={method} />
|
||||
<span className="filter-option-label">{method}</span>
|
||||
<span className="filter-option-count">({requestCounts[method] || 0})</span>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const RequestRow = ({ request, isSelected, onClick }) => {
|
||||
const { data } = request;
|
||||
const { request: req, response: res, timestamp } = data;
|
||||
@@ -241,22 +146,6 @@ const NetworkTab = () => {
|
||||
});
|
||||
}, [allRequests, networkFilters]);
|
||||
|
||||
const requestCounts = useMemo(() => {
|
||||
return allRequests.reduce((counts, request) => {
|
||||
const method = request.data?.request?.method?.toUpperCase() || 'GET';
|
||||
counts[method] = (counts[method] || 0) + 1;
|
||||
return counts;
|
||||
}, {});
|
||||
}, [allRequests]);
|
||||
|
||||
const handleFilterToggle = (method, enabled) => {
|
||||
dispatch(updateNetworkFilter({ method, enabled }));
|
||||
};
|
||||
|
||||
const handleToggleAllFilters = (enabled) => {
|
||||
dispatch(toggleAllNetworkFilters(enabled));
|
||||
};
|
||||
|
||||
const handleRequestClick = (request) => {
|
||||
dispatch(setSelectedRequest(request));
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 16px;
|
||||
padding: 2px 8px;
|
||||
background: ${(props) => props.theme.console.headerBg};
|
||||
border-bottom: 1px solid ${(props) => props.theme.console.border};
|
||||
flex-shrink: 0;
|
||||
@@ -27,7 +27,6 @@ const StyledWrapper = styled.div`
|
||||
gap: 8px;
|
||||
color: ${(props) => props.theme.console.titleColor};
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
font-weight: 500;
|
||||
|
||||
.request-time {
|
||||
color: ${(props) => props.theme.console.countColor};
|
||||
@@ -66,7 +65,7 @@ const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 16px;
|
||||
padding: 4px 8px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
@@ -92,7 +91,7 @@ const StyledWrapper = styled.div`
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 16px;
|
||||
padding: 8px;
|
||||
min-height: 0;
|
||||
height: 0;
|
||||
}
|
||||
@@ -170,8 +169,7 @@ const StyledWrapper = styled.div`
|
||||
z-index: 10;
|
||||
|
||||
td {
|
||||
padding: 8px 12px;
|
||||
font-weight: 500;
|
||||
padding: 4px 8px;
|
||||
color: ${(props) => props.theme.console.titleColor};
|
||||
text-transform: uppercase;
|
||||
font-size: ${(props) => props.theme.font.size.xs};
|
||||
@@ -198,7 +196,7 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 8px 12px;
|
||||
padding: 2px 8px;
|
||||
vertical-align: top;
|
||||
word-break: break-word;
|
||||
}
|
||||
@@ -256,7 +254,7 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
.response-body-container {
|
||||
border-radius: 4px;
|
||||
border-radius: ${(props) => props.theme.border.radius.sm};
|
||||
overflow: hidden;
|
||||
height: 400px;
|
||||
display: flex;
|
||||
@@ -270,7 +268,7 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
div[role="tablist"] {
|
||||
padding: 8px 12px;
|
||||
padding: 4px 8px;
|
||||
border-bottom: 1px solid ${(props) => props.theme.console.border};
|
||||
display: flex !important;
|
||||
gap: 8px !important;
|
||||
@@ -305,7 +303,7 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
}
|
||||
|
||||
.network-logs-container {
|
||||
.network-logs-wrapper {
|
||||
border: 1px solid ${(props) => props.theme.console.border};
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
@@ -313,17 +311,17 @@ const StyledWrapper = styled.div`
|
||||
min-height: 200px;
|
||||
max-height: 400px;
|
||||
|
||||
.network-logs {
|
||||
.network-logs-container {
|
||||
background: ${(props) => props.theme.console.contentBg} !important;
|
||||
color: ${(props) => props.theme.console.messageColor} !important;
|
||||
height: 100% !important;
|
||||
max-height: 400px !important;
|
||||
|
||||
pre {
|
||||
padding: 0.5rem !important;
|
||||
|
||||
.network-logs-pre {
|
||||
color: ${(props) => props.theme.console.messageColor} !important;
|
||||
font-size: ${(props) => props.theme.font.size.xs} !important;
|
||||
line-height: 1.4 !important;
|
||||
padding: 12px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ const NetworkTab = ({ response }) => {
|
||||
<div className="tab-content">
|
||||
<div className="section">
|
||||
<h4>Network Logs</h4>
|
||||
<div className="network-logs-container">
|
||||
<div className="network-logs-wrapper">
|
||||
{timeline.length > 0 ? (
|
||||
<Network logs={timeline} />
|
||||
) : (
|
||||
|
||||
@@ -13,7 +13,7 @@ const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
padding: 0 8px;
|
||||
background: ${(props) => props.theme.console.headerBg};
|
||||
border-bottom: 1px solid ${(props) => props.theme.console.border};
|
||||
flex-shrink: 0;
|
||||
@@ -30,7 +30,7 @@ const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
padding: 4px 8px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
@@ -38,8 +38,6 @@ const StyledWrapper = styled.div`
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
font-weight: 500;
|
||||
border-radius: 4px 4px 0 0;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.console.buttonHoverBg};
|
||||
@@ -47,9 +45,9 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: ${(props) => props.theme.console.checkboxColor};
|
||||
border-bottom-color: ${(props) => props.theme.console.checkboxColor};
|
||||
background: ${(props) => props.theme.console.contentBg};
|
||||
color: ${(props) => props.theme.primary.strong};
|
||||
border-bottom-color: ${(props) => props.theme.primary.strong};
|
||||
background: ${(props) => props.theme.background.mantle};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,9 +142,6 @@ const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-right: 8px;
|
||||
padding-right: 8px;
|
||||
border-right: 1px solid ${(props) => props.theme.console.border};
|
||||
}
|
||||
|
||||
.action-controls {
|
||||
@@ -159,23 +154,21 @@ const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: ${(props) => props.theme.console.buttonColor};
|
||||
color: ${(props) => props.theme.text};
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.console.buttonHoverBg};
|
||||
color: ${(props) => props.theme.console.buttonHoverColor};
|
||||
background: ${(props) => props.theme.background.surface0};
|
||||
}
|
||||
|
||||
&.close-button:hover {
|
||||
background: #e81123;
|
||||
color: white;
|
||||
background: ${(props) => props.theme.background.surface0};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,19 +180,17 @@ const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 8px;
|
||||
padding: 2px 8px;
|
||||
background: transparent;
|
||||
border: 1px solid ${(props) => props.theme.console.border};
|
||||
border-radius: 4px;
|
||||
color: ${(props) => props.theme.console.buttonColor};
|
||||
border: 1px solid ${(props) => props.theme.border.border0};
|
||||
border-radius: ${(props) => props.theme.border.radius.sm};
|
||||
color: ${(props) => props.theme.text};
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.console.buttonHoverBg};
|
||||
color: ${(props) => props.theme.console.buttonHoverColor};
|
||||
border-color: ${(props) => props.theme.console.border};
|
||||
background: ${(props) => props.theme.background.surface0};
|
||||
}
|
||||
|
||||
.filter-summary {
|
||||
@@ -232,7 +223,7 @@ const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
padding: 4px 12px;
|
||||
background: ${(props) => props.theme.console.dropdownHeaderBg};
|
||||
border-bottom: 1px solid ${(props) => props.theme.console.border};
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
@@ -263,7 +254,7 @@ const StyledWrapper = styled.div`
|
||||
.filter-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 12px;
|
||||
padding: 4px 12px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
@@ -325,20 +316,6 @@ const StyledWrapper = styled.div`
|
||||
.logs-container {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.method-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
min-width: 45px;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
display: flex;
|
||||
|
||||
@@ -5,10 +5,9 @@ import ToolHint from 'components/ToolHint/index';
|
||||
|
||||
const StyledSessionList = styled.div`
|
||||
.session-list-item {
|
||||
padding: 10px 12px;
|
||||
padding: 2px 6px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid ${(props) => props.theme.border || 'rgba(255, 255, 255, 0.05)'};
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
@@ -24,7 +23,8 @@ const StyledSessionList = styled.div`
|
||||
|
||||
&.active {
|
||||
background: ${(props) => props.theme.sidebarActive || 'rgba(59, 142, 234, 0.12)'};
|
||||
border-left: 2px solid ${(props) => props.theme.brandColor || '#3b8eea'};
|
||||
border-left: 2px solid ${(props) => props.theme.primary.subtle};
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
|
||||
@@ -30,7 +30,7 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
.terminal-sessions-header {
|
||||
padding: 12px;
|
||||
padding: 6px 8px;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
color: ${(props) => props.theme.text};
|
||||
|
||||
@@ -2,10 +2,37 @@ import React, { useRef, useEffect, useState, useCallback } from 'react';
|
||||
import { Terminal } from '@xterm/xterm';
|
||||
import { FitAddon } from '@xterm/addon-fit';
|
||||
import { IconTerminal2, IconPlus } from '@tabler/icons';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import SessionList from './SessionList';
|
||||
import '@xterm/xterm/css/xterm.css';
|
||||
|
||||
// Build xterm.js theme from app theme
|
||||
const getTerminalTheme = (theme) => {
|
||||
return {
|
||||
background: theme.console.bg,
|
||||
foreground: theme.console.messageColor,
|
||||
cursor: theme.console.messageColor,
|
||||
selectionBackground: theme.status.info.background,
|
||||
black: theme.background.base,
|
||||
red: theme.status.danger.text,
|
||||
green: theme.status.success.text,
|
||||
yellow: theme.status.warning.text,
|
||||
blue: theme.status.info.text,
|
||||
magenta: theme.colors.text.purple,
|
||||
cyan: theme.codemirror.variable.prompt,
|
||||
white: theme.text,
|
||||
brightBlack: theme.colors.text.muted,
|
||||
brightRed: theme.status.danger.text,
|
||||
brightGreen: theme.status.success.text,
|
||||
brightYellow: theme.status.warning.text,
|
||||
brightBlue: theme.status.info.text,
|
||||
brightMagenta: theme.colors.text.purple,
|
||||
brightCyan: theme.codemirror.variable.prompt,
|
||||
brightWhite: theme.text
|
||||
};
|
||||
};
|
||||
|
||||
// Terminal instances per session - Map<sessionId, { terminal, fitAddon, inputDisposable, resizeDisposable }>
|
||||
const terminalInstances = new Map();
|
||||
|
||||
@@ -33,7 +60,7 @@ const ensureParkingHost = () => {
|
||||
return parkingHost;
|
||||
};
|
||||
|
||||
const createTerminalForSession = (sessionId) => {
|
||||
const createTerminalForSession = (sessionId, terminalTheme) => {
|
||||
if (terminalInstances.has(sessionId)) {
|
||||
return terminalInstances.get(sessionId);
|
||||
}
|
||||
@@ -42,28 +69,7 @@ const createTerminalForSession = (sessionId) => {
|
||||
cursorBlink: true,
|
||||
fontSize: 14,
|
||||
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
|
||||
theme: {
|
||||
background: '#1e1e1e',
|
||||
foreground: '#d4d4d4',
|
||||
cursor: '#d4d4d4',
|
||||
selection: '#264f78',
|
||||
black: '#1e1e1e',
|
||||
red: '#f14c4c',
|
||||
green: '#23d18b',
|
||||
yellow: '#f5f543',
|
||||
blue: '#3b8eea',
|
||||
magenta: '#d670d6',
|
||||
cyan: '#29b8db',
|
||||
white: '#e5e5e5',
|
||||
brightBlack: '#666666',
|
||||
brightRed: '#f14c4c',
|
||||
brightGreen: '#23d18b',
|
||||
brightYellow: '#f5f543',
|
||||
brightBlue: '#3b8eea',
|
||||
brightMagenta: '#d670d6',
|
||||
brightCyan: '#29b8db',
|
||||
brightWhite: '#e5e5e5'
|
||||
},
|
||||
theme: terminalTheme,
|
||||
allowProposedApi: true
|
||||
});
|
||||
|
||||
@@ -156,10 +162,10 @@ const cleanupTerminalInstance = (sessionId) => {
|
||||
}
|
||||
};
|
||||
|
||||
const openTerminalIntoContainer = async (container, sessionId) => {
|
||||
const openTerminalIntoContainer = async (container, sessionId, terminalTheme) => {
|
||||
if (!container || !sessionId) return;
|
||||
|
||||
const instance = createTerminalForSession(sessionId);
|
||||
const instance = createTerminalForSession(sessionId, terminalTheme);
|
||||
const { terminal, fitAddon } = instance;
|
||||
|
||||
if (!terminal.element) {
|
||||
@@ -174,6 +180,7 @@ const openTerminalIntoContainer = async (container, sessionId) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
try {
|
||||
fitAddon.fit();
|
||||
terminal.focus();
|
||||
const { cols, rows } = terminal;
|
||||
if (cols && rows && window.ipcRenderer) {
|
||||
window.ipcRenderer.send('terminal:resize', sessionId, { cols, rows });
|
||||
@@ -211,6 +218,8 @@ const TerminalTab = () => {
|
||||
const [sessions, setSessions] = useState([]);
|
||||
const [activeSessionId, setActiveSessionId] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const { theme } = useTheme();
|
||||
const terminalTheme = getTerminalTheme(theme);
|
||||
|
||||
// Load sessions list
|
||||
const loadSessions = useCallback(async (currentActiveSessionId = null) => {
|
||||
@@ -354,6 +363,15 @@ const TerminalTab = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Update all terminal themes when app theme changes
|
||||
useEffect(() => {
|
||||
terminalInstances.forEach((instance) => {
|
||||
if (instance.terminal) {
|
||||
instance.terminal.options.theme = terminalTheme;
|
||||
}
|
||||
});
|
||||
}, [theme.mode]);
|
||||
|
||||
// Handle terminal display for active session
|
||||
useEffect(() => {
|
||||
if (!activeSessionId || !terminalRef.current) return;
|
||||
@@ -361,7 +379,7 @@ const TerminalTab = () => {
|
||||
let mounted = true;
|
||||
|
||||
const setupTerminal = async () => {
|
||||
await openTerminalIntoContainer(terminalRef.current, activeSessionId);
|
||||
await openTerminalIntoContainer(terminalRef.current, activeSessionId, terminalTheme);
|
||||
|
||||
if (mounted) {
|
||||
const instance = terminalInstances.get(activeSessionId);
|
||||
|
||||
@@ -182,19 +182,6 @@ const NetworkFilterDropdown = ({ filters, requestCounts, onFilterToggle, onToggl
|
||||
const allFiltersEnabled = Object.values(filters).every((f) => f);
|
||||
const activeFilters = Object.entries(filters).filter(([_, enabled]) => enabled);
|
||||
|
||||
const getMethodColor = (method) => {
|
||||
switch (method?.toUpperCase()) {
|
||||
case 'GET': return '#10b981';
|
||||
case 'POST': return '#8b5cf6';
|
||||
case 'PUT': return '#f59e0b';
|
||||
case 'DELETE': return '#ef4444';
|
||||
case 'PATCH': return '#06b6d4';
|
||||
case 'HEAD': return '#6b7280';
|
||||
case 'OPTIONS': return '#84cc16';
|
||||
default: return '#6b7280';
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
||||
@@ -241,9 +228,6 @@ const NetworkFilterDropdown = ({ filters, requestCounts, onFilterToggle, onToggl
|
||||
onChange={(e) => onFilterToggle(method, e.target.checked)}
|
||||
/>
|
||||
<div className="filter-option-content">
|
||||
<span className="method-badge" style={{ backgroundColor: getMethodColor(method) }}>
|
||||
{method}
|
||||
</span>
|
||||
<span className="filter-option-label">{method}</span>
|
||||
<span className="filter-option-count">({requestCounts[method] || 0})</span>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState, useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { darken } from 'polished';
|
||||
import Console from './Console';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
|
||||
const MIN_DEVTOOLS_HEIGHT = 150;
|
||||
const MAX_DEVTOOLS_HEIGHT = window.innerHeight * 0.7;
|
||||
@@ -10,6 +12,9 @@ const Devtools = ({ mainSectionRef }) => {
|
||||
const isDevtoolsOpen = useSelector((state) => state.logs.isConsoleOpen);
|
||||
const [devtoolsHeight, setDevtoolsHeight] = useState(DEFAULT_DEVTOOLS_HEIGHT);
|
||||
const [isResizingDevtools, setIsResizingDevtools] = useState(false);
|
||||
const { theme } = useTheme();
|
||||
|
||||
const dragHandleColor = useMemo(() => darken(0.1, theme.primary.subtle), [theme.primary.subtle]);
|
||||
|
||||
const handleDevtoolsResizeStart = useCallback((e) => {
|
||||
e.preventDefault();
|
||||
@@ -68,15 +73,15 @@ const Devtools = ({ mainSectionRef }) => {
|
||||
<div
|
||||
onMouseDown={handleDevtoolsResizeStart}
|
||||
style={{
|
||||
height: '4px',
|
||||
height: '2px',
|
||||
cursor: 'row-resize',
|
||||
backgroundColor: isResizingDevtools ? '#0078d4' : 'transparent',
|
||||
backgroundColor: isResizingDevtools ? dragHandleColor : 'transparent',
|
||||
transition: 'background-color 0.2s ease',
|
||||
zIndex: 20,
|
||||
position: 'relative'
|
||||
}}
|
||||
onMouseEnter={(e) => e.target.style.backgroundColor = '#0078d4'}
|
||||
onMouseLeave={(e) => e.target.style.backgroundColor = isResizingDevtools ? '#0078d4' : 'transparent'}
|
||||
onMouseEnter={(e) => e.target.style.backgroundColor = dragHandleColor}
|
||||
onMouseLeave={(e) => e.target.style.backgroundColor = isResizingDevtools ? dragHandleColor : 'transparent'}
|
||||
/>
|
||||
<div style={{ height: `${devtoolsHeight}px`, overflow: 'hidden', position: 'relative' }}>
|
||||
<Console />
|
||||
|
||||
@@ -173,6 +173,18 @@ const Wrapper = styled.div`
|
||||
background-color: ${(props) => props.theme.dropdown.separator};
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.submenu-trigger {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.submenu-arrow {
|
||||
color: ${(props) => props.theme.dropdown.mutedText};
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import Tippy from '@tippyjs/react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Dropdown = ({ icon, children, onCreate, placement, transparent, visible, appendTo, ...props }) => {
|
||||
const Dropdown = ({ icon, children, onCreate, placement, transparent, visible, appendTo, onMouseEnter, onMouseLeave, ...props }) => {
|
||||
// When in controlled mode (visible prop is provided), don't use trigger prop
|
||||
const tippyProps = visible !== undefined
|
||||
? { ...props, visible, interactive: true, appendTo: appendTo || 'parent' }
|
||||
@@ -11,7 +11,14 @@ const Dropdown = ({ icon, children, onCreate, placement, transparent, visible, a
|
||||
return (
|
||||
<Tippy
|
||||
render={(attrs) => (
|
||||
<StyledWrapper className="tippy-box dropdown" transparent={transparent} tabIndex={-1} {...attrs}>
|
||||
<StyledWrapper
|
||||
className="tippy-box dropdown"
|
||||
transparent={transparent}
|
||||
tabIndex={-1}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
{...attrs}
|
||||
>
|
||||
{children}
|
||||
</StyledWrapper>
|
||||
)}
|
||||
|
||||
@@ -6,8 +6,13 @@ const StyledWrapper = styled.div`
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
&.is-resizing {
|
||||
cursor: col-resize !important;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow-y: auto;
|
||||
overflow: auto;
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
border: solid 1px ${(props) => props.theme.border.border0};
|
||||
}
|
||||
@@ -24,6 +29,7 @@ const StyledWrapper = styled.div`
|
||||
color: ${(props) => props.theme.table.thead.color} !important;
|
||||
background: ${(props) => props.theme.sidebar.bg};
|
||||
user-select: none;
|
||||
overflow: visible;
|
||||
|
||||
border: none !important;
|
||||
|
||||
@@ -34,10 +40,36 @@ const StyledWrapper = styled.div`
|
||||
border-bottom: solid 1px ${(props) => props.theme.border.border0};
|
||||
border-right: solid 1px ${(props) => props.theme.border.border0};
|
||||
vertical-align: middle;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.column-name {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.resize-handle {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
cursor: col-resize;
|
||||
background: transparent;
|
||||
z-index: 100;
|
||||
|
||||
&:hover,
|
||||
&.resizing {
|
||||
background: ${(props) => props.theme.colors.accent};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,10 +93,32 @@ const StyledWrapper = styled.div`
|
||||
border-bottom: solid 1px ${(props) => props.theme.border.border0};
|
||||
border-right: solid 1px ${(props) => props.theme.border.border0};
|
||||
vertical-align: middle;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* Handle CodeMirror editors overflow */
|
||||
.cm-editor {
|
||||
max-width: 100%;
|
||||
|
||||
.cm-scroller {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.cm-content {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.cm-line {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,7 +137,6 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
.tooltip-mod {
|
||||
font-size: 11px !important;
|
||||
max-width: 200px !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import React, { useCallback, useMemo, useRef, useState, useEffect } from 'react';
|
||||
import { IconTrash, IconAlertCircle, IconGripVertical, IconMinusVertical } from '@tabler/icons';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { uuid } from 'utils/common';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const MIN_COLUMN_WIDTH = 80;
|
||||
|
||||
const EditableTable = ({
|
||||
columns,
|
||||
rows,
|
||||
@@ -12,6 +14,7 @@ const EditableTable = ({
|
||||
getRowError,
|
||||
showCheckbox = true,
|
||||
showDelete = true,
|
||||
disableCheckbox = false,
|
||||
checkboxLabel = '',
|
||||
checkboxKey = 'enabled',
|
||||
reorderable = false,
|
||||
@@ -22,7 +25,101 @@ const EditableTable = ({
|
||||
const tableRef = useRef(null);
|
||||
const emptyRowUidRef = useRef(null);
|
||||
const [hoveredRow, setHoveredRow] = useState(null);
|
||||
const [dragStart, setDragStart] = useState(null);
|
||||
const [resizing, setResizing] = useState(null);
|
||||
const [tableHeight, setTableHeight] = useState(0);
|
||||
const [columnWidths, setColumnWidths] = useState(() => {
|
||||
const initialWidths = {};
|
||||
columns.forEach((col) => {
|
||||
initialWidths[col.key] = col.width || 'auto';
|
||||
});
|
||||
return initialWidths;
|
||||
});
|
||||
|
||||
const handleResizeStart = useCallback((e, columnKey) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const currentCell = e.target.closest('td');
|
||||
const nextCell = currentCell?.nextElementSibling;
|
||||
if (!currentCell || !nextCell) return;
|
||||
|
||||
const columnIndex = columns.findIndex((col) => col.key === columnKey);
|
||||
if (columnIndex >= columns.length - 1) return;
|
||||
|
||||
const startX = e.clientX;
|
||||
const startWidth = currentCell.offsetWidth;
|
||||
const nextColumnKey = columns[columnIndex + 1].key;
|
||||
const nextColumnStartWidth = nextCell.offsetWidth;
|
||||
|
||||
setResizing(columnKey);
|
||||
|
||||
const handleMouseMove = (moveEvent) => {
|
||||
const diff = moveEvent.clientX - startX;
|
||||
const maxGrow = nextColumnStartWidth - MIN_COLUMN_WIDTH;
|
||||
const maxShrink = startWidth - MIN_COLUMN_WIDTH;
|
||||
const clampedDiff = Math.max(-maxShrink, Math.min(maxGrow, diff));
|
||||
|
||||
setColumnWidths((prev) => ({
|
||||
...prev,
|
||||
[columnKey]: `${startWidth + clampedDiff}px`,
|
||||
[nextColumnKey]: `${nextColumnStartWidth - clampedDiff}px`
|
||||
}));
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
// Convert pixel widths to percentages for responsive scaling
|
||||
const table = tableRef.current?.querySelector('table');
|
||||
if (table) {
|
||||
const tableWidth = table.offsetWidth;
|
||||
const headerCells = table.querySelectorAll('thead td');
|
||||
const newWidths = {};
|
||||
|
||||
headerCells.forEach((cell, cellIndex) => {
|
||||
const checkboxOffset = showCheckbox ? 1 : 0;
|
||||
const colIndex = cellIndex - checkboxOffset;
|
||||
|
||||
if (colIndex >= 0 && colIndex < columns.length) {
|
||||
const colKey = columns[colIndex]?.key;
|
||||
if (colKey) {
|
||||
const percentage = (cell.offsetWidth / tableWidth) * 100;
|
||||
newWidths[colKey] = `${percentage}%`;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(newWidths).length > 0) {
|
||||
setColumnWidths((prev) => ({ ...prev, ...newWidths }));
|
||||
}
|
||||
}
|
||||
setResizing(null);
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
}, [columns, showCheckbox]);
|
||||
|
||||
// Track table height for resize handles
|
||||
useEffect(() => {
|
||||
const table = tableRef.current?.querySelector('table');
|
||||
if (!table) return;
|
||||
|
||||
const updateHeight = () => {
|
||||
setTableHeight(table.offsetHeight);
|
||||
};
|
||||
|
||||
updateHeight();
|
||||
|
||||
const resizeObserver = new ResizeObserver(updateHeight);
|
||||
resizeObserver.observe(table);
|
||||
|
||||
return () => resizeObserver.disconnect();
|
||||
}, [rows.length]);
|
||||
|
||||
const getColumnWidth = useCallback((column) => {
|
||||
return columnWidths[column.key] || column.width || 'auto';
|
||||
}, [columnWidths]);
|
||||
|
||||
const createEmptyRow = useCallback(() => {
|
||||
const newUid = uuid();
|
||||
@@ -137,15 +234,9 @@ const EditableTable = ({
|
||||
onChange(filteredRows);
|
||||
}, [rows, onChange]);
|
||||
|
||||
const getColumnWidth = useCallback((column) => {
|
||||
if (column.width) return column.width;
|
||||
return 'auto';
|
||||
}, []);
|
||||
|
||||
const handleDragStart = useCallback((e, index) => {
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('text/plain', index);
|
||||
setDragStart(index);
|
||||
}, []);
|
||||
|
||||
const handleDragOver = useCallback((e, index) => {
|
||||
@@ -161,15 +252,17 @@ const EditableTable = ({
|
||||
const reorderableRows = showAddRow ? rowsWithEmpty.slice(0, -1) : rowsWithEmpty;
|
||||
const updatedOrder = [...reorderableRows];
|
||||
const [movedRow] = updatedOrder.splice(fromIndex, 1);
|
||||
if (!movedRow) {
|
||||
setHoveredRow(null);
|
||||
return;
|
||||
}
|
||||
updatedOrder.splice(toIndex, 0, movedRow);
|
||||
onReorder({ updateReorderedItem: updatedOrder.map((row) => row.uid) });
|
||||
}
|
||||
setDragStart(null);
|
||||
setHoveredRow(null);
|
||||
}, [onReorder, rowsWithEmpty, showAddRow]);
|
||||
|
||||
const handleDragEnd = useCallback(() => {
|
||||
setDragStart(null);
|
||||
setHoveredRow(null);
|
||||
}, []);
|
||||
|
||||
@@ -178,15 +271,34 @@ const EditableTable = ({
|
||||
const value = column.getValue ? column.getValue(row) : row[column.key];
|
||||
const error = getRowError?.(row, rowIndex, column.key);
|
||||
|
||||
const errorIcon = error && !isEmpty ? (
|
||||
<span>
|
||||
<IconAlertCircle
|
||||
data-tooltip-id={`error-${row.uid}-${column.key}`}
|
||||
className="text-red-600 cursor-pointer ml-1"
|
||||
size={20}
|
||||
/>
|
||||
<Tooltip
|
||||
className="tooltip-mod"
|
||||
id={`error-${row.uid}-${column.key}`}
|
||||
html={error}
|
||||
/>
|
||||
</span>
|
||||
) : null;
|
||||
|
||||
if (column.render) {
|
||||
return column.render({
|
||||
row,
|
||||
value,
|
||||
rowIndex,
|
||||
isLastEmptyRow: isEmpty,
|
||||
onChange: (newValue) => handleValueChange(row.uid, column.key, newValue),
|
||||
error
|
||||
});
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
{column.render({
|
||||
row,
|
||||
value,
|
||||
rowIndex,
|
||||
isLastEmptyRow: isEmpty,
|
||||
onChange: (newValue) => handleValueChange(row.uid, column.key, newValue)
|
||||
})}
|
||||
{errorIcon}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -200,23 +312,10 @@ const EditableTable = ({
|
||||
className="mousetrap"
|
||||
value={value || ''}
|
||||
readOnly={column.readOnly}
|
||||
placeholder={isEmpty ? column.placeholder || column.name : ''}
|
||||
placeholder={!value ? column.placeholder || column.name : ''}
|
||||
onChange={(e) => handleValueChange(row.uid, column.key, e.target.value)}
|
||||
/>
|
||||
{error && !isEmpty && (
|
||||
<span>
|
||||
<IconAlertCircle
|
||||
data-tooltip-id={`error-${row.uid}-${column.key}`}
|
||||
className="text-red-600 cursor-pointer"
|
||||
size={20}
|
||||
/>
|
||||
<Tooltip
|
||||
className="tooltip-mod"
|
||||
id={`error-${row.uid}-${column.key}`}
|
||||
html={error}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
{errorIcon}
|
||||
</div>
|
||||
);
|
||||
}, [isLastEmptyRow, getRowError, handleValueChange]);
|
||||
@@ -224,7 +323,7 @@ const EditableTable = ({
|
||||
const reorderableRowCount = showAddRow ? rowsWithEmpty.length - 1 : rowsWithEmpty.length;
|
||||
|
||||
return (
|
||||
<StyledWrapper className={showCheckbox ? 'has-checkbox' : 'no-checkbox'}>
|
||||
<StyledWrapper className={`${showCheckbox ? 'has-checkbox' : 'no-checkbox'} ${resizing ? 'is-resizing' : ''}`}>
|
||||
<div className="table-container" ref={tableRef} data-testid={testId}>
|
||||
<table>
|
||||
<thead>
|
||||
@@ -232,12 +331,19 @@ const EditableTable = ({
|
||||
{showCheckbox && (
|
||||
<td className="text-center">{checkboxLabel}</td>
|
||||
)}
|
||||
{columns.map((column) => (
|
||||
{columns.map((column, colIndex) => (
|
||||
<td
|
||||
key={column.key}
|
||||
style={{ width: getColumnWidth(column) }}
|
||||
>
|
||||
{column.name}
|
||||
<span className="column-name">{column.name}</span>
|
||||
{colIndex < columns.length - 1 && (
|
||||
<div
|
||||
className={`resize-handle ${resizing === column.key ? 'resizing' : ''}`}
|
||||
style={{ height: tableHeight > 0 ? `${tableHeight}px` : undefined }}
|
||||
onMouseDown={(e) => handleResizeStart(e, column.key)}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
))}
|
||||
{showDelete && (
|
||||
@@ -288,6 +394,7 @@ const EditableTable = ({
|
||||
className="mousetrap"
|
||||
data-testid="column-checkbox"
|
||||
checked={row[checkboxKey] ?? true}
|
||||
disabled={disableCheckbox}
|
||||
onChange={(e) => handleCheckboxChange(row.uid, e.target.checked)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -6,6 +6,11 @@ const Wrapper = styled.div`
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
&.is-resizing {
|
||||
cursor: col-resize !important;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow-y: auto;
|
||||
border-radius: 8px;
|
||||
@@ -32,10 +37,6 @@ const Wrapper = styled.div`
|
||||
&:nth-child(5) {
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
width: 30%;
|
||||
}
|
||||
}
|
||||
|
||||
thead {
|
||||
@@ -48,10 +49,26 @@ const Wrapper = styled.div`
|
||||
padding: 5px 10px !important;
|
||||
border-bottom: solid 1px ${(props) => props.theme.border.border0};
|
||||
border-right: solid 1px ${(props) => props.theme.border.border0};
|
||||
position: relative;
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.resize-handle {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 4px;
|
||||
cursor: col-resize;
|
||||
background: transparent;
|
||||
z-index: 100;
|
||||
|
||||
&:hover,
|
||||
&.resizing {
|
||||
background: ${(props) => props.theme.colors.accent};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +93,6 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
.tooltip-mod {
|
||||
font-size: 11px !important;
|
||||
max-width: 200px !important;
|
||||
}
|
||||
|
||||
@@ -148,21 +164,6 @@ const Wrapper = styled.div`
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.discard {
|
||||
padding: 6px 16px;
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
background: transparent;
|
||||
color: ${(props) => props.theme.text};
|
||||
border: ${(props) => props.theme.sidebar.collection.item.indentBorder};
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,546 @@
|
||||
import React, { useCallback, useRef, useState, useEffect, useMemo } from 'react';
|
||||
import { TableVirtuoso } from 'react-virtuoso';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash, IconAlertCircle, IconInfoCircle } from '@tabler/icons';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useSelector } from 'react-redux';
|
||||
import MultiLineEditor from 'components/MultiLineEditor/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { uuid } from 'utils/common';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import { variableNameRegex } from 'utils/common/regex';
|
||||
import toast from 'react-hot-toast';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { getGlobalEnvironmentVariables } from 'utils/collections';
|
||||
|
||||
const MIN_H = 35 * 2;
|
||||
const MIN_COLUMN_WIDTH = 80;
|
||||
|
||||
const TableRow = React.memo(
|
||||
({ children, item }) => (
|
||||
<tr key={item.uid} data-testid={`env-var-row-${item.name}`}>
|
||||
{children}
|
||||
</tr>
|
||||
),
|
||||
(prevProps, nextProps) => {
|
||||
const prevUid = prevProps?.item?.uid;
|
||||
const nextUid = nextProps?.item?.uid;
|
||||
return prevUid === nextUid && prevProps.children === nextProps.children;
|
||||
}
|
||||
);
|
||||
|
||||
const EnvironmentVariablesTable = ({
|
||||
environment,
|
||||
collection,
|
||||
onSave,
|
||||
draft,
|
||||
onDraftChange,
|
||||
onDraftClear,
|
||||
setIsModified,
|
||||
renderExtraValueContent,
|
||||
searchQuery = ''
|
||||
}) => {
|
||||
const { storedTheme } = useTheme();
|
||||
const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments);
|
||||
|
||||
const hasDraftForThisEnv = draft?.environmentUid === environment.uid;
|
||||
|
||||
const [tableHeight, setTableHeight] = useState(MIN_H);
|
||||
const [columnWidths, setColumnWidths] = useState({ name: '30%', value: 'auto' });
|
||||
const [resizing, setResizing] = useState(null);
|
||||
|
||||
const handleResizeStart = useCallback((e, columnKey) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const currentCell = e.target.closest('td');
|
||||
const nextCell = currentCell?.nextElementSibling;
|
||||
if (!currentCell || !nextCell) return;
|
||||
|
||||
const startX = e.clientX;
|
||||
const startWidth = currentCell.offsetWidth;
|
||||
const nextColumnKey = 'value';
|
||||
const nextColumnStartWidth = nextCell.offsetWidth;
|
||||
|
||||
setResizing(columnKey);
|
||||
|
||||
const handleMouseMove = (moveEvent) => {
|
||||
const diff = moveEvent.clientX - startX;
|
||||
const maxGrow = nextColumnStartWidth - MIN_COLUMN_WIDTH;
|
||||
const maxShrink = startWidth - MIN_COLUMN_WIDTH;
|
||||
const clampedDiff = Math.max(-maxShrink, Math.min(maxGrow, diff));
|
||||
|
||||
setColumnWidths({
|
||||
[columnKey]: `${startWidth + clampedDiff}px`,
|
||||
[nextColumnKey]: `${nextColumnStartWidth - clampedDiff}px`
|
||||
});
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setResizing(null);
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
}, []);
|
||||
|
||||
const handleTotalHeightChanged = useCallback((h) => {
|
||||
setTableHeight(h);
|
||||
}, []);
|
||||
|
||||
const prevEnvUidRef = useRef(null);
|
||||
const mountedRef = useRef(false);
|
||||
|
||||
let _collection = collection ? cloneDeep(collection) : {};
|
||||
const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid });
|
||||
if (_collection) {
|
||||
_collection.globalEnvironmentVariables = globalEnvironmentVariables;
|
||||
}
|
||||
|
||||
const initialValues = useMemo(() => {
|
||||
const vars = environment.variables || [];
|
||||
return [
|
||||
...vars,
|
||||
{
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
type: 'text',
|
||||
secret: false,
|
||||
enabled: true
|
||||
}
|
||||
];
|
||||
}, [environment.uid, environment.variables]);
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: initialValues,
|
||||
validationSchema: Yup.array().of(
|
||||
Yup.object({
|
||||
enabled: Yup.boolean(),
|
||||
name: Yup.string().when('$isLastRow', {
|
||||
is: true,
|
||||
then: (schema) => schema.optional(),
|
||||
otherwise: (schema) =>
|
||||
schema
|
||||
.required('Name cannot be empty')
|
||||
.matches(
|
||||
variableNameRegex,
|
||||
'Name contains invalid characters. Must only contain alphanumeric characters, "-", "_", "." and cannot start with a digit.'
|
||||
)
|
||||
.trim()
|
||||
}),
|
||||
secret: Yup.boolean(),
|
||||
type: Yup.string(),
|
||||
uid: Yup.string(),
|
||||
value: Yup.mixed().nullable()
|
||||
})
|
||||
),
|
||||
validate: (values) => {
|
||||
const errors = {};
|
||||
values.forEach((variable, index) => {
|
||||
const isLastRow = index === values.length - 1;
|
||||
const isEmptyRow = !variable.name || variable.name.trim() === '';
|
||||
|
||||
if (isLastRow && isEmptyRow) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!variable.name || variable.name.trim() === '') {
|
||||
if (!errors[index]) errors[index] = {};
|
||||
errors[index].name = 'Name cannot be empty';
|
||||
} else if (!variableNameRegex.test(variable.name)) {
|
||||
if (!errors[index]) errors[index] = {};
|
||||
errors[index].name
|
||||
= 'Name contains invalid characters. Must only contain alphanumeric characters, "-", "_", "." and cannot start with a digit.';
|
||||
}
|
||||
});
|
||||
return Object.keys(errors).length > 0 ? errors : {};
|
||||
},
|
||||
onSubmit: () => {}
|
||||
});
|
||||
|
||||
// Restore draft values on mount or environment switch
|
||||
useEffect(() => {
|
||||
const isMount = !mountedRef.current;
|
||||
const envChanged = prevEnvUidRef.current !== null && prevEnvUidRef.current !== environment.uid;
|
||||
|
||||
prevEnvUidRef.current = environment.uid;
|
||||
mountedRef.current = true;
|
||||
|
||||
if ((isMount || envChanged) && hasDraftForThisEnv && draft?.variables) {
|
||||
formik.setValues([
|
||||
...draft.variables,
|
||||
{
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
type: 'text',
|
||||
secret: false,
|
||||
enabled: true
|
||||
}
|
||||
]);
|
||||
}
|
||||
}, [environment.uid, hasDraftForThisEnv, draft?.variables]);
|
||||
|
||||
const savedValuesJson = useMemo(() => {
|
||||
return JSON.stringify(environment.variables || []);
|
||||
}, [environment.variables]);
|
||||
|
||||
// Sync modified state
|
||||
useEffect(() => {
|
||||
const currentValues = formik.values.filter((variable) => variable.name && variable.name.trim() !== '');
|
||||
const currentValuesJson = JSON.stringify(currentValues);
|
||||
const hasActualChanges = currentValuesJson !== savedValuesJson;
|
||||
setIsModified(hasActualChanges);
|
||||
}, [formik.values, savedValuesJson, setIsModified]);
|
||||
|
||||
// Sync draft state
|
||||
useEffect(() => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
const currentValues = formik.values.filter((variable) => variable.name && variable.name.trim() !== '');
|
||||
const currentValuesJson = JSON.stringify(currentValues);
|
||||
const hasActualChanges = currentValuesJson !== savedValuesJson;
|
||||
|
||||
const existingDraftVariables = hasDraftForThisEnv ? draft?.variables : null;
|
||||
const existingDraftJson = existingDraftVariables ? JSON.stringify(existingDraftVariables) : null;
|
||||
|
||||
if (hasActualChanges) {
|
||||
if (currentValuesJson !== existingDraftJson) {
|
||||
onDraftChange(currentValues);
|
||||
}
|
||||
} else if (hasDraftForThisEnv) {
|
||||
onDraftClear();
|
||||
}
|
||||
}, 300);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [formik.values, savedValuesJson, environment.uid, hasDraftForThisEnv, draft?.variables, onDraftChange, onDraftClear]);
|
||||
|
||||
const ErrorMessage = ({ name, index }) => {
|
||||
const meta = formik.getFieldMeta(name);
|
||||
const id = `error-${name}-${index}`;
|
||||
|
||||
const isLastRow = index === formik.values.length - 1;
|
||||
const variable = formik.values[index];
|
||||
const isEmptyRow = !variable?.name || variable.name.trim() === '';
|
||||
|
||||
if (isLastRow && isEmptyRow) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!meta.error || !meta.touched) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<span>
|
||||
<IconAlertCircle id={id} className="text-red-600 cursor-pointer" size={20} />
|
||||
<Tooltip className="tooltip-mod" anchorId={id} html={meta.error || ''} />
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const handleRemoveVar = useCallback(
|
||||
(id) => {
|
||||
const currentValues = formik.values;
|
||||
|
||||
if (!currentValues || currentValues.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastRow = currentValues[currentValues.length - 1];
|
||||
const isLastEmptyRow = lastRow?.uid === id && (!lastRow.name || lastRow.name.trim() === '');
|
||||
|
||||
if (isLastEmptyRow) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filteredValues = currentValues.filter((variable) => variable.uid !== id);
|
||||
|
||||
const hasEmptyLastRow
|
||||
= filteredValues.length > 0
|
||||
&& (!filteredValues[filteredValues.length - 1].name
|
||||
|| filteredValues[filteredValues.length - 1].name.trim() === '');
|
||||
|
||||
const newValues = hasEmptyLastRow
|
||||
? filteredValues
|
||||
: [
|
||||
...filteredValues,
|
||||
{
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
type: 'text',
|
||||
secret: false,
|
||||
enabled: true
|
||||
}
|
||||
];
|
||||
|
||||
formik.setValues(newValues);
|
||||
},
|
||||
[formik.values]
|
||||
);
|
||||
|
||||
const handleNameChange = (index, e) => {
|
||||
formik.handleChange(e);
|
||||
const isLastRow = index === formik.values.length - 1;
|
||||
|
||||
if (isLastRow) {
|
||||
const newVariable = {
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
type: 'text',
|
||||
secret: false,
|
||||
enabled: true
|
||||
};
|
||||
setTimeout(() => {
|
||||
formik.setFieldValue(formik.values.length, newVariable, false);
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNameBlur = (index) => {
|
||||
formik.setFieldTouched(`${index}.name`, true, true);
|
||||
};
|
||||
|
||||
const handleNameKeyDown = (index, e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
formik.setFieldTouched(`${index}.name`, true, true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
const variablesToSave = formik.values.filter((variable) => variable.name && variable.name.trim() !== '');
|
||||
const savedValues = environment.variables || [];
|
||||
|
||||
const hasChanges = JSON.stringify(variablesToSave) !== JSON.stringify(savedValues);
|
||||
if (!hasChanges) {
|
||||
toast.error('No changes to save');
|
||||
return;
|
||||
}
|
||||
|
||||
const hasValidationErrors = variablesToSave.some((variable) => {
|
||||
if (!variable.name || variable.name.trim() === '') {
|
||||
return true;
|
||||
}
|
||||
if (!variableNameRegex.test(variable.name)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (hasValidationErrors) {
|
||||
toast.error('Please fix validation errors before saving');
|
||||
return;
|
||||
}
|
||||
|
||||
onSave(cloneDeep(variablesToSave))
|
||||
.then(() => {
|
||||
toast.success('Changes saved successfully');
|
||||
const newValues = [
|
||||
...variablesToSave,
|
||||
{
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
type: 'text',
|
||||
secret: false,
|
||||
enabled: true
|
||||
}
|
||||
];
|
||||
formik.resetForm({ values: newValues });
|
||||
setIsModified(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
toast.error('An error occurred while saving the changes');
|
||||
});
|
||||
}, [formik.values, environment.variables, onSave, setIsModified]);
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
const originalVars = environment.variables || [];
|
||||
const resetValues = [
|
||||
...originalVars,
|
||||
{
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
type: 'text',
|
||||
secret: false,
|
||||
enabled: true
|
||||
}
|
||||
];
|
||||
formik.resetForm({ values: resetValues });
|
||||
setIsModified(false);
|
||||
}, [environment.variables, setIsModified]);
|
||||
|
||||
const handleSaveRef = useRef(handleSave);
|
||||
handleSaveRef.current = handleSave;
|
||||
|
||||
useEffect(() => {
|
||||
const handleSaveEvent = () => {
|
||||
handleSaveRef.current();
|
||||
};
|
||||
|
||||
window.addEventListener('environment-save', handleSaveEvent);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('environment-save', handleSaveEvent);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const filteredVariables = useMemo(() => {
|
||||
const allVariables = formik.values.map((variable, index) => ({ variable, index }));
|
||||
if (!searchQuery?.trim()) {
|
||||
return allVariables;
|
||||
}
|
||||
|
||||
const query = searchQuery.toLowerCase().trim();
|
||||
|
||||
return allVariables.filter(({ variable, index }) => {
|
||||
const isLastRow = index === formik.values.length - 1;
|
||||
const isEmptyRow = !variable.name || variable.name.trim() === '';
|
||||
if (isLastRow && isEmptyRow) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const nameMatch = variable.name ? variable.name.toLowerCase().includes(query) : false;
|
||||
const valueMatch = typeof variable.value === 'string' ? variable.value.toLowerCase().includes(query) : false;
|
||||
|
||||
return !!(nameMatch || valueMatch);
|
||||
});
|
||||
}, [formik.values, searchQuery]);
|
||||
|
||||
return (
|
||||
<StyledWrapper className={resizing ? 'is-resizing' : ''}>
|
||||
<TableVirtuoso
|
||||
className="table-container"
|
||||
style={{ height: tableHeight }}
|
||||
components={{ TableRow }}
|
||||
data={filteredVariables}
|
||||
totalListHeightChanged={handleTotalHeightChanged}
|
||||
fixedHeaderContent={() => (
|
||||
<tr>
|
||||
<td className="text-center"></td>
|
||||
<td style={{ width: columnWidths.name }}>
|
||||
Name
|
||||
<div
|
||||
className={`resize-handle ${resizing === 'name' ? 'resizing' : ''}`}
|
||||
style={{ height: tableHeight > 0 ? `${tableHeight}px` : undefined }}
|
||||
onMouseDown={(e) => handleResizeStart(e, 'name')}
|
||||
/>
|
||||
</td>
|
||||
<td style={{ width: columnWidths.value }}>Value</td>
|
||||
<td className="text-center">Secret</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
)}
|
||||
fixedItemHeight={35}
|
||||
computeItemKey={(index, item) => item.variable.uid}
|
||||
itemContent={(index, { variable, index: actualIndex }) => {
|
||||
const isLastRow = actualIndex === formik.values.length - 1;
|
||||
const isEmptyRow = !variable.name || variable.name.trim() === '';
|
||||
const isLastEmptyRow = isLastRow && isEmptyRow;
|
||||
|
||||
return (
|
||||
<>
|
||||
<td className="text-center">
|
||||
{!isLastEmptyRow && (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mousetrap"
|
||||
name={`${actualIndex}.enabled`}
|
||||
checked={variable.enabled}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td style={{ width: columnWidths.name }}>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
className="mousetrap"
|
||||
id={`${actualIndex}.name`}
|
||||
name={`${actualIndex}.name`}
|
||||
value={variable.name}
|
||||
placeholder={!variable.value || (typeof variable.value === 'string' && variable.value.trim() === '') ? 'Value' : ''}
|
||||
onChange={(e) => handleNameChange(actualIndex, e)}
|
||||
onBlur={() => handleNameBlur(actualIndex)}
|
||||
onKeyDown={(e) => handleNameKeyDown(actualIndex, e)}
|
||||
/>
|
||||
<ErrorMessage name={`${actualIndex}.name`} index={actualIndex} />
|
||||
</div>
|
||||
</td>
|
||||
<td className="flex flex-row flex-nowrap items-center" style={{ width: columnWidths.value }}>
|
||||
<div className="overflow-hidden grow w-full relative">
|
||||
<MultiLineEditor
|
||||
theme={storedTheme}
|
||||
collection={_collection}
|
||||
name={`${actualIndex}.value`}
|
||||
value={variable.value}
|
||||
placeholder={isLastEmptyRow ? 'Value' : ''}
|
||||
isSecret={variable.secret}
|
||||
readOnly={typeof variable.value !== 'string'}
|
||||
onChange={(newValue) => formik.setFieldValue(`${actualIndex}.value`, newValue, true)}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
</div>
|
||||
{typeof variable.value !== 'string' && (
|
||||
<span className="ml-2 flex items-center">
|
||||
<IconInfoCircle id={`${variable.uid}-disabled-info-icon`} className="text-muted" size={16} />
|
||||
<Tooltip
|
||||
anchorId={`${variable.uid}-disabled-info-icon`}
|
||||
content="Non-string values set via scripts are read-only and can only be updated through scripts."
|
||||
place="top"
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
{renderExtraValueContent && renderExtraValueContent(variable)}
|
||||
</td>
|
||||
<td className="text-center">
|
||||
{!isLastEmptyRow && (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mousetrap"
|
||||
name={`${actualIndex}.secret`}
|
||||
checked={variable.secret}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{!isLastEmptyRow && (
|
||||
<button onClick={() => handleRemoveVar(variable.uid)}>
|
||||
<IconTrash strokeWidth={1.5} size={18} />
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="button-container">
|
||||
<div className="flex items-center">
|
||||
<button type="button" className="submit" onClick={handleSave} data-testid="save-env">
|
||||
Save
|
||||
</button>
|
||||
<button type="button" className="submit reset ml-2" onClick={handleReset} data-testid="reset-env">
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnvironmentVariablesTable;
|
||||
@@ -0,0 +1,105 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.collapsed {
|
||||
flex-shrink: 0;
|
||||
|
||||
.section-content {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.expanded {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
|
||||
.section-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
border-radius: 4px;
|
||||
transition: background 0.15s ease;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.workspace.button.bg};
|
||||
}
|
||||
|
||||
.section-title-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.section-icon {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&.expanded {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: ${(props) => props.theme.sidebar.color};
|
||||
}
|
||||
|
||||
.section-badge {
|
||||
font-size: 10px;
|
||||
padding: 1px 6px;
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
border-radius: 10px;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
.section-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
|
||||
.btn-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.section-content {
|
||||
padding: 4px 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { IconChevronRight } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const CollapsibleSection = ({
|
||||
title,
|
||||
expanded,
|
||||
onToggle,
|
||||
badge,
|
||||
actions,
|
||||
children
|
||||
}) => {
|
||||
return (
|
||||
<StyledWrapper className={expanded ? 'expanded' : 'collapsed'}>
|
||||
<div className="section-header" onClick={onToggle}>
|
||||
<div className="section-title-wrapper">
|
||||
<IconChevronRight
|
||||
size={14}
|
||||
strokeWidth={2}
|
||||
className={`section-icon ${expanded ? 'expanded' : ''}`}
|
||||
/>
|
||||
<span className="section-title">{title}</span>
|
||||
{badge !== undefined && badge !== null && (
|
||||
<span className="section-badge">{badge}</span>
|
||||
)}
|
||||
</div>
|
||||
{actions && (
|
||||
<div className="section-actions" onClick={(e) => e.stopPropagation()}>
|
||||
{actions}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="section-content">
|
||||
{children}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default CollapsibleSection;
|
||||
@@ -26,15 +26,15 @@ const ConfirmCloseEnvironment = ({ onCancel, onCloseWithoutSave, onSaveAndClose,
|
||||
|
||||
<div className="flex justify-between mt-6">
|
||||
<div>
|
||||
<Button size="sm" color="danger" onClick={onCloseWithoutSave}>
|
||||
<Button color="danger" onClick={onCloseWithoutSave}>
|
||||
Don't Save
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" color="secondary" variant="ghost" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="sm" onClick={onSaveAndClose}>
|
||||
<Button onClick={onSaveAndClose}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: ${(props) => props.theme.bg};
|
||||
|
||||
.header {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px 8px 20px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.title {
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
font-weight: 500;
|
||||
color: ${(props) => props.theme.text};
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.view-toggle {
|
||||
display: flex;
|
||||
border: 1px solid ${(props) => props.theme.border.border0};
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
.toggle-btn {
|
||||
padding: 4px 12px;
|
||||
font-size: 12px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:first-child {
|
||||
border-right: 1px solid ${(props) => props.theme.border.border0};
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.sidebar.bg};
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: ${(props) => props.theme.brand};
|
||||
color: ${(props) => props.theme.bg};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 6px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.sidebar.bg};
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
|
||||
&.delete-btn:hover {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 20px 20px 20px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,74 @@
|
||||
import React, { useState } from 'react';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import DeleteDotEnvFile from 'components/Environments/EnvironmentSettings/DeleteDotEnvFile';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const DotEnvFileDetails = ({
|
||||
title,
|
||||
children,
|
||||
onDelete,
|
||||
dotEnvExists,
|
||||
viewMode,
|
||||
onViewModeChange
|
||||
}) => {
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
|
||||
const handleDeleteClick = () => {
|
||||
setShowDeleteModal(true);
|
||||
};
|
||||
|
||||
const handleConfirmDelete = () => {
|
||||
if (onDelete) {
|
||||
onDelete();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="header">
|
||||
<h3 className="title">{title}</h3>
|
||||
<div className="actions">
|
||||
{dotEnvExists && (
|
||||
<>
|
||||
<div className="view-toggle" role="group" aria-label="View mode">
|
||||
<button
|
||||
type="button"
|
||||
className={`toggle-btn ${viewMode === 'table' ? 'active' : ''}`}
|
||||
onClick={() => onViewModeChange?.('table')}
|
||||
aria-pressed={viewMode === 'table'}
|
||||
>
|
||||
Table
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`toggle-btn ${viewMode === 'raw' ? 'active' : ''}`}
|
||||
onClick={() => onViewModeChange?.('raw')}
|
||||
aria-pressed={viewMode === 'raw'}
|
||||
>
|
||||
Raw
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" onClick={handleDeleteClick} title="Delete .env file" className="action-btn delete-btn">
|
||||
<IconTrash size={15} strokeWidth={1.5} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showDeleteModal && (
|
||||
<DeleteDotEnvFile
|
||||
onClose={() => setShowDeleteModal(false)}
|
||||
onConfirm={handleConfirmDelete}
|
||||
filename={title}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="content">
|
||||
{children}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default DotEnvFileDetails;
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import { IconFileOff } from '@tabler/icons';
|
||||
|
||||
const DotEnvEmptyState = () => {
|
||||
return (
|
||||
<div className="empty-state">
|
||||
<IconFileOff size={48} strokeWidth={1.5} />
|
||||
<div className="title">No .env File</div>
|
||||
<div className="description">
|
||||
Add a variable below to create a .env file in this location.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DotEnvEmptyState;
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { IconAlertCircle } from '@tabler/icons';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
|
||||
const DotEnvErrorMessage = React.memo(({ formik, name, index }) => {
|
||||
const meta = formik.getFieldMeta(name);
|
||||
const id = `error-${name}-${index}`;
|
||||
|
||||
const isLastRow = index === formik.values.length - 1;
|
||||
const variable = formik.values[index];
|
||||
const isEmptyRow = !variable?.name || variable.name.trim() === '';
|
||||
|
||||
if ((isLastRow && isEmptyRow) || !meta.error || !meta.touched) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
<IconAlertCircle id={id} className="text-red-600 cursor-pointer" size={20} />
|
||||
<Tooltip className="tooltip-mod" anchorId={id} html={meta.error || ''} />
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
||||
export default DotEnvErrorMessage;
|
||||
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
|
||||
const DotEnvRawView = ({
|
||||
collection,
|
||||
item,
|
||||
theme,
|
||||
value,
|
||||
onChange,
|
||||
onSave,
|
||||
onReset,
|
||||
isSaving
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div className="raw-editor-container">
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
item={item}
|
||||
theme={theme}
|
||||
value={value}
|
||||
onEdit={onChange}
|
||||
onSave={onSave}
|
||||
mode="text/plain"
|
||||
enableVariableHighlighting={false}
|
||||
enableBrunoVarInfo={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="button-container">
|
||||
<div className="flex items-center">
|
||||
<button type="button" className="submit" onClick={onSave} disabled={isSaving} data-testid="save-dotenv-raw">
|
||||
{isSaving ? 'Saving...' : 'Save'}
|
||||
</button>
|
||||
<button type="button" className="submit reset ml-2" onClick={onReset} disabled={isSaving} data-testid="reset-dotenv-raw">
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DotEnvRawView;
|
||||
@@ -0,0 +1,130 @@
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import { TableVirtuoso } from 'react-virtuoso';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import MultiLineEditor from 'components/MultiLineEditor/index';
|
||||
import DotEnvErrorMessage from './DotEnvErrorMessage';
|
||||
import { MIN_TABLE_HEIGHT } from './utils';
|
||||
|
||||
const TableRow = React.memo(({ children, item }) => (
|
||||
<tr key={item.uid} data-testid={`dotenv-var-row-${item.name}`}>{children}</tr>
|
||||
), (prevProps, nextProps) => {
|
||||
const prevUid = prevProps?.item?.uid;
|
||||
const nextUid = nextProps?.item?.uid;
|
||||
return prevUid === nextUid && prevProps.children === nextProps.children;
|
||||
});
|
||||
|
||||
const DotEnvTableView = ({
|
||||
formik,
|
||||
theme,
|
||||
showValueColumn,
|
||||
tableHeight,
|
||||
onHeightChange,
|
||||
onNameChange,
|
||||
onNameBlur,
|
||||
onNameKeyDown,
|
||||
onRemoveVar,
|
||||
onSave,
|
||||
onReset,
|
||||
isSaving
|
||||
}) => {
|
||||
const handleTotalHeightChanged = useCallback((h) => {
|
||||
onHeightChange(h);
|
||||
}, [onHeightChange]);
|
||||
|
||||
// Use refs for stable access to formik values in callbacks
|
||||
const formikRef = useRef(formik);
|
||||
formikRef.current = formik;
|
||||
|
||||
// Don't memoize itemContent - TableVirtuoso handles this internally
|
||||
// and we need fresh access to formik values
|
||||
const itemContent = (index, variable) => {
|
||||
const currentFormik = formikRef.current;
|
||||
const isLastRow = index === currentFormik.values.length - 1;
|
||||
const isEmptyRow = !variable.name || variable.name.trim() === '';
|
||||
const isLastEmptyRow = isLastRow && isEmptyRow;
|
||||
|
||||
return (
|
||||
<>
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
className="mousetrap"
|
||||
id={`${index}.name`}
|
||||
name={`${index}.name`}
|
||||
value={variable.name}
|
||||
placeholder={isLastEmptyRow ? 'Name' : ''}
|
||||
onChange={(e) => onNameChange(index, e)}
|
||||
onBlur={() => onNameBlur(index)}
|
||||
onKeyDown={(e) => onNameKeyDown(index, e)}
|
||||
/>
|
||||
<DotEnvErrorMessage formik={currentFormik} name={`${index}.name`} index={index} />
|
||||
</div>
|
||||
</td>
|
||||
{showValueColumn && (
|
||||
<td className="flex flex-row flex-nowrap items-center">
|
||||
<div className="overflow-hidden grow w-full relative">
|
||||
<MultiLineEditor
|
||||
theme={theme}
|
||||
name={`${index}.value`}
|
||||
value={variable.value}
|
||||
placeholder={isLastEmptyRow ? 'Value' : ''}
|
||||
onChange={(newValue) => currentFormik.setFieldValue(`${index}.value`, newValue, true)}
|
||||
onSave={onSave}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
<td className="delete-col">
|
||||
{!isLastEmptyRow && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Delete variable"
|
||||
onClick={() => onRemoveVar(variable.uid)}
|
||||
>
|
||||
<IconTrash strokeWidth={1.5} size={18} />
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableVirtuoso
|
||||
className="table-container"
|
||||
style={{ height: tableHeight || MIN_TABLE_HEIGHT }}
|
||||
components={{ TableRow }}
|
||||
data={formik.values}
|
||||
totalListHeightChanged={handleTotalHeightChanged}
|
||||
fixedHeaderContent={() => (
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
{showValueColumn && <td>Value</td>}
|
||||
<td className="delete-col"></td>
|
||||
</tr>
|
||||
)}
|
||||
fixedItemHeight={35}
|
||||
computeItemKey={(index, variable) => variable.uid}
|
||||
itemContent={itemContent}
|
||||
/>
|
||||
<div className="button-container">
|
||||
<div className="flex items-center">
|
||||
<button type="button" className="submit" onClick={onSave} disabled={isSaving} data-testid="save-dotenv">
|
||||
{isSaving ? 'Saving...' : 'Save'}
|
||||
</button>
|
||||
<button type="button" className="submit reset ml-2" onClick={onReset} disabled={isSaving} data-testid="reset-dotenv">
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DotEnvTableView;
|
||||
@@ -1,11 +1,22 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
|
||||
.raw-editor-container {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
border: solid 1px ${(props) => props.theme.border.border0};
|
||||
|
||||
.CodeMirror {
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
}
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow-y: auto;
|
||||
border-radius: 8px;
|
||||
@@ -16,24 +27,20 @@ const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
font-size: 12px;
|
||||
|
||||
td {
|
||||
vertical-align: middle;
|
||||
padding: 2px 10px;
|
||||
|
||||
&:nth-child(1) {
|
||||
width: 25px;
|
||||
border-right: none;
|
||||
}
|
||||
&:nth-child(4) {
|
||||
width: 80px;
|
||||
}
|
||||
&:nth-child(5) {
|
||||
width: 60px;
|
||||
&:first-child {
|
||||
width: 35%;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
width: 30%;
|
||||
&.delete-col {
|
||||
width: 40px;
|
||||
text-align: center;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,30 +49,30 @@ const Wrapper = styled.div`
|
||||
background: ${(props) => props.theme.sidebar.bg};
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
user-select: none;
|
||||
|
||||
|
||||
td {
|
||||
padding: 5px 10px !important;
|
||||
border-bottom: solid 1px ${(props) => props.theme.border.border0};
|
||||
border-right: solid 1px ${(props) => props.theme.border.border0};
|
||||
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
transition: background 0.1s ease;
|
||||
|
||||
|
||||
&:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
|
||||
td {
|
||||
border-bottom: solid 1px ${(props) => props.theme.border.border0};
|
||||
border-right: solid 1px ${(props) => props.theme.border.border0};
|
||||
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
@@ -75,7 +82,6 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
.tooltip-mod {
|
||||
font-size: 11px !important;
|
||||
max-width: 200px !important;
|
||||
}
|
||||
|
||||
@@ -102,12 +108,78 @@ const Wrapper = styled.div`
|
||||
vertical-align: middle;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: color 0.15s ease, background 0.15s ease;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
padding: 12px 2px;
|
||||
background: ${(props) => props.theme.bg};
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.submit {
|
||||
padding: 6px 16px;
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
border: none;
|
||||
background: ${(props) => props.theme.brand};
|
||||
color: ${(props) => props.theme.bg};
|
||||
cursor: pointer;
|
||||
transition: opacity 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.reset {
|
||||
background: transparent;
|
||||
padding: 6px 16px;
|
||||
color: ${(props) => props.theme.brand};
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 20px;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
|
||||
svg {
|
||||
opacity: 0.4;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
max-width: 300px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,340 @@
|
||||
import React, { useCallback, useRef, useMemo, useEffect, useState } from 'react';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { uuid } from 'utils/common';
|
||||
import { useFormik } from 'formik';
|
||||
import { variableNameRegex } from 'utils/common/regex';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import DotEnvTableView from './DotEnvTableView';
|
||||
import DotEnvRawView from './DotEnvRawView';
|
||||
import DotEnvEmptyState from './DotEnvEmptyState';
|
||||
import { variablesToRaw, rawToVariables, MIN_TABLE_HEIGHT } from './utils';
|
||||
|
||||
const DotEnvFileEditor = ({
|
||||
variables,
|
||||
onSave,
|
||||
onSaveRaw,
|
||||
isModified,
|
||||
setIsModified,
|
||||
dotEnvExists,
|
||||
rawContent,
|
||||
viewMode = 'table',
|
||||
collection,
|
||||
item
|
||||
}) => {
|
||||
const { displayedTheme } = useTheme();
|
||||
const [tableHeight, setTableHeight] = useState(MIN_TABLE_HEIGHT);
|
||||
// Derive a single baseline raw value for consistent dirty-tracking
|
||||
const baselineRaw = rawContent ?? variablesToRaw(variables || []);
|
||||
const initialRawValue = baselineRaw;
|
||||
const [rawValue, setRawValue] = useState(initialRawValue);
|
||||
const [prevViewMode, setPrevViewMode] = useState(viewMode);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const formikRef = useRef(null);
|
||||
|
||||
const initialValues = useMemo(() => {
|
||||
const vars = (variables || []).map((v) => ({
|
||||
...v,
|
||||
uid: v.uid || uuid()
|
||||
}));
|
||||
return [
|
||||
...vars,
|
||||
{
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: ''
|
||||
}
|
||||
];
|
||||
}, [variables]);
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: initialValues,
|
||||
validate: (values) => {
|
||||
const errors = {};
|
||||
values.forEach((variable, index) => {
|
||||
const isLastRow = index === values.length - 1;
|
||||
const isEmptyRow = !variable.name || variable.name.trim() === '';
|
||||
|
||||
if (isLastRow && isEmptyRow) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!variable.name || variable.name.trim() === '') {
|
||||
if (!errors[index]) errors[index] = {};
|
||||
errors[index].name = 'Name cannot be empty';
|
||||
} else if (!variableNameRegex.test(variable.name)) {
|
||||
if (!errors[index]) errors[index] = {};
|
||||
errors[index].name
|
||||
= 'Name contains invalid characters. Must only contain alphanumeric characters, "-", "_", "." and cannot start with a digit.';
|
||||
}
|
||||
});
|
||||
return Object.keys(errors).length > 0 ? errors : {};
|
||||
},
|
||||
onSubmit: () => {}
|
||||
});
|
||||
|
||||
formikRef.current = formik;
|
||||
|
||||
// Sync raw value with external changes
|
||||
useEffect(() => {
|
||||
setRawValue(baselineRaw);
|
||||
}, [baselineRaw]);
|
||||
|
||||
// Handle view mode switching
|
||||
useEffect(() => {
|
||||
if (viewMode !== prevViewMode) {
|
||||
if (viewMode === 'raw' && prevViewMode === 'table') {
|
||||
const currentVars = formikRef.current.values.filter((v) => v.name && v.name.trim() !== '');
|
||||
const newRawValue = variablesToRaw(currentVars);
|
||||
setRawValue(newRawValue);
|
||||
} else if (viewMode === 'table' && prevViewMode === 'raw') {
|
||||
const parsedVars = rawToVariables(rawValue);
|
||||
const newValues = [
|
||||
...parsedVars,
|
||||
{ uid: uuid(), name: '', value: '' }
|
||||
];
|
||||
formikRef.current.setValues(newValues);
|
||||
}
|
||||
setPrevViewMode(viewMode);
|
||||
}
|
||||
}, [viewMode, prevViewMode, rawValue]);
|
||||
|
||||
const normalizeForComparison = (vars) => {
|
||||
return vars
|
||||
.filter((v) => v.name && v.name.trim() !== '')
|
||||
.map(({ name, value }) => ({ name, value: value || '' }));
|
||||
};
|
||||
|
||||
const savedValuesJson = useMemo(() => {
|
||||
return JSON.stringify(normalizeForComparison(variables || []));
|
||||
}, [variables]);
|
||||
|
||||
useEffect(() => {
|
||||
if (viewMode === 'raw') {
|
||||
const hasRawChanges = rawValue !== baselineRaw;
|
||||
setIsModified(hasRawChanges);
|
||||
} else {
|
||||
const currentValuesJson = JSON.stringify(normalizeForComparison(formik.values));
|
||||
const hasActualChanges = currentValuesJson !== savedValuesJson;
|
||||
setIsModified(hasActualChanges);
|
||||
}
|
||||
}, [formik.values, savedValuesJson, setIsModified, viewMode, rawValue, baselineRaw]);
|
||||
|
||||
// Ref for stable formik.values access
|
||||
const valuesRef = useRef(formik.values);
|
||||
valuesRef.current = formik.values;
|
||||
|
||||
const handleRemoveVar = useCallback((id) => {
|
||||
const currentValues = valuesRef.current;
|
||||
|
||||
if (!currentValues || currentValues.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastRow = currentValues[currentValues.length - 1];
|
||||
const isLastEmptyRow = lastRow?.uid === id && (!lastRow.name || lastRow.name.trim() === '');
|
||||
|
||||
if (isLastEmptyRow) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filteredValues = currentValues.filter((variable) => variable.uid !== id);
|
||||
|
||||
const hasEmptyLastRow
|
||||
= filteredValues.length > 0
|
||||
&& (!filteredValues[filteredValues.length - 1].name
|
||||
|| filteredValues[filteredValues.length - 1].name.trim() === '');
|
||||
|
||||
const newValues = hasEmptyLastRow
|
||||
? filteredValues
|
||||
: [
|
||||
...filteredValues,
|
||||
{ uid: uuid(), name: '', value: '' }
|
||||
];
|
||||
|
||||
formikRef.current.setValues(newValues);
|
||||
}, []);
|
||||
|
||||
const handleNameChange = useCallback((index, e) => {
|
||||
formik.handleChange(e);
|
||||
const isLastRow = index === valuesRef.current.length - 1;
|
||||
|
||||
if (isLastRow) {
|
||||
const newVariable = { uid: uuid(), name: '', value: '' };
|
||||
setTimeout(() => {
|
||||
formik.setValues((prev) => {
|
||||
const lastRow = prev[prev.length - 1];
|
||||
if (lastRow?.name?.trim()) {
|
||||
return [...prev, newVariable];
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleNameBlur = useCallback((index) => {
|
||||
formik.setFieldTouched(`${index}.name`, true, true);
|
||||
}, []);
|
||||
|
||||
const handleNameKeyDown = useCallback((index, e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
formik.setFieldTouched(`${index}.name`, true, true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
if (isSaving) return;
|
||||
|
||||
const variablesToSave = formik.values.filter((variable) => variable.name && variable.name.trim() !== '');
|
||||
|
||||
const hasValidationErrors = variablesToSave.some((variable) => {
|
||||
if (!variable.name || variable.name.trim() === '') {
|
||||
return true;
|
||||
}
|
||||
if (!variableNameRegex.test(variable.name)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (hasValidationErrors) {
|
||||
toast.error('Please fix validation errors before saving');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
onSave(variablesToSave)
|
||||
.then(() => {
|
||||
toast.success('Changes saved successfully');
|
||||
const newValues = [
|
||||
...variablesToSave,
|
||||
{ uid: uuid(), name: '', value: '' }
|
||||
];
|
||||
formik.resetForm({ values: newValues });
|
||||
setIsModified(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
toast.error('An error occurred while saving the changes');
|
||||
})
|
||||
.finally(() => {
|
||||
setIsSaving(false);
|
||||
});
|
||||
}, [isSaving, formik.values, onSave, setIsModified]);
|
||||
|
||||
const handleSaveRaw = useCallback(() => {
|
||||
if (isSaving) return;
|
||||
|
||||
if (!onSaveRaw) {
|
||||
toast.error('Raw save is not supported');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
onSaveRaw(rawValue)
|
||||
.then(() => {
|
||||
toast.success('Changes saved successfully');
|
||||
setIsModified(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
toast.error('An error occurred while saving the changes');
|
||||
})
|
||||
.finally(() => {
|
||||
setIsSaving(false);
|
||||
});
|
||||
}, [isSaving, rawValue, onSaveRaw, setIsModified]);
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
if (viewMode === 'raw') {
|
||||
setRawValue(baselineRaw);
|
||||
setIsModified(false);
|
||||
} else {
|
||||
const originalVars = (variables || []).map((v) => ({
|
||||
...v,
|
||||
uid: v.uid || uuid()
|
||||
}));
|
||||
const resetValues = [
|
||||
...originalVars,
|
||||
{ uid: uuid(), name: '', value: '' }
|
||||
];
|
||||
formik.resetForm({ values: resetValues });
|
||||
setIsModified(false);
|
||||
}
|
||||
}, [viewMode, baselineRaw, variables, setIsModified]);
|
||||
|
||||
const handleRawChange = useCallback((newValue) => {
|
||||
setRawValue(newValue);
|
||||
}, []);
|
||||
|
||||
// Global save event listener
|
||||
const handleSaveRef = useRef(handleSave);
|
||||
handleSaveRef.current = handleSave;
|
||||
|
||||
const handleSaveRawRef = useRef(handleSaveRaw);
|
||||
handleSaveRawRef.current = handleSaveRaw;
|
||||
|
||||
useEffect(() => {
|
||||
const handleSaveEvent = () => {
|
||||
if (viewMode === 'raw') {
|
||||
handleSaveRawRef.current();
|
||||
} else {
|
||||
handleSaveRef.current();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('dotenv-save', handleSaveEvent);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('dotenv-save', handleSaveEvent);
|
||||
};
|
||||
}, [viewMode]);
|
||||
|
||||
// Raw view mode
|
||||
if (viewMode === 'raw') {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<DotEnvRawView
|
||||
collection={collection}
|
||||
item={item}
|
||||
theme={displayedTheme}
|
||||
value={rawValue}
|
||||
onChange={handleRawChange}
|
||||
onSave={handleSaveRaw}
|
||||
onReset={handleReset}
|
||||
isSaving={isSaving}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
// Empty state (no .env file exists yet)
|
||||
const showEmptyState = !dotEnvExists && (!variables || variables.length === 0);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
{showEmptyState && <DotEnvEmptyState />}
|
||||
<DotEnvTableView
|
||||
formik={formik}
|
||||
theme={displayedTheme}
|
||||
showValueColumn={!showEmptyState}
|
||||
tableHeight={showEmptyState ? MIN_TABLE_HEIGHT : tableHeight}
|
||||
onHeightChange={setTableHeight}
|
||||
onNameChange={handleNameChange}
|
||||
onNameBlur={handleNameBlur}
|
||||
onNameKeyDown={handleNameKeyDown}
|
||||
onRemoveVar={handleRemoveVar}
|
||||
onSave={handleSave}
|
||||
onReset={handleReset}
|
||||
isSaving={isSaving}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default DotEnvFileEditor;
|
||||
@@ -0,0 +1,59 @@
|
||||
import { uuid } from 'utils/common';
|
||||
|
||||
export const variablesToRaw = (variables) => {
|
||||
return variables
|
||||
.filter((v) => v.name && v.name.trim() !== '')
|
||||
.map((v) => {
|
||||
const value = v.value || '';
|
||||
if (value.includes('\n') || value.includes('"') || value.includes('\'')) {
|
||||
const escapedValue = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
|
||||
return `${v.name}="${escapedValue}"`;
|
||||
}
|
||||
return `${v.name}=${value}`;
|
||||
})
|
||||
.join('\n');
|
||||
};
|
||||
|
||||
export const rawToVariables = (rawContent) => {
|
||||
if (!rawContent || rawContent.trim() === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const variables = [];
|
||||
const lines = rawContent.split('\n');
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim();
|
||||
|
||||
if (!trimmedLine || trimmedLine.startsWith('#')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const equalIndex = trimmedLine.indexOf('=');
|
||||
if (equalIndex === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const name = trimmedLine.substring(0, equalIndex).trim();
|
||||
let value = trimmedLine.substring(equalIndex + 1);
|
||||
|
||||
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith('\'') && value.endsWith('\''))) {
|
||||
value = value.slice(1, -1);
|
||||
value = value.replace(/\\n/g, '\n').replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
||||
}
|
||||
|
||||
if (name) {
|
||||
variables.push({
|
||||
uid: uuid(),
|
||||
name,
|
||||
value,
|
||||
enabled: true,
|
||||
secret: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return variables;
|
||||
};
|
||||
|
||||
export const MIN_TABLE_HEIGHT = 35 * 2;
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { IconPlus, IconDownload, IconSettings } from '@tabler/icons';
|
||||
import ToolHint from 'components/ToolHint';
|
||||
import ColorBadge from 'components/ColorBadge';
|
||||
|
||||
const EnvironmentListContent = ({
|
||||
environments,
|
||||
@@ -38,6 +39,7 @@ const EnvironmentListContent = ({
|
||||
data-tooltip-content={env.name}
|
||||
data-tooltip-hidden={env.name?.length < 90}
|
||||
>
|
||||
<ColorBadge color={env.color} size={8} showEmptyBorder={false} />
|
||||
<span className="max-w-100% truncate no-wrap">{env.name}</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user