mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +00:00
Merge branch 'main' of usebruno/bruno into workspaces
This commit is contained in:
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -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
5
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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—don’t 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.
|
||||
|
||||
196
eslint.config.js
196
eslint.config.js
@@ -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,16 @@ 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/**/*'
|
||||
]
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
'diff': fixupPluginRules(eslintPluginDiff),
|
||||
@@ -34,13 +44,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 +66,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 +74,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 +94,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 +212,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'
|
||||
}
|
||||
}
|
||||
]));
|
||||
|
||||
110
package-lock.json
generated
110
package-lock.json
generated
@@ -5078,6 +5078,98 @@
|
||||
"jsep": "^0.4.0||^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lydell/node-pty": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@lydell/node-pty/-/node-pty-1.1.0.tgz",
|
||||
"integrity": "sha512-VDD8LtlMTOrPKWMXUAcB9+LTktzuunqrMwkYR1DMRBkS6LQrCt+0/Ws1o2rMml/n3guePpS7cxhHF7Nm5K4iMw==",
|
||||
"license": "MIT",
|
||||
"optionalDependencies": {
|
||||
"@lydell/node-pty-darwin-arm64": "1.1.0",
|
||||
"@lydell/node-pty-darwin-x64": "1.1.0",
|
||||
"@lydell/node-pty-linux-arm64": "1.1.0",
|
||||
"@lydell/node-pty-linux-x64": "1.1.0",
|
||||
"@lydell/node-pty-win32-arm64": "1.1.0",
|
||||
"@lydell/node-pty-win32-x64": "1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lydell/node-pty-darwin-arm64": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@lydell/node-pty-darwin-arm64/-/node-pty-darwin-arm64-1.1.0.tgz",
|
||||
"integrity": "sha512-7kFD+owAA61qmhJCtoMbqj3Uvff3YHDiU+4on5F2vQdcMI3MuwGi7dM6MkFG/yuzpw8LF2xULpL71tOPUfxs0w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@lydell/node-pty-darwin-x64": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@lydell/node-pty-darwin-x64/-/node-pty-darwin-x64-1.1.0.tgz",
|
||||
"integrity": "sha512-XZdvqj5FjAMjH8bdp0YfaZjur5DrCIDD1VYiE9EkkYVMDQqRUPHYV3U8BVEQVT9hYfjmpr7dNaELF2KyISWSNA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@lydell/node-pty-linux-arm64": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@lydell/node-pty-linux-arm64/-/node-pty-linux-arm64-1.1.0.tgz",
|
||||
"integrity": "sha512-yyDBmalCfHpLiQMT2zyLcqL2Fay4Xy7rIs8GH4dqKLnEviMvPGOK7LADVkKAsbsyXBSISL3Lt1m1MtxhPH6ckg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@lydell/node-pty-linux-x64": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@lydell/node-pty-linux-x64/-/node-pty-linux-x64-1.1.0.tgz",
|
||||
"integrity": "sha512-NcNqRTD14QT+vXcEuqSSvmWY+0+WUBn2uRE8EN0zKtDpIEr9d+YiFj16Uqds6QfcLCHfZmC+Ls7YzwTaqDnanA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@lydell/node-pty-win32-arm64": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@lydell/node-pty-win32-arm64/-/node-pty-win32-arm64-1.1.0.tgz",
|
||||
"integrity": "sha512-JOMbCou+0fA7d/m97faIIfIU0jOv8sn2OR7tI45u3AmldKoKoLP8zHY6SAvDDnI3fccO1R2HeR1doVjpS7HM0w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@lydell/node-pty-win32-x64": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@lydell/node-pty-win32-x64/-/node-pty-win32-x64-1.1.0.tgz",
|
||||
"integrity": "sha512-3N56BZ+WDFnUMYRtsrr7Ky2mhWGl9xXcyqR6cexfuCqcz9RNWL+KoXRv/nZylY5dYaXkft4JaR1uVu+roiZDAw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@malept/flatpak-bundler": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz",
|
||||
@@ -9133,6 +9225,21 @@
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@xterm/addon-fit": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz",
|
||||
"integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@xterm/xterm": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@xterm/xterm": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
|
||||
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@xtuc/ieee754": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
|
||||
@@ -26841,6 +26948,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",
|
||||
@@ -30234,6 +30343,7 @@
|
||||
"@aws-sdk/credential-providers": "3.750.0",
|
||||
"@grpc/grpc-js": "^1.13.2",
|
||||
"@grpc/proto-loader": "^0.7.13",
|
||||
"@lydell/node-pty": "^1.1.0",
|
||||
"@usebruno/common": "0.1.0",
|
||||
"@usebruno/converters": "^0.1.0",
|
||||
"@usebruno/filestore": "^0.1.0",
|
||||
|
||||
@@ -6,4 +6,4 @@ module.exports = {
|
||||
}]
|
||||
],
|
||||
plugins: ['babel-plugin-styled-components']
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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)'
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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', () => {});
|
||||
});
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -87,7 +87,7 @@ const AuthMode = ({ collection }) => {
|
||||
}}
|
||||
>
|
||||
NTLM Auth
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from "react";
|
||||
import React from 'react';
|
||||
import { getTotalRequestCountInCollection } from 'utils/collections/';
|
||||
import { IconBox, IconFolder, IconWorld, IconApi, IconShare } from '@tabler/icons';
|
||||
import { areItemsLoading, getItemsLoadStats } from "utils/collections/index";
|
||||
import { useState } from "react";
|
||||
import ShareCollection from "components/ShareCollection/index";
|
||||
import { areItemsLoading, getItemsLoadStats } from 'utils/collections/index';
|
||||
import { useState } from 'react';
|
||||
import ShareCollection from 'components/ShareCollection/index';
|
||||
|
||||
const Info = ({ collection }) => {
|
||||
const totalRequestsInCollection = getTotalRequestCountInCollection(collection);
|
||||
@@ -11,10 +11,10 @@ const Info = ({ collection }) => {
|
||||
const isCollectionLoading = areItemsLoading(collection);
|
||||
const { loading: itemsLoadingCount, total: totalItems } = getItemsLoadStats(collection);
|
||||
const [showShareCollectionModal, toggleShowShareCollectionModal] = useState(false);
|
||||
|
||||
|
||||
const handleToggleShowShareCollectionModal = (value) => (e) => {
|
||||
toggleShowShareCollectionModal(value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col h-fit">
|
||||
@@ -55,7 +55,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 +79,4 @@ const Info = ({ collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default Info;
|
||||
export default Info;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -127,8 +127,7 @@ const VarsTable = ({ collection, vars, varType }) => {
|
||||
},
|
||||
_var,
|
||||
'value'
|
||||
)
|
||||
}
|
||||
)}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -160,4 +160,4 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -225,4 +225,4 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -290,4 +290,4 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -344,4 +344,4 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -517,4 +517,4 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -6,7 +6,8 @@ import {
|
||||
IconDatabase,
|
||||
IconClock,
|
||||
IconServer,
|
||||
IconChevronDown
|
||||
IconChevronDown,
|
||||
IconChartLine
|
||||
} from '@tabler/icons';
|
||||
|
||||
const getProcessOptions = (processes) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 });
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -19,4 +19,4 @@ const Wrapper = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
export default Wrapper;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -13,4 +13,4 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -126,8 +126,7 @@ const VarsTable = ({ folder, collection, vars, varType }) => {
|
||||
},
|
||||
_var,
|
||||
'value'
|
||||
)
|
||||
}
|
||||
)}
|
||||
collection={collection}
|
||||
item={folder}
|
||||
/>
|
||||
|
||||
@@ -100,7 +100,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>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -358,4 +358,4 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -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 doesn’t 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;
|
||||
|
||||
@@ -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('/');
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -25,4 +25,4 @@ const IconSidebarToggle = ({ collapsed = false, size = 16, strokeWidth = 1.5, cl
|
||||
);
|
||||
};
|
||||
|
||||
export default IconSidebarToggle;
|
||||
export default IconSidebarToggle;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -36,4 +36,4 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -160,8 +160,7 @@ const AssertionRow = ({
|
||||
},
|
||||
assertion,
|
||||
'value'
|
||||
)
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
@@ -179,9 +178,8 @@ const AssertionRow = ({
|
||||
},
|
||||
assertion,
|
||||
'value'
|
||||
)
|
||||
}
|
||||
}
|
||||
);
|
||||
}}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
|
||||
@@ -83,33 +83,33 @@ const Assertions = ({ item, collection }) => {
|
||||
<ReorderTable updateReorderedItem={handleAssertionDrag}>
|
||||
{assertions && assertions.length
|
||||
? assertions.map((assertion) => {
|
||||
return (
|
||||
<tr key={assertion.uid} data-uid={assertion.uid}>
|
||||
<td className='flex relative'>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={assertion.name}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleAssertionChange(e, assertion, 'name')}
|
||||
return (
|
||||
<tr key={assertion.uid} data-uid={assertion.uid}>
|
||||
<td className="flex relative">
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={assertion.name}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleAssertionChange(e, assertion, 'name')}
|
||||
/>
|
||||
</td>
|
||||
<AssertionRow
|
||||
key={assertion.uid}
|
||||
assertion={assertion}
|
||||
item={item}
|
||||
collection={collection}
|
||||
handleAssertionChange={handleAssertionChange}
|
||||
handleRemoveAssertion={handleRemoveAssertion}
|
||||
onSave={onSave}
|
||||
handleRun={handleRun}
|
||||
/>
|
||||
</td>
|
||||
<AssertionRow
|
||||
key={assertion.uid}
|
||||
assertion={assertion}
|
||||
item={item}
|
||||
collection={collection}
|
||||
handleAssertionChange={handleAssertionChange}
|
||||
handleRemoveAssertion={handleRemoveAssertion}
|
||||
onSave={onSave}
|
||||
handleRun={handleRun}
|
||||
/>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</ReorderTable>
|
||||
</Table>
|
||||
|
||||
@@ -18,7 +18,7 @@ const ApiKeyAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
const apikeyAuth = get(request, 'auth.apikey', {});
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
|
||||
|
||||
const handleSave = () => {
|
||||
save();
|
||||
};
|
||||
@@ -47,17 +47,17 @@ const ApiKeyAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
!apikeyAuth?.placement &&
|
||||
dispatch(
|
||||
updateAuth({
|
||||
mode: 'apikey',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
placement: 'header'
|
||||
}
|
||||
})
|
||||
);
|
||||
!apikeyAuth?.placement
|
||||
&& dispatch(
|
||||
updateAuth({
|
||||
mode: 'apikey',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
placement: 'header'
|
||||
}
|
||||
})
|
||||
);
|
||||
}, [apikeyAuth]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -18,7 +18,7 @@ const AwsV4Auth = ({ item, collection, updateAuth, request, save }) => {
|
||||
const { showWarning, warningMessage } = isSensitive(awsv4Auth?.secretAccessKey);
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
|
||||
|
||||
const handleSave = () => {
|
||||
save();
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ const BasicAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
const { showWarning, warningMessage } = isSensitive(basicAuth?.password);
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
|
||||
|
||||
const handleSave = () => {
|
||||
save();
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@ const BearerAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
const { showWarning, warningMessage } = isSensitive(bearerToken);
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
|
||||
|
||||
const handleSave = () => {
|
||||
save();
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@ const DigestAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
const { showWarning, warningMessage } = isSensitive(digestAuth?.password);
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
|
||||
|
||||
const handleSave = () => {
|
||||
save();
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ const NTLMAuth = ({ item, collection, request, save, updateAuth }) => {
|
||||
const { showWarning, warningMessage } = isSensitive(ntlmAuth?.password);
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
|
||||
|
||||
const handleSave = () => {
|
||||
save();
|
||||
};
|
||||
@@ -66,7 +66,7 @@ const NTLMAuth = ({ item, collection, request, save, updateAuth }) => {
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
|
||||
@@ -60,6 +60,6 @@ const StyledWrapper = styled.div`
|
||||
color: ${(props) => props.theme.mode === 'dark' ? '#6366f1' : '#4f46e5'};
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useDispatch } from 'react-redux';
|
||||
import React, { useState } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { IconPlus, IconTrash, IconAdjustmentsHorizontal } from '@tabler/icons';
|
||||
import { cloneDeep } from "lodash";
|
||||
import { cloneDeep } from 'lodash';
|
||||
import SingleLineEditor from 'components/SingleLineEditor/index';
|
||||
import MultiLineEditor from 'components/MultiLineEditor/index';
|
||||
import StyledWrapper from "./StyledWrapper";
|
||||
import Table from "components/Table/index";
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Table from 'components/Table/index';
|
||||
|
||||
const AdditionalParams = ({ item = {}, request, updateAuth, collection, handleSave }) => {
|
||||
const AdditionalParams = ({ item = {}, request, updateAuth, collection, handleSave }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
@@ -34,13 +34,13 @@ const AdditionalParams = ({ item = {}, request, updateAuth, collection, handleS
|
||||
|
||||
const updateAdditionalParameters = ({ updatedAdditionalParameters }) => {
|
||||
const filteredParams = cloneDeep(updatedAdditionalParameters);
|
||||
|
||||
Object.keys(filteredParams).forEach(paramType => {
|
||||
|
||||
Object.keys(filteredParams).forEach((paramType) => {
|
||||
if (filteredParams[paramType]?.length) {
|
||||
filteredParams[paramType] = filteredParams[paramType].filter(param =>
|
||||
filteredParams[paramType] = filteredParams[paramType].filter((param) =>
|
||||
param.name.trim() || param.value.trim()
|
||||
);
|
||||
|
||||
|
||||
if (filteredParams[paramType].length === 0) {
|
||||
delete filteredParams[paramType];
|
||||
}
|
||||
@@ -61,15 +61,15 @@ const AdditionalParams = ({ item = {}, request, updateAuth, collection, handleS
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateAdditionalParam = ({ paramType, key, paramIndex, value }) => {
|
||||
const updatedAdditionalParameters = cloneDeep(additionalParameters);
|
||||
|
||||
|
||||
if (!updatedAdditionalParameters[paramType]) {
|
||||
updatedAdditionalParameters[paramType] = [];
|
||||
}
|
||||
|
||||
|
||||
if (!updatedAdditionalParameters[paramType][paramIndex]) {
|
||||
updatedAdditionalParameters[paramType][paramIndex] = {
|
||||
name: '',
|
||||
@@ -78,27 +78,27 @@ const AdditionalParams = ({ item = {}, request, updateAuth, collection, handleS
|
||||
enabled: true
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
updatedAdditionalParameters[paramType][paramIndex][key] = value;
|
||||
|
||||
|
||||
// Only filter when updating a parameter
|
||||
updateAdditionalParameters({ updatedAdditionalParameters });
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteAdditionalParam = ({ paramType, paramIndex }) => {
|
||||
const updatedAdditionalParameters = cloneDeep(additionalParameters);
|
||||
|
||||
|
||||
if (updatedAdditionalParameters[paramType]?.length) {
|
||||
updatedAdditionalParameters[paramType] = updatedAdditionalParameters[paramType].filter((_, index) => index !== paramIndex);
|
||||
|
||||
|
||||
// If the array is now empty, ensure we're not sending empty arrays
|
||||
if (updatedAdditionalParameters[paramType].length === 0) {
|
||||
delete updatedAdditionalParameters[paramType];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
updateAdditionalParameters({ updatedAdditionalParameters });
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddNewAdditionalParam = () => {
|
||||
// Prevent adding multiple empty rows
|
||||
@@ -108,11 +108,11 @@ const AdditionalParams = ({ item = {}, request, updateAuth, collection, handleS
|
||||
|
||||
const paramType = activeTab;
|
||||
const localAdditionalParameters = cloneDeep(additionalParameters);
|
||||
|
||||
|
||||
if (!localAdditionalParameters[paramType]) {
|
||||
localAdditionalParameters[paramType] = [];
|
||||
}
|
||||
|
||||
|
||||
localAdditionalParameters[paramType] = [
|
||||
...localAdditionalParameters[paramType],
|
||||
{
|
||||
@@ -122,7 +122,7 @@ const AdditionalParams = ({ item = {}, request, updateAuth, collection, handleS
|
||||
enabled: true
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
// Don't filter here to allow the empty row to display in UI
|
||||
// But don't permanently store it in state until it has values
|
||||
dispatch(
|
||||
@@ -132,11 +132,11 @@ const AdditionalParams = ({ item = {}, request, updateAuth, collection, handleS
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
...oAuth,
|
||||
additionalParameters: localAdditionalParameters,
|
||||
additionalParameters: localAdditionalParameters
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Add a class to the Add Parameter button if it's disabled
|
||||
const addButtonDisabled = hasEmptyRow();
|
||||
@@ -144,10 +144,10 @@ const AdditionalParams = ({ item = {}, request, updateAuth, collection, handleS
|
||||
// Define available tabs for each grant type
|
||||
const getAvailableTabs = (grantType) => {
|
||||
const tabConfig = {
|
||||
'authorization_code': ['authorization', 'token', 'refresh'],
|
||||
'implicit': ['authorization'],
|
||||
'password': ['token', 'refresh'],
|
||||
'client_credentials': ['token', 'refresh']
|
||||
authorization_code: ['authorization', 'token', 'refresh'],
|
||||
implicit: ['authorization'],
|
||||
password: ['token', 'refresh'],
|
||||
client_credentials: ['token', 'refresh']
|
||||
};
|
||||
return tabConfig[grantType] || ['token', 'refresh'];
|
||||
};
|
||||
@@ -155,9 +155,9 @@ const AdditionalParams = ({ item = {}, request, updateAuth, collection, handleS
|
||||
const availableTabs = getAvailableTabs(grantType);
|
||||
|
||||
const renderTab = (tabKey, tabLabel) => (
|
||||
<div
|
||||
<div
|
||||
key={tabKey}
|
||||
className={`tab ${activeTab === tabKey ? 'active' : ''}`}
|
||||
className={`tab ${activeTab === tabKey ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab(tabKey)}
|
||||
>
|
||||
{tabLabel}
|
||||
@@ -174,7 +174,7 @@ const AdditionalParams = ({ item = {}, request, updateAuth, collection, handleS
|
||||
Additional Parameters
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="tabs flex w-full gap-2 my-2">
|
||||
{availableTabs.includes('authorization') && renderTab('authorization', 'Authorization')}
|
||||
{availableTabs.includes('token') && renderTab('token', 'Token')}
|
||||
@@ -189,13 +189,13 @@ const AdditionalParams = ({ item = {}, request, updateAuth, collection, handleS
|
||||
]}
|
||||
>
|
||||
<tbody>
|
||||
{(additionalParameters?.[activeTab] || []).map((param, index) =>
|
||||
{(additionalParameters?.[activeTab] || []).map((param, index) => (
|
||||
<tr key={index}>
|
||||
<td className='flex relative'>
|
||||
<td className="flex relative">
|
||||
<SingleLineEditor
|
||||
value={param?.name || ''}
|
||||
theme={storedTheme}
|
||||
onChange={(value) => handleUpdateAdditionalParam({
|
||||
onChange={(value) => handleUpdateAdditionalParam({
|
||||
paramType: activeTab,
|
||||
key: 'name',
|
||||
paramIndex: index,
|
||||
@@ -209,7 +209,7 @@ const AdditionalParams = ({ item = {}, request, updateAuth, collection, handleS
|
||||
<MultiLineEditor
|
||||
value={param?.value || ''}
|
||||
theme={storedTheme}
|
||||
onChange={(value) => handleUpdateAdditionalParam({
|
||||
onChange={(value) => handleUpdateAdditionalParam({
|
||||
paramType: activeTab,
|
||||
key: 'value',
|
||||
paramIndex: index,
|
||||
@@ -221,16 +221,16 @@ const AdditionalParams = ({ item = {}, request, updateAuth, collection, handleS
|
||||
</td>
|
||||
<td>
|
||||
<div className="w-full additional-parameter-sends-in-selector">
|
||||
<select
|
||||
value={param?.sendIn || 'headers'}
|
||||
onChange={e => {
|
||||
handleUpdateAdditionalParam({
|
||||
<select
|
||||
value={param?.sendIn || 'headers'}
|
||||
onChange={(e) => {
|
||||
handleUpdateAdditionalParam({
|
||||
paramType: activeTab,
|
||||
key: 'sendIn',
|
||||
paramIndex: index,
|
||||
value: e.target.value
|
||||
})
|
||||
}}
|
||||
});
|
||||
}}
|
||||
className="mousetrap bg-transparent"
|
||||
>
|
||||
{sendInOptionsMap[grantType || 'authorization_code'][activeTab].map((optionValue) => (
|
||||
@@ -249,21 +249,21 @@ const AdditionalParams = ({ item = {}, request, updateAuth, collection, handleS
|
||||
tabIndex="-1"
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => {
|
||||
handleUpdateAdditionalParam({
|
||||
handleUpdateAdditionalParam({
|
||||
paramType: activeTab,
|
||||
key: 'enabled',
|
||||
paramIndex: index,
|
||||
value: e.target.checked
|
||||
})
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
tabIndex="-1"
|
||||
<button
|
||||
tabIndex="-1"
|
||||
onClick={() => {
|
||||
handleDeleteAdditionalParam({
|
||||
paramType: activeTab,
|
||||
paramIndex: index
|
||||
})
|
||||
});
|
||||
}}
|
||||
>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
@@ -271,37 +271,38 @@ const AdditionalParams = ({ item = {}, request, updateAuth, collection, handleS
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
)}
|
||||
</tbody>
|
||||
</Table>
|
||||
<div
|
||||
className={`add-additional-param-actions w-fit flex items-center mt-2 ${addButtonDisabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
|
||||
<div
|
||||
className={`add-additional-param-actions w-fit flex items-center mt-2 ${addButtonDisabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
|
||||
onClick={addButtonDisabled ? null : handleAddNewAdditionalParam}
|
||||
>
|
||||
<IconPlus size={16} strokeWidth={1.5} style={{ marginLeft: '2px' }} />
|
||||
<span className="ml-1 text-gray-500">Add Parameter</span>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default AdditionalParams;
|
||||
|
||||
const sendInOptionsMap = {
|
||||
'authorization_code': {
|
||||
'authorization': ['headers', 'queryparams'],
|
||||
'token': ['headers', 'queryparams', 'body'],
|
||||
'refresh': ['headers', 'queryparams', 'body']
|
||||
authorization_code: {
|
||||
authorization: ['headers', 'queryparams'],
|
||||
token: ['headers', 'queryparams', 'body'],
|
||||
refresh: ['headers', 'queryparams', 'body']
|
||||
},
|
||||
'password': {
|
||||
'token': ['headers', 'queryparams', 'body'],
|
||||
'refresh': ['headers', 'queryparams', 'body']
|
||||
password: {
|
||||
token: ['headers', 'queryparams', 'body'],
|
||||
refresh: ['headers', 'queryparams', 'body']
|
||||
},
|
||||
'client_credentials': {
|
||||
'token': ['headers', 'queryparams', 'body'],
|
||||
'refresh': ['headers', 'queryparams', 'body']
|
||||
client_credentials: {
|
||||
token: ['headers', 'queryparams', 'body'],
|
||||
refresh: ['headers', 'queryparams', 'body']
|
||||
},
|
||||
'implicit': {
|
||||
'authorization': ['headers', 'queryparams']
|
||||
implicit: {
|
||||
authorization: ['headers', 'queryparams']
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -88,7 +88,7 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
|
||||
autoRefreshToken,
|
||||
autoFetchToken,
|
||||
additionalParameters,
|
||||
[key]: value,
|
||||
[key]: value
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -137,7 +137,7 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
|
||||
const { key, label, isSecret } = input;
|
||||
const value = oAuth[key] || '';
|
||||
const { showWarning, warningMessage } = isSensitive(value);
|
||||
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-4 w-full" key={`input-${key}`}>
|
||||
<label className="block min-w-[140px]">{label}</label>
|
||||
@@ -157,7 +157,7 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="flex items-center gap-4 w-full" key={`input-credentials-placement`}>
|
||||
<div className="flex items-center gap-4 w-full" key="input-credentials-placement">
|
||||
<label className="block min-w-[140px]">Add Credentials to</label>
|
||||
<div className="inline-flex items-center cursor-pointer token-placement-selector">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<CredentialsPlacementIcon />} placement="bottom-end">
|
||||
@@ -199,7 +199,7 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
|
||||
Token
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 w-full" key={`input-token-name`}>
|
||||
<div className="flex items-center gap-4 w-full" key="input-token-name">
|
||||
<label className="block min-w-[140px]">Token ID</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
@@ -213,7 +213,7 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 w-full" key={`input-token-placement`}>
|
||||
<div className="flex items-center gap-4 w-full" key="input-token-placement">
|
||||
<label className="block min-w-[140px]">Add token to</label>
|
||||
<div className="inline-flex items-center cursor-pointer token-placement-selector">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<TokenPlacementIcon />} placement="bottom-end">
|
||||
@@ -239,34 +239,37 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
tokenPlacement === 'header' ?
|
||||
<div className="flex items-center gap-4 w-full" key={`input-token-prefix`}>
|
||||
<label className="block min-w-[140px]">Header Prefix</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={oAuth['tokenHeaderPrefix'] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange('tokenHeaderPrefix', val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
<div className="flex items-center gap-4 w-full" key={`input-token-query-param-key`}>
|
||||
<label className="block font-medium min-w-[140px]">Query Param Key</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={oAuth['tokenQueryKey'] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange('tokenQueryKey', val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
tokenPlacement === 'header'
|
||||
? (
|
||||
<div className="flex items-center gap-4 w-full" key="input-token-prefix">
|
||||
<label className="block min-w-[140px]">Header Prefix</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={oAuth['tokenHeaderPrefix'] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange('tokenHeaderPrefix', val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className="flex items-center gap-4 w-full" key="input-token-query-param-key">
|
||||
<label className="block font-medium min-w-[140px]">Query Param Key</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={oAuth['tokenQueryKey'] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange('tokenQueryKey', val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className="flex items-center gap-2.5 mt-4 mb-2">
|
||||
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
|
||||
@@ -284,7 +287,7 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
|
||||
value={get(request, 'auth.oauth2.refreshTokenUrl', '')}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange("refreshTokenUrl", val)}
|
||||
onChange={(val) => handleChange('refreshTokenUrl', val)}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
@@ -348,4 +351,4 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
|
||||
);
|
||||
};
|
||||
|
||||
export default OAuth2AuthorizationCode;
|
||||
export default OAuth2AuthorizationCode;
|
||||
|
||||
@@ -21,16 +21,16 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const oAuth = get(request, 'auth.oauth2', {});
|
||||
|
||||
const {
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
credentialsPlacement,
|
||||
credentialsId,
|
||||
tokenPlacement,
|
||||
tokenHeaderPrefix,
|
||||
tokenQueryKey,
|
||||
const {
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
credentialsPlacement,
|
||||
credentialsId,
|
||||
tokenPlacement,
|
||||
tokenHeaderPrefix,
|
||||
tokenQueryKey,
|
||||
refreshTokenUrl,
|
||||
autoRefreshToken,
|
||||
autoFetchToken,
|
||||
@@ -40,13 +40,12 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu
|
||||
const refreshTokenUrlAvailable = refreshTokenUrl?.trim() !== '';
|
||||
const isAutoRefreshDisabled = !refreshTokenUrlAvailable;
|
||||
|
||||
|
||||
const handleSave = () => { save(); };
|
||||
|
||||
const TokenPlacementIcon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-end token-placement-label select-none">
|
||||
{tokenPlacement == 'url' ? 'URL' : 'Headers'}
|
||||
{tokenPlacement == 'url' ? 'URL' : 'Headers'}
|
||||
<IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
@@ -55,7 +54,7 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu
|
||||
const CredentialsPlacementIcon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-end token-placement-label select-none">
|
||||
{credentialsPlacement == 'body' ? 'Request Body' : 'Basic Auth Header'}
|
||||
{credentialsPlacement == 'body' ? 'Request Body' : 'Basic Auth Header'}
|
||||
<IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
@@ -103,7 +102,7 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu
|
||||
const { key, label, isSecret } = input;
|
||||
const value = oAuth[key] || '';
|
||||
const { showWarning, warningMessage } = isSensitive(value);
|
||||
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-4 w-full" key={`input-${key}`}>
|
||||
<label className="block min-w-[140px]">{label}</label>
|
||||
@@ -123,7 +122,7 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="flex items-center gap-4 w-full" key={`input-credentials-placement`}>
|
||||
<div className="flex items-center gap-4 w-full" key="input-credentials-placement">
|
||||
<label className="block min-w-[140px]">Add Credentials to</label>
|
||||
<div className="inline-flex items-center cursor-pointer token-placement-selector">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<CredentialsPlacementIcon />} placement="bottom-end">
|
||||
@@ -156,7 +155,7 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu
|
||||
Token
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 w-full" key={`input-token-name`}>
|
||||
<div className="flex items-center gap-4 w-full" key="input-token-name">
|
||||
<label className="block min-w-[140px]">Token ID</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
@@ -170,7 +169,7 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 w-full" key={`input-token-placement`}>
|
||||
<div className="flex items-center gap-4 w-full" key="input-token-placement">
|
||||
<label className="block min-w-[140px]">Add token to</label>
|
||||
<div className="inline-flex items-center cursor-pointer token-placement-selector w-fit">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<TokenPlacementIcon />} placement="bottom-end">
|
||||
@@ -196,34 +195,37 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
tokenPlacement === 'header' ?
|
||||
<div className="flex items-center gap-4 w-full" key={`input-token-prefix`}>
|
||||
<label className="block min-w-[140px]">Header Prefix</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={oAuth['tokenHeaderPrefix'] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange('tokenHeaderPrefix', val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
<div className="flex items-center gap-4 w-full" key={`input-token-query-param-key`}>
|
||||
<label className="block font-medium min-w-[140px]">Query Param Key</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={oAuth['tokenQueryKey'] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange('tokenQueryKey', val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
tokenPlacement === 'header'
|
||||
? (
|
||||
<div className="flex items-center gap-4 w-full" key="input-token-prefix">
|
||||
<label className="block min-w-[140px]">Header Prefix</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={oAuth['tokenHeaderPrefix'] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange('tokenHeaderPrefix', val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className="flex items-center gap-4 w-full" key="input-token-query-param-key">
|
||||
<label className="block font-medium min-w-[140px]">Query Param Key</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={oAuth['tokenQueryKey'] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange('tokenQueryKey', val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className="flex items-center gap-2.5 mt-4 mb-2">
|
||||
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
|
||||
@@ -241,7 +243,7 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu
|
||||
value={get(request, 'auth.oauth2.refreshTokenUrl', '')}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange("refreshTokenUrl", val)}
|
||||
onChange={(val) => handleChange('refreshTokenUrl', val)}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
|
||||
@@ -47,28 +47,28 @@ const GrantTypeSelector = ({ item = {}, request, updateAuth, collection }) => {
|
||||
useEffect(() => {
|
||||
// initialize redux state with a default oauth2 grant type
|
||||
// authorization_code - default option
|
||||
!oAuth?.grantType &&
|
||||
dispatch(
|
||||
updateAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
grantType: 'authorization_code',
|
||||
accessTokenUrl: '',
|
||||
username: '',
|
||||
password: '',
|
||||
clientId: '',
|
||||
clientSecret: '',
|
||||
scope: '',
|
||||
credentialsPlacement: 'body',
|
||||
credentialsId: 'credentials',
|
||||
tokenPlacement: 'header',
|
||||
tokenHeaderPrefix: 'Bearer',
|
||||
tokenQueryKey: 'access_token',
|
||||
}
|
||||
})
|
||||
);
|
||||
!oAuth?.grantType
|
||||
&& dispatch(
|
||||
updateAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
grantType: 'authorization_code',
|
||||
accessTokenUrl: '',
|
||||
username: '',
|
||||
password: '',
|
||||
clientId: '',
|
||||
clientSecret: '',
|
||||
scope: '',
|
||||
credentialsPlacement: 'body',
|
||||
credentialsId: 'credentials',
|
||||
tokenPlacement: 'header',
|
||||
tokenHeaderPrefix: 'Bearer',
|
||||
tokenQueryKey: 'access_token'
|
||||
}
|
||||
})
|
||||
);
|
||||
}, [oAuth]);
|
||||
|
||||
return (
|
||||
@@ -124,4 +124,4 @@ const GrantTypeSelector = ({ item = {}, request, updateAuth, collection }) => {
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
export default GrantTypeSelector;
|
||||
export default GrantTypeSelector;
|
||||
|
||||
@@ -58,4 +58,4 @@ const Wrapper = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
export default Wrapper;
|
||||
|
||||
@@ -67,7 +67,7 @@ const OAuth2Implicit = ({ save, item = {}, request, handleRun, updateAuth, colle
|
||||
tokenHeaderPrefix,
|
||||
tokenQueryKey,
|
||||
autoFetchToken,
|
||||
[key]: value,
|
||||
[key]: value
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -118,7 +118,7 @@ const OAuth2Implicit = ({ save, item = {}, request, handleRun, updateAuth, colle
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 w-full" key={`input-token-name`}>
|
||||
<div className="flex items-center gap-4 w-full" key="input-token-name">
|
||||
<label className="block min-w-[140px]">Token ID</label>
|
||||
<div className="oauth2-input-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
@@ -133,7 +133,7 @@ const OAuth2Implicit = ({ save, item = {}, request, handleRun, updateAuth, colle
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 w-full" key={`input-token-placement`}>
|
||||
<div className="flex items-center gap-4 w-full" key="input-token-placement">
|
||||
<label className="block min-w-[140px]">Add Token to</label>
|
||||
<div className="inline-flex items-center cursor-pointer token-placement-selector">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<TokenPlacementIcon />} placement="bottom-end">
|
||||
@@ -160,7 +160,7 @@ const OAuth2Implicit = ({ save, item = {}, request, handleRun, updateAuth, colle
|
||||
</div>
|
||||
|
||||
{tokenPlacement == 'header' ? (
|
||||
<div className="flex items-center gap-4 w-full" key={`input-token-header-prefix`}>
|
||||
<div className="flex items-center gap-4 w-full" key="input-token-header-prefix">
|
||||
<label className="block min-w-[140px]">Header Prefix</label>
|
||||
<div className="oauth2-input-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
@@ -175,7 +175,7 @@ const OAuth2Implicit = ({ save, item = {}, request, handleRun, updateAuth, colle
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-4 w-full" key={`input-token-query-key`}>
|
||||
<div className="flex items-center gap-4 w-full" key="input-token-query-key">
|
||||
<label className="block min-w-[140px]">URL Query Key</label>
|
||||
<div className="oauth2-input-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
@@ -230,4 +230,4 @@ const OAuth2Implicit = ({ save, item = {}, request, handleRun, updateAuth, colle
|
||||
);
|
||||
};
|
||||
|
||||
export default OAuth2Implicit;
|
||||
export default OAuth2Implicit;
|
||||
|
||||
@@ -21,4 +21,4 @@ const inputsConfig = [
|
||||
}
|
||||
];
|
||||
|
||||
export { inputsConfig };
|
||||
export { inputsConfig };
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import toast from 'react-hot-toast';
|
||||
import { cloneDeep, find } from 'lodash';
|
||||
import { IconLoader2 } from '@tabler/icons';
|
||||
import { interpolate } from '@usebruno/common';
|
||||
import { fetchOauth2Credentials, clearOauth2Cache, refreshOauth2Credentials } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { getAllVariables } from "utils/collections/index";
|
||||
import { getAllVariables } from 'utils/collections/index';
|
||||
|
||||
const Oauth2ActionButtons = ({ item, request, collection, url: accessTokenUrl, credentialsId }) => {
|
||||
const { uid: collectionUid } = collection;
|
||||
@@ -19,7 +19,7 @@ const Oauth2ActionButtons = ({ item, request, collection, url: accessTokenUrl, c
|
||||
return interpolate(accessTokenUrl, variables);
|
||||
}, [collection, item, accessTokenUrl]);
|
||||
|
||||
const credentialsData = find(collection?.oauth2Credentials, creds => creds?.url == interpolatedAccessTokenUrl && creds?.collectionUid == collectionUid && creds?.credentialsId == credentialsId);
|
||||
const credentialsData = find(collection?.oauth2Credentials, (creds) => creds?.url == interpolatedAccessTokenUrl && creds?.collectionUid == collectionUid && creds?.credentialsId == credentialsId);
|
||||
const creds = credentialsData?.credentials || {};
|
||||
|
||||
const handleFetchOauth2Credentials = async () => {
|
||||
@@ -28,15 +28,15 @@ const Oauth2ActionButtons = ({ item, request, collection, url: accessTokenUrl, c
|
||||
requestCopy.headers = {};
|
||||
toggleFetchingToken(true);
|
||||
try {
|
||||
const result = await dispatch(fetchOauth2Credentials({
|
||||
itemUid: item.uid,
|
||||
request: requestCopy,
|
||||
const result = await dispatch(fetchOauth2Credentials({
|
||||
itemUid: item.uid,
|
||||
request: requestCopy,
|
||||
collection,
|
||||
forceGetToken: true
|
||||
}));
|
||||
|
||||
|
||||
toggleFetchingToken(false);
|
||||
|
||||
|
||||
// Check if the result contains error or if access_token is missing
|
||||
if (!result || !result.access_token) {
|
||||
const errorMessage = result?.error || 'No access token received from authorization server';
|
||||
@@ -44,16 +44,15 @@ const Oauth2ActionButtons = ({ item, request, collection, url: accessTokenUrl, c
|
||||
toast.error(errorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
toast.success('Token fetched successfully!');
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.error('could not fetch the token!');
|
||||
console.error(error);
|
||||
toggleFetchingToken(false);
|
||||
toast.error(error?.message || 'An error occurred while fetching token!');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefreshAccessToken = async () => {
|
||||
let requestCopy = cloneDeep(request);
|
||||
@@ -61,15 +60,15 @@ const Oauth2ActionButtons = ({ item, request, collection, url: accessTokenUrl, c
|
||||
requestCopy.headers = {};
|
||||
toggleRefreshingToken(true);
|
||||
try {
|
||||
const result = await dispatch(refreshOauth2Credentials({
|
||||
itemUid: item.uid,
|
||||
request: requestCopy,
|
||||
const result = await dispatch(refreshOauth2Credentials({
|
||||
itemUid: item.uid,
|
||||
request: requestCopy,
|
||||
collection,
|
||||
forceGetToken: true
|
||||
}));
|
||||
|
||||
|
||||
toggleRefreshingToken(false);
|
||||
|
||||
|
||||
// Check if the result contains error or if access_token is missing
|
||||
if (!result || !result.access_token) {
|
||||
const errorMessage = result?.error || 'No access token received from authorization server';
|
||||
@@ -77,10 +76,9 @@ const Oauth2ActionButtons = ({ item, request, collection, url: accessTokenUrl, c
|
||||
toast.error(errorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
toast.success('Token refreshed successfully!');
|
||||
}
|
||||
catch(error) {
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toggleRefreshingToken(false);
|
||||
toast.error(error?.message || 'An error occurred while refreshing token!');
|
||||
@@ -89,37 +87,39 @@ const Oauth2ActionButtons = ({ item, request, collection, url: accessTokenUrl, c
|
||||
|
||||
const handleClearCache = (e) => {
|
||||
dispatch(clearOauth2Cache({ collectionUid: collection?.uid, url: interpolatedAccessTokenUrl, credentialsId }))
|
||||
.then(() => {
|
||||
toast.success('Cleared cache successfully');
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
});
|
||||
.then(() => {
|
||||
toast.success('Cleared cache successfully');
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-row gap-4 mt-4">
|
||||
<button
|
||||
onClick={handleFetchOauth2Credentials}
|
||||
className={`submit btn btn-sm btn-secondary w-fit flex flex-row`}
|
||||
<button
|
||||
onClick={handleFetchOauth2Credentials}
|
||||
className="submit btn btn-sm btn-secondary w-fit flex flex-row"
|
||||
disabled={fetchingToken || refreshingToken}
|
||||
>
|
||||
Get Access Token{fetchingToken? <IconLoader2 className="animate-spin ml-2" size={18} strokeWidth={1.5} /> : ""}
|
||||
Get Access Token{fetchingToken ? <IconLoader2 className="animate-spin ml-2" size={18} strokeWidth={1.5} /> : ''}
|
||||
</button>
|
||||
{creds?.refresh_token ?
|
||||
<button
|
||||
onClick={handleRefreshAccessToken}
|
||||
className={`submit btn btn-sm btn-secondary w-fit flex flex-row`}
|
||||
disabled={fetchingToken || refreshingToken}
|
||||
>
|
||||
Refresh Token{refreshingToken? <IconLoader2 className="animate-spin ml-2" size={18} strokeWidth={1.5} /> : ""}
|
||||
</button>
|
||||
: null}
|
||||
{creds?.refresh_token
|
||||
? (
|
||||
<button
|
||||
onClick={handleRefreshAccessToken}
|
||||
className="submit btn btn-sm btn-secondary w-fit flex flex-row"
|
||||
disabled={fetchingToken || refreshingToken}
|
||||
>
|
||||
Refresh Token{refreshingToken ? <IconLoader2 className="animate-spin ml-2" size={18} strokeWidth={1.5} /> : ''}
|
||||
</button>
|
||||
)
|
||||
: null}
|
||||
<button onClick={handleClearCache} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Clear Cache
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default Oauth2ActionButtons;
|
||||
export default Oauth2ActionButtons;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { find } from "lodash";
|
||||
import StyledWrapper from "./StyledWrapper";
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { find } from 'lodash';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { IconChevronDown, IconChevronRight, IconCopy, IconCheck } from '@tabler/icons';
|
||||
import { getAllVariables } from 'utils/collections/index';
|
||||
import { interpolate } from '@usebruno/common';
|
||||
@@ -39,10 +39,9 @@ const TokenSection = ({ title, token }) => {
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
<div className="flex items-center space-x-2 w-full">
|
||||
{isExpanded ?
|
||||
<IconChevronDown size={18} className="text-gray-500" /> :
|
||||
<IconChevronRight size={18} className="text-gray-500" />
|
||||
}
|
||||
{isExpanded
|
||||
? <IconChevronDown size={18} className="text-gray-500" />
|
||||
: <IconChevronRight size={18} className="text-gray-500" />}
|
||||
<div className="flex flex-row justify-between w-full">
|
||||
<h3 className="font-medium">{title}</h3>
|
||||
{decodedToken?.exp && <ExpiryTimer expiresIn={decodedToken?.exp} />}
|
||||
@@ -58,10 +57,9 @@ const TokenSection = ({ title, token }) => {
|
||||
className="p-1 bg-indigo-100 dark:hover:bg-indigo-200 rounded"
|
||||
title="Copy token"
|
||||
>
|
||||
{copied ?
|
||||
<IconCheck size={16} className="text-green-700" /> :
|
||||
<IconCopy size={16} className="text-gray-500" />
|
||||
}
|
||||
{copied
|
||||
? <IconCheck size={16} className="text-green-700" />
|
||||
: <IconCopy size={16} className="text-gray-500" />}
|
||||
</button>
|
||||
</div>
|
||||
<div className="font-mono text-xs bg-gray-50 dark:bg-gray-800 p-2 rounded break-all">
|
||||
@@ -115,16 +113,15 @@ const ExpiryTimer = ({ expiresIn }) => {
|
||||
return (
|
||||
<div
|
||||
className={`text-xs px-2 py-1 rounded-full min-w-[120px] text-center ${timeLeft <= 30
|
||||
? "bg-red-50 dark:bg-red-900/30 text-red-600 dark:text-red-400"
|
||||
: "bg-blue-50 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400"
|
||||
}`}
|
||||
? 'bg-red-50 dark:bg-red-900/30 text-red-600 dark:text-red-400'
|
||||
: 'bg-blue-50 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400'
|
||||
}`}
|
||||
>
|
||||
{timeLeft > 0 ? `Expires in ${formatExpiryTime(timeLeft)}` : `Expired`}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const Oauth2TokenViewer = ({ collection, item, url, credentialsId, handleRun }) => {
|
||||
const { uid: collectionUid } = collection;
|
||||
|
||||
@@ -133,7 +130,7 @@ const Oauth2TokenViewer = ({ collection, item, url, credentialsId, handleRun })
|
||||
return interpolate(url, variables);
|
||||
}, [collection, item, url]);
|
||||
|
||||
const credentialsData = find(collection?.oauth2Credentials, creds => creds?.url == interpolatedUrl && creds?.collectionUid == collectionUid && creds?.credentialsId == credentialsId);
|
||||
const credentialsData = find(collection?.oauth2Credentials, (creds) => creds?.url == interpolatedUrl && creds?.collectionUid == collectionUid && creds?.credentialsId == credentialsId);
|
||||
const creds = credentialsData?.credentials || {};
|
||||
|
||||
return (
|
||||
@@ -146,22 +143,28 @@ const Oauth2TokenViewer = ({ collection, item, url, credentialsId, handleRun })
|
||||
<TokenSection title="Access Token" token={creds.access_token} />
|
||||
<TokenSection title="Refresh Token" token={creds.refresh_token} />
|
||||
<TokenSection title="ID Token" token={creds.id_token} />
|
||||
{(creds.token_type || creds.scope) ? <div className="mt-3 p-2 bg-gray-50 dark:bg-gray-800 rounded-lg text-xs">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{creds.token_type ? <div className="flex items-center space-x-1">
|
||||
<span className="font-medium">Token Type:</span>
|
||||
<span className="text-gray-600 dark:text-gray-300">{creds.token_type}</span>
|
||||
</div> : null}
|
||||
{creds?.scope ? <div className="flex items-center space-x-1 min-w-0">
|
||||
<span className="font-medium flex-shrink-0">Scope:</span>
|
||||
<span className="text-gray-600 dark:text-gray-300 truncate" title={creds.scope}>
|
||||
{creds.scope}
|
||||
</span>
|
||||
</div> : null}
|
||||
{(creds.token_type || creds.scope) ? (
|
||||
<div className="mt-3 p-2 bg-gray-50 dark:bg-gray-800 rounded-lg text-xs">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{creds.token_type ? (
|
||||
<div className="flex items-center space-x-1">
|
||||
<span className="font-medium">Token Type:</span>
|
||||
<span className="text-gray-600 dark:text-gray-300">{creds.token_type}</span>
|
||||
</div>
|
||||
) : null}
|
||||
{creds?.scope ? (
|
||||
<div className="flex items-center space-x-1 min-w-0">
|
||||
<span className="font-medium flex-shrink-0">Scope:</span>
|
||||
<span className="text-gray-600 dark:text-gray-300 truncate" title={creds.scope}>
|
||||
{creds.scope}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div> : null}
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
)
|
||||
) : (
|
||||
<div className="text-gray-500 dark:text-gray-400">No token found</div>
|
||||
)}
|
||||
@@ -169,4 +172,4 @@ const Oauth2TokenViewer = ({ collection, item, url, credentialsId, handleRun })
|
||||
);
|
||||
};
|
||||
|
||||
export default Oauth2TokenViewer;
|
||||
export default Oauth2TokenViewer;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user