Compare commits

..

53 Commits

Author SHA1 Message Date
Bijin A B
6dc76e023a fix: update unit tests 2025-12-09 12:00:20 +05:30
Anoop M D
2c34844172 chore: addressed coderabbit review comments 2025-12-09 04:17:03 +05:30
Anoop M D
2f8986624a feat: toolbar design updates 2025-12-09 04:05:50 +05:30
Abhishek S Lal
0197ae37c8 refactor: update AppTitleBar and SidebarHeader components (#6341)
* refactor: update AppTitleBar and SidebarHeader components to use MenuDropdown and ActionIcon for improved UI consistency

* refactor: update button locators in tests to use data-testid for consistency and improved readability
2025-12-08 22:06:24 +05:30
Dániel Seres
cf969dfcd6 fix: Support @contentType for multiline values (#6217)
* fix: Support @contentType for multiline values

Fixes the issue where the @contentType annotation broke the parsing of multiline values.

* chore: add dotall flag to fileExtractContentType

Not strictly needed since body:file uses single-line values in practice,
but doesn't hurt and matches what multipartExtractContentType does.

---------

Co-authored-by: Márk Dániel Seres <markdaniel.seres@tesco.com>
2025-12-08 18:39:25 +05:30
Pragadesh-45
a66be21523 feat: Enhance runCollectionFolder to support selected request ordering (#6320)
* Refactor `runCollectionFolder` action to accept `selectedRequestUids` for filtering and ordering requests.
* Update IPC handler to process `selectedRequestUids`, ensuring requests are executed in the specified order while preserving folder data.
2025-12-08 17:16:24 +05:30
naman-bruno
4016754d71 feat: integrate import/export modals and refactor environment handling (#6346) 2025-12-08 15:17:53 +05:30
Anoop M D
f3aebf6374 Merge pull request #6345 from usebruno/feat/design-updates
feat: design updates
2025-12-08 14:42:06 +05:30
naman-bruno
f87460b00e refactor: simplify last opened workspaces management by removing workspace config from storage and improving path handling (#6343) 2025-12-08 14:30:16 +05:30
naman-bruno
354e8d7496 feat: add hideApiSpecPage dispatch to Collection and CollectionItem components (#6344) 2025-12-08 14:24:47 +05:30
Anoop M D
dc107f8b96 init (#6337) 2025-12-08 01:37:16 +05:30
naman-bruno
cd0f1e45ba init 2025-12-07 21:53:47 +05:30
Bijin A B
33022843f2 fix: CWE-347: Improper Verification of Cryptographic Signature (#6336) 2025-12-07 14:16:39 +05:30
Anoop M D
facdf3264a feat: changes to incorporate oc schema updates (#6335)
* feat: changes to incorporate oc schema updates
* chore: fixed oc types resolution issue
2025-12-07 06:05:48 +05:30
naman-bruno
4ffb447c53 fix: path for newly added collection & remove option for outside collections (#6331)
* fixes

* fixes

* fix
2025-12-06 18:43:53 +05:30
Sanjai Kumar
3e5ae613f5 feat: Increase visibility of text in Request tabs (#6243)
* refactor(RequestTabs): update tab width calculation and improve styling

* refactor: replace close icon implementation with GradientCloseButton and adjust styles

* changes: design

* fix: failing tests

* fixes

* fixes: coderabbit

* fixes

* fixes

* gradient color fix

---------

Co-authored-by: naman-bruno <naman@usebruno.com>
Co-authored-by: Bijin A B <bijin@usebruno.com>
2025-12-06 18:42:57 +05:30
naman-bruno
42bef4ae1e fix: traffic light styling on light mode (#6333)
* fix

* fixes
2025-12-06 18:16:37 +05:30
naman-bruno
e93e545b81 improve: tests (#6321)
* improve: tests

* fixes

* fixes
2025-12-06 15:36:58 +05:30
Abhishek S Lal
4a8d787f31 feat: Moved Workspace Selector to the Titlebar of the window. (#6319)
* refactor: update sidebar components and styles, replace TitleBar with SidebarHeader, and enhance collections search functionality

* refactor: improve event listener management in AppTitleBar and clean up SidebarHeader styles

* fix: ensure safe access to layout preferences in AppTitleBar and set default order in SidebarHeader

* refactor: centralize toTitleCase utility and remove redundant implementations in AppTitleBar and WorkspaceSelector

* feat: enhance accessibility and testing for sidebar and devtools toggle buttons in AppTitleBar

* chore: quick fix on a flaky test

---------

Co-authored-by: Bijin A B <bijin@usebruno.com>
2025-12-06 02:07:05 +05:30
Bijin A B
f5211f6a08 Update quotes rule for string in CODING_STANDARDS.md (#6327) 2025-12-06 02:01:03 +05:30
Sanjai Kumar
57222d2500 feat: enhance collection settings with environment modals (#6242)
* feat: enhance collection settings with environment modals

* refactor: remove unused environment modal state and simplify Info component structure
2025-12-05 19:20:04 +05:30
Sanjai Kumar
f479e0d325 refactor: Rename runtime to runDuration (#6323)
* refactor: Rename 'runtime' to 'runDuration'

* revert changes made in report.html
2025-12-05 19:17:06 +05:30
naman-bruno
5302addda0 fix: clone collection (#6322)
* fix: clone collection
2025-12-05 17:26:06 +05:30
Sanjai Kumar
80b017f224 feat: Include pre-request and post-response tests in JUnit reports (#6284)
* enhance: JUnit output to include preRequest and postResponse test results

* fix: lint
2025-12-05 12:04:32 +05:30
Bijin A B
b18d582004 Merge pull request #6310 from sanjaikumar-bruno/chore/eslint-ignore-paths
chore: update ESLint configuration to ignore additional directories
2025-12-04 19:23:25 +05:30
Bijin A B
109394c65b Merge pull request #6308 from Pragadesh-45/fix/6254
feat: Streamline gRPC requests to use right context
2025-12-04 18:58:10 +05:30
Sid
c355153f26 Revert: Re-add post response vars (#6307)
* Partial Revert "remove: presets and response var (#6195)"

This reverts commit 786a3414b8 while keeping code related to presets deleted

* revert: remove global environment variables assignment
2025-12-04 18:04:47 +05:30
Pragadesh-45
b87a02beb3 feat: Streamline gRPC requests to use right context 2025-12-04 18:00:32 +05:45
sanjai
4624ffb116 chore: update ESLint configuration to ignore additional directories 2025-12-04 16:34:48 +05:30
Sid
a9ce97fb1b fix: update content security policy to remove unsafe-inline (#6305) 2025-12-04 12:40:52 +05:30
Pooja
72ce6cadeb fix: request and response pane height (#6294) 2025-12-04 12:31:57 +05:30
Bijin A B
c4ff2918a2 Merge pull request #6080 from dssagar93/feature/auto-scroll-on-tab-change
Auto scroll to show this item when its tab becomes active
2025-12-04 10:05:32 +05:30
Bijin A B
9972eb3de6 Merge branch 'main' of github.com:usebruno/bruno into feature/auto-scroll-on-tab-change 2025-12-04 05:15:39 +05:30
naman-bruno
ebe0203415 init: workspaces (#6264)
* init: workspaces
2025-12-04 04:56:43 +05:30
Bijin A B
6786f19d04 Merge pull request #6300 from usebruno/chore/oss-lint-fixes
chore: repo-wide lint fixes
2025-12-04 03:40:29 +05:30
Bijin A B
05fe8b1b27 chore: repo-wide lint fixes 2025-12-04 01:37:20 +05:30
sanish chirayath
b5722bf11c fix: wrap script in async IIFE to create isolated scope (#6229)
* fix: lexical scope for running scripts

* review fixes
2025-12-04 00:23:06 +05:30
Siddharth Gelera (reaper)
2b8da39bcf fix: update content security policy to allow inline scripts (#6139) 2025-12-03 19:57:10 +05:30
Pragadesh-45
38ba53be9f fix: stringify rawValue in brunoVarInfo (#6281) 2025-12-03 18:27:12 +05:30
Chirag Chandrashekhar
9159f523d9 Inbuilt Terminal (#6066)
* add: terminal

* added support for multiple terminal sessions and opening terminal from collection's working directory

* Use PowerShell as default shell on Windows

Replace cmd.exe with powershell.exe for terminal sessions on Windows.
Falls back to PWSH environment variable if set (for PowerShell Core).

* refactor(ui): improved session list by moving path display to hover for cleaner view

* chore: format

* refactor: improve terminal code quality and UI consistency

- Add terminal icon to 'Open in Terminal' dropdown item in CollectionItem
- Remove unused imports and functions (IconPlus, callIpc, canWriteToTerminal)
- Fix React key prop placement in SessionList component
- Replace deprecated substr with substring in terminal session ID generation
- Improve error handling for terminal cleanup on app quit
- Simplify terminal cleanup logic in window close handler

---------

Co-authored-by: naman-bruno <naman@usebruno.com>
Co-authored-by: Sid <siddharth@usebruno.com>
2025-12-03 17:48:28 +05:30
Abhishek S Lal
a3d2d35d2e feat: introduce REQUEST_TYPES constant and update item deletion logic (#6244)
- Added REQUEST_TYPES constant to centralize request type definitions.
- Updated deleteItem action to filter items based on REQUEST_TYPES and folders.
- Modified collection.js to handle REQUEST_TYPES during file resequence operations.
2025-12-03 16:07:38 +05:30
Chirag Chandrashekhar
9caef9e573 Fix: WS variable interpolation (#6184)
* feat: add variable interpolation support for WebSocket requests

- Add WebSocket body interpolation in interpolateVars function
- Interpolate URL, headers, and all messages in request.body.ws array with full variable context
- Refactor sendWsRequest to use main process preparation (removes duplication)
- Add mode property to wsRequest object for proper request type detection
- Ensure consistent variable precedence matching HTTP/gRPC requests
- Centralize all interpolation logic in main process via prepareWsRequest

* Add Playwright tests for WebSocket variable interpolation

- Add tests for URL interpolation (wss://echo.{{url}}.org)
- Add tests for message content interpolation ({"test": "{{data}}"})
- Update test fixtures to use wss://echo.websocket.org echo server
- Add WEBSOCKET_FLOWS.md documentation
- Refactor queueWsMessage to handle variable interpolation in main process

* removed ws flow documentation

* chore: updated the network/index.js file to reduce merge conflicts by moving around code

* fix: added collection and item to WsQueryUrl Editor to fix available variable highlight

* chore: remove unnecessary whitespace in WebSocket event handlers

* feat: add automatic WebSocket reconnection on URL variable changes

- Detect changes to interpolated URL (including variable changes)
- Automatically disconnect and reconnect when interpolated URL changes
- Add debouncing (400ms) to prevent excessive reconnections
- Track previous interpolated URL to avoid unnecessary reconnects
- Store interpolated URL when connection becomes active
- Improve error handling and cleanup

* chore: removing diff

* Add WebSocket connection status IPC method

- Add connectionStatus() method to WsClient that returns detailed status
  ('disconnected', 'connecting', 'connected') instead of boolean
- Add renderer:ws:connection-status IPC handler in electron layer
- Add getWsConnectionStatus() utility function in network utils
- Provides more granular connection state information for UI components

* refactor: improve WebSocket connection status tracking in WsQueryUrl

- Replace boolean isConnectionActive with connectionStatus state ('disconnected', 'connecting', 'connected')
- Add useWsConnectionStatus hook to poll connection status every 2 seconds
- Refactor connection handlers: handleConnect, handleDisconnect, handleReconnect
- Update to use getWsConnectionStatus instead of isWsConnectionActive for more granular status
- Improve reconnect logic to handle URL variable interpolation changes
- Add proper connection status indicators in UI (connecting state with pulse animation)

* fix: improve WebSocket URL handling and request initialization

- Fix WebSocket URL state management by reading directly from item instead of local state
- Add handleUrlChange function to properly dispatch URL changes
- Fix interpolated URL change detection logic in useEffect
- Initialize params array for new WebSocket requests to prevent undefined errors
- Ensure params array is initialized when URL changes in draft/request
- Remove console.log statements and unused imports
- Update persistence test replacement URL to avoid port conflicts

These changes ensure WebSocket requests properly handle URL changes and
maintain consistent state between draft and saved requests.

* feat: refactor WebSocket connection status handling

---------

Co-authored-by: Sid <siddharth@usebruno.com>
2025-12-03 15:52:52 +05:30
Siddharth Gelera (reaper)
893058067d chore: improve coderabbit review instructions (#6282)
* chore: add more review items

* Update CODING_STANDARDS.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Sid <siddharth@usebruno.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-03 13:39:24 +05:30
Anoop M D
4a38f2d49f Update PR template to consent for AI usage 2025-12-03 03:49:02 +05:30
Tarunkumar
d56e4f625b fixed min width issue so two windows can be used in parallel (#5863)
* fixed min width issue so two windows can be used in parallel

* updated the min height as well, so that Bruno can be viewed in quarter window on screen

---------

Co-authored-by: Tarunkumar N Bagmar <tarunkumar@Tarunkumars-MacBook-Air.local>
Co-authored-by: Anoop M D <anoop@usebruno.com>
2025-12-02 21:39:03 +05:30
Sid
9bbcf7ecbe fix:match to full string (#6272) 2025-12-02 17:54:53 +05:30
Chirag Chandrashekhar
ee4c923bc5 Merge pull request #6117 from chirag-bruno/bugfix/postman-export-omit-collection-vars
fix: Exporting Bruno collection as Postman collection omits collection variables
2025-12-02 16:47:15 +05:30
naman-bruno
bc82536a82 fix: ws and grpc request not saving (#6267) 2025-12-02 15:54:54 +05:30
Sid
b95ef99ef2 Merge pull request #6266 from usebruno/feat/copy-response-to-clipboard-5409
added copy button to copy response (#5409)
2025-12-02 15:47:18 +05:30
Pooja
06a024a1d9 feat: add copy paste feature for folder (#6097)
* feat: add copy paste feature for folder

* add: playwright test

* fix

* fix

* add: keyboardFocusBg in light mode

* fix: copy paste in yaml collection

* improvement
2025-12-02 15:29:32 +05:30
SAGAR KHATRI
f7ea1f8dbb Added semi colon 2025-11-13 19:31:21 +05:30
SAGAR KHATRI
cf19035b0b Merge branch 'usebruno:main' into feature/auto-scroll-on-tab-change 2025-11-12 23:53:05 +05:30
SAGAR KHATRI
d9a3f74cb7 Auto scroll to show this item when its tab becomes active 2025-11-12 20:50:44 +05:30
711 changed files with 24014 additions and 10518 deletions

View File

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

5
.gitignore vendored
View File

@@ -52,7 +52,10 @@ bruno.iml
# Playwright
/blob-report/
# packages
# Development plan files
*.plan.md
# packages dist
packages/bruno-filestore/dist
packages/bruno-requests/dist
packages/bruno-schema-types/dist

View File

@@ -6,7 +6,7 @@
- Use 2 spaces for indentation. No tabs, just spaces keeps everything neat and uniform.
- Stick to single quotes for strings. Double quotes are cool elsewhere, but here we go single.
- Stick to single quotes for strings. For JSX/TSX attributes, use double quotes (e.g., <svg xmlns="..." viewBox="...">) to follow React conventions.
- Always add semicolons at the end of statements. It's like putting a period at the end of a sentence clarity matters.
@@ -34,6 +34,40 @@
Remember, these rules are here to make our codebase harmonious. If something doesn't fit perfectly, let's chat about it. Happy coding! 🚀
## Tests
- Add tests for any new functionality or meaningful changes. If code is added, removed, or significantly modified, corresponding tests should be updated or created.
- Prioritise high-value tests over maximum coverage. Focus on testing behaviour that is critical, complex, or likely to break—dont chase coverage numbers for their own sake.
- Write behaviour-driven tests, not implementation-driven ones. Tests should validate real expected output and observable behaviour, not internal details or mocked-out logic unless absolutely necessary.
- Minimise mocking unless it meaningfully increases clarity or isolates external dependencies. Prefer real flows where practical; only mock external services, slow systems, or non-deterministic behaviour.
- Keep tests readable and maintainable. Optimise for clarity over cleverness. Name tests descriptively, keep setup minimal, and avoid unnecessary abstraction.
- Aim for tests that fail usefully. When a test fails, it should clearly indicate what behaviour broke and why.
- Cover both the “happy path” and the realistically problematic paths. Validate expected success behaviour, but also validate error handling, edge cases, and degraded-mode behaviour when appropriate.
- Ensure tests are deterministic and reproducible. No randomness, timing dependencies, or environment-specific assumptions without explicit control.
- Avoid overfitting tests to current behaviour if future flexibility matters. Only assert what needs to be true, not incidental details.
- Use consistent patterns and helper utilities where they improve clarity. Prefer shared test utilities over copy-pasted setup code, but only when it actually reduces complexity.
- Tests should be fast enough to run continuously. Avoid long-running operations unless absolutely necessary; prefer lightweight fixtures and isolated units.
## UI Specific instructions
### React
- Use styled component's theme prop to manage CSS colors and not CSS variables when in the context of a styled component or any react component using the styled component
- Styled Components are used as wrappers to define both self and children components style, tailwind classes are used specifically for layout based styles.
- Styled Component CSS might also change layout but tailwind classes shouldn't define colors.
## Readability and Abstractions
- Avoid abstractions unless the exact same code is being used in more than 3 places.

View File

@@ -1,6 +1,6 @@
// eslint.config.js
const { defineConfig } = require("eslint/config");
const globals = require("globals");
const { defineConfig } = require('eslint/config');
const globals = require('globals');
const { fixupPluginRules } = require('@eslint/compat');
const eslintPluginDiff = require('eslint-plugin-diff');
@@ -11,6 +11,18 @@ const runESMImports = async () => {
};
module.exports = runESMImports().then(() => defineConfig([
// Global ignores - must be a standalone object with ONLY ignores
{
ignores: [
'**/node_modules/**/*',
'**/dist/**/*',
'**/*.bru',
'packages/bruno-js/src/sandbox/bundle-browser-rollup.js',
'packages/bruno-app/public/static/**/*',
'packages/bruno-app/.next/**/*',
'packages/bruno-electron/web/**/*'
]
},
{
plugins: {
'diff': fixupPluginRules(eslintPluginDiff),
@@ -34,13 +46,13 @@ module.exports = runESMImports().then(() => defineConfig([
'packages/bruno-converters/**/*.js',
'packages/bruno-electron/**/*.js',
'packages/bruno-filestore/**/*.ts',
'packages/bruno-schema-types/**/*.ts',
'packages/bruno-js/**/*.js',
'packages/bruno-lang/**/*.js',
'packages/bruno-requests/**/*.ts',
'packages/bruno-requests/**/*.js',
'packages/bruno-tests/**/*.{js,ts}'
],
processor: 'diff/diff',
rules: {
...stylistic.configs.customize({
indent: 2,
@@ -56,7 +68,7 @@ module.exports = runESMImports().then(() => defineConfig([
minElements: 2,
consistent: true
}],
'@stylistic/function-paren-newline': ['error', 'never'],
'@stylistic/function-paren-newline': ['off'],
'@stylistic/array-bracket-spacing': ['error', 'never'],
'@stylistic/arrow-spacing': ['error', { before: true, after: true }],
'@stylistic/function-call-spacing': ['error', 'never'],
@@ -64,12 +76,14 @@ module.exports = runESMImports().then(() => defineConfig([
'@stylistic/padding-line-between-statements': ['off'],
'@stylistic/semi-style': ['error', 'last'],
'@stylistic/max-len': ['off'],
'@stylistic/jsx-one-expression-per-line': ['off']
'@stylistic/jsx-one-expression-per-line': ['off'],
'@stylistic/max-statements-per-line': ['off'],
'@stylistic/no-mixed-operators': ['off']
}
},
{
files: ["packages/bruno-app/**/*.{js,jsx,ts}"],
ignores: ["**/*.config.js", "**/public/**/*"],
files: ['packages/bruno-app/**/*.{js,jsx,ts}'],
ignores: ['**/*.config.js', '**/public/**/*'],
languageOptions: {
globals: {
...globals.browser,
@@ -82,114 +96,114 @@ module.exports = runESMImports().then(() => defineConfig([
},
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
jsx: true
}
}
},
rules: {
"no-undef": "error",
},
'no-undef': 'error'
}
},
{
// It prevents lint errors when using CommonJS exports (module.exports) in Jest mocks.
files: ["packages/bruno-app/src/test-utils/mocks/codemirror.js"],
files: ['packages/bruno-app/src/test-utils/mocks/codemirror.js'],
languageOptions: {
globals: {
...globals.node,
...globals.jest,
},
...globals.jest
}
},
rules: {
"no-undef": "error",
},
'no-undef': 'error'
}
},
{
files: ["packages/bruno-cli/**/*.js"],
ignores: ["**/*.config.js"],
files: ['packages/bruno-cli/**/*.js'],
ignores: ['**/*.config.js'],
languageOptions: {
globals: {
...globals.node,
...globals.jest,
...globals.jest
},
parserOptions: {
ecmaVersion: "latest"
},
ecmaVersion: 'latest'
}
},
rules: {
"no-undef": "error",
},
'no-undef': 'error'
}
},
{
files: ["packages/bruno-common/**/*.ts"],
ignores: ["**/*.config.js", "**/dist/**/*"],
files: ['packages/bruno-common/**/*.ts'],
ignores: ['**/*.config.js', '**/dist/**/*'],
languageOptions: {
globals: {
...globals.node,
...globals.jest,
...globals.jest
},
parser: require("@typescript-eslint/parser"),
parser: require('@typescript-eslint/parser'),
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
project: "./packages/bruno-common/tsconfig.json",
},
ecmaVersion: 'latest',
sourceType: 'module',
project: './packages/bruno-common/tsconfig.json'
}
},
rules: {
"no-undef": "error",
},
'no-undef': 'error'
}
},
{
files: ["packages/bruno-converters/**/*.js"],
ignores: ["**/*.config.js", "**/dist/**/*"],
files: ['packages/bruno-converters/**/*.js'],
ignores: ['**/*.config.js', '**/dist/**/*'],
languageOptions: {
globals: {
...globals.node,
...globals.jest,
...globals.jest
},
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
ecmaVersion: 'latest',
sourceType: 'module'
}
},
rules: {
"no-undef": "error",
},
'no-undef': 'error'
}
},
{
files: ["packages/bruno-electron/**/*.js"],
ignores: ["**/*.config.js", "**/web/**/*"],
files: ['packages/bruno-electron/**/*.js'],
ignores: ['**/*.config.js', '**/web/**/*'],
languageOptions: {
globals: {
...globals.node,
...globals.jest,
},
...globals.jest
}
},
rules: {
"no-undef": "error",
},
'no-undef': 'error'
}
},
{
files: ["packages/bruno-filestore/**/*.ts"],
ignores: ["**/*.config.js", "**/dist/**/*"],
files: ['packages/bruno-filestore/**/*.ts'],
ignores: ['**/*.config.js', '**/dist/**/*'],
languageOptions: {
globals: {
...globals.node,
...globals.jest,
...globals.jest
},
parser: require("@typescript-eslint/parser"),
parser: require('@typescript-eslint/parser'),
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
project: "./packages/bruno-filestore/tsconfig.json",
},
ecmaVersion: 'latest',
sourceType: 'module',
project: './packages/bruno-filestore/tsconfig.json'
}
},
rules: {
"no-undef": "error",
},
'no-undef': 'error'
}
},
{
files: ["packages/bruno-js/**/*.js"],
ignores: ["**/*.config.js", "**/dist/**/*"],
files: ['packages/bruno-js/**/*.js'],
ignores: ['**/*.config.js', '**/dist/**/*'],
languageOptions: {
globals: {
...globals.node,
@@ -200,65 +214,65 @@ module.exports = runESMImports().then(() => defineConfig([
typeDetectGlobalObject: false
},
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
ecmaVersion: 'latest',
sourceType: 'module'
}
},
rules: {
"no-undef": "error",
},
'no-undef': 'error'
}
},
{
files: ["packages/bruno-lang/**/*.js"],
ignores: ["**/*.config.js", "**/dist/**/*"],
files: ['packages/bruno-lang/**/*.js'],
ignores: ['**/*.config.js', '**/dist/**/*'],
languageOptions: {
globals: {
...globals.node,
...globals.jest,
...globals.jest
},
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
ecmaVersion: 'latest',
sourceType: 'module'
}
},
rules: {
"no-undef": "error",
},
'no-undef': 'error'
}
},
{
files: ["packages/bruno-requests/**/*.ts"],
ignores: ["**/*.config.js", "**/dist/**/*"],
files: ['packages/bruno-requests/**/*.ts'],
ignores: ['**/*.config.js', '**/dist/**/*'],
languageOptions: {
globals: {
...globals.node,
...globals.jest,
...globals.jest
},
parser: require("@typescript-eslint/parser"),
parser: require('@typescript-eslint/parser'),
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
project: "./packages/bruno-requests/tsconfig.json",
},
ecmaVersion: 'latest',
sourceType: 'module',
project: './packages/bruno-requests/tsconfig.json'
}
},
rules: {
"no-undef": "error",
},
'no-undef': 'error'
}
},
{
files: ["packages/bruno-requests/**/*.js"],
ignores: ["**/*.config.js", "**/dist/**/*"],
files: ['packages/bruno-requests/**/*.js'],
ignores: ['**/*.config.js', '**/dist/**/*'],
languageOptions: {
globals: {
...globals.node,
...globals.jest,
...globals.jest
},
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
ecmaVersion: 'latest',
sourceType: 'module'
}
},
rules: {
"no-undef": "error",
},
},
'no-undef': 'error'
}
}
]));

1914
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -21,6 +21,8 @@
"@usebruno/common": "0.1.0",
"@usebruno/graphql-docs": "0.1.0",
"@usebruno/schema": "0.7.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0",
"classnames": "^2.3.1",
"codemirror": "5.65.2",
"codemirror-graphql": "2.1.1",
@@ -43,6 +45,8 @@
"idb": "^7.0.0",
"immer": "^9.0.15",
"jsesc": "^3.0.2",
"js-yaml": "^4.1.0",
"xml2js": "^0.6.2",
"jshint": "^2.13.6",
"json5": "^2.2.3",
"jsonc-parser": "^3.2.1",
@@ -81,6 +85,7 @@
"shell-quote": "^1.8.3",
"strip-json-comments": "^5.0.1",
"styled-components": "^5.3.3",
"swagger-ui-react": "5.17.12",
"system": "^2.0.1",
"url": "^0.11.3",
"xml-formatter": "^3.5.0",

View File

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

View File

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

View File

@@ -0,0 +1,129 @@
const yamlPlugin = (cm) => {
cm.defineMode('yaml', function () {
var cons = ['true', 'false', 'on', 'off', 'yes', 'no'];
var keywordRegex = new RegExp('\\b((' + cons.join(')|(') + '))$', 'i');
return {
token: function (stream, state) {
var ch = stream.peek();
var esc = state.escaped;
state.escaped = false;
/* comments */
if (ch == '#' && (stream.pos == 0 || /\s/.test(stream.string.charAt(stream.pos - 1)))) {
stream.skipToEnd();
return 'comment';
}
if (stream.match(/^('([^']|\\.)*'?|"([^"]|\\.)*"?)/)) return 'string';
if (state.literal && stream.indentation() > state.keyCol) {
stream.skipToEnd();
return 'string';
} else if (state.literal) {
state.literal = false;
}
if (stream.sol()) {
state.keyCol = 0;
state.pair = false;
state.pairStart = false;
/* document start */
if (stream.match('---')) {
return 'def';
}
/* document end */
if (stream.match('...')) {
return 'def';
}
/* array list item */
if (stream.match(/\s*-\s+/)) {
return 'meta';
}
}
/* inline pairs/lists */
if (stream.match(/^(\{|\}|\[|\])/)) {
if (ch == '{') state.inlinePairs++;
else if (ch == '}') state.inlinePairs--;
else if (ch == '[') state.inlineList++;
else state.inlineList--;
return 'meta';
}
/* list separator */
if (state.inlineList > 0 && !esc && ch == ',') {
stream.next();
return 'meta';
}
/* pairs separator */
if (state.inlinePairs > 0 && !esc && ch == ',') {
state.keyCol = 0;
state.pair = false;
state.pairStart = false;
stream.next();
return 'meta';
}
/* start of value of a pair */
if (state.pairStart) {
/* block literals */
if (stream.match(/^\s*(\||\>)\s*/)) {
state.literal = true;
return 'meta';
}
/* references */
if (stream.match(/^\s*(\&|\*)[a-z0-9\._-]+\b/i)) {
return 'variable-2';
}
/* numbers */
if (state.inlinePairs == 0 && stream.match(/^\s*-?[0-9\.\,]+\s?$/)) {
return 'number';
}
if (state.inlinePairs > 0 && stream.match(/^\s*-?[0-9\.\,]+\s?(?=(,|}))/)) {
return 'number';
}
/* keywords */
if (stream.match(keywordRegex)) {
return 'keyword';
}
}
/* pairs (associative arrays) -> key */
if (
!state.pair
&& stream.match(/^\s*(?:[,\[\]{}&*!|>'"%@`][^\s'":]|[^\s,\[\]{}#&*!|>'"%@`])[^#:]*(?=:($|\s))/)
) {
state.pair = true;
state.keyCol = stream.indentation();
return 'atom';
}
if (state.pair && stream.match(/^:\s*/)) {
state.pairStart = true;
return 'meta';
}
/* nothing found, continue */
state.pairStart = false;
state.escaped = ch == '\\';
stream.next();
return null;
},
startState: function () {
return {
pair: false,
pairStart: false,
keyCol: 0,
inlinePairs: 0,
inlineList: 0,
literal: false,
escaped: false
};
},
lineComment: '#',
fold: 'indent'
};
});
cm.defineMIME('text/x-yaml', 'yaml');
cm.defineMIME('text/yaml', 'yaml');
};
export default yamlPlugin;

View File

@@ -0,0 +1,65 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
div.CodeMirror {
height: calc(100vh - 4rem);
background: ${(props) => props.theme.codemirror.bg};
border: solid 1px ${(props) => props.theme.codemirror.border};
font-family: ${(props) => (props.font ? props.font : 'default')};
line-break: anywhere;
}
.CodeMirror-dialog {
overflow: visible;
input {
background: transparent;
border: 1px solid #d3d6db;
outline: none;
border-radius: 0px;
}
}
.CodeMirror-overlayscroll-horizontal div,
.CodeMirror-overlayscroll-vertical div {
background: #d2d7db;
}
textarea.cm-editor {
position: relative;
}
// Todo: dark mode temporary fix
// Clean this
.CodeMirror.cm-s-monokai {
.CodeMirror-overlayscroll-horizontal div,
.CodeMirror-overlayscroll-vertical div {
background: #444444;
}
}
.cm-s-monokai span.cm-property,
.cm-s-monokai span.cm-attribute {
color: #9cdcfe !important;
}
.cm-s-monokai span.cm-string {
color: #ce9178 !important;
}
.cm-s-monokai span.cm-number {
color: #b5cea8 !important;
}
.cm-s-monokai span.cm-atom {
color: #569cd6 !important;
}
.cm-variable-valid {
color: green;
}
.cm-variable-invalid {
color: red;
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,138 @@
/**
* Copyright (c) 2021 GraphQL Contributors.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import StyledWrapper from './StyledWrapper';
import yamlPlugin from './Plugins/Yaml/index';
let CodeMirror;
const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
if (!SERVER_RENDERED) {
CodeMirror = require('codemirror');
}
export default class CodeEditor extends React.Component {
constructor(props) {
super(props);
this.cachedValue = props.value || '';
this.variables = {};
this.lintOptions = {
esversion: 11,
expr: true,
asi: true
};
}
componentWillMount() {
switch (this.props.mode) {
case 'yaml':
// YAML linting and hightlighting plugin
yamlPlugin(CodeMirror);
break;
default:
break;
}
}
componentDidMount() {
const editor = (this.editor = CodeMirror(this._node, {
value: this.props.value || '',
lineNumbers: true,
lineWrapping: true,
tabSize: 2,
mode: this.props.mode || 'application/text',
keyMap: 'sublime',
autoCloseBrackets: true,
matchBrackets: true,
showCursorWhenSelecting: true,
foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'],
lint: this.lintOptions,
readOnly: this.props.readOnly,
scrollbarStyle: 'overlay',
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
extraKeys: {
'Cmd-S': () => {
if (this.props.onSave) {
this.props.onSave();
}
},
'Ctrl-S': () => {
if (this.props.onSave) {
this.props.onSave();
}
},
'Cmd-F': 'findPersistent',
'Ctrl-F': 'findPersistent',
'Cmd-H': 'replace',
'Ctrl-H': 'replace',
'Tab': function (cm) {
cm.getSelection().includes('\n') || editor.getLine(cm.getCursor().line) == cm.getSelection()
? cm.execCommand('indentMore')
: cm.replaceSelection(' ', 'end');
},
'Shift-Tab': 'indentLess',
'Ctrl-Space': 'autocomplete',
'Cmd-Space': 'autocomplete',
'Ctrl-Y': 'foldAll',
'Cmd-Y': 'foldAll',
'Ctrl-I': 'unfoldAll',
'Cmd-I': 'unfoldAll'
}
}));
if (editor) {
editor.setOption('lint', this.props.mode && editor.getValue().trim().length > 0 ? this.lintOptions : false);
editor.on('change', this._onEdit);
}
}
componentDidUpdate(prevProps) {
this.ignoreChangeEvent = true;
if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) {
this.cachedValue = this.props.value;
this.editor.setValue(this.props.value);
}
if (this.props.theme !== prevProps.theme && this.editor) {
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
}
this.ignoreChangeEvent = false;
}
componentWillUnmount() {
if (this.editor) {
this.editor.off('change', this._onEdit);
this.editor = null;
}
}
render() {
if (this.editor) {
this.editor.refresh();
}
return (
<StyledWrapper
className="h-full w-full graphiql-container"
aria-label="Code Editor"
font={this.props.font}
ref={(node) => {
this._node = node;
}}
/>
);
}
_onEdit = () => {
if (!this.ignoreChangeEvent && this.editor) {
this.editor.setOption('lint', this.editor.getValue().trim().length > 0 ? this.lintOptions : false);
this.cachedValue = this.editor.getValue();
if (this.props.onEdit) {
this.props.onEdit(this.cachedValue);
}
}
};
}

View File

@@ -0,0 +1,51 @@
import get from 'lodash/get';
import { useTheme } from 'providers/Theme';
import { useDispatch, useSelector } from 'react-redux';
import CodeEditor from './CodeEditor/index';
import { IconDeviceFloppy } from '@tabler/icons';
import { saveApiSpecToFile } from 'providers/ReduxStore/slices/apiSpec';
import { useState } from 'react';
const FileEditor = ({ apiSpec }) => {
const dispatch = useDispatch();
const { displayedTheme, theme } = useTheme();
const preferences = useSelector((state) => state.app.preferences);
const [content, setContent] = useState(apiSpec?.raw);
const onEdit = (value) => {
setContent(value);
};
const onSave = () => {
dispatch(saveApiSpecToFile({ uid: apiSpec?.uid, content }));
};
const hasChanges = Boolean(content != apiSpec?.raw);
const editorMode = 'yaml';
return (
<div className="flex flex-grow relative">
<CodeEditor
theme={displayedTheme}
value={content}
onEdit={onEdit}
onSave={onSave}
mode={editorMode}
font={get(preferences, 'font.codeFont', 'default')}
/>
<IconDeviceFloppy
onClick={onSave}
color={hasChanges ? theme.colors.text.yellow : theme.requestTabs.icon.color}
strokeWidth={1.5}
size={22}
className={`absolute right-0 top-0 m-4 ${
hasChanges ? 'cursor-pointer oapcity-100' : 'cursor-default opacity-50'
}`}
/>
</div>
);
};
export default FileEditor;

View File

@@ -0,0 +1,19 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.swagger-root {
height: calc(100vh - 4rem);
border: solid 1px ${(props) => props.theme.codemirror.border};
&.dark {
.swagger-ui {
filter: invert(88%) hue-rotate(180deg);
}
.swagger-ui .microlight {
filter: invert(100%) hue-rotate(180deg);
}
}
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,19 @@
import SwaggerUI from 'swagger-ui-react';
import StyledWrapper from './StyledWrapper';
import { useTheme } from 'providers/Theme';
const Swagger = ({ string }) => {
const { displayedTheme } = useTheme();
console.log('string', string);
return (
<StyledWrapper>
<div className={`swagger-root w-full overflow-y-scroll ${displayedTheme}`}>
<SwaggerUI spec={string} />
</div>
</StyledWrapper>
);
};
export default Swagger;

View File

@@ -0,0 +1,22 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.menu-icon {
cursor: pointer;
color: ${(props) => props.theme.sidebar.dropdownIcon.color};
}
div.dropdown-item.menu-item {
color: ${(props) => props.theme.colors.danger};
&:hover {
background-color: ${(props) => props.theme.colors.bg.danger};
color: white;
}
}
.react-tooltip {
z-index: 10;
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,97 @@
import React, { forwardRef, useRef } from 'react';
import find from 'lodash/find';
import { useSelector, useDispatch } from 'react-redux';
import { IconFileCode, IconDots } from '@tabler/icons';
import StyledWrapper from './StyledWrapper';
import FileEditor from './FileEditor';
import Dropdown from 'components/Dropdown';
import { openApiSpec } from 'providers/ReduxStore/slices/apiSpec';
import { useState } from 'react';
import CreateApiSpec from 'components/Sidebar/ApiSpecs/CreateApiSpec';
import { Suspense } from 'react';
import Swagger from './Renderers/Swagger';
import toast from 'react-hot-toast';
const ApiSpecPanel = () => {
const dispatch = useDispatch();
const [createApiSpecModalOpen, setCreateApiSpecModalOpen] = useState(false);
const { apiSpecs, activeApiSpecUid } = useSelector((state) => state.apiSpec);
const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
let apiSpec = find(apiSpecs, (c) => c.uid === activeApiSpecUid);
const { filename, pathname, raw, uid } = apiSpec || {};
if (!uid) {
return <div className="p-4 opacity-50">API Spec not found!</div>;
}
const MenuIcon = forwardRef((props, ref) => {
return (
<div ref={ref}>
<IconDots size={22} />
</div>
);
});
const handleOpenApiSpec = () => {
dispatch(openApiSpec()).catch(
(err) => console.log(err) && toast.error('An error occurred while opening the API spec')
);
};
return (
<StyledWrapper className="flex flex-col flex-grow relative">
{createApiSpecModalOpen ? <CreateApiSpec onClose={() => setCreateApiSpecModalOpen(false)} /> : null}
<div className="p-3 mb-2 w-full flex flex-row justify-between grid grid-cols-3">
<div className="flex flex-row justify-start gap-x-4 col-span-1">
<div className="flex w-fit items-center cursor-pointer">
<IconFileCode size={18} strokeWidth={1.5} />
<span className="ml-2 mr-4 font-semibold">API Designer</span>
</div>
</div>
<div className="w-full col-span-1 flex justify-center" title={pathname}>
{filename}
</div>
<div className="menu-icon pr-2 col-span-1 flex justify-end">
<Dropdown onCreate={onDropdownCreate} icon={<MenuIcon />} placement="bottom-start">
<div
className="dropdown-item"
onClick={(e) => {
dropdownTippyRef.current.hide();
setCreateApiSpecModalOpen(true);
}}
>
Create API Spec
</div>
<div
className="dropdown-item"
onClick={(e) => {
dropdownTippyRef.current.hide();
handleOpenApiSpec();
}}
>
Open API Spec
</div>
</Dropdown>
</div>
</div>
<section className="main flex flex-grow px-4 relative">
<div className="w-full grid grid-cols-2">
<div className="col-span-1">
<FileEditor apiSpec={apiSpec} />
</div>
<div className="col-span-1">
<Suspense fallback="">
<Swagger string={raw} />
</Suspense>
</div>
</div>
</section>
</StyledWrapper>
);
};
export default ApiSpecPanel;

View File

@@ -0,0 +1,198 @@
import styled from 'styled-components';
const Wrapper = styled.div`
height: 36px;
display: flex;
align-items: center;
background: ${(props) => props.theme.sidebar.bg};
border-bottom: 1px solid ${(props) => props.theme.sidebar.collection.item.hoverBg};
-webkit-app-region: drag;
user-select: none;
.titlebar-content {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
height: 100%;
padding: 0 12px;
padding-left: 70px; /* Space for macOS window controls */
transition: padding-left 0.15s ease;
}
/* When in full screen, no traffic lights so reduce padding */
&.fullscreen .titlebar-content {
padding-left: 4px;
}
/* Remove drag region from interactive elements */
.workspace-name-container,
.dropdown-item,
.home-button,
.dropdown,
button {
-webkit-app-region: no-drag;
}
/* Left section */
.titlebar-left {
display: flex;
align-items: center;
flex-shrink: 0;
margin-left: 10px;
-webkit-app-region: no-drag;
}
/* When in full screen, no traffic lights so remove margin-left */
&.fullscreen .titlebar-left {
margin-left: 0px;
}
/* Workspace Name Dropdown Trigger */
.workspace-name-container {
display: flex;
align-items: center;
gap: 6px;
padding: 5px 10px;
border-radius: 6px;
cursor: pointer;
transition: all 0.15s ease;
&:hover {
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
}
.workspace-name {
font-size: 13px;
font-weight: 500;
color: ${(props) => props.theme.sidebar.color};
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 180px;
}
.chevron-icon {
flex-shrink: 0;
color: ${(props) => props.theme.sidebar.muted};
transition: transform 0.2s ease;
}
}
/* Center section - Bruno branding */
.titlebar-center {
position: absolute;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 6px;
pointer-events: none;
.bruno-text {
font-size: 13px;
font-weight: 600;
color: ${(props) => props.theme.text};
letter-spacing: 0.5px;
}
}
/* Right section */
.titlebar-right {
display: flex;
align-items: center;
justify-content: flex-end;
flex-shrink: 0;
}
/* Workspace Dropdown Styles */
.workspace-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 10px !important;
margin: 0 !important;
&.active {
.check-icon {
opacity: 1;
}
}
&:hover {
.pin-btn:not(.pinned) {
opacity: 1;
}
}
.workspace-name {
flex: 1;
min-width: 0;
font-size: 13px;
font-weight: 400;
color: ${(props) => props.theme.dropdown.color};
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.workspace-actions {
display: flex;
align-items: center;
gap: 4px;
margin-left: 8px;
flex-shrink: 0;
pointer-events: none;
> * {
pointer-events: auto;
}
}
.check-icon {
color: ${(props) => props.theme.workspace?.accent || props.theme.colors?.text?.yellow};
flex-shrink: 0;
}
.pin-btn {
display: flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
padding: 0;
border: none;
background: transparent;
border-radius: 4px;
cursor: pointer;
color: ${(props) => props.theme.dropdown.mutedText};
transition: background 0.15s ease, color 0.15s ease, opacity 0.15s ease;
opacity: 0;
&.pinned {
opacity: 1;
}
&:hover {
background: ${(props) => props.theme.dropdown.hoverBg};
color: ${(props) => props.theme.dropdown.mutedText};
}
}
}
/* Adjust for non-macOS platforms */
body:not(.os-mac) & {
.titlebar-content {
padding-left: 12px;
}
}
/* Leave room for Windows caption buttons when the overlay is enabled */
body.os-windows & {
.titlebar-content {
padding-right: 120px;
}
}
`;
export default Wrapper;

View File

@@ -0,0 +1,252 @@
import React from 'react';
import { IconCheck, IconChevronDown, IconFolder, IconHome, IconLayoutColumns, IconLayoutRows, IconPin, IconPinned, IconPlus } from '@tabler/icons';
import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react';
import toast from 'react-hot-toast';
import { useDispatch, useSelector } from 'react-redux';
import { savePreferences, showHomePage, toggleSidebarCollapse } from 'providers/ReduxStore/slices/app';
import { closeConsole, openConsole } from 'providers/ReduxStore/slices/logs';
import { openWorkspaceDialog, switchWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
import { sortWorkspaces, toggleWorkspacePin } from 'utils/workspaces';
import Bruno from 'components/Bruno';
import MenuDropdown from 'ui/MenuDropdown';
import ActionIcon from 'ui/ActionIcon';
import IconSidebarToggle from 'components/Icons/IconSidebarToggle';
import CreateWorkspace from 'components/WorkspaceSidebar/CreateWorkspace';
import IconBottombarToggle from 'components/Icons/IconBottombarToggle/index';
import StyledWrapper from './StyledWrapper';
import { toTitleCase } from 'utils/common/index';
const AppTitleBar = () => {
const dispatch = useDispatch();
const [isFullScreen, setIsFullScreen] = useState(false);
// Listen for fullscreen changes
useEffect(() => {
const { ipcRenderer } = window;
if (!ipcRenderer) return;
const removeEnterFullScreenListener = ipcRenderer.on('main:enter-full-screen', () => {
setIsFullScreen(true);
});
const removeLeaveFullScreenListener = ipcRenderer.on('main:leave-full-screen', () => {
setIsFullScreen(false);
});
return () => {
removeEnterFullScreenListener();
removeLeaveFullScreenListener();
};
}, []);
// Get workspace info
const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces);
const preferences = useSelector((state) => state.app.preferences);
const sidebarCollapsed = useSelector((state) => state.app.sidebarCollapsed);
const isConsoleOpen = useSelector((state) => state.logs.isConsoleOpen);
const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid);
// Sort workspaces according to preferences
const sortedWorkspaces = useMemo(() => {
return sortWorkspaces(workspaces, preferences);
}, [workspaces, preferences]);
const [createWorkspaceModalOpen, setCreateWorkspaceModalOpen] = useState(false);
const WorkspaceName = forwardRef((props, ref) => {
return (
<div ref={ref} className="workspace-name-container" {...props}>
<span className="workspace-name">{toTitleCase(activeWorkspace?.name) || 'Default Workspace'}</span>
<IconChevronDown size={14} stroke={1.5} className="chevron-icon" />
</div>
);
});
const handleHomeClick = () => {
dispatch(showHomePage());
};
const handleWorkspaceSwitch = (workspaceUid) => {
dispatch(switchWorkspace(workspaceUid));
toast.success(`Switched to ${workspaces.find((w) => w.uid === workspaceUid)?.name}`);
};
const handleOpenWorkspace = async () => {
try {
await dispatch(openWorkspaceDialog());
toast.success('Workspace opened successfully');
} catch (error) {
toast.error(error.message || 'Failed to open workspace');
}
};
const handleCreateWorkspace = () => {
setCreateWorkspaceModalOpen(true);
};
const handlePinWorkspace = useCallback((workspaceUid, e) => {
e.preventDefault();
e.stopPropagation();
const newPreferences = toggleWorkspacePin(workspaceUid, preferences);
dispatch(savePreferences(newPreferences));
}, [dispatch, preferences]);
const orientation = preferences?.layout?.responsePaneOrientation || 'horizontal';
const handleToggleSidebar = () => {
dispatch(toggleSidebarCollapse());
};
const handleToggleDevtools = () => {
if (isConsoleOpen) {
dispatch(closeConsole());
} else {
dispatch(openConsole());
}
};
const handleToggleVerticalLayout = () => {
const newOrientation = orientation === 'horizontal' ? 'vertical' : 'horizontal';
const updatedPreferences = {
...preferences,
layout: {
...preferences?.layout || {},
responsePaneOrientation: newOrientation
}
};
dispatch(savePreferences(updatedPreferences));
};
// Build workspace menu items
const workspaceMenuItems = useMemo(() => {
const items = sortedWorkspaces.map((workspace) => {
const isActive = workspace.uid === activeWorkspaceUid;
const isPinned = preferences?.workspaces?.pinnedWorkspaceUids?.includes(workspace.uid);
return {
id: workspace.uid,
label: toTitleCase(workspace.name),
onClick: () => handleWorkspaceSwitch(workspace.uid),
className: `workspace-item ${isActive ? 'active' : ''}`,
rightSection: (
<div className="workspace-actions">
{workspace.type !== 'default' && (
<ActionIcon
className={`pin-btn ${isPinned ? 'pinned' : ''}`}
onClick={(e) => handlePinWorkspace(workspace.uid, e)}
label={isPinned ? 'Unpin workspace' : 'Pin workspace'}
size="sm"
>
{isPinned ? (
<IconPinned size={14} stroke={1.5} />
) : (
<IconPin size={14} stroke={1.5} />
)}
</ActionIcon>
)}
{isActive && <IconCheck size={16} stroke={1.5} className="check-icon" />}
</div>
)
};
});
// Add label and action items
items.push(
{ type: 'label', label: 'Workspaces' },
{
id: 'create-workspace',
leftSection: IconPlus,
label: 'Create workspace',
onClick: handleCreateWorkspace
},
{
id: 'open-workspace',
leftSection: IconFolder,
label: 'Open workspace',
onClick: handleOpenWorkspace
}
);
return items;
}, [sortedWorkspaces, activeWorkspaceUid, preferences, handlePinWorkspace]);
return (
<StyledWrapper className={`app-titlebar ${isFullScreen ? 'fullscreen' : ''}`}>
{createWorkspaceModalOpen && (
<CreateWorkspace onClose={() => setCreateWorkspaceModalOpen(false)} />
)}
<div className="titlebar-content">
{/* Left section: Home + Workspace */}
<div className="titlebar-left">
<ActionIcon
onClick={handleHomeClick}
label="Home"
size="lg"
className="home-button"
>
<IconHome size={16} stroke={1.5} />
</ActionIcon>
{/* Workspace Dropdown */}
<MenuDropdown
data-testid="workspace-menu"
items={workspaceMenuItems}
placement="bottom-start"
selectedItemId={activeWorkspaceUid}
>
<WorkspaceName />
</MenuDropdown>
</div>
{/* Center section: Bruno logo + text */}
<div className="titlebar-center">
<Bruno width={18} />
<span className="bruno-text">Bruno</span>
</div>
{/* Right section: Action buttons */}
<div className="titlebar-right">
{/* Toggle sidebar */}
<ActionIcon
onClick={handleToggleSidebar}
label={sidebarCollapsed ? 'Show sidebar' : 'Hide sidebar'}
size="lg"
data-testid="toggle-sidebar-button"
>
<IconSidebarToggle collapsed={sidebarCollapsed} size={16} strokeWidth={1.5} />
</ActionIcon>
{/* Toggle devtools */}
<ActionIcon
onClick={handleToggleDevtools}
label={isConsoleOpen ? 'Hide devtools' : 'Show devtools'}
size="lg"
data-testid="toggle-devtools-button"
>
<IconBottombarToggle collapsed={!isConsoleOpen} size={16} strokeWidth={1.5} />
</ActionIcon>
{/* Toggle vertical layout */}
<ActionIcon
onClick={handleToggleVerticalLayout}
label={orientation === 'horizontal' ? 'Switch to vertical layout' : 'Switch to horizontal layout'}
size="lg"
data-testid="toggle-vertical-layout-button"
>
{orientation === 'horizontal' ? (
<IconLayoutColumns size={16} stroke={1.5} />
) : (
<IconLayoutRows size={16} stroke={1.5} />
)}
</ActionIcon>
</div>
</div>
</StyledWrapper>
);
};
export default AppTitleBar;

View File

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

View File

@@ -105,7 +105,7 @@ export default class CodeEditor extends React.Component {
},
'Cmd-H': 'replace',
'Ctrl-H': 'replace',
Tab: function (cm) {
'Tab': function (cm) {
cm.getSelection().includes('\n') || editor.getLine(cm.getCursor().line) == cm.getSelection()
? cm.execCommand('indentMore')
: cm.replaceSelection(' ', 'end');
@@ -151,7 +151,7 @@ export default class CodeEditor extends React.Component {
} else if (this.props.mode == 'application/xml') {
var doc = new DOMParser();
try {
//add header element and remove prefix namespaces for DOMParser
// add header element and remove prefix namespaces for DOMParser
var dcm = doc.parseFromString(
'<a> ' + internal.replace(/(?<=\<|<\/)\w+:/g, '') + '</a>',
'application/xml'
@@ -188,7 +188,7 @@ export default class CodeEditor extends React.Component {
}
return found;
});
if (editor) {
editor.setOption('lint', this.props.mode && editor.getValue().trim().length > 0 ? this.lintOptions : false);
editor.on('change', this._onEdit);
@@ -197,7 +197,7 @@ export default class CodeEditor extends React.Component {
this.addOverlay();
const getAllVariablesHandler = () => getAllVariables(this.props.collection, this.props.item);
// Setup AutoComplete Helper for all modes
const autoCompleteOptions = {
showHintsFor: this.props.showHintsFor,

View File

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

View File

@@ -43,16 +43,16 @@ const ApiKeyAuth = ({ collection }) => {
};
useEffect(() => {
!apikeyAuth?.placement &&
dispatch(
updateCollectionAuth({
mode: 'apikey',
collectionUid: collection.uid,
content: {
placement: 'header'
}
})
);
!apikeyAuth?.placement
&& dispatch(
updateCollectionAuth({
mode: 'apikey',
collectionUid: collection.uid,
content: {
placement: 'header'
}
})
);
}, [apikeyAuth]);
return (

View File

@@ -87,7 +87,7 @@ const AuthMode = ({ collection }) => {
}}
>
NTLM Auth
</div>
</div>
<div
className="dropdown-item"
onClick={() => {

View File

@@ -9,13 +9,7 @@ import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const NTLMAuth = ({ collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
@@ -25,7 +19,6 @@ const NTLMAuth = ({ collection }) => {
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
const handleUsernameChange = (username) => {
dispatch(
updateCollectionAuth({
@@ -67,10 +60,7 @@ const NTLMAuth = ({ collection }) => {
}
})
);
};
};
return (
<StyledWrapper className="mt-2 w-full">

View File

@@ -10,7 +10,7 @@ import OAuth2ClientCredentials from 'components/RequestPane/Auth/OAuth2/ClientCr
import OAuth2Implicit from 'components/RequestPane/Auth/OAuth2/Implicit/index';
import GrantTypeSelector from 'components/RequestPane/Auth/OAuth2/GrantTypeSelector/index';
const GrantTypeComponentMap = ({collection }) => {
const GrantTypeComponentMap = ({ collection }) => {
const dispatch = useDispatch();
const save = () => {

View File

@@ -13,7 +13,6 @@ import StyledWrapper from './StyledWrapper';
import OAuth2 from './OAuth2';
import NTLMAuth from './NTLMAuth';
const Auth = ({ collection }) => {
const authMode = collection.draft?.root ? get(collection, 'draft.root.request.auth.mode') : get(collection, 'root.request.auth.mode');
const dispatch = useDispatch();
@@ -36,7 +35,7 @@ const Auth = ({ collection }) => {
}
case 'ntlm': {
return <NTLMAuth collection={collection} />;
}
}
case 'oauth2': {
return <OAuth2 collection={collection} />;
}

View File

@@ -39,7 +39,7 @@ const ClientCertSettings = ({ collection }) => {
domain: Yup.string()
.required()
.trim()
.test('not-empty-after-trim', 'Domain is required', value => value && value.trim().length > 0),
.test('not-empty-after-trim', 'Domain is required', (value) => value && value.trim().length > 0),
type: Yup.string().required().oneOf(['cert', 'pfx']),
certFilePath: Yup.string().when('type', {
is: (type) => type == 'cert',
@@ -151,22 +151,22 @@ const ClientCertSettings = ({ collection }) => {
{!clientCertConfig.length
? 'No client certificates added'
: clientCertConfig.map((clientCert, index) => (
<li key={`client-cert-${index}`} className="flex items-center available-certificates p-2 rounded-lg mb-2">
<div className="flex items-center w-full justify-between">
<div className="flex w-full items-center">
<IconWorld className="mr-2" size={18} strokeWidth={1.5} />
{clientCert.domain}
</div>
<div className="flex w-full items-center">
<IconCertificate className="mr-2 flex-shrink-0" size={18} strokeWidth={1.5} />
{clientCert.type === 'cert' ? clientCert.certFilePath : clientCert.pfxFilePath}
</div>
<li key={`client-cert-${index}`} className="flex items-center available-certificates p-2 rounded-lg mb-2">
<div className="flex items-center w-full justify-between">
<div className="flex w-full items-center">
<IconWorld className="mr-2" size={18} strokeWidth={1.5} />
{clientCert.domain}
</div>
<div className="flex w-full items-center">
<IconCertificate className="mr-2 flex-shrink-0" size={18} strokeWidth={1.5} />
{clientCert.type === 'cert' ? clientCert.certFilePath : clientCert.pfxFilePath}
</div>
<button onClick={() => handleRemove(index)} className="remove-certificate ml-2">
<IconTrash size={18} strokeWidth={1.5} />
<IconTrash size={18} strokeWidth={1.5} />
</button>
</div>
</li>
))}
</div>
</li>
))}
</ul>
<h1 className="font-medium mt-8 mb-2">Add Client Certificate</h1>
@@ -373,7 +373,7 @@ const ClientCertSettings = ({ collection }) => {
) : null}
</div>
<div className="mt-6 flex flex-row gap-2 items-center">
<button type="submit" className="submit btn btn-sm btn-secondary">
<button type="submit" className="submit btn btn-sm btn-secondary" data-testid="add-client-cert">
Add
</button>
<div className="h-4 border-l border-gray-600"></div>

View File

@@ -38,21 +38,21 @@ const Docs = ({ collection }) => {
}))
);
toggleViewMode();
}
};
const onSave = () => {
dispatch(saveCollectionSettings(collection.uid));
toggleViewMode();
}
};
return (
<StyledWrapper className="h-full w-full relative flex flex-col">
<div className='flex flex-row w-full justify-between items-center mb-4'>
<div className='text-lg font-medium flex items-center gap-2'>
<div className="flex flex-row w-full justify-between items-center mb-4">
<div className="text-lg font-medium flex items-center gap-2">
<IconFileText size={20} strokeWidth={1.5} />
Documentation
</div>
<div className='flex flex-row gap-2 items-center justify-center'>
<div className="flex flex-row gap-2 items-center justify-center">
{isEditing ? (
<>
<div className="editing-mode" role="tab" onClick={handleDiscardChanges}>
@@ -81,14 +81,13 @@ const Docs = ({ collection }) => {
fontSize={get(preferences, 'font.codeFontSize')}
/>
) : (
<div className='h-full overflow-auto pl-1'>
<div className='h-[1px] min-h-[500px]'>
<div className="h-full overflow-auto pl-1">
<div className="h-[1px] min-h-[500px]">
{
docs?.length > 0 ?
<Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={docs} />
:
<Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={documentationPlaceholder} />
}
docs?.length > 0
? <Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={docs} />
: <Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={documentationPlaceholder} />
}
</div>
</div>
)}
@@ -98,7 +97,6 @@ const Docs = ({ collection }) => {
export default Docs;
const documentationPlaceholder = `
Welcome to your collection documentation! This space is designed to help you document your API collection effectively.

View File

@@ -123,8 +123,7 @@ const Headers = ({ collection }) => {
},
header,
'name'
)
}
)}
autocomplete={headerAutoCompleteList}
collection={collection}
/>
@@ -143,8 +142,7 @@ const Headers = ({ collection }) => {
},
header,
'value'
)
}
)}
collection={collection}
autocomplete={MimeTypes}
/>

View File

@@ -1,20 +1,28 @@
import React from "react";
import React from 'react';
import { getTotalRequestCountInCollection } from 'utils/collections/';
import { IconBox, IconFolder, IconWorld, IconApi, IconShare } from '@tabler/icons';
import { areItemsLoading, getItemsLoadStats } from "utils/collections/index";
import { useState } from "react";
import ShareCollection from "components/ShareCollection/index";
import { areItemsLoading, getItemsLoadStats } from 'utils/collections/index';
import { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import ShareCollection from 'components/ShareCollection/index';
import { updateEnvironmentSettingsModalVisibility, updateGlobalEnvironmentSettingsModalVisibility } from 'providers/ReduxStore/slices/app';
const Info = ({ collection }) => {
const dispatch = useDispatch();
const totalRequestsInCollection = getTotalRequestCountInCollection(collection);
const isCollectionLoading = areItemsLoading(collection);
const { loading: itemsLoadingCount, total: totalItems } = getItemsLoadStats(collection);
const [showShareCollectionModal, toggleShowShareCollectionModal] = useState(false);
const globalEnvironments = useSelector((state) => state.globalEnvironments.globalEnvironments);
const collectionEnvironmentCount = collection.environments?.length || 0;
const globalEnvironmentCount = globalEnvironments?.length || 0;
const handleToggleShowShareCollectionModal = (value) => (e) => {
toggleShowShareCollectionModal(value);
}
};
return (
<div className="w-full flex flex-col h-fit">
@@ -39,9 +47,24 @@ const Info = ({ collection }) => {
<IconWorld className="w-5 h-5 text-green-500" stroke={1.5} />
</div>
<div className="ml-4">
<div className="font-medium">Environments</div>
<div className="mt-1 text-muted text-xs">
{collection.environments?.length || 0} environment{collection.environments?.length !== 1 ? 's' : ''} configured
<div className="font-medium text-sm">Environments</div>
<div className="mt-1 flex flex-col gap-1">
<button
type="button"
className="text-sm text-link cursor-pointer hover:underline text-left bg-transparent"
onClick={() => {
dispatch(updateEnvironmentSettingsModalVisibility(true));
}}
>
{collectionEnvironmentCount} collection environment{collectionEnvironmentCount !== 1 ? 's' : ''}
</button>
<button
type="button"
className="text-sm text-link cursor-pointer hover:underline text-left bg-transparent"
onClick={() => dispatch(updateGlobalEnvironmentSettingsModalVisibility(true))}
>
{globalEnvironmentCount} global environment{globalEnvironmentCount !== 1 ? 's' : ''}
</button>
</div>
</div>
</div>
@@ -55,7 +78,7 @@ const Info = ({ collection }) => {
<div className="font-medium">Requests</div>
<div className="mt-1 text-muted text-xs">
{
isCollectionLoading? `${totalItems - itemsLoadingCount} out of ${totalItems} requests in the collection loaded` : `${totalRequestsInCollection} request${totalRequestsInCollection !== 1 ? 's' : ''} in collection`
isCollectionLoading ? `${totalItems - itemsLoadingCount} out of ${totalItems} requests in the collection loaded` : `${totalRequestsInCollection} request${totalRequestsInCollection !== 1 ? 's' : ''} in collection`
}
</div>
</div>
@@ -79,4 +102,4 @@ const Info = ({ collection }) => {
);
};
export default Info;
export default Info;

View File

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

View File

@@ -1,8 +1,8 @@
import StyledWrapper from "./StyledWrapper";
import Docs from "../Docs";
import Info from "./Info";
import StyledWrapper from './StyledWrapper';
import Docs from '../Docs';
import Info from './Info';
import { IconBox } from '@tabler/icons';
import RequestsNotLoaded from "./RequestsNotLoaded";
import RequestsNotLoaded from './RequestsNotLoaded';
const Overview = ({ collection }) => {
return (
@@ -22,6 +22,6 @@ const Overview = ({ collection }) => {
</div>
</div>
);
}
};
export default Overview;
export default Overview;

View File

@@ -156,9 +156,9 @@ const ProxySettings = ({ collection }) => {
<InfoTip infotipId="request-var">
<div>
<ul>
<li><span style={{width: "50px", display: "inline-block"}}>global</span> - use global proxy config</li>
<li><span style={{width: "50px", display: "inline-block"}}>enabled</span> - use collection proxy config</li>
<li><span style={{width: "50px", display: "inline-block"}}>disable</span> - disable proxy</li>
<li><span style={{ width: '50px', display: 'inline-block' }}>global</span> - use global proxy config</li>
<li><span style={{ width: '50px', display: 'inline-block' }}>enabled</span> - use collection proxy config</li>
<li><span style={{ width: '50px', display: 'inline-block' }}>disable</span> - disable proxy</li>
</ul>
</div>
</InfoTip>
@@ -367,4 +367,4 @@ const ProxySettings = ({ collection }) => {
);
};
export default ProxySettings;
export default ProxySettings;

View File

@@ -6,7 +6,7 @@ const StyledWrapper = styled.div`
padding: 6px 0px;
border: none;
border-bottom: solid 2px transparent;
margin-right: 1.25rem;
margin-right: ${(props) => props.theme.tabs.marginRight};
color: var(--color-tab-inactive);
cursor: pointer;

View File

@@ -127,8 +127,7 @@ const VarsTable = ({ collection, vars, varType }) => {
},
_var,
'value'
)
}
)}
collection={collection}
/>
</td>

View File

@@ -4,18 +4,23 @@ import VarsTable from './VarsTable';
import StyledWrapper from './StyledWrapper';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch } from 'react-redux';
import DeprecationWarning from 'components/DeprecationWarning';
const Vars = ({ collection }) => {
const dispatch = useDispatch();
const requestVars = collection.draft?.root ? get(collection, 'draft.root.request.vars.req', []) : get(collection, 'root.request.vars.req', []);
const responseVars = collection.draft?.root ? get(collection, 'draft.root.request.vars.res', []) : get(collection, 'root.request.vars.res', []);
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
return (
<StyledWrapper className="w-full flex flex-col">
<div className="flex-1 mt-2">
<div className="mb-1 title text-xs">Pre Request</div>
<VarsTable collection={collection} vars={requestVars} varType="request" />
</div>
<div className="flex-1">
<div className="mt-1 mb-1 title text-xs">Post Response</div>
<VarsTable collection={collection} vars={responseVars} varType="response" />
</div>
<div className="mt-6">
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
Save

View File

@@ -32,17 +32,32 @@ const CollectionSettings = ({ collection }) => {
const hasTests = root?.request?.tests;
const hasDocs = root?.docs;
const headers = collection.draft?.root ? get(collection, 'draft.root.request.headers', []) : get(collection, 'root.request.headers', []);
const headers = collection.draft?.root
? get(collection, 'draft.root.request.headers', [])
: get(collection, 'root.request.headers', []);
const activeHeadersCount = headers.filter((header) => header.enabled).length;
const requestVars = collection.draft?.root ? get(collection, 'draft.root.request.vars.req', []) : get(collection, 'root.request.vars.req', []);
const activeVarsCount = requestVars.filter((v) => v.enabled).length;
const authMode = (collection.draft?.root ? get(collection, 'draft.root.request.auth', {}) : get(collection, 'root.request.auth', {})).mode || 'none';
const requestVars = collection.draft?.root
? get(collection, 'draft.root.request.vars.req', [])
: get(collection, 'root.request.vars.req', []);
const responseVars = collection.draft?.root
? get(collection, 'draft.root.request.vars.res', [])
: get(collection, 'root.request.vars.res', []);
const activeVarsCount = requestVars.filter((v) => v.enabled).length + responseVars.filter((v) => v.enabled).length;
const authMode
= (collection.draft?.root ? get(collection, 'draft.root.request.auth', {}) : get(collection, 'root.request.auth', {}))
.mode || 'none';
const proxyConfig = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.proxy', {}) : get(collection, 'brunoConfig.proxy', {});
const proxyConfig = collection.draft?.brunoConfig
? get(collection, 'draft.brunoConfig.proxy', {})
: get(collection, 'brunoConfig.proxy', {});
const proxyEnabled = proxyConfig.hostname ? true : false;
const clientCertConfig = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.clientCertificates.certs', []) : get(collection, 'brunoConfig.clientCertificates.certs', []);
const protobufConfig = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.protobuf', {}) : get(collection, 'brunoConfig.protobuf', {});
const clientCertConfig = collection.draft?.brunoConfig
? get(collection, 'draft.brunoConfig.clientCertificates.certs', [])
: get(collection, 'brunoConfig.clientCertificates.certs', []);
const protobufConfig = collection.draft?.brunoConfig
? get(collection, 'draft.brunoConfig.protobuf', {})
: get(collection, 'brunoConfig.protobuf', {});
const getTabPanel = (tab) => {
switch (tab) {
@@ -68,11 +83,7 @@ const CollectionSettings = ({ collection }) => {
return <ProxySettings collection={collection} />;
}
case 'clientCert': {
return (
<ClientCertSettings
collection={collection}
/>
);
return <ClientCertSettings collection={collection} />;
}
case 'protobuf': {
return <Protobuf collection={collection} />;

View File

@@ -125,7 +125,7 @@ const ModifyCookieModal = ({ onClose, domain, cookie }) => {
);
if (!isEmpty(validationErrors)) {
toast.error(Object.values(validationErrors).join("\n"));
toast.error(Object.values(validationErrors).join('\n'));
return;
}
@@ -208,7 +208,7 @@ const ModifyCookieModal = ({ onClose, domain, cookie }) => {
onClose={onClose}
handleCancel={onClose}
handleConfirm={onSubmit}
customHeader={
customHeader={(
<div className="flex items-center justify-between w-full">
<h2 className="font-bold">{title}</h2>
<div className="ml-auto flex items-center ">
@@ -223,7 +223,7 @@ const ModifyCookieModal = ({ onClose, domain, cookie }) => {
<label className="font-normal mr-4 normal-case">Edit Raw</label>
</div>
</div>
}
)}
>
<form onSubmit={(e) => e.preventDefault()} className="px-2">
{isRawMode ? (

View File

@@ -72,7 +72,7 @@ const CollectionProperties = ({ onClose }) => {
const [searchText, setSearchText] = useState(null);
const handleAddCookie = (domain) => {
if(domain) setCurrentDomain(domain);
if (domain) setCurrentDomain(domain);
setIsModifyCookieModalOpen(true);
};

View File

@@ -160,4 +160,4 @@ const StyledWrapper = styled.div`
}
`;
export default StyledWrapper;
export default StyledWrapper;

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { IconBug } from '@tabler/icons';
import {
import {
setSelectedError,
clearDebugErrors
} from 'providers/ReduxStore/slices/logs';
@@ -10,10 +10,10 @@ import StyledWrapper from './StyledWrapper';
const ErrorRow = ({ error, isSelected, onClick }) => {
const formatTime = (timestamp) => {
const date = new Date(timestamp);
return date.toLocaleTimeString('en-US', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
return date.toLocaleTimeString('en-US', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
fractionalSecondDigits: 3
});
@@ -38,18 +38,18 @@ const ErrorRow = ({ error, isSelected, onClick }) => {
};
return (
<div
<div
className={`error-row ${isSelected ? 'selected' : ''}`}
onClick={onClick}
>
<div className="error-message" title={error.message}>
{getShortMessage(error.message)}
</div>
<div className="error-location" title={error.filename}>
{getLocation(error)}
</div>
<div className="error-time">
{formatTime(error.timestamp)}
</div>
@@ -59,7 +59,7 @@ const ErrorRow = ({ error, isSelected, onClick }) => {
const DebugTab = () => {
const dispatch = useDispatch();
const { debugErrors, selectedError } = useSelector(state => state.logs);
const { debugErrors, selectedError } = useSelector((state) => state.logs);
const handleErrorClick = (error) => {
dispatch(setSelectedError(error));
@@ -85,7 +85,7 @@ const DebugTab = () => {
<div>Location</div>
<div className="text-right">Time</div>
</div>
<div className="errors-list">
{debugErrors.map((error, index) => (
<ErrorRow
@@ -103,4 +103,4 @@ const DebugTab = () => {
);
};
export default DebugTab;
export default DebugTab;

View File

@@ -225,4 +225,4 @@ const StyledWrapper = styled.div`
}
`;
export default StyledWrapper;
export default StyledWrapper;

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
import {
IconX,
IconBug,
IconFileText,
@@ -15,7 +15,7 @@ import StyledWrapper from './StyledWrapper';
const ErrorInfoTab = ({ error }) => {
const { version } = useApp();
const formatTimestamp = (timestamp) => {
const date = new Date(timestamp);
return date.toLocaleString();
@@ -23,7 +23,7 @@ const ErrorInfoTab = ({ error }) => {
const generateGitHubIssueUrl = () => {
const title = `Bug: ${error.message.substring(0, 50)}${error.message.length > 50 ? '...' : ''}`;
const body = `## Bug Report
### Error Details
@@ -66,7 +66,7 @@ ${error.args ? error.args.map((arg, index) => {
const encodedTitle = encodeURIComponent(title);
const encodedBody = encodeURIComponent(body);
return `https://github.com/usebruno/bruno/issues/new?template=BLANK_ISSUE&title=${encodedTitle}&body=${encodedBody}`;
};
@@ -84,33 +84,33 @@ ${error.args ? error.args.map((arg, index) => {
<label>Message:</label>
<span className="error-message-full">{error.message || 'No message available'}</span>
</div>
{error.filename && (
<div className="info-item">
<label>File:</label>
<span className="file-path">{error.filename}</span>
</div>
)}
{error.lineno && (
<div className="info-item">
<label>Line:</label>
<span>{error.lineno}{error.colno ? `:${error.colno}` : ''}</span>
</div>
)}
<div className="info-item">
<label>Timestamp:</label>
<span>{formatTimestamp(error.timestamp)}</span>
</div>
</div>
</div>
<div className="section">
<h4>Report Issue</h4>
<div className="report-section">
<p>Found a bug? Help us improve Bruno by reporting this error on GitHub.</p>
<button
<button
className="report-button"
onClick={handleReportIssue}
title="Report this error on GitHub"
@@ -127,11 +127,11 @@ ${error.args ? error.args.map((arg, index) => {
const StackTraceTab = ({ error }) => {
const formatStackTrace = (stack) => {
if (!stack) return 'Stack trace not available';
return stack
.split('\n')
.map(line => line.trim())
.filter(line => line.length > 0)
.map((line) => line.trim())
.filter((line) => line.length > 0)
.join('\n');
};
@@ -152,18 +152,18 @@ const StackTraceTab = ({ error }) => {
const ArgumentsTab = ({ error }) => {
const formatArguments = (args) => {
if (!args || args.length === 0) return 'No arguments available';
try {
return args.map((arg, index) => {
// Handle special Error object format
if (arg && typeof arg === 'object' && arg.__type === 'Error') {
return `[${index}]: Error: ${arg.message}\n Name: ${arg.name}\n Stack: ${arg.stack || 'No stack trace'}`;
}
if (typeof arg === 'object' && arg !== null) {
return `[${index}]: ${JSON.stringify(arg, null, 2)}`;
}
return `[${index}]: ${String(arg)}`;
}).join('\n\n');
} catch (e) {
@@ -187,7 +187,7 @@ const ArgumentsTab = ({ error }) => {
const ErrorDetailsPanel = () => {
const dispatch = useDispatch();
const { selectedError } = useSelector(state => state.logs);
const { selectedError } = useSelector((state) => state.logs);
const [activeTab, setActiveTab] = useState('info');
if (!selectedError) return null;
@@ -222,8 +222,8 @@ const ErrorDetailsPanel = () => {
<span>Error Details</span>
<span className="error-time">({formatTime(selectedError.timestamp)})</span>
</div>
<button
<button
className="close-button"
onClick={handleClose}
title="Close details panel"
@@ -233,23 +233,23 @@ const ErrorDetailsPanel = () => {
</div>
<div className="panel-tabs">
<button
<button
className={`tab-button ${activeTab === 'info' ? 'active' : ''}`}
onClick={() => setActiveTab('info')}
>
<IconFileText size={14} strokeWidth={1.5} />
Info
</button>
<button
<button
className={`tab-button ${activeTab === 'stack' ? 'active' : ''}`}
onClick={() => setActiveTab('stack')}
>
<IconStack size={14} strokeWidth={1.5} />
Stack
</button>
<button
<button
className={`tab-button ${activeTab === 'args' ? 'active' : ''}`}
onClick={() => setActiveTab('args')}
>
@@ -265,4 +265,4 @@ const ErrorDetailsPanel = () => {
);
};
export default ErrorDetailsPanel;
export default ErrorDetailsPanel;

View File

@@ -290,4 +290,4 @@ const StyledWrapper = styled.div`
}
`;
export default StyledWrapper;
export default StyledWrapper;

View File

@@ -1,13 +1,13 @@
import React, { useState, useRef, useEffect, useMemo } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
import {
IconFilter,
IconChevronDown,
IconNetwork,
IconNetwork
} from '@tabler/icons';
import {
updateNetworkFilter,
toggleAllNetworkFilters,
import {
updateNetworkFilter,
toggleAllNetworkFilters,
setSelectedRequest
} from 'providers/ReduxStore/slices/logs';
import StyledWrapper from './StyledWrapper';
@@ -27,8 +27,8 @@ const MethodBadge = ({ method }) => {
};
return (
<span
className="method-badge"
<span
className="method-badge"
style={{ backgroundColor: getMethodColor(method) }}
>
{method?.toUpperCase() || 'GET'}
@@ -46,10 +46,10 @@ const StatusBadge = ({ status, statusCode }) => {
};
const displayStatus = statusCode || status;
return (
<span
className="status-badge"
<span
className="status-badge"
style={{ color: getStatusColor(statusCode) }}
>
{displayStatus}
@@ -61,7 +61,7 @@ const NetworkFilterDropdown = ({ filters, requestCounts, onFilterToggle, onToggl
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null);
const allFiltersEnabled = Object.values(filters).every(f => f);
const allFiltersEnabled = Object.values(filters).every((f) => f);
const activeFilters = Object.entries(filters).filter(([_, enabled]) => enabled);
useEffect(() => {
@@ -77,7 +77,7 @@ const NetworkFilterDropdown = ({ filters, requestCounts, onFilterToggle, onToggl
return (
<div className="filter-dropdown" ref={dropdownRef}>
<button
<button
className="filter-dropdown-trigger"
onClick={() => setIsOpen(!isOpen)}
title="Filter requests by method"
@@ -88,21 +88,21 @@ const NetworkFilterDropdown = ({ filters, requestCounts, onFilterToggle, onToggl
</span>
<IconChevronDown size={14} strokeWidth={1.5} />
</button>
{isOpen && (
<div className={`filter-dropdown-menu right`}>
<div className="filter-dropdown-menu right">
<div className="filter-dropdown-header">
<span>Filter by Method</span>
<button
<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 => (
{Object.keys(filters).map((method) => (
<label key={method} className="filter-option">
<input
type="checkbox"
@@ -126,13 +126,13 @@ const NetworkFilterDropdown = ({ filters, requestCounts, onFilterToggle, onToggl
const RequestRow = ({ request, isSelected, onClick }) => {
const { data } = request;
const { request: req, response: res, timestamp } = data;
const formatTime = (timestamp) => {
const date = new Date(timestamp);
return date.toLocaleTimeString('en-US', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
return date.toLocaleTimeString('en-US', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
fractionalSecondDigits: 3
});
@@ -174,34 +174,34 @@ const RequestRow = ({ request, isSelected, onClick }) => {
};
return (
<div
<div
className={`request-row ${isSelected ? 'selected' : ''}`}
onClick={onClick}
>
<div className="request-method">
<MethodBadge method={req?.method} />
</div>
<div className="request-status">
<StatusBadge status={res?.status} statusCode={res?.statusCode} />
</div>
<div className="request-domain" title={getDomain()}>
{getDomain()}
</div>
<div className="request-path" title={getPath()}>
{getPath()}
</div>
<div className="request-time">
{formatTime(timestamp)}
</div>
<div className="request-duration">
{formatDuration(res?.duration)}
</div>
<div className="request-size">
{formatSize(res?.size)}
</div>
@@ -211,17 +211,17 @@ const RequestRow = ({ request, isSelected, onClick }) => {
const NetworkTab = () => {
const dispatch = useDispatch();
const { networkFilters, selectedRequest } = useSelector(state => state.logs);
const collections = useSelector(state => state.collections.collections);
const { networkFilters, selectedRequest } = useSelector((state) => state.logs);
const collections = useSelector((state) => state.collections.collections);
const allRequests = useMemo(() => {
const requests = [];
collections.forEach(collection => {
collections.forEach((collection) => {
if (collection.timeline) {
collection.timeline
.filter(entry => entry.type === 'request')
.forEach(entry => {
.filter((entry) => entry.type === 'request')
.forEach((entry) => {
requests.push({
...entry,
collectionName: collection.name,
@@ -230,12 +230,12 @@ const NetworkTab = () => {
});
}
});
return requests.sort((a, b) => a.timestamp - b.timestamp);
}, [collections]);
const filteredRequests = useMemo(() => {
return allRequests.filter(request => {
return allRequests.filter((request) => {
const method = request.data?.request?.method?.toUpperCase() || 'GET';
return networkFilters[method];
});
@@ -281,7 +281,7 @@ const NetworkTab = () => {
<div className="text-right">Duration</div>
<div className="text-right">Size</div>
</div>
<div className="requests-list">
{filteredRequests.map((request, index) => (
<RequestRow
@@ -299,4 +299,4 @@ const NetworkTab = () => {
);
};
export default NetworkTab;
export default NetworkTab;

View File

@@ -344,4 +344,4 @@ const StyledWrapper = styled.div`
}
`;
export default StyledWrapper;
export default StyledWrapper;

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
import {
IconX,
IconFileText,
IconArrowRight,
@@ -117,7 +117,7 @@ const ResponseTab = ({ response, request, collection }) => {
<div className="response-body-container">
{response?.data || response?.dataBuffer ? (
<QueryResult
item={{ uid: uuid()}}
item={{ uid: uuid() }}
collection={collection}
data={response.data}
dataBuffer={response.dataBuffer}
@@ -155,8 +155,8 @@ const NetworkTab = ({ response }) => {
const RequestDetailsPanel = () => {
const dispatch = useDispatch();
const { selectedRequest } = useSelector(state => state.logs);
const collections = useSelector(state => state.collections.collections);
const { selectedRequest } = useSelector((state) => state.logs);
const collections = useSelector((state) => state.collections.collections);
const [activeTab, setActiveTab] = useState('request');
if (!selectedRequest) return null;
@@ -164,7 +164,7 @@ const RequestDetailsPanel = () => {
const { data } = selectedRequest;
const { request, response } = data;
const collection = collections.find(c => c.uid === selectedRequest.collectionUid);
const collection = collections.find((c) => c.uid === selectedRequest.collectionUid);
const handleClose = () => {
dispatch(clearSelectedRequest());
@@ -196,8 +196,8 @@ const RequestDetailsPanel = () => {
<span>Request Details</span>
<span className="request-time">({formatTime(selectedRequest.timestamp)})</span>
</div>
<button
<button
className="close-button"
onClick={handleClose}
title="Close details panel"
@@ -207,23 +207,23 @@ const RequestDetailsPanel = () => {
</div>
<div className="panel-tabs">
<button
<button
className={`tab-button ${activeTab === 'request' ? 'active' : ''}`}
onClick={() => setActiveTab('request')}
>
<IconArrowRight size={14} strokeWidth={1.5} />
Request
</button>
<button
<button
className={`tab-button ${activeTab === 'response' ? 'active' : ''}`}
onClick={() => setActiveTab('response')}
>
<IconFileText size={14} strokeWidth={1.5} />
Response
</button>
<button
<button
className={`tab-button ${activeTab === 'network' ? 'active' : ''}`}
onClick={() => setActiveTab('network')}
>
@@ -239,4 +239,4 @@ const RequestDetailsPanel = () => {
);
};
export default RequestDetailsPanel;
export default RequestDetailsPanel;

View File

@@ -517,4 +517,4 @@ const StyledWrapper = styled.div`
}
`;
export default StyledWrapper;
export default StyledWrapper;

View File

@@ -0,0 +1,151 @@
import React from 'react';
import { IconTerminal, IconX } from '@tabler/icons';
import styled from 'styled-components';
import ToolHint from 'components/ToolHint/index';
const StyledSessionList = styled.div`
.session-list-item {
padding: 10px 12px;
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;
position: relative;
&:hover {
background: ${(props) => props.theme.sidebarHover || 'rgba(255, 255, 255, 0.05)'};
.session-close-btn {
opacity: 1;
}
}
&.active {
background: ${(props) => props.theme.sidebarActive || 'rgba(59, 142, 234, 0.12)'};
border-left: 2px solid ${(props) => props.theme.brandColor || '#3b8eea'};
}
&:last-child {
border-bottom: none;
}
}
.session-close-btn {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
opacity: 0;
transition: opacity 0.2s;
padding: 4px;
cursor: pointer;
color: ${(props) => props.theme.textSecondary || '#888'};
&:hover {
color: ${(props) => props.theme.text};
background: ${(props) => props.theme.sidebarHover || 'rgba(255, 255, 255, 0.1)'};
border-radius: 4px;
}
}
.session-name {
font-size: 13px;
font-weight: 500;
color: ${(props) => props.theme.text};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-right: 24px;
display: flex;
align-items: center;
gap: 6px;
}
.session-icon {
flex-shrink: 0;
opacity: 0.7;
}
.session-path {
font-size: 11px;
color: ${(props) => props.theme.textSecondary || '#888'};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
`;
const SessionList = ({ sessions, activeSessionId, onSelectSession, onCloseSession }) => {
const getSessionDisplayInfo = (session) => {
if (session.name) {
return { name: session.name };
}
if (session.cwd) {
// Normalize path and get the last directory name
const normalizedPath = session.cwd.replace(/\\/g, '/').replace(/\/$/, '');
const pathParts = normalizedPath.split('/').filter((p) => p);
if (pathParts.length > 0) {
const folderName = pathParts[pathParts.length - 1];
return { name: folderName };
}
// If it's root or home directory
if (normalizedPath === '' || normalizedPath === '/' || normalizedPath.match(/^[A-Z]:\/?$/)) {
return { name: 'Root' };
}
}
// Fallback: use a cool name based on session ID
const shortId = session.sessionId.split('_')[1]?.slice(-6) || session.sessionId.slice(-6);
return { name: `Terminal ${shortId}` };
};
const getFullPath = (session) => {
if (session.cwd) {
return session.cwd;
}
return '~ (Home Directory)';
};
return (
<StyledSessionList>
{sessions.map((session) => {
const { name } = getSessionDisplayInfo(session);
return (
<ToolHint
key={session.sessionId}
text={getFullPath(session)}
toolhintId={`session-path-${session.sessionId}`}
place="bottom-start"
delayShow={100}
>
<div
className={`session-list-item ${activeSessionId === session.sessionId ? 'active' : ''}`}
onClick={() => onSelectSession(session.sessionId)}
>
<div className="session-name">
<IconTerminal className="session-icon" size={14} />
<span>{name}</span>
</div>
<div
className="session-close-btn"
onClick={(e) => {
e.stopPropagation();
onCloseSession(session.sessionId);
}}
>
<IconX size={14} />
</div>
</div>
</ToolHint>
);
})}
</StyledSessionList>
);
};
export default SessionList;

View File

@@ -0,0 +1,201 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
color: ${(props) => props.theme.text};
.xterm-rows {
color: ${(props) => props.theme.text} !important;
}
.terminal-content {
height: 100%;
width: 100%;
position: relative;
display: flex;
flex-direction: row;
}
.terminal-sessions-sidebar {
width: 200px;
min-width: 200px;
border-right: 1px solid ${(props) => props.theme.border || 'rgba(255, 255, 255, 0.08)'};
background: ${(props) => props.theme.sidebarBackground || props.theme.background};
display: flex;
flex-direction: column;
overflow-y: auto;
}
.terminal-sessions-header {
padding: 12px;
font-weight: 600;
font-size: 13px;
color: ${(props) => props.theme.text};
border-bottom: 1px solid ${(props) => props.theme.border || 'rgba(255, 255, 255, 0.08)'};
display: flex;
align-items: center;
justify-content: space-between;
}
.terminal-sessions-list {
flex: 1;
overflow-y: auto;
/* Custom scrollbar styling - subtle */
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
}
&::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.15);
}
}
.terminal-session-item {
padding: 10px 12px;
cursor: pointer;
border-bottom: 1px solid ${(props) => props.theme.border};
transition: background 0.2s;
display: flex;
flex-direction: column;
gap: 4px;
&:hover {
background: ${(props) => props.theme.sidebarHover || 'rgba(255, 255, 255, 0.05)'};
}
&.active {
background: ${(props) => props.theme.sidebarActive || 'rgba(59, 142, 234, 0.15)'};
border-left: 3px solid ${(props) => props.theme.brandColor || '#3b8eea'};
}
}
.terminal-session-name {
font-size: 13px;
font-weight: 500;
color: ${(props) => props.theme.text};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.terminal-session-path {
font-size: 11px;
color: ${(props) => props.theme.textSecondary || '#888'};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.terminal-display-container {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
}
.terminal-loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
color: #888;
font-size: 14px;
z-index: 10;
svg {
opacity: 0.7;
}
span {
font-weight: 500;
}
}
.terminal-container {
flex: 1;
position: relative;
.xterm {
height: 100% !important;
width: 100% !important;
padding: 8px;
}
.xterm-viewport {
background: transparent !important;
}
.xterm-screen {
background: transparent !important;
}
.xterm-decoration-overview-ruler {
display: none;
}
/* Custom scrollbar for terminal */
.xterm-viewport::-webkit-scrollbar {
width: 8px;
}
.xterm-viewport::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 4px;
}
.xterm-viewport::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 4px;
}
.xterm-viewport::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
}
/* Dark theme adjustments */
.xterm-helper-textarea {
position: absolute !important;
left: -9999px !important;
top: -9999px !important;
}
/* Selection styling */
.xterm .xterm-selection div {
background-color: rgba(255, 255, 255, 0.3) !important;
}
/* Cursor styling */
.xterm .xterm-cursor-layer .xterm-cursor {
background-color: #d4d4d4 !important;
}
/* Link styling */
.xterm .xterm-decoration-link {
text-decoration: underline;
color: #3b8eea;
}
.xterm .xterm-decoration-link:hover {
color: #5ba7f7;
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,449 @@
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 StyledWrapper from './StyledWrapper';
import SessionList from './SessionList';
import '@xterm/xterm/css/xterm.css';
// Terminal instances per session - Map<sessionId, { terminal, fitAddon, inputDisposable, resizeDisposable }>
const terminalInstances = new Map();
// Data listeners per session - Map<sessionId, { onData, onExit }>
const sessionListeners = new Map();
// Parking host for terminal DOM when view unmounts
let parkingHost = null;
// Export function to get current session ID (for backward compatibility)
export const getSessionId = () => {
// Return the first active session ID if any
if (terminalInstances.size > 0) {
return Array.from(terminalInstances.keys())[0];
}
return null;
};
const ensureParkingHost = () => {
if (parkingHost && document.body.contains(parkingHost)) return parkingHost;
parkingHost = document.createElement('div');
parkingHost.style.display = 'none';
parkingHost.setAttribute('data-terminal-parking-host', 'true');
document.body.appendChild(parkingHost);
return parkingHost;
};
const createTerminalForSession = (sessionId) => {
if (terminalInstances.has(sessionId)) {
return terminalInstances.get(sessionId);
}
const terminal = new Terminal({
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'
},
allowProposedApi: true
});
const fitAddon = new FitAddon();
terminal.loadAddon(fitAddon);
const inputDisposable = terminal.onData((data) => {
if (data && sessionId && window.ipcRenderer) {
window.ipcRenderer.send('terminal:input', sessionId, data);
}
});
const resizeDisposable = terminal.onResize(({ cols, rows }) => {
if (sessionId && window.ipcRenderer) {
window.ipcRenderer.send('terminal:resize', sessionId, { cols, rows });
}
});
const instance = {
terminal,
fitAddon,
inputDisposable,
resizeDisposable
};
terminalInstances.set(sessionId, instance);
// Setup IPC listeners for this session
if (window.ipcRenderer && !sessionListeners.has(sessionId)) {
const onData = (data) => {
if (!data) return;
const instance = terminalInstances.get(sessionId);
if (instance && instance.terminal) {
try {
instance.terminal.write(data);
} catch (err) {
console.warn('Failed to write terminal data:', err);
}
}
};
const onExit = ({ exitCode, signal } = {}) => {
const msg = `\r\n[Process exited with code ${exitCode ?? ''} ${signal ? `(signal ${signal})` : ''}]\r\n`;
const instance = terminalInstances.get(sessionId);
if (instance && instance.terminal) {
try {
instance.terminal.write(msg);
} catch (err) {
console.warn('Failed to write terminal exit message:', err);
}
}
// Cleanup on exit
cleanupTerminalInstance(sessionId);
};
window.ipcRenderer.on(`terminal:data:${sessionId}`, onData);
window.ipcRenderer.on(`terminal:exit:${sessionId}`, onExit);
sessionListeners.set(sessionId, { onData, onExit });
}
return instance;
};
const cleanupTerminalInstance = (sessionId) => {
const instance = terminalInstances.get(sessionId);
if (instance) {
try {
if (instance.inputDisposable) instance.inputDisposable.dispose();
if (instance.resizeDisposable) instance.resizeDisposable.dispose();
if (instance.terminal) {
instance.terminal.dispose();
}
} catch (err) {
console.warn('Error disposing terminal instance:', err);
}
terminalInstances.delete(sessionId);
}
// Remove IPC listeners
const listeners = sessionListeners.get(sessionId);
if (listeners && window.ipcRenderer) {
try {
window.ipcRenderer.removeAllListeners(`terminal:data:${sessionId}`);
window.ipcRenderer.removeAllListeners(`terminal:exit:${sessionId}`);
} catch (err) {
console.warn('Error removing IPC listeners:', err);
}
sessionListeners.delete(sessionId);
}
};
const openTerminalIntoContainer = async (container, sessionId) => {
if (!container || !sessionId) return;
const instance = createTerminalForSession(sessionId);
const { terminal, fitAddon } = instance;
if (!terminal.element) {
terminal.open(container);
} else {
// Move terminal element to new container
if (terminal.element.parentElement !== container) {
container.appendChild(terminal.element);
}
}
await new Promise((resolve) => setTimeout(resolve, 50));
try {
fitAddon.fit();
const { cols, rows } = terminal;
if (cols && rows && window.ipcRenderer) {
window.ipcRenderer.send('terminal:resize', sessionId, { cols, rows });
}
} catch (e) {
console.warn('Error fitting terminal:', e);
}
};
const TerminalTab = () => {
const terminalRef = useRef(null);
const [sessions, setSessions] = useState([]);
const [activeSessionId, setActiveSessionId] = useState(null);
const [isLoading, setIsLoading] = useState(true);
// Load sessions list
const loadSessions = useCallback(async (currentActiveSessionId = null) => {
if (!window.ipcRenderer) return [];
try {
const sessionList = await window.ipcRenderer.invoke('terminal:list-sessions');
setSessions(sessionList);
// Use functional state updates to get the current activeSessionId
setActiveSessionId((prevActiveSessionId) => {
const activeId = currentActiveSessionId !== null ? currentActiveSessionId : prevActiveSessionId;
// Auto-select first session if none selected
if (!activeId && sessionList.length > 0) {
return sessionList[0].sessionId;
}
// If active session no longer exists, select first available
if (activeId && !sessionList.find((s) => s.sessionId === activeId)) {
return sessionList.length > 0 ? sessionList[0].sessionId : null;
}
// Keep current selection if it still exists
return activeId;
});
return sessionList;
} catch (err) {
console.error('Failed to load sessions:', err);
return [];
}
}, []);
// Create new terminal session
const createNewSession = useCallback(async (cwd = null) => {
if (!window.ipcRenderer) return null;
try {
const options = cwd ? { cwd } : {};
const newSessionId = await window.ipcRenderer.invoke('terminal:create', options);
if (newSessionId) {
await loadSessions(newSessionId);
setActiveSessionId(newSessionId);
return newSessionId;
}
} catch (err) {
console.error('Failed to create terminal session:', err);
}
return null;
}, [loadSessions]);
// Listen for requests to open terminal at specific CWD
useEffect(() => {
const normalizePath = (path) => {
if (!path) return '';
// Normalize path separators and remove trailing separators for comparison
return path.replace(/\\/g, '/').replace(/\/$/, '') || '/';
};
const handleOpenTerminalAtCwd = async (event) => {
const { cwd } = event.detail;
if (!cwd) return;
const normalizedCwd = normalizePath(cwd);
// Check if session already exists at this CWD
const sessionList = await window.ipcRenderer.invoke('terminal:list-sessions');
const existingSession = sessionList.find((s) => normalizePath(s.cwd) === normalizedCwd);
if (existingSession) {
// Switch to existing session
await loadSessions(existingSession.sessionId);
setActiveSessionId(existingSession.sessionId);
} else {
// Create new session at this CWD
await createNewSession(cwd);
}
};
window.addEventListener('terminal:open-at-cwd', handleOpenTerminalAtCwd);
return () => {
window.removeEventListener('terminal:open-at-cwd', handleOpenTerminalAtCwd);
};
}, [loadSessions, createNewSession]);
// Close terminal session
const closeSession = async (sessionId) => {
if (!window.ipcRenderer) return;
try {
window.ipcRenderer.send('terminal:kill', sessionId);
cleanupTerminalInstance(sessionId);
// Load updated sessions (this will also handle active session switching)
const updatedSessions = await loadSessions();
// If we closed the active session and there are no sessions left, clear selection
if (activeSessionId === sessionId && updatedSessions.length === 0) {
setActiveSessionId(null);
}
} catch (err) {
console.error('Failed to close terminal session:', err);
}
};
// Load sessions on mount and set up polling
useEffect(() => {
if (!window.ipcRenderer) {
setIsLoading(false);
return;
}
let mounted = true;
const initialLoad = async () => {
const sessionList = await loadSessions();
if (mounted) {
setIsLoading(false);
}
};
initialLoad();
// Poll for session updates every 2 seconds
// Note: We don't pass currentActiveSessionId here to avoid stale closures
// The functional update inside loadSessions will use the current state
const pollInterval = setInterval(() => {
if (mounted) {
loadSessions();
}
}, 2000);
return () => {
mounted = false;
clearInterval(pollInterval);
};
}, []);
// Handle terminal display for active session
useEffect(() => {
if (!activeSessionId || !terminalRef.current) return;
let mounted = true;
const setupTerminal = async () => {
await openTerminalIntoContainer(terminalRef.current, activeSessionId);
if (mounted) {
const instance = terminalInstances.get(activeSessionId);
if (instance && instance.fitAddon) {
const onResize = () => {
try {
instance.fitAddon.fit();
} catch (e) {}
};
window.addEventListener('resize', onResize);
// Initial resize
setTimeout(() => {
try {
instance.fitAddon.fit();
const { cols, rows } = instance.terminal;
if (cols && rows && window.ipcRenderer) {
window.ipcRenderer.send('terminal:resize', activeSessionId, { cols, rows });
}
} catch (err) {
console.warn('Failed to perform initial resize:', err);
}
}, 100);
return () => {
window.removeEventListener('resize', onResize);
// Park terminal element when switching sessions
if (instance.terminal && instance.terminal.element) {
const host = ensureParkingHost();
if (instance.terminal.element.parentElement !== host) {
host.appendChild(instance.terminal.element);
}
}
};
}
}
};
const cleanup = setupTerminal();
return () => {
mounted = false;
Promise.resolve(cleanup).then((fn) => {
if (typeof fn === 'function') fn();
});
};
}, [activeSessionId]);
return (
<StyledWrapper>
<div className="terminal-content">
{/* Left Sidebar */}
<div className="terminal-sessions-sidebar">
<div className="terminal-sessions-header">
<span>Sessions</span>
<IconPlus
size={16}
style={{ cursor: 'pointer', color: '#888' }}
onClick={(e) => {
e.stopPropagation();
createNewSession();
}}
title="New Terminal Session"
/>
</div>
<div className="terminal-sessions-list">
{isLoading ? (
<div style={{ padding: '12px', color: '#888', fontSize: '13px' }}>
Loading sessions...
</div>
) : sessions.length === 0 ? (
<div style={{ padding: '12px', color: '#888', fontSize: '13px' }}>
No active sessions
</div>
) : (
<SessionList
sessions={sessions}
activeSessionId={activeSessionId}
onSelectSession={setActiveSessionId}
onCloseSession={closeSession}
/>
)}
</div>
</div>
{/* Right Terminal Display */}
<div className="terminal-display-container">
{!activeSessionId && window.ipcRenderer && (
<div className="terminal-loading">
<IconTerminal2 size={24} strokeWidth={1.5} />
<span>No terminal session selected</span>
</div>
)}
<div
ref={terminalRef}
className="terminal-container"
style={{
height: '100%',
width: '100%',
display: activeSessionId ? 'block' : 'none'
}}
/>
</div>
</div>
</StyledWrapper>
);
};
export default TerminalTab;

View File

@@ -2,12 +2,12 @@ import React, { useEffect, useRef, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import ReactJson from 'react-json-view';
import { useTheme } from 'providers/Theme';
import {
IconX,
IconTrash,
import {
IconX,
IconTrash,
IconFilter,
IconAlertTriangle,
IconAlertCircle,
IconAlertTriangle,
IconAlertCircle,
IconBug,
IconCode,
IconChevronDown,
@@ -15,10 +15,10 @@ import {
IconNetwork,
IconDashboard
} from '@tabler/icons';
import {
closeConsole,
clearLogs,
updateFilter,
import {
closeConsole,
clearLogs,
updateFilter,
toggleAllFilters,
setActiveTab,
clearDebugErrors,
@@ -27,6 +27,7 @@ import {
} from 'providers/ReduxStore/slices/logs';
import NetworkTab from './NetworkTab';
import TerminalTab from './TerminalTab';
import RequestDetailsPanel from './RequestDetailsPanel';
// import DebugTab from './DebugTab';
import ErrorDetailsPanel from './ErrorDetailsPanel';
@@ -35,7 +36,7 @@ import StyledWrapper from './StyledWrapper';
const LogIcon = ({ type }) => {
const iconProps = { size: 16, strokeWidth: 1.5 };
switch (type) {
case 'error':
return <IconAlertCircle className="log-icon error" {...iconProps} />;
@@ -52,20 +53,20 @@ const LogIcon = ({ type }) => {
const LogTimestamp = ({ timestamp }) => {
const date = new Date(timestamp);
const time = date.toLocaleTimeString('en-US', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
const time = date.toLocaleTimeString('en-US', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
fractionalSecondDigits: 3
});
return <span className="log-timestamp">{time}</span>;
};
const LogMessage = ({ message, args }) => {
const { displayedTheme } = useTheme();
const formatMessage = (msg, originalArgs) => {
if (originalArgs && originalArgs.length > 0) {
return originalArgs.map((arg, index) => {
@@ -98,7 +99,7 @@ const LogMessage = ({ message, args }) => {
};
const formattedMessage = formatMessage(message, args);
return (
<span className="log-message">
{Array.isArray(formattedMessage) ? formattedMessage.map((item, index) => (
@@ -112,7 +113,7 @@ const FilterDropdown = ({ filters, logCounts, onFilterToggle, onToggleAll }) =>
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null);
const allFiltersEnabled = Object.values(filters).every(f => f);
const allFiltersEnabled = Object.values(filters).every((f) => f);
const activeFilters = Object.entries(filters).filter(([_, enabled]) => enabled);
useEffect(() => {
@@ -128,7 +129,7 @@ const FilterDropdown = ({ filters, logCounts, onFilterToggle, onToggleAll }) =>
return (
<div className="filter-dropdown" ref={dropdownRef}>
<button
<button
className="filter-dropdown-trigger"
onClick={() => setIsOpen(!isOpen)}
title="Filter logs by type"
@@ -139,19 +140,19 @@ const FilterDropdown = ({ filters, logCounts, onFilterToggle, onToggleAll }) =>
</span>
<IconChevronDown size={14} strokeWidth={1.5} />
</button>
{isOpen && (
<div className={`filter-dropdown-menu right`}>
<div className="filter-dropdown-menu right">
<div className="filter-dropdown-header">
<span>Filter by Type</span>
<button
<button
className="filter-toggle-all"
onClick={() => onToggleAll(!allFiltersEnabled)}
>
{allFiltersEnabled ? 'Hide All' : 'Show All'}
</button>
</div>
<div className="filter-dropdown-options">
{Object.entries(filters).map(([filterType, enabled]) => (
<label key={filterType} className="filter-option">
@@ -178,7 +179,7 @@ const NetworkFilterDropdown = ({ filters, requestCounts, onFilterToggle, onToggl
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null);
const allFiltersEnabled = Object.values(filters).every(f => f);
const allFiltersEnabled = Object.values(filters).every((f) => f);
const activeFilters = Object.entries(filters).filter(([_, enabled]) => enabled);
const getMethodColor = (method) => {
@@ -207,7 +208,7 @@ const NetworkFilterDropdown = ({ filters, requestCounts, onFilterToggle, onToggl
return (
<div className="filter-dropdown" ref={dropdownRef}>
<button
<button
className="filter-dropdown-trigger"
onClick={() => setIsOpen(!isOpen)}
title="Filter requests by method"
@@ -218,19 +219,19 @@ const NetworkFilterDropdown = ({ filters, requestCounts, onFilterToggle, onToggl
</span>
<IconChevronDown size={14} strokeWidth={1.5} />
</button>
{isOpen && (
<div className={`filter-dropdown-menu right`}>
<div className="filter-dropdown-menu right">
<div className="filter-dropdown-header">
<span>Filter by Method</span>
<button
<button
className="filter-toggle-all"
onClick={() => onToggleAll(!allFiltersEnabled)}
>
{allFiltersEnabled ? 'Hide All' : 'Show All'}
</button>
</div>
<div className="filter-dropdown-options">
{Object.entries(filters).map(([method, enabled]) => (
<label key={method} className="filter-option">
@@ -258,7 +259,7 @@ const NetworkFilterDropdown = ({ filters, requestCounts, onFilterToggle, onToggl
const ConsoleTab = ({ logs, filters, logCounts, onFilterToggle, onToggleAll, onClearLogs }) => {
const logsEndRef = useRef(null);
const prevLogsCountRef = useRef(0);
useEffect(() => {
// Only scroll when new logs are added, not when switching tabs
if (logsEndRef.current && logs.length > prevLogsCountRef.current) {
@@ -267,7 +268,7 @@ const ConsoleTab = ({ logs, filters, logCounts, onFilterToggle, onToggleAll, onC
prevLogsCountRef.current = logs.length;
}, [logs]);
const filteredLogs = logs.filter(log => filters[log.type]);
const filteredLogs = logs.filter((log) => filters[log.type]);
return (
<div className="tab-content">
@@ -299,8 +300,8 @@ const ConsoleTab = ({ logs, filters, logCounts, onFilterToggle, onToggleAll, onC
const Console = () => {
const dispatch = useDispatch();
const { logs, filters, activeTab, selectedRequest, selectedError, networkFilters, debugErrors } = useSelector(state => state.logs);
const collections = useSelector(state => state.collections.collections);
const { logs, filters, activeTab, selectedRequest, selectedError, networkFilters, debugErrors } = useSelector((state) => state.logs);
const collections = useSelector((state) => state.collections.collections);
const consoleRef = useRef(null);
const logCounts = logs.reduce((counts, log) => {
@@ -310,12 +311,12 @@ const Console = () => {
const allRequests = React.useMemo(() => {
const requests = [];
collections.forEach(collection => {
collections.forEach((collection) => {
if (collection.timeline) {
collection.timeline
.filter(entry => entry.type === 'request')
.forEach(entry => {
.filter((entry) => entry.type === 'request')
.forEach((entry) => {
requests.push({
...entry,
collectionName: collection.name,
@@ -324,12 +325,12 @@ const Console = () => {
});
}
});
return requests.sort((a, b) => a.timestamp - b.timestamp);
}, [collections]);
const filteredLogs = logs.filter(log => filters[log.type]);
const filteredRequests = allRequests.filter(request => {
const filteredLogs = logs.filter((log) => filters[log.type]);
const filteredRequests = allRequests.filter((request) => {
const method = request.data?.request?.method?.toUpperCase() || 'GET';
return networkFilters[method];
});
@@ -389,6 +390,8 @@ const Console = () => {
return <NetworkTab />;
case 'performance':
return <Performance />;
case 'terminal':
return <TerminalTab />;
// case 'debug':
// return <DebugTab />;
default:
@@ -419,7 +422,7 @@ const Console = () => {
/>
</div>
<div className="action-controls">
<button
<button
className="control-button"
onClick={handleClearLogs}
title="Clear all logs"
@@ -442,12 +445,14 @@ const Console = () => {
</div>
</div>
);
case 'terminal':
return null; // No controls needed for terminal
// case 'debug':
// return (
// <div className="tab-controls">
// <div className="action-controls">
// {debugErrors.length > 0 && (
// <button
// <button
// className="control-button"
// onClick={handleClearDebugErrors}
// title="Clear all errors"
@@ -463,32 +468,30 @@ const Console = () => {
}
};
return (
<StyledWrapper ref={consoleRef}>
<div
<div
className="console-resize-handle"
/>
<div className="console-header">
<div className="console-tabs">
<button
<button
className={`console-tab ${activeTab === 'console' ? 'active' : ''}`}
onClick={() => handleTabChange('console')}
>
<IconTerminal2 size={16} strokeWidth={1.5} />
<span>Console</span>
</button>
<button
<button
className={`console-tab ${activeTab === 'network' ? 'active' : ''}`}
onClick={() => handleTabChange('network')}
>
<IconNetwork size={16} strokeWidth={1.5} />
<span>Network</span>
</button>
<button
className={`console-tab ${activeTab === 'performance' ? 'active' : ''}`}
onClick={() => handleTabChange('performance')}
@@ -497,7 +500,15 @@ const Console = () => {
<span>Performance</span>
</button>
{/* <button
<button
className={`console-tab ${activeTab === 'terminal' ? 'active' : ''}`}
onClick={() => handleTabChange('terminal')}
>
<IconTerminal2 size={16} strokeWidth={1.5} />
<span>Terminal</span>
</button>
{/* <button
className={`console-tab ${activeTab === 'debug' ? 'active' : ''}`}
onClick={() => handleTabChange('debug')}
>
@@ -508,7 +519,7 @@ const Console = () => {
<div className="console-controls">
{renderTabControls()}
<button
<button
className="control-button close-button"
onClick={handlecloseConsole}
title="Close console"
@@ -541,4 +552,4 @@ const Console = () => {
);
};
export default Console;
export default Console;

View File

@@ -6,7 +6,8 @@ import {
IconDatabase,
IconClock,
IconServer,
IconChevronDown
IconChevronDown,
IconChartLine
} from '@tabler/icons';
const getProcessOptions = (processes) => {

View File

@@ -18,11 +18,11 @@ const Devtools = ({ mainSectionRef }) => {
const handleDevtoolsResize = useCallback((e) => {
if (!isResizingDevtools || !mainSectionRef.current) return;
const windowHeight = window.innerHeight;
const statusBarHeight = 22;
const mouseY = e.clientY;
// Calculate new devtools height - expanding upward from bottom
const newHeight = windowHeight - mouseY - statusBarHeight;
const clampedHeight = Math.min(MAX_DEVTOOLS_HEIGHT, Math.max(MIN_DEVTOOLS_HEIGHT, newHeight));
@@ -43,7 +43,7 @@ const Devtools = ({ mainSectionRef }) => {
document.addEventListener('mousemove', handleDevtoolsResize);
document.addEventListener('mouseup', handleDevtoolsResizeEnd);
document.body.style.userSelect = 'none';
return () => {
document.removeEventListener('mousemove', handleDevtoolsResize);
document.removeEventListener('mouseup', handleDevtoolsResizeEnd);
@@ -65,7 +65,7 @@ const Devtools = ({ mainSectionRef }) => {
return (
<>
<div
<div
onMouseDown={handleDevtoolsResizeStart}
style={{
height: '4px',
@@ -85,4 +85,4 @@ const Devtools = ({ mainSectionRef }) => {
);
};
export default Devtools;
export default Devtools;

View File

@@ -25,6 +25,16 @@ const Wrapper = styled.div`
padding-top: 0;
padding-bottom: 0;
[role="menu"] {
outline: none;
&:focus {
outline: none;
}
&:focus-visible {
outline: none;
}
}
.label-item {
display: flex;
align-items: center;
@@ -59,6 +69,10 @@ const Wrapper = styled.div`
}
}
.dropdown-label {
flex: 1;
}
.dropdown-icon {
flex-shrink: 0;
width: 16px;
@@ -70,10 +84,31 @@ const Wrapper = styled.div`
opacity: 0.8;
}
.dropdown-right-section {
margin-left: auto;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
&:hover:not(:disabled) {
background-color: ${(props) => props.theme.dropdown.hoverBg};
}
&.selected-focused:not(:disabled) {
background-color: ${(props) => props.theme.dropdown.hoverBg};
}
&:focus-visible:not(:disabled) {
outline: none;
background-color: ${(props) => props.theme.dropdown.hoverBg};
}
&:focus:not(:focus-visible) {
outline: none;
}
&:disabled {
cursor: not-allowed;
opacity: 0.5;

View File

@@ -2,46 +2,55 @@ import styled from 'styled-components';
const Wrapper = styled.div`
.current-environment {
border-radius: 0.9375rem;
border-radius: ${(props) => props.theme.border.radius.base};
padding: 0.25rem 0.5rem 0.25rem 0.75rem;
user-select: none;
background-color: transparent;
border: 1px solid ${(props) => props.theme.dropdown.selectedColor};
background-color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.bg};
border: 1px solid ${(props) => props.theme.app.collection.toolbar.environmentSelector.border};
line-height: 1rem;
transition: all 0.15s ease;
&:hover {
border-color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.hoverBorder};
background-color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.hoverBg};
}
.caret {
margin-left: 0.25rem;
color: rgb(140, 140, 140);
fill: rgb(140, 140, 140);
color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.caret};
fill: ${(props) => props.theme.app.collection.toolbar.environmentSelector.caret};
align-self: center;
}
.env-icon {
margin-right: 0.25rem;
color: ${(props) => props.theme.dropdown.selectedColor};
color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.icon};
}
.env-text {
color: ${(props) => props.theme.dropdown.selectedColor};
font-size: ${(props) => props.theme.font.size.base};
color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.text};
display: block;
}
.env-separator {
color: #8c8c8c;
color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.separator};
margin: 0 0.25rem;
opacity: 0.7;
}
.env-text-inactive {
color: ${(props) => props.theme.dropdown.color};
font-size: ${(props) => props.theme.font.size.base};
opacity: 0.7;
color: ${(props) => props.theme.colors.text.muted};
font-size: ${(props) => props.theme.font.size.sm};
}
&.no-environments {
background-color: ${(props) => props.theme.sidebar.badge.bg};
border: 1px solid transparent;
color: ${(props) => props.theme.dropdown.secondaryText};
color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.noEnvironment.text};
background-color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.noEnvironment.bg};
border: 1px dashed ${(props) => props.theme.app.collection.toolbar.environmentSelector.noEnvironment.border};
&:hover {
border-color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.noEnvironment.hoverBorder};
background-color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.noEnvironment.hoverBg};
}
}
}

View File

@@ -3,7 +3,7 @@ import find from 'lodash/find';
import Dropdown from 'components/Dropdown';
import { IconWorld, IconDatabase, IconCaretDown, IconSettings, IconPlus, IconDownload } from '@tabler/icons';
import { useSelector, useDispatch } from 'react-redux';
import { updateEnvironmentSettingsModalVisibility } from 'providers/ReduxStore/slices/app';
import { updateEnvironmentSettingsModalVisibility, updateGlobalEnvironmentSettingsModalVisibility } from 'providers/ReduxStore/slices/app';
import { selectEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import { selectGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
import toast from 'react-hot-toast';
@@ -20,8 +20,6 @@ const EnvironmentSelector = ({ collection }) => {
const dispatch = useDispatch();
const dropdownTippyRef = useRef();
const [activeTab, setActiveTab] = useState('collection');
const [showGlobalSettings, setShowGlobalSettings] = useState(false);
const [showCollectionSettings, setShowCollectionSettings] = useState(false);
const [showCreateGlobalModal, setShowCreateGlobalModal] = useState(false);
const [showImportGlobalModal, setShowImportGlobalModal] = useState(false);
const [showCreateCollectionModal, setShowCreateCollectionModal] = useState(false);
@@ -29,6 +27,8 @@ const EnvironmentSelector = ({ collection }) => {
const globalEnvironments = useSelector((state) => state.globalEnvironments.globalEnvironments);
const activeGlobalEnvironmentUid = useSelector((state) => state.globalEnvironments.activeGlobalEnvironmentUid);
const isEnvironmentSettingsModalOpen = useSelector((state) => state.app.isEnvironmentSettingsModalOpen);
const isGlobalEnvironmentSettingsModalOpen = useSelector((state) => state.app.isGlobalEnvironmentSettingsModalOpen);
const activeGlobalEnvironment = activeGlobalEnvironmentUid
? find(globalEnvironments, (e) => e.uid === activeGlobalEnvironmentUid)
: null;
@@ -49,15 +49,15 @@ const EnvironmentSelector = ({ collection }) => {
};
// Get description based on active tab
const description =
activeTab === 'collection'
const description
= activeTab === 'collection'
? 'Create your first environment to begin working with your collection.'
: 'Create your first global environment to begin working across collections.';
// Environment selection handler
const handleEnvironmentSelect = (environment) => {
const action =
activeTab === 'collection'
const action
= activeTab === 'collection'
? selectEnvironment(environment ? environment.uid : null, collection.uid)
: selectGlobalEnvironment({ environmentUid: environment ? environment.uid : null });
@@ -79,9 +79,8 @@ const EnvironmentSelector = ({ collection }) => {
const handleSettingsClick = () => {
if (activeTab === 'collection') {
dispatch(updateEnvironmentSettingsModalVisibility(true));
setShowCollectionSettings(true);
} else {
setShowGlobalSettings(true);
dispatch(updateGlobalEnvironmentSettingsModalVisibility(true));
}
dropdownTippyRef.current.hide();
};
@@ -108,9 +107,8 @@ const EnvironmentSelector = ({ collection }) => {
// Modal handlers
const handleCloseSettings = () => {
setShowGlobalSettings(false);
setShowCollectionSettings(false);
dispatch(updateEnvironmentSettingsModalVisibility(false));
dispatch(updateGlobalEnvironmentSettingsModalVisibility(false));
};
// Calculate dropdown width based on the longest environment name.
@@ -164,7 +162,7 @@ const EnvironmentSelector = ({ collection }) => {
)}
</>
) : (
<span className="env-text-inactive max-w-36 truncate no-wrap">No environments</span>
<span className="env-text-inactive max-w-36 truncate no-wrap">No Environment</span>
);
return (
@@ -176,7 +174,7 @@ const EnvironmentSelector = ({ collection }) => {
data-testid="environment-selector-trigger"
>
{displayContent}
<IconCaretDown className="caret" size={14} strokeWidth={2} />
<IconCaretDown className="caret flex items-center justify-center" size={12} strokeWidth={2} />
</div>
);
});
@@ -220,7 +218,7 @@ const EnvironmentSelector = ({ collection }) => {
</div>
{/* Modals - Rendered outside dropdown to avoid conflicts */}
{showGlobalSettings && (
{isGlobalEnvironmentSettingsModalOpen && (
<GlobalEnvironmentSettings
globalEnvironments={globalEnvironments}
collection={collection}
@@ -229,13 +227,15 @@ const EnvironmentSelector = ({ collection }) => {
/>
)}
{showCollectionSettings && <EnvironmentSettings collection={collection} onClose={handleCloseSettings} />}
{isEnvironmentSettingsModalOpen && (
<EnvironmentSettings collection={collection} onClose={handleCloseSettings} />
)}
{showCreateGlobalModal && (
<CreateGlobalEnvironment
onClose={() => setShowCreateGlobalModal(false)}
onEnvironmentCreated={() => {
setShowGlobalSettings(true);
dispatch(updateGlobalEnvironmentSettingsModalVisibility(true));
}}
/>
)}
@@ -245,7 +245,7 @@ const EnvironmentSelector = ({ collection }) => {
type="global"
onClose={() => setShowImportGlobalModal(false)}
onEnvironmentCreated={() => {
setShowGlobalSettings(true);
dispatch(updateGlobalEnvironmentSettingsModalVisibility(true));
}}
/>
)}
@@ -255,7 +255,7 @@ const EnvironmentSelector = ({ collection }) => {
collection={collection}
onClose={() => setShowCreateCollectionModal(false)}
onEnvironmentCreated={() => {
setShowCollectionSettings(true);
dispatch(updateEnvironmentSettingsModalVisibility(true));
}}
/>
)}
@@ -266,7 +266,7 @@ const EnvironmentSelector = ({ collection }) => {
collection={collection}
onClose={() => setShowImportCollectionModal(false)}
onEnvironmentCreated={() => {
setShowCollectionSettings(true);
dispatch(updateEnvironmentSettingsModalVisibility(true));
}}
/>
)}

View File

@@ -43,8 +43,8 @@ const CopyEnvironment = ({ collection, environment, onClose }) => {
return (
<Portal>
<Modal size="sm" title={'Copy Environment'} confirmText="Copy" handleConfirm={onSubmit} handleCancel={onClose}>
<form className="bruno-form" onSubmit={e => e.preventDefault()}>
<Modal size="sm" title="Copy Environment" confirmText="Copy" handleConfirm={onSubmit} handleCancel={onClose}>
<form className="bruno-form" onSubmit={(e) => e.preventDefault()}>
<div>
<label htmlFor="name" className="block font-medium">
New Environment Name

View File

@@ -12,9 +12,9 @@ const CreateEnvironment = ({ collection, onClose, onEnvironmentCreated }) => {
const dispatch = useDispatch();
const inputRef = useRef();
const validateEnvironmentName = (name) => {
return !collection?.environments?.some((env) => env?.name?.toLowerCase().trim() === name?.toLowerCase().trim());
};
const validateEnvironmentName = (name) => {
return !collection?.environments?.some((env) => env?.name?.toLowerCase().trim() === name?.toLowerCase().trim());
};
const formik = useFormik({
enableReinitialize: true,
@@ -25,7 +25,7 @@ const CreateEnvironment = ({ collection, onClose, onEnvironmentCreated }) => {
name: Yup.string()
.min(1, 'Must be at least 1 character')
.max(255, 'Must be 255 characters or less')
.test('is-valid-filename', function(value) {
.test('is-valid-filename', function (value) {
const isValid = validateName(value);
return isValid ? true : this.createError({ message: validateNameError(value) });
})
@@ -60,12 +60,12 @@ const CreateEnvironment = ({ collection, onClose, onEnvironmentCreated }) => {
<Portal>
<Modal
size="sm"
title={'Create Environment'}
title="Create Environment"
confirmText="Create"
handleConfirm={onSubmit}
handleCancel={onClose}
>
<form className="bruno-form" onSubmit={e => e.preventDefault()}>
<form className="bruno-form" onSubmit={(e) => e.preventDefault()}>
<div>
<label htmlFor="name" className="block font-medium">
Environment Name

View File

@@ -22,7 +22,7 @@ const DeleteEnvironment = ({ onClose, environment, collection }) => {
<StyledWrapper>
<Modal
size="sm"
title={'Delete Environment'}
title="Delete Environment"
confirmText="Delete"
handleConfirm={onConfirm}
handleCancel={onClose}

View File

@@ -25,7 +25,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments);
let _collection = cloneDeep(collection);
const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid });
_collection.globalEnvironmentVariables = globalEnvironmentVariables;
@@ -39,7 +39,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
return result;
}
const varNames = new Set(nonSecretVars.map((v) => v.name));
const checkSensitiveField = (obj, fieldPath) => {
const value = get(obj, fieldPath);
if (typeof value === 'string') {
@@ -62,7 +62,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
sensitiveFields.forEach((fieldPath) => {
checkSensitiveField(collectionObj, fieldPath);
});
const items = flattenItems(collection.items || []);
items.forEach((item) => {
const objToProcess = getObjectToProcess(item);
@@ -123,7 +123,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
}
return (
<span>
<IconAlertCircle id={id} className="text-red-600 cursor-pointer " size={20} />
<IconAlertCircle id={id} className="text-red-600 cursor-pointer" size={20} />
<Tooltip className="tooltip-mod" anchorId={id} html={meta.error || ''} />
</span>
);
@@ -160,7 +160,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
useEffect(() => {
if (formik.dirty) {
// Smooth scrolling to the changed parameter is temporarily disabled
// Smooth scrolling to the changed parameter is temporarily disabled
// due to UX issues when editing the first row in a long list of environment variables.
// addButtonRef.current?.scrollIntoView({ behavior: 'smooth' });
}

View File

@@ -25,7 +25,7 @@ const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collecti
useEffect(() => {
if (selectedEnvironment) {
const _selectedEnvironment = environments?.find(env => env?.uid === selectedEnvironment?.uid);
const _selectedEnvironment = environments?.find((env) => env?.uid === selectedEnvironment?.uid);
const hasSelectedEnvironmentChanged = !isEqual(selectedEnvironment, _selectedEnvironment);
if (hasSelectedEnvironmentChanged) {
setSelectedEnvironment(_selectedEnvironment);
@@ -107,16 +107,16 @@ const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collecti
</div>
)}
<div className="environments-sidebar flex flex-col">
{environments &&
environments.length &&
environments.map((env) => (
{environments
&& environments.length
&& environments.map((env) => (
<ToolHint key={env.uid} text={env.name} toolhintId={env.uid} place="right">
<div
id={env.uid}
className={selectedEnvironment.uid === env.uid ? 'environment-item active' : 'environment-item'}
onClick={() => handleEnvironmentClick(env)} // Use handleEnvironmentClick to handle clicks
>
<span className="break-all">{env.name}</span>
<span className="break-all">{env.name}</span>
</div>
</ToolHint>
))}

View File

@@ -20,7 +20,7 @@ const RenameEnvironment = ({ onClose, environment, collection }) => {
name: Yup.string()
.min(1, 'must be at least 1 character')
.max(255, 'Must be 255 characters or less')
.test('is-valid-filename', function(value) {
.test('is-valid-filename', function (value) {
const isValid = validateName(value);
return isValid ? true : this.createError({ message: validateNameError(value) });
})
@@ -53,12 +53,12 @@ const RenameEnvironment = ({ onClose, environment, collection }) => {
<Portal>
<Modal
size="sm"
title={'Rename Environment'}
title="Rename Environment"
confirmText="Rename"
handleConfirm={onSubmit}
handleCancel={onClose}
>
<form className="bruno-form" onSubmit={e => e.preventDefault()}>
<form className="bruno-form" onSubmit={(e) => e.preventDefault()}>
<div>
<label htmlFor="name" className="block font-medium">
Environment Name

View File

@@ -33,7 +33,7 @@ class ErrorBoundary extends Component {
}
const serializeArgs = (args) => {
return args.map(arg => {
return args.map((arg) => {
try {
if (arg === null) return 'null';
if (arg === undefined) return 'undefined';
@@ -65,12 +65,12 @@ const serializeArgs = (args) => {
// Helper function to extract file and line info from stack trace
const extractFileInfo = (stack) => {
if (!stack) return { filename: null, lineno: null, colno: null };
try {
const lines = stack.split('\n');
for (let line of lines) {
if (line.includes('ErrorCapture') || line.trim() === 'Error') continue;
const match = line.match(/(?:at\s+.*?\s+)?\(?([^)]+):(\d+):(\d+)\)?/);
if (match) {
return {
@@ -83,7 +83,7 @@ const extractFileInfo = (stack) => {
} catch (e) {
// Ignore parsing errors
}
return { filename: null, lineno: null, colno: null };
};
@@ -95,7 +95,7 @@ const useGlobalErrorCapture = () => {
console.error = (...args) => {
const currentStack = new Error().stack;
originalConsoleError.apply(console, args);
if (currentStack && currentStack.includes('useIpcEvents.js')) {
@@ -130,7 +130,7 @@ const useGlobalErrorCapture = () => {
const ErrorCapture = ({ children }) => {
const dispatch = useDispatch();
useGlobalErrorCapture();
const handleReactError = (errorData) => {
@@ -144,4 +144,4 @@ const ErrorCapture = ({ children }) => {
);
};
export default ErrorCapture;
export default ErrorCapture;

View File

@@ -18,7 +18,7 @@ const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = fa
const title = filenames.map((v) => `- ${v}`).join('\n');
const browse = () => {
dispatch(browseFiles([], [!isSingleFilePicker ? "multiSelections": ""]))
dispatch(browseFiles([], [!isSingleFilePicker ? 'multiSelections' : '']))
.then((filePaths) => {
// If file is in the collection's directory, then we use relative path
// Otherwise, we use the absolute path
@@ -73,4 +73,4 @@ const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = fa
);
};
export default FilePickerEditor;
export default FilePickerEditor;

View File

@@ -19,4 +19,4 @@ const Wrapper = styled.div`
}
`;
export default Wrapper;
export default Wrapper;

View File

@@ -63,7 +63,7 @@ const Auth = ({ collection, folder }) => {
// Get path from collection to current folder
const folderTreePath = getTreePathFromCollectionToItem(collection, folder);
// Check parent folders to find closest auth configuration
// Skip the last item which is the current folder
for (let i = 0; i < folderTreePath.length - 1; i++) {
@@ -172,8 +172,8 @@ const Auth = ({ collection, folder }) => {
return (
<>
<GrantTypeSelector
request={request}
updateAuth={updateFolderAuth}
request={request}
updateAuth={updateFolderAuth}
collection={collection}
item={folder}
/>
@@ -200,7 +200,6 @@ const Auth = ({ collection, folder }) => {
}
};
return (
<StyledWrapper className="w-full">
<div className="text-xs mb-4 text-muted">
@@ -220,4 +219,4 @@ const Auth = ({ collection, folder }) => {
);
};
export default Auth;
export default Auth;

View File

@@ -13,4 +13,4 @@ const StyledWrapper = styled.div`
}
`;
export default StyledWrapper;
export default StyledWrapper;

View File

@@ -34,7 +34,7 @@ const AuthMode = ({ collection, folder }) => {
return (
<StyledWrapper>
<div className="inline-flex items-center cursor-pointer">
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
<div
className="dropdown-item"
onClick={() => {

View File

@@ -121,8 +121,7 @@ const Headers = ({ collection, folder }) => {
},
header,
'name'
)
}
)}
autocomplete={headerAutoCompleteList}
collection={collection}
/>
@@ -141,8 +140,7 @@ const Headers = ({ collection, folder }) => {
},
header,
'value'
)
}
)}
collection={collection}
item={folder}
autocomplete={MimeTypes}

View File

@@ -8,7 +8,7 @@ const StyledWrapper = styled.div`
padding: 6px 0px;
border: none;
border-bottom: solid 2px transparent;
margin-right: 1.25rem;
margin-right: ${(props) => props.theme.tabs.marginRight};
color: var(--color-tab-inactive);
cursor: pointer;

View File

@@ -126,8 +126,7 @@ const VarsTable = ({ folder, collection, vars, varType }) => {
},
_var,
'value'
)
}
)}
collection={collection}
item={folder}
/>

View File

@@ -8,13 +8,19 @@ import { useDispatch } from 'react-redux';
const Vars = ({ collection, folder }) => {
const dispatch = useDispatch();
const requestVars = folder.draft ? get(folder, 'draft.request.vars.req', []) : get(folder, 'root.request.vars.req', []);
const responseVars = folder.draft ? get(folder, 'draft.request.vars.res', []) : get(folder, 'root.request.vars.res', []);
const handleSave = () => dispatch(saveFolderRoot(collection.uid, folder.uid));
return (
<StyledWrapper className="w-full flex flex-col">
<div className="flex-1 mt-2">
<div className="mb-1 title text-xs">Pre Request</div>
<VarsTable folder={folder} collection={collection} vars={requestVars} varType="request" />
</div>
<div className="flex-1">
<div className="mt-1 mb-1 title text-xs">Post Response</div>
<VarsTable folder={folder} collection={collection} vars={responseVars} varType="response" />
</div>
<div className="mt-6">
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
Save

View File

@@ -28,7 +28,8 @@ const FolderSettings = ({ collection, folder }) => {
const activeHeadersCount = headers.filter((header) => header.enabled).length;
const requestVars = folderRoot?.request?.vars?.req || [];
const activeVarsCount = requestVars.filter((v) => v.enabled).length;
const responseVars = folderRoot?.request?.vars?.res || [];
const activeVarsCount = requestVars.filter((v) => v.enabled).length + responseVars.filter((v) => v.enabled).length;
const auth = get(folderRoot, 'request.auth.mode');
const hasAuth = auth && auth !== 'none';
@@ -100,7 +101,7 @@ const FolderSettings = ({ collection, folder }) => {
Docs
</div>
</div>
<section className={`flex mt-4 h-full overflow-auto`}>{getTabPanel(tab)}</section>
<section className="flex mt-4 h-full overflow-auto">{getTabPanel(tab)}</section>
</div>
</StyledWrapper>
);

View File

@@ -46,8 +46,8 @@ const CopyEnvironment = ({ environment, onClose }) => {
return (
<Portal>
<Modal size="sm" title={'Copy Global Environment'} confirmText="Copy" handleConfirm={onSubmit} handleCancel={onClose}>
<form className="bruno-form" onSubmit={e => e.preventDefault()}>
<Modal size="sm" title="Copy Global Environment" confirmText="Copy" handleConfirm={onSubmit} handleCancel={onClose}>
<form className="bruno-form" onSubmit={(e) => e.preventDefault()}>
<div>
<label htmlFor="name" className="block font-medium">
New Environment Name

View File

@@ -27,7 +27,7 @@ const CreateEnvironment = ({ onClose, onEnvironmentCreated }) => {
name: Yup.string()
.min(1, 'Must be at least 1 character')
.max(255, 'Must be 255 characters or less')
.test('is-valid-filename', function(value) {
.test('is-valid-filename', function (value) {
const isValid = validateName(value);
return isValid ? true : this.createError({ message: validateNameError(value) });
})
@@ -62,12 +62,12 @@ const CreateEnvironment = ({ onClose, onEnvironmentCreated }) => {
<Portal>
<Modal
size="sm"
title={'Create Global Environment'}
title="Create Global Environment"
confirmText="Create"
handleConfirm={onSubmit}
handleCancel={onClose}
>
<form className="bruno-form" onSubmit={e => e.preventDefault()}>
<form className="bruno-form" onSubmit={(e) => e.preventDefault()}>
<div>
<label htmlFor="name" className="block font-medium">
Environment Name

View File

@@ -22,7 +22,7 @@ const DeleteEnvironment = ({ onClose, environment }) => {
<StyledWrapper>
<Modal
size="sm"
title={'Delete Global Environment'}
title="Delete Global Environment"
confirmText="Delete"
handleConfirm={onConfirm}
handleCancel={onClose}

View File

@@ -58,7 +58,7 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV
})
.catch((error) => {
console.error(error);
toast.error('An error occurred while saving the changes')
toast.error('An error occurred while saving the changes');
});
}
});
@@ -76,7 +76,7 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV
}
return (
<span>
<IconAlertCircle id={id} className="text-red-600 cursor-pointer " size={20} />
<IconAlertCircle id={id} className="text-red-600 cursor-pointer" size={20} />
<Tooltip className="tooltip-mod" anchorId={id} html={meta.error || ''} />
</span>
);

View File

@@ -29,7 +29,7 @@ const EnvironmentList = ({ environments, activeEnvironmentUid, selectedEnvironme
}
if (selectedEnvironment) {
const _selectedEnvironment = environments?.find(env => env?.uid === selectedEnvironment?.uid);
const _selectedEnvironment = environments?.find((env) => env?.uid === selectedEnvironment?.uid);
const hasSelectedEnvironmentChanged = !isEqual(selectedEnvironment, _selectedEnvironment);
if (hasSelectedEnvironmentChanged) {
setSelectedEnvironment(_selectedEnvironment);
@@ -43,7 +43,6 @@ const EnvironmentList = ({ environments, activeEnvironmentUid, selectedEnvironme
setSelectedEnvironment(environment);
setOriginalEnvironmentVariables(environment?.variables || []);
}, [environments, activeEnvironmentUid]);
useEffect(() => {
if (prevEnvUids && prevEnvUids.length && envUids.length > prevEnvUids.length) {
@@ -116,16 +115,16 @@ const EnvironmentList = ({ environments, activeEnvironmentUid, selectedEnvironme
</div>
)}
<div className="environments-sidebar flex flex-col">
{environments &&
environments.length &&
environments.map((env) => (
{environments
&& environments.length
&& environments.map((env) => (
<ToolHint key={env.uid} text={env.name} toolhintId={env.uid} place="right">
<div
id={env.uid}
className={selectedEnvironment.uid === env.uid ? 'environment-item active' : 'environment-item'}
onClick={() => handleEnvironmentClick(env)} // Use handleEnvironmentClick to handle click
>
<span className="break-all">{env.name}</span>
<span className="break-all">{env.name}</span>
</div>
</ToolHint>
))}

View File

@@ -20,7 +20,7 @@ const RenameEnvironment = ({ onClose, environment }) => {
name: Yup.string()
.min(1, 'must be at least 1 character')
.max(255, 'Must be 255 characters or less')
.test('is-valid-filename', function(value) {
.test('is-valid-filename', function (value) {
const isValid = validateName(value);
return isValid ? true : this.createError({ message: validateNameError(value) });
})
@@ -56,12 +56,12 @@ const RenameEnvironment = ({ onClose, environment }) => {
<Portal>
<Modal
size="sm"
title={'Rename Environment'}
title="Rename Environment"
confirmText="Rename"
handleConfirm={onSubmit}
handleCancel={onClose}
>
<form className="bruno-form" onSubmit={e => e.preventDefault()}>
<form className="bruno-form" onSubmit={(e) => e.preventDefault()}>
<div>
<label htmlFor="name" className="block font-medium">
Environment Name

View File

@@ -358,4 +358,4 @@ const StyledWrapper = styled.div`
}
`;
export default StyledWrapper;
export default StyledWrapper;

View File

@@ -31,7 +31,7 @@ const GlobalSearchModal = ({ isOpen, onClose }) => {
const tabs = useSelector((state) => state.tabs.tabs);
const createCollectionResults = () => {
const collectionResults = collections.map(collection => ({
const collectionResults = collections.map((collection) => ({
type: SEARCH_TYPES.COLLECTION,
item: collection,
name: collection.name,
@@ -49,13 +49,13 @@ const GlobalSearchModal = ({ isOpen, onClose }) => {
// Check for documentation match
const queryLower = searchTerms.join(' ');
if (['documentation', 'docs', 'bruno docs'].some(term => term.includes(queryLower))) {
if (['documentation', 'docs', 'bruno docs'].some((term) => term.includes(queryLower))) {
results.push(DOCUMENTATION_RESULT);
}
collections.forEach(collection => {
collections.forEach((collection) => {
// Search collection name
if (searchTerms.every(term => collection.name.toLowerCase().includes(term))) {
if (searchTerms.every((term) => collection.name.toLowerCase().includes(term))) {
results.push({
type: SEARCH_TYPES.COLLECTION,
item: collection,
@@ -68,28 +68,28 @@ const GlobalSearchModal = ({ isOpen, onClose }) => {
// Search collection items
const flattenedItems = flattenItems(collection.items);
flattenedItems.forEach(item => {
flattenedItems.forEach((item) => {
const itemPath = getItemPath(item, collection, findParentItemInCollection);
const itemPathLower = itemPath.toLowerCase();
if (isItemARequest(item)) {
// add an optional check for the item name to prevent a crash if it doesnt exist.
const nameMatch = searchTerms.every((term) => (item.name || '').toLowerCase().includes(term));
const urlMatch = searchTerms.every(term => (item.request?.url || '').toLowerCase().includes(term));
const pathMatch = enablePathMatch && searchTerms.every(term => itemPathLower.includes(term));
const urlMatch = searchTerms.every((term) => (item.request?.url || '').toLowerCase().includes(term));
const pathMatch = enablePathMatch && searchTerms.every((term) => itemPathLower.includes(term));
if (nameMatch || urlMatch || pathMatch) {
// Check if this is a gRPC request and get the method type
const isGrpcRequest = item.request?.type === 'grpc';
let method = item.request?.method || '';
if (isGrpcRequest) {
// For gRPC requests, use the methodType
const methodType = item.request?.methodType || 'UNARY';
method = methodType.toLowerCase().replace(/[_]/g, '-');
}
results.push({
type: SEARCH_TYPES.REQUEST,
item,
@@ -101,8 +101,8 @@ const GlobalSearchModal = ({ isOpen, onClose }) => {
});
}
} else if (isItemAFolder(item)) {
const nameMatch = searchTerms.every(term => item.name.toLowerCase().includes(term));
const pathMatch = enablePathMatch && searchTerms.every(term => itemPathLower.includes(term));
const nameMatch = searchTerms.every((term) => item.name.toLowerCase().includes(term));
const pathMatch = enablePathMatch && searchTerms.every((term) => itemPathLower.includes(term));
if (nameMatch || pathMatch) {
results.push({
@@ -161,7 +161,7 @@ const GlobalSearchModal = ({ isOpen, onClose }) => {
}, [collections]); // Depend on collections to recreate when they change
const expandItemPath = (result) => {
const collection = collections.find(c => c.uid === result.collectionUid);
const collection = collections.find((c) => c.uid === result.collectionUid);
if (!collection) return;
ensureCollectionIsMounted(collection);
@@ -170,8 +170,8 @@ const GlobalSearchModal = ({ isOpen, onClose }) => {
dispatch(toggleCollection(collection.uid));
}
let currentItem = result.type === SEARCH_TYPES.FOLDER
? result.item
let currentItem = result.type === SEARCH_TYPES.FOLDER
? result.item
: findParentItemInCollection(collection, result.item.uid);
while (currentItem?.type === 'folder') {
@@ -195,11 +195,11 @@ const GlobalSearchModal = ({ isOpen, onClose }) => {
const handlers = {
ArrowDown: () => {
e.preventDefault();
setSelectedIndex(prev => prev < results.length - 1 ? prev + 1 : 0);
setSelectedIndex((prev) => prev < results.length - 1 ? prev + 1 : 0);
},
ArrowUp: () => {
e.preventDefault();
setSelectedIndex(prev => prev > 0 ? prev - 1 : results.length - 1);
setSelectedIndex((prev) => prev > 0 ? prev - 1 : results.length - 1);
},
Enter: () => {
e.preventDefault();
@@ -213,11 +213,11 @@ const GlobalSearchModal = ({ isOpen, onClose }) => {
},
PageDown: () => {
e.preventDefault();
setSelectedIndex(prev => Math.min(prev + 5, results.length - 1));
setSelectedIndex((prev) => Math.min(prev + 5, results.length - 1));
},
PageUp: () => {
e.preventDefault();
setSelectedIndex(prev => Math.max(prev - 5, 0));
setSelectedIndex((prev) => Math.max(prev - 5, 0));
},
Home: () => {
e.preventDefault();
@@ -234,7 +234,7 @@ const GlobalSearchModal = ({ isOpen, onClose }) => {
};
const handleResultSelection = (result) => {
const targetCollection = collections.find(c => c.uid === result.collectionUid);
const targetCollection = collections.find((c) => c.uid === result.collectionUid);
ensureCollectionIsMounted(targetCollection);
if (result.type === SEARCH_TYPES.DOCUMENTATION) {
@@ -248,7 +248,7 @@ const GlobalSearchModal = ({ isOpen, onClose }) => {
if (result.type === SEARCH_TYPES.REQUEST) {
dispatch(hideHomePage());
const existingTab = tabs.find(tab => tab.uid === result.item.uid);
const existingTab = tabs.find((tab) => tab.uid === result.item.uid);
if (existingTab) {
dispatch(focusTab({ uid: result.item.uid }));
@@ -257,20 +257,20 @@ const GlobalSearchModal = ({ isOpen, onClose }) => {
uid: result.item.uid,
collectionUid: result.collectionUid,
requestPaneTab: getDefaultRequestPaneTab(result.item),
type: 'request',
type: 'request'
}));
}
} else if (result.type === SEARCH_TYPES.FOLDER) {
dispatch(addTab({
uid: result.item.uid,
collectionUid: result.collectionUid,
type: 'folder-settings',
type: 'folder-settings'
}));
} else if (result.type === SEARCH_TYPES.COLLECTION) {
dispatch(addTab({
uid: result.item.uid,
collectionUid: result.collectionUid,
type: 'collection-settings',
type: 'collection-settings'
}));
}
@@ -280,7 +280,7 @@ const GlobalSearchModal = ({ isOpen, onClose }) => {
const handleQueryChange = (e) => {
const newQuery = e.target.value;
setQuery(newQuery);
if (newQuery.trim()) {
debouncedSearch(newQuery);
} else {
@@ -294,7 +294,7 @@ const GlobalSearchModal = ({ isOpen, onClose }) => {
if (debounceTimeoutRef.current) {
clearTimeout(debounceTimeoutRef.current);
}
setQuery('');
setResults([]);
};
@@ -306,7 +306,7 @@ const GlobalSearchModal = ({ isOpen, onClose }) => {
setQuery('');
performSearch('');
setSelectedIndex(0);
return () => clearTimeout(timeoutId);
} else {
// Clear any pending debounced search when modal closes
@@ -351,8 +351,8 @@ const GlobalSearchModal = ({ isOpen, onClose }) => {
return (
<StyledWrapper>
<div
className="command-k-overlay"
<div
className="command-k-overlay"
onClick={onClose}
role="dialog"
aria-modal="true"
@@ -365,12 +365,11 @@ const GlobalSearchModal = ({ isOpen, onClose }) => {
Search through collections, requests, folders, and documentation. Use arrow keys to navigate results and Enter to select.
</p>
<div aria-live="polite" aria-atomic="true" className="sr-only">
{results.length > 0 && query
{results.length > 0 && query
? `${results.length} result${results.length === 1 ? '' : 's'} found`
: query && results.length === 0
: query && results.length === 0
? 'No results found'
: ''
}
: ''}
</div>
<div className="command-k-header">
<div className="search-input-container">
@@ -395,8 +394,8 @@ const GlobalSearchModal = ({ isOpen, onClose }) => {
aria-autocomplete="list"
/>
{query && (
<button
onClick={clearSearch}
<button
onClick={clearSearch}
className="clear-button"
aria-label="Clear search query"
type="button"
@@ -407,8 +406,8 @@ const GlobalSearchModal = ({ isOpen, onClose }) => {
</div>
</div>
<div
className="command-k-results"
<div
className="command-k-results"
ref={resultsRef}
id="search-results"
role="listbox"
@@ -470,7 +469,7 @@ const GlobalSearchModal = ({ isOpen, onClose }) => {
</div>
<div className="result-badges">
{result.type === SEARCH_TYPES.REQUEST && result.method && (
<span
<span
className={`method-badge ${result.method.toLowerCase()}`}
aria-label={`HTTP method ${result.method.toUpperCase().replace(/-/g, ' ')}`}
>
@@ -513,4 +512,4 @@ const GlobalSearchModal = ({ isOpen, onClose }) => {
);
};
export default GlobalSearchModal;
export default GlobalSearchModal;

View File

@@ -6,9 +6,9 @@ export const normalizeQuery = (searchQuery) => {
};
export const isValidQuery = (normalizedQuery) => {
return normalizedQuery &&
normalizedQuery !== '/' &&
!(normalizedQuery.length === 1 && !normalizedQuery.match(/[a-zA-Z0-9]/));
return normalizedQuery
&& normalizedQuery !== '/'
&& !(normalizedQuery.length === 1 && !normalizedQuery.match(/[a-zA-Z0-9]/));
};
export const highlightText = (text, searchQuery) => {
@@ -34,12 +34,12 @@ export const sortResults = (results) => {
if (b.type === SEARCH_TYPES.DOCUMENTATION) return 1;
// Sort by match type priority
const matchTypeOrder = {
[MATCH_TYPES.COLLECTION]: 0,
[MATCH_TYPES.FOLDER]: 1,
[MATCH_TYPES.REQUEST]: 2,
[MATCH_TYPES.URL]: 3,
[MATCH_TYPES.PATH]: 4
const matchTypeOrder = {
[MATCH_TYPES.COLLECTION]: 0,
[MATCH_TYPES.FOLDER]: 1,
[MATCH_TYPES.REQUEST]: 2,
[MATCH_TYPES.URL]: 3,
[MATCH_TYPES.PATH]: 4
};
const aMatchType = matchTypeOrder[a.matchType] ?? 5;
const bMatchType = matchTypeOrder[b.matchType] ?? 5;
@@ -47,10 +47,10 @@ export const sortResults = (results) => {
if (aMatchType !== bMatchType) return aMatchType - bMatchType;
// Sort by type priority
const typeOrder = {
[SEARCH_TYPES.COLLECTION]: 0,
[SEARCH_TYPES.FOLDER]: 1,
[SEARCH_TYPES.REQUEST]: 2
const typeOrder = {
[SEARCH_TYPES.COLLECTION]: 0,
[SEARCH_TYPES.FOLDER]: 1,
[SEARCH_TYPES.REQUEST]: 2
};
const aType = typeOrder[a.type] ?? 3;
const bType = typeOrder[b.type] ?? 3;
@@ -91,4 +91,4 @@ export const getItemPath = (item, collection, findParentItemInCollection) => {
pathParts.unshift(collection.name);
return pathParts.join('/');
};
};

View File

@@ -3,9 +3,9 @@ import styled from 'styled-components';
const Wrapper = styled.div`
font-weight: 400;
font-size: ${(props) => props.theme.font.size.sm};
background-color: ${props => props.theme.infoTip.bg};
border: 1px solid ${props => props.theme.infoTip.border};
box-shadow: ${props => props.theme.infoTip.boxShadow};
background-color: ${(props) => props.theme.infoTip.bg};
border: 1px solid ${(props) => props.theme.infoTip.border};
box-shadow: ${(props) => props.theme.infoTip.boxShadow};
`;
export default Wrapper;

View File

@@ -18,12 +18,12 @@ const Help = ({ children, width = 200 }) => {
onMouseEnter={() => setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)}
>
<HelpIcon size={14}/>
<HelpIcon size={14} />
</span>
{showTooltip && (
<StyledWrapper
className="absolute z-50 rounded-md p-3"
style={{
style={{
top: '50%',
left: 'calc(100% + 8px)',
transform: 'translateY(-50%)',
@@ -37,4 +37,4 @@ const Help = ({ children, width = 200 }) => {
);
};
export default Help;
export default Help;

View File

@@ -2,15 +2,22 @@ import React from 'react';
const DotIcon = ({ width }) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" width={width} height={width}
viewBox="0 0 24 24" strokeWidth="1.5"
stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round"
className='inline-block'
<svg
xmlns="http://www.w3.org/2000/svg"
width={width}
height={width}
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
className="inline-block"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" strokeWidth="0" fill="currentColor" />
</svg>
);
};
export default DotIcon;
export default DotIcon;

View File

@@ -2,17 +2,17 @@ import React from 'react';
// UNARY - Single request, single response (Blue)
export const IconGrpcUnary = ({ size = 18, strokeWidth = 1.5, className = '' }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
strokeLinecap="round"
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
{/* Request arrow (top) - right */}
<path d="M3 8h18" stroke="#3B82F6" strokeWidth={strokeWidth} />
<path d="M18 5l3 3l-3 3" stroke="#3B82F6" strokeWidth={strokeWidth} />
@@ -24,17 +24,17 @@ export const IconGrpcUnary = ({ size = 18, strokeWidth = 1.5, className = '' })
// CLIENT_STREAMING - Streaming request, single response (Purple)
export const IconGrpcClientStreaming = ({ size = 18, strokeWidth = 1.5, className = '' }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
strokeLinecap="round"
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
{/* Request arrow (top) - right with double heads */}
<path d="M3 8h18" stroke="#8B5CF6" strokeWidth={strokeWidth} />
<path d="M18 5l3 3l-3 3" stroke="#8B5CF6" strokeWidth={strokeWidth} />
@@ -47,17 +47,17 @@ export const IconGrpcClientStreaming = ({ size = 18, strokeWidth = 1.5, classNam
// SERVER_STREAMING - Single request, streaming response (Green)
export const IconGrpcServerStreaming = ({ size = 18, strokeWidth = 1.5, className = '' }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
strokeLinecap="round"
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
{/* Request arrow (top) - right */}
<path d="M3 8h18" stroke="#10B981" strokeWidth={strokeWidth} />
<path d="M18 5l3 3l-3 3" stroke="#10B981" strokeWidth={strokeWidth} />
@@ -70,17 +70,17 @@ export const IconGrpcServerStreaming = ({ size = 18, strokeWidth = 1.5, classNam
// BIDI_STREAMING - Streaming request, streaming response (Orange)
export const IconGrpcBidiStreaming = ({ size = 18, strokeWidth = 1.5, className = '' }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
strokeLinecap="round"
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
{/* Request arrow (top) - right with double heads */}
<path d="M3 8h18" stroke="#F97316" strokeWidth={strokeWidth} />
<path d="M18 5l3 3l-3 3" stroke="#F97316" strokeWidth={strokeWidth} />

View File

@@ -14,7 +14,7 @@ const HelpIcon = ({ size = 14 }) => {
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
<path d="M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z" />
</svg>
)
}
);
};
export default HelpIcon;
export default HelpIcon;

View File

@@ -0,0 +1,16 @@
import React from 'react';
const IconBottombarToggle = ({ collapsed = false, size = 16, strokeWidth = 1.5, className = '', ...rest }) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={strokeWidth} strokeLinecap="round" strokeLinejoin="round" className={`icon icon-tabler icons-tabler-outline icon-tabler-layout-bottombar ${className}`} {...rest}>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z" />
<path d="M4 15l16 0" />
{!collapsed && (
<rect x="4.6" y="15.6" width="14.8" height="2.8" rx="0.8" fill="currentColor" />
)}
</svg>
);
};
export default IconBottombarToggle;

View File

@@ -25,4 +25,4 @@ const IconSidebarToggle = ({ collapsed = false, size = 16, strokeWidth = 1.5, cl
);
};
export default IconSidebarToggle;
export default IconSidebarToggle;

View File

@@ -3,102 +3,126 @@ const OpenApiLogo = () => {
<svg width="28" height="28" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
<path
style={{
fill: "#91d400",
fill: '#91d400',
fillOpacity: 1,
fillRule: "nonzero",
stroke: "none"
}} d="M43.125 51.148H20.781l.012.325c.012.21.027.418.039.625.004.09.008.18.016.27a41.442 41.442 0 0 0 .164 1.687c0 .027.004.05.008.078.035.285.07.574.113.86 0 .003 0 .007.004.01a36.98 36.98 0 0 0 1.152 5.255c.004.008.008.012.008.02a27.978 27.978 0 0 0 .265.859c.004.015.012.031.016.047.078.242.164.484.246.73.024.059.043.121.067.184.074.207.148.418.226.629.04.093.074.187.11.285.07.172.136.343.203.52.054.128.11.257.164.39.054.133.113.27.168.406.074.164.148.328.218.492.047.102.09.2.133.297.09.195.184.395.278.59l.093.191c.11.227.223.45.336.672.02.035.035.07.051.102a36.344 36.344 0 0 0 .41.773c.028.051.059.102.086.149L44.45 56.156l.07-.043a15.031 15.031 0 0 1-1.394-4.965Zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
/>
<path
style={{
fill: "#91d400",
fillOpacity: 1,
fillRule: "nonzero",
stroke: "none"
}} d="m22.563 61.137-.727.207.008.02zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
/>
<path
style={{
fill: "#4c5930",
fillOpacity: 1,
fillRule: "nonzero",
stroke: "none"
}} d="m48.613 61.355-.05.055-15.739 15.664c.082.074.16.149.242.223.149.133.297.266.446.394.078.067.152.137.23.204.18.152.36.3.54.449.046.043.097.082.144.12a21.669 21.669 0 0 0 .691.556c.227.175.45.343.676.515.012.008.02.012.027.02.95.707 1.93 1.367 2.946 1.98.035.024.07.043.105.067l.578.34c.117.066.239.132.356.203.113.062.222.125.336.187.207.11.41.223.617.328a35.567 35.567 0 0 0 1.824.887l.559-1.348 7.918-19.133.027-.07a15.337 15.337 0 0 1-2.473-1.64Zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
fillRule: 'nonzero',
stroke: 'none'
}}
d="M43.125 51.148H20.781l.012.325c.012.21.027.418.039.625.004.09.008.18.016.27a41.442 41.442 0 0 0 .164 1.687c0 .027.004.05.008.078.035.285.07.574.113.86 0 .003 0 .007.004.01a36.98 36.98 0 0 0 1.152 5.255c.004.008.008.012.008.02a27.978 27.978 0 0 0 .265.859c.004.015.012.031.016.047.078.242.164.484.246.73.024.059.043.121.067.184.074.207.148.418.226.629.04.093.074.187.11.285.07.172.136.343.203.52.054.128.11.257.164.39.054.133.113.27.168.406.074.164.148.328.218.492.047.102.09.2.133.297.09.195.184.395.278.59l.093.191c.11.227.223.45.336.672.02.035.035.07.051.102a36.344 36.344 0 0 0 .41.773c.028.051.059.102.086.149L44.45 56.156l.07-.043a15.031 15.031 0 0 1-1.394-4.965Zm0 0"
transform="translate(-26.793 -.606) scale(1.44332)"
/>
<path
style={{
fill: "#68a338",
fill: '#91d400',
fillOpacity: 1,
fillRule: "nonzero",
stroke: "none"
}} d="M46.977 59.797a16.778 16.778 0 0 1-.899-1.102c-.152-.203-.3-.406-.437-.617a15.93 15.93 0 0 1-.41-.633L26.124 68.902c.297.485.602.957.914 1.422.012.016.02.035.031.051l.012.016c.008.015.02.03.027.046.004.004.004.004.004.008.028.035.051.07.078.11 0 .004 0 .004.004.007v.004a35.121 35.121 0 0 0 1.051 1.465c.008.012.016.02.02.031.156.2.308.403.464.602.024.027.043.05.063.078.164.203.328.406.496.61.04.046.078.093.117.144.153.18.305.36.457.535.067.078.137.153.203.227.13.148.262.297.395.445.074.082.152.16.226.242.032.035.067.075.102.11l.293.316.121.121c.176.184.352.363.531.54l15.758-15.684a22.18 22.18 0 0 1-.515-.551Zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
fillRule: 'nonzero',
stroke: 'none'
}}
d="m22.563 61.137-.727.207.008.02zm0 0"
transform="translate(-26.793 -.606) scale(1.44332)"
/>
<path
style={{
fill: "#4c5930",
fill: '#4c5930',
fillOpacity: 1,
fillRule: "nonzero",
stroke: "none"
}} d="M67.867 61.348c-.176.136-.347.273-.527.406l.039.066L78.87 80.805a38.54 38.54 0 0 0 1.57-1.078 38.099 38.099 0 0 0 3.227-2.653L67.93 61.41Zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
fillRule: 'nonzero',
stroke: 'none'
}}
d="m48.613 61.355-.05.055-15.739 15.664c.082.074.16.149.242.223.149.133.297.266.446.394.078.067.152.137.23.204.18.152.36.3.54.449.046.043.097.082.144.12a21.669 21.669 0 0 0 .691.556c.227.175.45.343.676.515.012.008.02.012.027.02.95.707 1.93 1.367 2.946 1.98.035.024.07.043.105.067l.578.34c.117.066.239.132.356.203.113.062.222.125.336.187.207.11.41.223.617.328a35.567 35.567 0 0 0 1.824.887l.559-1.348 7.918-19.133.027-.07a15.337 15.337 0 0 1-2.473-1.64Zm0 0"
transform="translate(-26.793 -.606) scale(1.44332)"
/>
<path
style={{
fill: "#91d400",
fill: '#68a338',
fillOpacity: 1,
fillRule: "nonzero",
stroke: "none"
}} d="m77.418 81.707.023-.012-.023.012zm.023-.012c.051-.027.102-.054.153-.086l-.004-.004c-.05.032-.098.06-.149.09zm-.023.012-.008.004zm-.039-.039.027.047zm.039.039.023-.012-.023.012zm-.016.012.004-.004zm.008-.009-.004.005c.004-.004.008-.004.012-.008-.004.004-.004.004-.008.004zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
fillRule: 'nonzero',
stroke: 'none'
}}
d="M46.977 59.797a16.778 16.778 0 0 1-.899-1.102c-.152-.203-.3-.406-.437-.617a15.93 15.93 0 0 1-.41-.633L26.124 68.902c.297.485.602.957.914 1.422.012.016.02.035.031.051l.012.016c.008.015.02.03.027.046.004.004.004.004.004.008.028.035.051.07.078.11 0 .004 0 .004.004.007v.004a35.121 35.121 0 0 0 1.051 1.465c.008.012.016.02.02.031.156.2.308.403.464.602.024.027.043.05.063.078.164.203.328.406.496.61.04.046.078.093.117.144.153.18.305.36.457.535.067.078.137.153.203.227.13.148.262.297.395.445.074.082.152.16.226.242.032.035.067.075.102.11l.293.316.121.121c.176.184.352.363.531.54l15.758-15.684a22.18 22.18 0 0 1-.515-.551Zm0 0"
transform="translate(-26.793 -.606) scale(1.44332)"
/>
<path
style={{
fill: "#91d400",
fill: '#4c5930',
fillOpacity: 1,
fillRule: "nonzero",
stroke: "none"
}} d="M77.441 81.695c.051-.03.102-.054.153-.086-.051.032-.102.059-.153.086zm.149-.09.004.004zm-.2.118.005-.004zm-.19-.763-.391-.644-10.727-17.722c-.215.133-.437.25-.66.367-.223.121-.45.23-.676.34a15.303 15.303 0 0 1-6.523 1.469c-1.465 0-2.93-.211-4.344-.63-.238-.074-.473-.167-.711-.25-.238-.085-.48-.156-.715-.253l-7.91 19.117-.309.75-.265.644h-.004l.062.024c.024.012.043.016.067.027h.004c.004 0 .007.004.011.004.188.078.375.145.563.215.238.094.473.184.707.27.121.042.238.097.36.136a38.13 38.13 0 0 0 7.648 1.824c.105.012.207.028.308.04l.32.035c.2.023.4.047.602.066l.149.012c.25.023.496.043.742.062.082.004.168.008.25.016.219.016.433.027.648.039.133.004.266.008.399.016.172.004.343.011.515.015.246.008.496.008.746.012h.176c2.082 0 4.164-.172 6.223-.516l.105-.015c.215-.04.434-.078.653-.117.125-.024.246-.047.37-.075.126-.023.255-.05.384-.078.21-.043.421-.09.632-.14l.118-.024a37.8 37.8 0 0 0 8.992-3.34c.187-.097.367-.207.554-.308.22-.121.438-.246.657-.371.152-.086.308-.164.457-.254l.004-.004h.004l.003-.004c.004 0 .004 0 .004-.004l-.027-.047.027.047h.004c.004-.004.008-.004.008-.004.008-.008.016-.012.023-.016.051-.03.102-.058.149-.09zM48.625 37.938c.172-.141.348-.274.523-.407l-.039-.066L37.621 18.48c-.535.348-1.062.704-1.578 1.082a37.213 37.213 0 0 0-3.219 2.649l15.739 15.664Zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
fillRule: 'nonzero',
stroke: 'none'
}}
d="M67.867 61.348c-.176.136-.347.273-.527.406l.039.066L78.87 80.805a38.54 38.54 0 0 0 1.57-1.078 38.099 38.099 0 0 0 3.227-2.653L67.93 61.41Zm0 0"
transform="translate(-26.793 -.606) scale(1.44332)"
/>
<path
style={{
fill: "#4c5930",
fill: '#91d400',
fillOpacity: 1,
fillRule: "nonzero",
stroke: "none"
}} d="M31.73 23.254c-.18.18-.347.363-.523.543-.172.18-.352.36-.523.543a38.163 38.163 0 0 0-3.32 4.125c-.106.156-.216.312-.321.469-.11.164-.219.328-.324.496-.04.058-.078.12-.117.18a37.099 37.099 0 0 0-5.82 18.527c-.009.25-.016.504-.02.754s-.012.5-.012.75h22.29c0-.25.023-.5.034-.75.012-.254.016-.504.043-.754a15.002 15.002 0 0 1 3.36-8.078c.16-.192.34-.375.507-.559.172-.188.329-.379.508-.559zm45.993-5.508a46.43 46.43 0 0 0-.684-.402c-.117-.067-.23-.133-.348-.196-.117-.066-.23-.128-.347-.195-.2-.11-.403-.219-.606-.324-.031-.016-.062-.031-.093-.05a38.014 38.014 0 0 0-4.02-1.798c-.035-.015-.074-.027-.11-.039a37.527 37.527 0 0 0-8.41-2.102c-.101-.015-.207-.027-.312-.042l-.313-.036a31.754 31.754 0 0 0-.777-.078c-.238-.023-.48-.043-.719-.062-.093-.004-.187-.012-.28-.016-.204-.015-.415-.027-.618-.039l-.328-.012v22.239c1.144.117 2.281.363 3.383.734l16.445-16.367a41.117 41.117 0 0 0-1.863-1.215zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
fillRule: 'nonzero',
stroke: 'none'
}}
d="m77.418 81.707.023-.012-.023.012zm.023-.012c.051-.027.102-.054.153-.086l-.004-.004c-.05.032-.098.06-.149.09zm-.023.012-.008.004zm-.039-.039.027.047zm.039.039.023-.012-.023.012zm-.016.012.004-.004zm.008-.009-.004.005c.004-.004.008-.004.012-.008-.004.004-.004.004-.008.004zm0 0"
transform="translate(-26.793 -.606) scale(1.44332)"
/>
<path
style={{
fill: "#68a338",
fill: '#91d400',
fillOpacity: 1,
fillRule: "nonzero",
stroke: "none"
}} d="m38.898 17.68.391.644zm18.59-5.34c-.25.004-.504.004-.754.015a38.117 38.117 0 0 0-4.71.48c-.036.009-.07.013-.106.02-.219.036-.434.079-.652.118l-.371.07c-.13.027-.258.05-.383.082a36.99 36.99 0 0 0-.75.164 37.925 37.925 0 0 0-8.996 3.336c-.184.098-.368.21-.551.312-.219.118-.438.243-.66.368-.16.093-.325.18-.489.277h-.003a.162.162 0 0 1-.036.02c-.043.027-.086.046-.129.074l.004.004.387.644 11.117 18.367c.215-.132.438-.25.66-.367a15.15 15.15 0 0 1 5.668-1.73c.25-.028.5-.047.754-.063.25-.011.504-.023.758-.023V12.324c-.254 0-.504.008-.758.016zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
fillRule: 'nonzero',
stroke: 'none'
}}
d="M77.441 81.695c.051-.03.102-.054.153-.086-.051.032-.102.059-.153.086zm.149-.09.004.004zm-.2.118.005-.004zm-.19-.763-.391-.644-10.727-17.722c-.215.133-.437.25-.66.367-.223.121-.45.23-.676.34a15.303 15.303 0 0 1-6.523 1.469c-1.465 0-2.93-.211-4.344-.63-.238-.074-.473-.167-.711-.25-.238-.085-.48-.156-.715-.253l-7.91 19.117-.309.75-.265.644h-.004l.062.024c.024.012.043.016.067.027h.004c.004 0 .007.004.011.004.188.078.375.145.563.215.238.094.473.184.707.27.121.042.238.097.36.136a38.13 38.13 0 0 0 7.648 1.824c.105.012.207.028.308.04l.32.035c.2.023.4.047.602.066l.149.012c.25.023.496.043.742.062.082.004.168.008.25.016.219.016.433.027.648.039.133.004.266.008.399.016.172.004.343.011.515.015.246.008.496.008.746.012h.176c2.082 0 4.164-.172 6.223-.516l.105-.015c.215-.04.434-.078.653-.117.125-.024.246-.047.37-.075.126-.023.255-.05.384-.078.21-.043.421-.09.632-.14l.118-.024a37.8 37.8 0 0 0 8.992-3.34c.187-.097.367-.207.554-.308.22-.121.438-.246.657-.371.152-.086.308-.164.457-.254l.004-.004h.004l.003-.004c.004 0 .004 0 .004-.004l-.027-.047.027.047h.004c.004-.004.008-.004.008-.004.008-.008.016-.012.023-.016.051-.03.102-.058.149-.09zM48.625 37.938c.172-.141.348-.274.523-.407l-.039-.066L37.621 18.48c-.535.348-1.062.704-1.578 1.082a37.213 37.213 0 0 0-3.219 2.649l15.739 15.664Zm0 0"
transform="translate(-26.793 -.606) scale(1.44332)"
/>
<path
style={{
fill: "#4c5930",
fill: '#4c5930',
fillOpacity: 1,
fillRule: "nonzero",
stroke: "none"
}} d="m95.695 47.809-.035-.598c-.008-.102-.012-.2-.02-.3l-.058-.704c-.008-.059-.012-.121-.016-.18a53.501 53.501 0 0 0-.086-.785c-.003-.023-.003-.043-.007-.062 0-.012 0-.02-.004-.032-.035-.28-.07-.562-.114-.843 0-.012 0-.02-.003-.028a36.772 36.772 0 0 0-1.153-5.246 47.929 47.929 0 0 0-.258-.832c-.011-.035-.023-.07-.03-.105-.083-.242-.161-.48-.247-.719-.023-.063-.043-.129-.066-.195a39.302 39.302 0 0 0-.34-.91c-.067-.172-.133-.344-.2-.512-.054-.137-.109-.27-.163-.403-.055-.132-.114-.261-.168-.394l-.223-.504-.129-.285c-.09-.2-.188-.402-.281-.602-.032-.058-.059-.12-.086-.18-.113-.23-.227-.456-.34-.683-.016-.031-.031-.062-.05-.094-.13-.25-.259-.5-.395-.746l-.012-.023a36.958 36.958 0 0 0-2.133-3.446L72.628 44.77c.376 1.097.618 2.23.735 3.37h22.344l-.012-.331zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
fillRule: 'nonzero',
stroke: 'none'
}}
d="M31.73 23.254c-.18.18-.347.363-.523.543-.172.18-.352.36-.523.543a38.163 38.163 0 0 0-3.32 4.125c-.106.156-.216.312-.321.469-.11.164-.219.328-.324.496-.04.058-.078.12-.117.18a37.099 37.099 0 0 0-5.82 18.527c-.009.25-.016.504-.02.754s-.012.5-.012.75h22.29c0-.25.023-.5.034-.75.012-.254.016-.504.043-.754a15.002 15.002 0 0 1 3.36-8.078c.16-.192.34-.375.507-.559.172-.188.329-.379.508-.559zm45.993-5.508a46.43 46.43 0 0 0-.684-.402c-.117-.067-.23-.133-.348-.196-.117-.066-.23-.128-.347-.195-.2-.11-.403-.219-.606-.324-.031-.016-.062-.031-.093-.05a38.014 38.014 0 0 0-4.02-1.798c-.035-.015-.074-.027-.11-.039a37.527 37.527 0 0 0-8.41-2.102c-.101-.015-.207-.027-.312-.042l-.313-.036a31.754 31.754 0 0 0-.777-.078c-.238-.023-.48-.043-.719-.062-.093-.004-.187-.012-.28-.016-.204-.015-.415-.027-.618-.039l-.328-.012v22.239c1.144.117 2.281.363 3.383.734l16.445-16.367a41.117 41.117 0 0 0-1.863-1.215zm0 0"
transform="translate(-26.793 -.606) scale(1.44332)"
/>
<path
style={{
fill: "#68a338",
fill: '#68a338',
fillOpacity: 1,
fillRule: "nonzero",
stroke: "none"
}} d="M73.45 49.64c0 .255-.024.505-.036.755-.012.25-.016.503-.043.753a15.002 15.002 0 0 1-3.36 8.079c-.16.191-.335.375-.507.558-.168.188-.328.38-.508.559L84.758 76.03c.18-.18.347-.363.523-.543.172-.183.352-.363.52-.547a36.965 36.965 0 0 0 3.195-3.937c.04-.055.074-.11.11-.16.117-.168.234-.34.347-.508.102-.152.203-.3.3-.453.048-.074.095-.153.142-.223a37.055 37.055 0 0 0 5.808-18.515c.012-.25.016-.5.024-.75.003-.25.011-.5.011-.754zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
fillRule: 'nonzero',
stroke: 'none'
}}
d="m38.898 17.68.391.644zm18.59-5.34c-.25.004-.504.004-.754.015a38.117 38.117 0 0 0-4.71.48c-.036.009-.07.013-.106.02-.219.036-.434.079-.652.118l-.371.07c-.13.027-.258.05-.383.082a36.99 36.99 0 0 0-.75.164 37.925 37.925 0 0 0-8.996 3.336c-.184.098-.368.21-.551.312-.219.118-.438.243-.66.368-.16.093-.325.18-.489.277h-.003a.162.162 0 0 1-.036.02c-.043.027-.086.046-.129.074l.004.004.387.644 11.117 18.367c.215-.132.438-.25.66-.367a15.15 15.15 0 0 1 5.668-1.73c.25-.028.5-.047.754-.063.25-.011.504-.023.758-.023V12.324c-.254 0-.504.008-.758.016zm0 0"
transform="translate(-26.793 -.606) scale(1.44332)"
/>
<path
style={{
fill: "#3f3f42",
fill: '#4c5930',
fillOpacity: 1,
fillRule: "nonzero",
stroke: "none"
}} d="M102.36 5.734c-4.079-4.058-10.688-4.058-14.766 0-3.254 3.235-3.903 8.075-1.965 11.961l-22.746 22.64c-3.906-1.925-8.77-1.28-12.024 1.958-4.078 4.059-4.074 10.64 0 14.7 4.082 4.058 10.692 4.054 14.77 0a10.356 10.356 0 0 0 1.965-11.966l22.746-22.64c3.906 1.93 8.765 1.281 12.02-1.957a10.355 10.355 0 0 0 0-14.696zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
fillRule: 'nonzero',
stroke: 'none'
}}
d="m95.695 47.809-.035-.598c-.008-.102-.012-.2-.02-.3l-.058-.704c-.008-.059-.012-.121-.016-.18a53.501 53.501 0 0 0-.086-.785c-.003-.023-.003-.043-.007-.062 0-.012 0-.02-.004-.032-.035-.28-.07-.562-.114-.843 0-.012 0-.02-.003-.028a36.772 36.772 0 0 0-1.153-5.246 47.929 47.929 0 0 0-.258-.832c-.011-.035-.023-.07-.03-.105-.083-.242-.161-.48-.247-.719-.023-.063-.043-.129-.066-.195a39.302 39.302 0 0 0-.34-.91c-.067-.172-.133-.344-.2-.512-.054-.137-.109-.27-.163-.403-.055-.132-.114-.261-.168-.394l-.223-.504-.129-.285c-.09-.2-.188-.402-.281-.602-.032-.058-.059-.12-.086-.18-.113-.23-.227-.456-.34-.683-.016-.031-.031-.062-.05-.094-.13-.25-.259-.5-.395-.746l-.012-.023a36.958 36.958 0 0 0-2.133-3.446L72.628 44.77c.376 1.097.618 2.23.735 3.37h22.344l-.012-.331zm0 0"
transform="translate(-26.793 -.606) scale(1.44332)"
/>
<path
style={{
fill: '#68a338',
fillOpacity: 1,
fillRule: 'nonzero',
stroke: 'none'
}}
d="M73.45 49.64c0 .255-.024.505-.036.755-.012.25-.016.503-.043.753a15.002 15.002 0 0 1-3.36 8.079c-.16.191-.335.375-.507.558-.168.188-.328.38-.508.559L84.758 76.03c.18-.18.347-.363.523-.543.172-.183.352-.363.52-.547a36.965 36.965 0 0 0 3.195-3.937c.04-.055.074-.11.11-.16.117-.168.234-.34.347-.508.102-.152.203-.3.3-.453.048-.074.095-.153.142-.223a37.055 37.055 0 0 0 5.808-18.515c.012-.25.016-.5.024-.75.003-.25.011-.5.011-.754zm0 0"
transform="translate(-26.793 -.606) scale(1.44332)"
/>
<path
style={{
fill: '#3f3f42',
fillOpacity: 1,
fillRule: 'nonzero',
stroke: 'none'
}}
d="M102.36 5.734c-4.079-4.058-10.688-4.058-14.766 0-3.254 3.235-3.903 8.075-1.965 11.961l-22.746 22.64c-3.906-1.925-8.77-1.28-12.024 1.958-4.078 4.059-4.074 10.64 0 14.7 4.082 4.058 10.692 4.054 14.77 0a10.356 10.356 0 0 0 1.965-11.966l22.746-22.64c3.906 1.93 8.765 1.281 12.02-1.957a10.355 10.355 0 0 0 0-14.696zm0 0"
transform="translate(-26.793 -.606) scale(1.44332)"
/>
</svg>
);
};
export default OpenApiLogo;
export default OpenApiLogo;

View File

@@ -25,6 +25,7 @@ class MultiLineEditor extends Component {
maskInput: props.isSecret || false // Always mask the input by default (if it's a secret)
};
}
componentDidMount() {
// Initialize CodeMirror as a single line editor
/** @type {import("codemirror").Editor} */
@@ -67,15 +68,14 @@ class MultiLineEditor extends Component {
'Cmd-F': () => {},
'Ctrl-F': () => {},
// Tabbing disabled to make tabindex work
Tab: false,
'Tab': false,
'Shift-Tab': false
}
});
const getAllVariablesHandler = () => getAllVariables(this.props.collection, this.props.item);
const getAnywordAutocompleteHints = () => this.props.autocomplete || [];
// Setup AutoComplete Helper
const autoCompleteOptions = {
showHintsFor: ['variables'],
@@ -89,7 +89,7 @@ class MultiLineEditor extends Component {
);
setupLinkAware(this.editor);
this.editor.setValue(String(this.props.value) || '');
this.editor.on('change', this._onEdit);
this.addOverlay(variables);

View File

@@ -89,7 +89,7 @@ const Notifications = () => {
className={`select-none ${1 == 2 ? 'opacity-50' : 'text-link mark-as-read cursor-pointer hover:underline'}`}
onClick={() => dispatch(markAllNotificationsAsRead())}
>
{'Mark all as read'}
Mark all as read
</button>
</>
)}
@@ -123,88 +123,89 @@ const Notifications = () => {
{showNotificationsModal && (
<Portal>
<Modal
size="lg"
title="Notifications"
confirmText={'Close'}
handleConfirm={() => {
setShowNotificationsModal(false);
}}
handleCancel={() => {
setShowNotificationsModal(false);
}}
hideFooter={true}
customHeader={modalCustomHeader}
disableCloseOnOutsideClick={true}
disableEscapeKey={true}
>
<div className="notifications-modal">
{notifications?.length > 0 ? (
<Modal
size="lg"
title="Notifications"
confirmText="Close"
handleConfirm={() => {
setShowNotificationsModal(false);
}}
handleCancel={() => {
setShowNotificationsModal(false);
}}
hideFooter={true}
customHeader={modalCustomHeader}
disableCloseOnOutsideClick={true}
disableEscapeKey={true}
>
<div className="notifications-modal">
{notifications?.length > 0 ? (
<div className="grid grid-cols-4 flex flex-row">
<div className="col-span-1 flex flex-col">
<ul
className="notifications w-full flex flex-col h-[50vh] max-h-[50vh] overflow-y-auto"
style={{ maxHeight: '50vh', height: '46vh' }}
>
{notifications?.slice(notificationsStartIndex, notificationsEndIndex)?.map((notification) => (
<li
key={notification.id}
className={`p-4 flex flex-col justify-center ${
selectedNotification?.id == notification.id ? 'active' : notification.read ? 'read' : ''
<div className="col-span-1 flex flex-col">
<ul
className="notifications w-full flex flex-col h-[50vh] max-h-[50vh] overflow-y-auto"
style={{ maxHeight: '50vh', height: '46vh' }}
>
{notifications?.slice(notificationsStartIndex, notificationsEndIndex)?.map((notification) => (
<li
key={notification.id}
className={`p-4 flex flex-col justify-center ${
selectedNotification?.id == notification.id ? 'active' : notification.read ? 'read' : ''
}`}
onClick={handleNotificationItemClick(notification)}
>
<div className="notification-title w-full">{notification?.title}</div>
<div className="notification-date text-xs py-2">{relativeDate(notification?.date)}</div>
</li>
))}
</ul>
<div className="w-full pagination flex flex-row gap-4 justify-center p-2 items-center text-xs">
<button
className={`pl-2 pr-2 py-3 select-none ${
pageNumber <= 1 ? 'opacity-50' : 'text-link cursor-pointer hover:underline'
}`}
onClick={handleNotificationItemClick(notification)}
onClick={handlePrev}
>
<div className="notification-title w-full">{notification?.title}</div>
<div className="notification-date text-xs py-2">{relativeDate(notification?.date)}</div>
</li>
))}
</ul>
<div className="w-full pagination flex flex-row gap-4 justify-center p-2 items-center text-xs">
<button
className={`pl-2 pr-2 py-3 select-none ${
pageNumber <= 1 ? 'opacity-50' : 'text-link cursor-pointer hover:underline'
}`}
onClick={handlePrev}
>
{'Prev'}
</button>
<div className="flex flex-row items-center justify-center gap-1">
Page
<div className="w-[20px] flex justify-center" style={{ width: '20px' }}>
{pageNumber}
</div>
of
<div className="w-[20px] flex justify-center" style={{ width: '20px' }}>
{totalPages}
Prev
</button>
<div className="flex flex-row items-center justify-center gap-1">
Page
<div className="w-[20px] flex justify-center" style={{ width: '20px' }}>
{pageNumber}
</div>
of
<div className="w-[20px] flex justify-center" style={{ width: '20px' }}>
{totalPages}
</div>
</div>
<button
className={`pl-2 pr-2 py-3 select-none ${
pageNumber == totalPages ? 'opacity-50' : 'text-link cursor-pointer hover:underline'
}`}
onClick={handleNext}
>
Next
</button>
</div>
<button
className={`pl-2 pr-2 py-3 select-none ${
pageNumber == totalPages ? 'opacity-50' : 'text-link cursor-pointer hover:underline'
}`}
onClick={handleNext}
</div>
<div className="flex w-full col-span-3 p-4 flex-col">
<div className="w-full text-lg flex flex-wrap h-fit mb-1">{selectedNotification?.title}</div>
<div className="w-full notification-date text-xs mb-4">
{humanizeDate(selectedNotification?.date)}
</div>
<iframe
src={`data:text/html,${getSanitizedDescription(selectedNotification?.description)}`}
sandbox="allow-popups"
style={{ width: '100%', height: '100%' }}
>
{'Next'}
</button>
</iframe>
</div>
</div>
<div className="flex w-full col-span-3 p-4 flex-col">
<div className="w-full text-lg flex flex-wrap h-fit mb-1">{selectedNotification?.title}</div>
<div className="w-full notification-date text-xs mb-4">
{humanizeDate(selectedNotification?.date)}
</div>
<iframe
src={`data:text/html,${getSanitizedDescription(selectedNotification?.description)}`}
sandbox="allow-popups"
style={{ width: '100%', height: '100%' }}
></iframe>
</div>
</div>
) : (
<div className="opacity-50 italic text-xs p-12 flex justify-center">You are all caught up!</div>
)}
</div>
</Modal>
) : (
<div className="opacity-50 italic text-xs p-12 flex justify-center">You are all caught up!</div>
)}
</div>
</Modal>
</Portal>
)}
</StyledWrapper>

View File

@@ -36,4 +36,4 @@ const StyledWrapper = styled.div`
}
`;
export default StyledWrapper;
export default StyledWrapper;

View File

@@ -3,16 +3,16 @@ import { IconFolder, IconFile } from '@tabler/icons';
import path from 'utils/common/path';
import StyledWrapper from './StyledWrapper';
const PathDisplay = ({
const PathDisplay = ({
baseName = '',
iconType = 'file'
}) => {
return (
<StyledWrapper>
<div className="path-display mt-2">
<div className="path-display mt-2">
<div className="path-layout flex font-mono">
<div className="icon-column flex">
{iconType === 'file' ? <IconFile size={16} /> : <IconFolder size={16} />}
{iconType === 'file' ? <IconFile size={16} /> : <IconFolder size={16} />}
</div>
<span className="name-container">
{baseName}
@@ -23,4 +23,4 @@ const PathDisplay = ({
);
};
export default PathDisplay;
export default PathDisplay;

View File

@@ -32,10 +32,10 @@ const Font = ({ close }) => {
}
})
).then(() => {
toast.success('Preferences saved successfully')
toast.success('Preferences saved successfully');
close();
}).catch(() => {
toast.error('Failed to save preferences')
toast.error('Failed to save preferences');
});
};

View File

@@ -4,18 +4,18 @@ import Theme from './Theme/index';
const Display = ({ close }) => {
return (
<div className="flex flex-col my-2 gap-10 w-full">
<div className='w-full flex flex-col gap-2'>
<span>
Theme
</span>
<Theme close={close} />
</div>
<div className='h-[1px] bg-[#aaa5] w-full'></div>
<div className='w-fit flex flex-col gap-2'>
<Font close={close} />
</div>
<div className="flex flex-col my-2 gap-10 w-full">
<div className="w-full flex flex-col gap-2">
<span>
Theme
</span>
<Theme close={close} />
</div>
<div className="h-[1px] bg-[#aaa5] w-full"></div>
<div className="w-fit flex flex-col gap-2">
<Font close={close} />
</div>
</div>
);
};

View File

@@ -84,10 +84,10 @@ const ProxySettings = ({ close }) => {
proxy: validatedProxy
})
).then(() => {
toast.success('Preferences saved successfully')
toast.success('Preferences saved successfully');
close();
}).catch(() => {
toast.error('Failed to save preferences')
toast.error('Failed to save preferences');
});
})
.catch((error) => {
@@ -163,7 +163,7 @@ const ProxySettings = ({ close }) => {
{formik?.values?.mode === 'system' ? (
<div className="mb-3 pt-1 text-muted system-proxy-settings">
<small>
Below values are sourced from your system environment variables and cannot be directly updated in Bruno.<br/>
Below values are sourced from your system environment variables and cannot be directly updated in Bruno.<br />
Please refer to your OS documentation to change these values.
</small>
<div className="flex flex-col justify-start items-start pt-2">

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