Merge branch 'main' of usebruno/bruno into workspaces

This commit is contained in:
Bijin A B
2025-12-04 04:17:45 +05:30
517 changed files with 8831 additions and 7011 deletions

View File

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

5
.gitignore vendored
View File

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

View File

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

View File

@@ -1,6 +1,6 @@
// eslint.config.js
const { defineConfig } = require("eslint/config");
const globals = require("globals");
const { defineConfig } = require('eslint/config');
const globals = require('globals');
const { fixupPluginRules } = require('@eslint/compat');
const eslintPluginDiff = require('eslint-plugin-diff');
@@ -11,6 +11,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
View File

@@ -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",

View File

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

View File

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

View File

@@ -21,6 +21,8 @@
"@usebruno/common": "0.1.0",
"@usebruno/graphql-docs": "0.1.0",
"@usebruno/schema": "0.7.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0",
"classnames": "^2.3.1",
"codemirror": "5.65.2",
"codemirror-graphql": "2.1.1",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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>
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -160,8 +160,7 @@ const AssertionRow = ({
},
assertion,
'value'
)
}
)}
/>
</td>
<td>
@@ -179,9 +178,8 @@ const AssertionRow = ({
},
assertion,
'value'
)
}
}
);
}}
onRun={handleRun}
collection={collection}
item={item}

View File

@@ -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>

View File

@@ -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 (

View File

@@ -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();
};

View File

@@ -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();
};

View File

@@ -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();
};

View File

@@ -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();
};

View File

@@ -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">

View File

@@ -60,6 +60,6 @@ const StyledWrapper = styled.div`
color: ${(props) => props.theme.mode === 'dark' ? '#6366f1' : '#4f46e5'};
}
}
`
`;
export default StyledWrapper;
export default StyledWrapper;

View File

@@ -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']
}
}
};

View File

@@ -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;

View File

@@ -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}
/>

View File

@@ -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;

View File

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

View File

@@ -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;

View File

@@ -21,4 +21,4 @@ const inputsConfig = [
}
];
export { inputsConfig };
export { inputsConfig };

View File

@@ -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;

View File

@@ -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