From b9da31d24e35d9e8107f2d24e88efcb0cf6b14dc Mon Sep 17 00:00:00 2001 From: naman-bruno Date: Mon, 14 Jul 2025 19:05:57 +0530 Subject: [PATCH] added: status bar & console (#4922) * added: status bar & console --- package-lock.json | 206 ++++++- packages/bruno-app/package.json | 1 + .../Console/DebugTab/StyledWrapper.js | 163 ++++++ .../Devtools/Console/DebugTab/index.js | 106 ++++ .../ErrorDetailsPanel/StyledWrapper.js | 228 ++++++++ .../Console/ErrorDetailsPanel/index.js | 268 +++++++++ .../Console/NetworkTab/StyledWrapper.js | 293 ++++++++++ .../Devtools/Console/NetworkTab/index.js | 302 ++++++++++ .../RequestDetailsPanel/StyledWrapper.js | 347 ++++++++++++ .../Console/RequestDetailsPanel/index.js | 242 ++++++++ .../Devtools/Console/StyledWrapper.js | 521 +++++++++++++++++ .../src/components/Devtools/Console/index.js | 531 ++++++++++++++++++ .../src/components/Devtools/index.js | 89 +++ .../src/components/ErrorCapture/index.js | 147 +++++ .../src/components/Notifications/index.js | 5 +- .../components/Sidebar/Collections/index.js | 2 +- .../bruno-app/src/components/Sidebar/index.js | 113 +--- .../src/components/StatusBar/StyledWrapper.js | 85 +++ .../src/components/StatusBar/index.js | 124 ++++ .../src/pages/Bruno/StyledWrapper.js | 3 +- packages/bruno-app/src/pages/Bruno/index.js | 58 +- .../src/providers/App/useIpcEvents.js | 10 +- .../src/providers/ReduxStore/index.js | 4 +- .../src/providers/ReduxStore/slices/logs.js | 143 +++++ packages/bruno-app/src/themes/dark.js | 27 + packages/bruno-app/src/themes/light.js | 27 + 26 files changed, 3907 insertions(+), 138 deletions(-) create mode 100644 packages/bruno-app/src/components/Devtools/Console/DebugTab/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/Devtools/Console/DebugTab/index.js create mode 100644 packages/bruno-app/src/components/Devtools/Console/ErrorDetailsPanel/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/Devtools/Console/ErrorDetailsPanel/index.js create mode 100644 packages/bruno-app/src/components/Devtools/Console/NetworkTab/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/Devtools/Console/NetworkTab/index.js create mode 100644 packages/bruno-app/src/components/Devtools/Console/RequestDetailsPanel/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/Devtools/Console/RequestDetailsPanel/index.js create mode 100644 packages/bruno-app/src/components/Devtools/Console/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/Devtools/Console/index.js create mode 100644 packages/bruno-app/src/components/Devtools/index.js create mode 100644 packages/bruno-app/src/components/ErrorCapture/index.js create mode 100644 packages/bruno-app/src/components/StatusBar/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/StatusBar/index.js create mode 100644 packages/bruno-app/src/providers/ReduxStore/slices/logs.js diff --git a/package-lock.json b/package-lock.json index 86631f441..e34e6f213 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9555,6 +9555,12 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "license": "MIT" + }, "node_modules/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -10019,6 +10025,12 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/base16": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base16/-/base16-1.0.0.tgz", + "integrity": "sha512-pNdYkNPiJUnEhnfXV56+sQy8+AaPcG3POZAUnwr4EeqCUZFz4u2PePbo3e5Gj4ziYPCWGUZT9RHisvJKnwFuBQ==", + "license": "MIT" + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -14963,6 +14975,36 @@ "bser": "2.1.1" } }, + "node_modules/fbemitter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fbemitter/-/fbemitter-3.0.0.tgz", + "integrity": "sha512-KWKaceCwKQU0+HPoop6gn4eOHk50bBv/VxjJtGMfwmJt3D29JpN4H4eisCtIPA+a8GVBam+ldMMpMjJUvpDyHw==", + "license": "BSD-3-Clause", + "dependencies": { + "fbjs": "^3.0.0" + } + }, + "node_modules/fbjs": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.5.tgz", + "integrity": "sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==", + "license": "MIT", + "dependencies": { + "cross-fetch": "^3.1.5", + "fbjs-css-vars": "^1.0.0", + "loose-envify": "^1.0.0", + "object-assign": "^4.1.0", + "promise": "^7.1.1", + "setimmediate": "^1.0.5", + "ua-parser-js": "^1.0.35" + } + }, + "node_modules/fbjs-css-vars": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz", + "integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==", + "license": "MIT" + }, "node_modules/fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", @@ -15259,6 +15301,19 @@ "node": ">=0.4.0" } }, + "node_modules/flux": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/flux/-/flux-4.0.4.tgz", + "integrity": "sha512-NCj3XlayA2UsapRpM7va6wU1+9rE5FIL7qoMcmxWHRzbp0yujihMBm9BBHZ1MDIk5h5o2Bl6eGiCe8rYELAmYw==", + "license": "BSD-3-Clause", + "dependencies": { + "fbemitter": "^3.0.0", + "fbjs": "^3.0.1" + }, + "peerDependencies": { + "react": "^15.0.2 || ^16.0.0 || ^17.0.0" + } + }, "node_modules/follow-redirects": { "version": "1.15.9", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", @@ -19388,6 +19443,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.curry": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.curry/-/lodash.curry-4.1.1.tgz", + "integrity": "sha512-/u14pXGviLaweY5JI0IUzgzF2J6Ne8INyzAZjImcryjgkZ+ebruBxy2/JaOOkTqScddcYtakjhSaeemV8lR0tA==", + "license": "MIT" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -19395,6 +19456,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.flow": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/lodash.flow/-/lodash.flow-3.5.0.tgz", + "integrity": "sha512-ff3BX/tSioo+XojX4MOsOMhJw0nZoUEF011LX8g8d3gvjVbxd89cCio4BCXronjxcTUIJUoqKEUA+n4CqvvRPw==", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -22762,6 +22829,15 @@ "node": ">=0.4.0" } }, + "node_modules/promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "license": "MIT", + "dependencies": { + "asap": "~2.0.3" + } + }, "node_modules/promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -22906,6 +22982,12 @@ "node": ">=6" } }, + "node_modules/pure-color": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pure-color/-/pure-color-1.3.0.tgz", + "integrity": "sha512-QFADYnsVoBMw1srW7OVKEYjG+MbIa49s54w1MA1EDY6r2r/sTcKKYqRX1f4GYvnXP7eN/Pe9HFcX+hwzmrXRHA==", + "license": "MIT" + }, "node_modules/pure-rand": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", @@ -23118,6 +23200,18 @@ "node": ">=0.10.0" } }, + "node_modules/react-base16-styling": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/react-base16-styling/-/react-base16-styling-0.6.0.tgz", + "integrity": "sha512-yvh/7CArceR/jNATXOKDlvTnPKPmGZz7zsenQ3jUwLzHkNUR0CvY3yGYJbWJ/nnxsL8Sgmt5cO3/SILVuPO6TQ==", + "license": "MIT", + "dependencies": { + "base16": "^1.0.0", + "lodash.curry": "^4.0.1", + "lodash.flow": "^3.3.0", + "pure-color": "^1.2.0" + } + }, "node_modules/react-copy-to-clipboard": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz", @@ -23241,6 +23335,28 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/react-json-view": { + "version": "1.21.3", + "resolved": "https://registry.npmjs.org/react-json-view/-/react-json-view-1.21.3.tgz", + "integrity": "sha512-13p8IREj9/x/Ye4WI/JpjhoIwuzEgUAtgJZNBJckfzJt1qyh24BdTm6UQNGnyTq9dapQdrqvquZTo3dz1X6Cjw==", + "license": "MIT", + "dependencies": { + "flux": "^4.0.1", + "react-base16-styling": "^0.6.0", + "react-lifecycles-compat": "^3.0.4", + "react-textarea-autosize": "^8.3.2" + }, + "peerDependencies": { + "react": "^17.0.0 || ^16.3.0 || ^15.5.4", + "react-dom": "^17.0.0 || ^16.3.0 || ^15.5.4" + } + }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", + "license": "MIT" + }, "node_modules/react-pdf": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-9.1.1.tgz", @@ -23411,6 +23527,23 @@ } } }, + "node_modules/react-textarea-autosize": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.9.tgz", + "integrity": "sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.13", + "use-composed-ref": "^1.3.0", + "use-latest": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-tooltip": { "version": "5.28.0", "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.28.0.tgz", @@ -25118,7 +25251,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "dev": true, "license": "MIT" }, "node_modules/setprototypeof": { @@ -26992,6 +27124,32 @@ "node": ">=4.2.0" } }, + "node_modules/ua-parser-js": { + "version": "1.0.40", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.40.tgz", + "integrity": "sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, "node_modules/uc.micro": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", @@ -27214,6 +27372,51 @@ } } }, + "node_modules/use-composed-ref": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.4.0.tgz", + "integrity": "sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz", + "integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-latest": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.3.0.tgz", + "integrity": "sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==", + "license": "MIT", + "dependencies": { + "use-isomorphic-layout-effect": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-sidecar": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", @@ -28023,6 +28226,7 @@ "react-hot-toast": "^2.4.0", "react-i18next": "^15.0.1", "react-inspector": "^6.0.2", + "react-json-view": "^1.21.3", "react-pdf": "9.1.1", "react-player": "^2.16.0", "react-redux": "^7.2.9", diff --git a/packages/bruno-app/package.json b/packages/bruno-app/package.json index ab30dfef0..5e0f6a8ea 100644 --- a/packages/bruno-app/package.json +++ b/packages/bruno-app/package.json @@ -67,6 +67,7 @@ "react-hot-toast": "^2.4.0", "react-i18next": "^15.0.1", "react-inspector": "^6.0.2", + "react-json-view": "^1.21.3", "react-pdf": "9.1.1", "react-player": "^2.16.0", "react-redux": "^7.2.9", diff --git a/packages/bruno-app/src/components/Devtools/Console/DebugTab/StyledWrapper.js b/packages/bruno-app/src/components/Devtools/Console/DebugTab/StyledWrapper.js new file mode 100644 index 000000000..a9c71b8c2 --- /dev/null +++ b/packages/bruno-app/src/components/Devtools/Console/DebugTab/StyledWrapper.js @@ -0,0 +1,163 @@ +import styled from 'styled-components'; + +const StyledWrapper = styled.div` + display: flex; + flex-direction: column; + height: 100%; + background: ${(props) => props.theme.console.contentBg}; + overflow: hidden; + + .debug-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 16px; + background: ${(props) => props.theme.console.headerBg}; + border-bottom: 1px solid ${(props) => props.theme.console.border}; + flex-shrink: 0; + } + + .debug-title { + display: flex; + align-items: center; + gap: 8px; + color: ${(props) => props.theme.console.titleColor}; + font-size: 13px; + font-weight: 500; + + .error-count { + color: ${(props) => props.theme.console.countColor}; + font-size: 12px; + font-weight: 400; + } + } + + .debug-controls { + display: flex; + align-items: center; + gap: 8px; + } + + .control-button { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + background: transparent; + border: 1px solid ${(props) => props.theme.console.border}; + border-radius: 4px; + color: ${(props) => props.theme.console.buttonColor}; + cursor: pointer; + transition: all 0.2s ease; + } + + .debug-content { + flex: 1; + overflow: hidden; + display: flex; + flex-direction: column; + min-height: 0; + } + + .debug-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + color: ${(props) => props.theme.console.emptyColor}; + text-align: center; + gap: 8px; + padding: 40px 20px; + + p { + margin: 0; + font-size: 14px; + font-weight: 500; + } + + span { + font-size: 12px; + opacity: 0.7; + } + } + + .errors-container { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; + min-height: 0; + } + + .errors-header { + display: grid; + grid-template-columns: 1fr 200px 120px; + gap: 12px; + padding: 8px 16px; + background: ${(props) => props.theme.console.headerBg}; + border-bottom: 1px solid ${(props) => props.theme.console.border}; + font-size: 11px; + font-weight: 600; + color: ${(props) => props.theme.console.titleColor}; + text-transform: uppercase; + letter-spacing: 0.5px; + flex-shrink: 0; + } + + .errors-list { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + min-height: 0; + } + + .error-row { + display: grid; + grid-template-columns: 1fr 200px 120px; + gap: 12px; + padding: 8px 16px; + border-bottom: 1px solid ${(props) => props.theme.console.border}; + cursor: pointer; + transition: background-color 0.1s ease; + font-size: 12px; + align-items: center; + + &:hover { + background: ${(props) => props.theme.console.logHoverBg}; + } + + &.selected { + background: ${(props) => props.theme.console.buttonHoverBg}; + border-left: 3px solid ${(props) => props.theme.console.checkboxColor}; + } + } + + .error-message { + color: ${(props) => props.theme.console.messageColor}; + font-weight: 500; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; + } + + .error-location { + color: ${(props) => props.theme.console.messageColor}; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; + font-size: 11px; + } + + .error-time { + color: ${(props) => props.theme.console.timestampColor}; + font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; + font-size: 11px; + text-align: right; + } +`; + +export default StyledWrapper; \ No newline at end of file diff --git a/packages/bruno-app/src/components/Devtools/Console/DebugTab/index.js b/packages/bruno-app/src/components/Devtools/Console/DebugTab/index.js new file mode 100644 index 000000000..cd00cd22d --- /dev/null +++ b/packages/bruno-app/src/components/Devtools/Console/DebugTab/index.js @@ -0,0 +1,106 @@ +import React from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { IconBug } from '@tabler/icons'; +import { + setSelectedError, + clearDebugErrors +} from 'providers/ReduxStore/slices/logs'; +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', + second: '2-digit', + fractionalSecondDigits: 3 + }); + }; + + const getShortMessage = (message, maxLength = 80) => { + if (!message) return 'Unknown error'; + return message.length > maxLength ? message.substring(0, maxLength) + '...' : message; + }; + + const getLocation = (error) => { + if (error.filename) { + const filename = error.filename.split('/').pop(); // Get just the filename + if (error.lineno && error.colno) { + return `${filename}:${error.lineno}:${error.colno}`; + } else if (error.lineno) { + return `${filename}:${error.lineno}`; + } + return filename; + } + return '-'; + }; + + return ( +
+
+ {getShortMessage(error.message)} +
+ +
+ {getLocation(error)} +
+ +
+ {formatTime(error.timestamp)} +
+
+ ); +}; + +const DebugTab = () => { + const dispatch = useDispatch(); + const { debugErrors, selectedError } = useSelector(state => state.logs); + + const handleErrorClick = (error) => { + dispatch(setSelectedError(error)); + }; + + const handleClearErrors = () => { + dispatch(clearDebugErrors()); + }; + + return ( + +
+ {debugErrors.length === 0 ? ( +
+ +

No errors

+ console.error() calls will appear here +
+ ) : ( +
+
+
Message
+
Location
+
Time
+
+ +
+ {debugErrors.map((error, index) => ( + handleErrorClick(error)} + /> + ))} +
+
+ )} +
+
+ ); +}; + +export default DebugTab; \ No newline at end of file diff --git a/packages/bruno-app/src/components/Devtools/Console/ErrorDetailsPanel/StyledWrapper.js b/packages/bruno-app/src/components/Devtools/Console/ErrorDetailsPanel/StyledWrapper.js new file mode 100644 index 000000000..94900df8f --- /dev/null +++ b/packages/bruno-app/src/components/Devtools/Console/ErrorDetailsPanel/StyledWrapper.js @@ -0,0 +1,228 @@ +import styled from 'styled-components'; + +const StyledWrapper = styled.div` + display: flex; + flex-direction: column; + height: 100%; + background: ${(props) => props.theme.console.contentBg}; + border-left: 1px solid ${(props) => props.theme.console.border}; + min-width: 400px; + max-width: 600px; + width: 40%; + overflow: hidden; + + .panel-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 16px; + background: ${(props) => props.theme.console.headerBg}; + border-bottom: 1px solid ${(props) => props.theme.console.border}; + flex-shrink: 0; + } + + .panel-title { + display: flex; + align-items: center; + gap: 8px; + color: ${(props) => props.theme.console.titleColor}; + font-size: 13px; + font-weight: 500; + + .error-time { + color: ${(props) => props.theme.console.countColor}; + font-size: 11px; + font-weight: 400; + } + } + + .close-button { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + background: transparent; + border: none; + border-radius: 4px; + color: ${(props) => props.theme.console.buttonColor}; + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + background: ${(props) => props.theme.console.buttonHoverBg}; + color: ${(props) => props.theme.console.buttonHoverColor}; + } + } + + .panel-tabs { + display: flex; + background: ${(props) => props.theme.console.headerBg}; + border-bottom: 1px solid ${(props) => props.theme.console.border}; + flex-shrink: 0; + } + + .tab-button { + display: flex; + align-items: center; + gap: 6px; + padding: 8px 16px; + background: transparent; + border: none; + border-bottom: 2px solid transparent; + color: ${(props) => props.theme.console.buttonColor}; + cursor: pointer; + transition: all 0.2s ease; + font-size: 12px; + font-weight: 500; + + &:hover { + background: ${(props) => props.theme.console.buttonHoverBg}; + color: ${(props) => props.theme.console.buttonHoverColor}; + } + + &.active { + color: ${(props) => props.theme.console.checkboxColor}; + border-bottom-color: ${(props) => props.theme.console.checkboxColor}; + background: ${(props) => props.theme.console.contentBg}; + } + } + + .panel-content { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + background: ${(props) => props.theme.console.contentBg}; + min-height: 0; + } + + .tab-content { + padding: 16px; + height: 100%; + overflow-y: auto; + } + + .section { + margin-bottom: 24px; + + &:last-child { + margin-bottom: 0; + } + + h4 { + margin: 0 0 12px 0; + font-size: 13px; + font-weight: 600; + color: ${(props) => props.theme.console.titleColor}; + text-transform: uppercase; + letter-spacing: 0.5px; + } + } + + .info-grid { + display: flex; + flex-direction: column; + gap: 12px; + } + + .info-item { + display: flex; + flex-direction: column; + gap: 4px; + + label { + font-size: 11px; + font-weight: 600; + color: ${(props) => props.theme.console.titleColor}; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + span { + font-size: 12px; + color: ${(props) => props.theme.console.messageColor}; + font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; + word-break: break-all; + line-height: 1.4; + } + } + + .error-message-full { + color: ${(props) => props.theme.console.messageColor} !important; + background: ${(props) => props.theme.console.headerBg}; + padding: 8px 12px; + border-radius: 4px; + border: 1px solid ${(props) => props.theme.console.border}; + } + + .file-path { + color: ${(props) => props.theme.console.checkboxColor} !important; + font-weight: 500 !important; + } + + .report-section { + display: flex; + flex-direction: column; + gap: 12px; + + p { + margin: 0; + font-size: 12px; + color: ${(props) => props.theme.console.messageColor}; + line-height: 1.4; + } + } + + .report-button { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 16px; + background: ${(props) => props.theme.console.buttonHoverBg}; + border: 1px solid ${(props) => props.theme.console.border}; + border-radius: 6px; + color: ${(props) => props.theme.console.buttonColor}; + cursor: pointer; + transition: all 0.2s ease; + font-size: 12px; + font-weight: 500; + text-decoration: none; + align-self: flex-start; + + &:hover { + background: ${(props) => props.theme.console.checkboxColor}; + color: white; + border-color: ${(props) => props.theme.console.checkboxColor}; + } + + span { + font-family: inherit; + } + } + + .stack-trace-container, + .arguments-container { + background: ${(props) => props.theme.console.headerBg}; + border: 1px solid ${(props) => props.theme.console.border}; + border-radius: 6px; + overflow: hidden; + } + + .stack-trace, + .arguments { + margin: 0; + padding: 16px; + font-size: 11px; + line-height: 1.5; + color: ${(props) => props.theme.console.messageColor}; + background: transparent; + font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; + white-space: pre-wrap; + word-break: break-word; + overflow-x: auto; + max-height: 400px; + overflow-y: auto; + } +`; + +export default StyledWrapper; \ No newline at end of file diff --git a/packages/bruno-app/src/components/Devtools/Console/ErrorDetailsPanel/index.js b/packages/bruno-app/src/components/Devtools/Console/ErrorDetailsPanel/index.js new file mode 100644 index 000000000..91499b4a3 --- /dev/null +++ b/packages/bruno-app/src/components/Devtools/Console/ErrorDetailsPanel/index.js @@ -0,0 +1,268 @@ +import React, { useState } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { + IconX, + IconBug, + IconFileText, + IconCode, + IconStack, + IconBrandGithub +} from '@tabler/icons'; +import { clearSelectedError } from 'providers/ReduxStore/slices/logs'; +import { useApp } from 'providers/App'; +import platformLib from 'platform'; +import StyledWrapper from './StyledWrapper'; + +const ErrorInfoTab = ({ error }) => { + const { version } = useApp(); + + const formatTimestamp = (timestamp) => { + const date = new Date(timestamp); + return date.toLocaleString(); + }; + + const generateGitHubIssueUrl = () => { + const title = `Bug: ${error.message.substring(0, 50)}${error.message.length > 50 ? '...' : ''}`; + + const body = `## Bug Report + +### Error Details +- **Message**: ${error.message} +- **File**: ${error.filename || 'Unknown'} +- **Line**: ${error.lineno || 'Unknown'}:${error.colno || 'Unknown'} +- **Timestamp**: ${formatTimestamp(error.timestamp)} + +### Environment +- **Bruno Version**: ${version} +- **OS**: ${platformLib.os.family} ${platformLib.os.version || ''} +- **Browser**: ${platformLib.name} ${platformLib.version || ''} + +### Stack Trace +\`\`\` +${error.stack || 'No stack trace available'} +\`\`\` + +### Arguments +\`\`\` +${error.args ? error.args.map((arg, index) => { + if (arg && typeof arg === 'object' && arg.__type === 'Error') { + return `[${index}]: Error: ${arg.message}`; + } + return `[${index}]: ${typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)}`; +}).join('\n') : 'No arguments'} +\`\`\` + +### Steps to Reproduce +1. +2. +3. + +### Expected Behavior + + +### Additional Context + +`; + + const encodedTitle = encodeURIComponent(title); + const encodedBody = encodeURIComponent(body); + + return `https://github.com/usebruno/bruno/issues/new?template=BLANK_ISSUE&title=${encodedTitle}&body=${encodedBody}`; + }; + + const handleReportIssue = () => { + const url = generateGitHubIssueUrl(); + window.open(url, '_blank'); + }; + + return ( +
+
+

Error Information

+
+
+ + {error.message || 'No message available'} +
+ + {error.filename && ( +
+ + {error.filename} +
+ )} + + {error.lineno && ( +
+ + {error.lineno}{error.colno ? `:${error.colno}` : ''} +
+ )} + +
+ + {formatTimestamp(error.timestamp)} +
+
+
+ +
+

Report Issue

+
+

Found a bug? Help us improve Bruno by reporting this error on GitHub.

+ +
+
+
+ ); +}; + +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) + .join('\n'); + }; + + return ( +
+
+

Stack Trace

+
+
+            {formatStackTrace(error.stack)}
+          
+
+
+
+ ); +}; + +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) { + return 'Arguments could not be formatted'; + } + }; + + return ( +
+
+

Arguments

+
+
+            {formatArguments(error.args)}
+          
+
+
+
+ ); +}; + +const ErrorDetailsPanel = () => { + const dispatch = useDispatch(); + const { selectedError } = useSelector(state => state.logs); + const [activeTab, setActiveTab] = useState('info'); + + if (!selectedError) return null; + + const handleClose = () => { + dispatch(clearSelectedError()); + }; + + const formatTime = (timestamp) => { + const date = new Date(timestamp); + return date.toLocaleString(); + }; + + const getTabContent = () => { + switch (activeTab) { + case 'info': + return ; + case 'stack': + return ; + case 'args': + return ; + default: + return ; + } + }; + + return ( + +
+
+ + Error Details + ({formatTime(selectedError.timestamp)}) +
+ + +
+ +
+ + + + + +
+ +
+ {getTabContent()} +
+
+ ); +}; + +export default ErrorDetailsPanel; \ No newline at end of file diff --git a/packages/bruno-app/src/components/Devtools/Console/NetworkTab/StyledWrapper.js b/packages/bruno-app/src/components/Devtools/Console/NetworkTab/StyledWrapper.js new file mode 100644 index 000000000..2fc45bafe --- /dev/null +++ b/packages/bruno-app/src/components/Devtools/Console/NetworkTab/StyledWrapper.js @@ -0,0 +1,293 @@ +import styled from 'styled-components'; + +const StyledWrapper = styled.div` + display: flex; + flex-direction: column; + height: 100%; + background: ${(props) => props.theme.console.contentBg}; + overflow: hidden; + + .network-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 16px; + background: ${(props) => props.theme.console.headerBg}; + border-bottom: 1px solid ${(props) => props.theme.console.border}; + flex-shrink: 0; + } + + .network-title { + display: flex; + align-items: center; + gap: 8px; + color: ${(props) => props.theme.console.titleColor}; + font-size: 13px; + font-weight: 500; + + .request-count { + color: ${(props) => props.theme.console.countColor}; + font-size: 12px; + font-weight: 400; + } + } + + .network-controls { + display: flex; + align-items: center; + gap: 8px; + } + + .network-content { + flex: 1; + overflow: hidden; + display: flex; + flex-direction: column; + min-height: 0; /* Important for proper flex behavior */ + } + + .network-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + color: ${(props) => props.theme.console.emptyColor}; + text-align: center; + gap: 8px; + padding: 40px 20px; + + p { + margin: 0; + font-size: 14px; + font-weight: 500; + } + + span { + font-size: 12px; + opacity: 0.7; + } + } + + .requests-container { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; + min-height: 0; /* Important for proper flex behavior */ + } + + .requests-header { + display: grid; + grid-template-columns: 80px 80px 150px 1fr 100px 80px 80px; + gap: 12px; + padding: 8px 16px; + background: ${(props) => props.theme.console.headerBg}; + border-bottom: 1px solid ${(props) => props.theme.console.border}; + font-size: 11px; + font-weight: 600; + color: ${(props) => props.theme.console.titleColor}; + text-transform: uppercase; + letter-spacing: 0.5px; + flex-shrink: 0; + } + + .requests-list { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + min-height: 0; /* Important for proper scrolling */ + } + + .request-row { + display: grid; + grid-template-columns: 80px 80px 150px 1fr 100px 80px 80px; + gap: 12px; + padding: 6px 16px; + border-bottom: 1px solid ${(props) => props.theme.console.border}; + cursor: pointer; + transition: background-color 0.1s ease; + font-size: 12px; + align-items: center; + + &:hover { + background: ${(props) => props.theme.console.logHoverBg}; + } + + &.selected { + background: ${(props) => props.theme.console.buttonHoverBg}; + border-left: 3px solid ${(props) => props.theme.console.checkboxColor}; + } + } + + .method-badge { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 2px 6px; + border-radius: 4px; + font-size: 10px; + font-weight: 600; + color: white; + text-transform: uppercase; + letter-spacing: 0.5px; + min-width: 45px; + } + + .status-badge { + font-weight: 600; + font-size: 12px; + } + + .request-domain { + color: ${(props) => props.theme.console.messageColor}; + font-weight: 500; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .request-path { + color: ${(props) => props.theme.console.messageColor}; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; + } + + .request-time { + color: ${(props) => props.theme.console.timestampColor}; + font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; + font-size: 11px; + } + + .request-duration { + color: ${(props) => props.theme.console.messageColor}; + font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; + font-size: 11px; + text-align: right; + } + + .request-size { + color: ${(props) => props.theme.console.messageColor}; + font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; + font-size: 11px; + text-align: right; + } + + .filter-dropdown { + position: relative; + } + + .filter-dropdown-trigger { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 8px; + background: transparent; + border: 1px solid ${(props) => props.theme.console.border}; + border-radius: 4px; + color: ${(props) => props.theme.console.buttonColor}; + cursor: pointer; + transition: all 0.2s ease; + font-size: 12px; + + &:hover { + background: ${(props) => props.theme.console.buttonHoverBg}; + color: ${(props) => props.theme.console.buttonHoverColor}; + } + + .filter-summary { + font-weight: 500; + min-width: 24px; + text-align: center; + } + } + + .filter-dropdown-menu { + position: absolute; + top: calc(100% + 4px); + right: 0; + min-width: 200px; + max-width: 250px; + background: ${(props) => props.theme.console.dropdownBg}; + border: 1px solid ${(props) => props.theme.console.border}; + border-radius: 6px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + z-index: 1000; + overflow: hidden; + } + + .filter-dropdown-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + background: ${(props) => props.theme.console.dropdownHeaderBg}; + border-bottom: 1px solid ${(props) => props.theme.console.border}; + font-size: 12px; + font-weight: 500; + color: ${(props) => props.theme.console.titleColor}; + } + + .filter-toggle-all { + background: transparent; + border: none; + color: ${(props) => props.theme.console.buttonColor}; + cursor: pointer; + font-size: 11px; + font-weight: 500; + padding: 2px 4px; + border-radius: 2px; + transition: all 0.2s ease; + + &:hover { + background: ${(props) => props.theme.console.buttonHoverBg}; + } + } + + .filter-dropdown-options { + padding: 4px 0; + } + + .filter-option { + display: flex; + align-items: center; + padding: 6px 12px; + cursor: pointer; + transition: background-color 0.2s ease; + + &:hover { + background: ${(props) => props.theme.console.optionHoverBg}; + } + + input[type="checkbox"] { + margin: 0 8px 0 0; + width: 14px; + height: 14px; + accent-color: ${(props) => props.theme.console.checkboxColor}; + } + } + + .filter-option-content { + display: flex; + align-items: center; + gap: 8px; + flex: 1; + } + + .filter-option-label { + color: ${(props) => props.theme.console.optionLabelColor}; + font-size: 12px; + font-weight: 400; + } + + .filter-option-count { + color: ${(props) => props.theme.console.optionCountColor}; + font-size: 11px; + font-weight: 400; + margin-left: auto; + } +`; + +export default StyledWrapper; \ No newline at end of file diff --git a/packages/bruno-app/src/components/Devtools/Console/NetworkTab/index.js b/packages/bruno-app/src/components/Devtools/Console/NetworkTab/index.js new file mode 100644 index 000000000..7501aafbd --- /dev/null +++ b/packages/bruno-app/src/components/Devtools/Console/NetworkTab/index.js @@ -0,0 +1,302 @@ +import React, { useState, useRef, useEffect, useMemo } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { + IconFilter, + IconChevronDown, + IconNetwork, +} from '@tabler/icons'; +import { + updateNetworkFilter, + toggleAllNetworkFilters, + setSelectedRequest +} from 'providers/ReduxStore/slices/logs'; +import StyledWrapper from './StyledWrapper'; + +const MethodBadge = ({ method }) => { + const getMethodColor = (method) => { + switch (method?.toUpperCase()) { + case 'GET': return '#10b981'; + case 'POST': return '#8b5cf6'; + case 'PUT': return '#f59e0b'; + case 'DELETE': return '#ef4444'; + case 'PATCH': return '#06b6d4'; + case 'HEAD': return '#6b7280'; + case 'OPTIONS': return '#84cc16'; + default: return '#6b7280'; + } + }; + + return ( + + {method?.toUpperCase() || 'GET'} + + ); +}; + +const StatusBadge = ({ status, statusCode }) => { + const getStatusColor = (code) => { + if (code >= 200 && code < 300) return '#10b981'; + if (code >= 300 && code < 400) return '#f59e0b'; + if (code >= 400 && code < 500) return '#ef4444'; + if (code >= 500) return '#dc2626'; + return '#6b7280'; + }; + + const displayStatus = statusCode || status; + + return ( + + {displayStatus} + + ); +}; + +const NetworkFilterDropdown = ({ filters, requestCounts, onFilterToggle, onToggleAll }) => { + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + + const allFiltersEnabled = Object.values(filters).every(f => f); + const activeFilters = Object.entries(filters).filter(([_, enabled]) => enabled); + + useEffect(() => { + const handleClickOutside = (event) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { + setIsOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + return ( +
+ + + {isOpen && ( +
+
+ Filter by Method + +
+ +
+ {Object.keys(filters).map(method => ( + + ))} +
+
+ )} +
+ ); +}; + +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', + second: '2-digit', + fractionalSecondDigits: 3 + }); + }; + + const formatDuration = (duration) => { + if (!duration) return '-'; + if (duration < 1000) return `${Math.round(duration)}ms`; + return `${(duration / 1000).toFixed(2)}s`; + }; + + const formatSize = (size) => { + if (!size) return '-'; + if (size < 1024) return `${size}B`; + if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)}KB`; + return `${(size / (1024 * 1024)).toFixed(1)}MB`; + }; + + const getUrl = () => { + return req?.url || 'Unknown URL'; + }; + + const getDomain = () => { + try { + const url = new URL(getUrl()); + return url.hostname; + } catch { + return getUrl(); + } + }; + + const getPath = () => { + try { + const url = new URL(getUrl()); + return url.pathname + url.search; + } catch { + return getUrl(); + } + }; + + return ( +
+
+ +
+ +
+ +
+ +
+ {getDomain()} +
+ +
+ {getPath()} +
+ +
+ {formatTime(timestamp)} +
+ +
+ {formatDuration(res?.duration)} +
+ +
+ {formatSize(res?.size)} +
+
+ ); +}; + +const NetworkTab = () => { + const dispatch = useDispatch(); + const { networkFilters, selectedRequest } = useSelector(state => state.logs); + const collections = useSelector(state => state.collections.collections); + + const allRequests = useMemo(() => { + const requests = []; + + collections.forEach(collection => { + if (collection.timeline) { + collection.timeline + .filter(entry => entry.type === 'request') + .forEach(entry => { + requests.push({ + ...entry, + collectionName: collection.name, + collectionUid: collection.uid + }); + }); + } + }); + + return requests.sort((a, b) => a.timestamp - b.timestamp); + }, [collections]); + + const filteredRequests = useMemo(() => { + return allRequests.filter(request => { + const method = request.data?.request?.method?.toUpperCase() || 'GET'; + return networkFilters[method]; + }); + }, [allRequests, networkFilters]); + + const requestCounts = useMemo(() => { + return allRequests.reduce((counts, request) => { + const method = request.data?.request?.method?.toUpperCase() || 'GET'; + counts[method] = (counts[method] || 0) + 1; + return counts; + }, {}); + }, [allRequests]); + + const handleFilterToggle = (method, enabled) => { + dispatch(updateNetworkFilter({ method, enabled })); + }; + + const handleToggleAllFilters = (enabled) => { + dispatch(toggleAllNetworkFilters(enabled)); + }; + + const handleRequestClick = (request) => { + dispatch(setSelectedRequest(request)); + }; + + return ( + +
+ {filteredRequests.length === 0 ? ( +
+ +

No network requests

+ Requests will appear here as you make API calls +
+ ) : ( +
+
+
Method
+
Status
+
Domain
+
Path
+
Time
+
Duration
+
Size
+
+ +
+ {filteredRequests.map((request, index) => ( + handleRequestClick(request)} + /> + ))} +
+
+ )} +
+
+ ); +}; + +export default NetworkTab; \ No newline at end of file diff --git a/packages/bruno-app/src/components/Devtools/Console/RequestDetailsPanel/StyledWrapper.js b/packages/bruno-app/src/components/Devtools/Console/RequestDetailsPanel/StyledWrapper.js new file mode 100644 index 000000000..3cc9ba03d --- /dev/null +++ b/packages/bruno-app/src/components/Devtools/Console/RequestDetailsPanel/StyledWrapper.js @@ -0,0 +1,347 @@ +import styled from 'styled-components'; + +const StyledWrapper = styled.div` + display: flex; + flex-direction: column; + height: 100%; + background: ${(props) => props.theme.console.contentBg}; + border-left: 1px solid ${(props) => props.theme.console.border}; + min-width: 400px; + max-width: 600px; + width: 40%; + overflow: hidden; + + .panel-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 16px; + background: ${(props) => props.theme.console.headerBg}; + border-bottom: 1px solid ${(props) => props.theme.console.border}; + flex-shrink: 0; + } + + .panel-title { + display: flex; + align-items: center; + gap: 8px; + color: ${(props) => props.theme.console.titleColor}; + font-size: 13px; + font-weight: 500; + + .request-time { + color: ${(props) => props.theme.console.countColor}; + font-size: 11px; + font-weight: 400; + } + } + + .close-button { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + background: transparent; + border: none; + border-radius: 4px; + color: ${(props) => props.theme.console.buttonColor}; + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + background: ${(props) => props.theme.console.buttonHoverBg}; + color: ${(props) => props.theme.console.buttonHoverColor}; + } + } + + .panel-tabs { + display: flex; + background: ${(props) => props.theme.console.headerBg}; + border-bottom: 1px solid ${(props) => props.theme.console.border}; + flex-shrink: 0; + } + + .tab-button { + display: flex; + align-items: center; + gap: 6px; + padding: 8px 16px; + background: transparent; + border: none; + border-bottom: 2px solid transparent; + color: ${(props) => props.theme.console.buttonColor}; + cursor: pointer; + transition: all 0.2s ease; + font-size: 12px; + font-weight: 500; + + &:hover { + background: ${(props) => props.theme.console.buttonHoverBg}; + color: ${(props) => props.theme.console.buttonHoverColor}; + } + + &.active { + color: ${(props) => props.theme.console.checkboxColor}; + border-bottom-color: ${(props) => props.theme.console.checkboxColor}; + background: ${(props) => props.theme.console.contentBg}; + } + } + + .panel-content { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + padding: 16px; + min-height: 0; + height: 0; + } + + .tab-content { + display: flex; + flex-direction: column; + gap: 20px; + min-height: min-content; + } + + .section { + display: flex; + flex-direction: column; + gap: 8px; + + h4 { + margin: 0; + font-size: 13px; + font-weight: 600; + color: ${(props) => props.theme.console.titleColor}; + padding-bottom: 4px; + border-bottom: 1px solid ${(props) => props.theme.console.border}; + } + } + + .info-grid { + display: flex; + flex-direction: column; + gap: 8px; + } + + .info-item { + display: flex; + flex-direction: column; + gap: 2px; + + .label { + font-size: 11px; + font-weight: 600; + color: ${(props) => props.theme.console.countColor}; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .value { + font-size: 12px; + color: ${(props) => props.theme.console.messageColor}; + font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; + word-break: break-all; + padding: 4px 8px; + background: ${(props) => props.theme.console.headerBg}; + border-radius: 4px; + border: 1px solid ${(props) => props.theme.console.border}; + } + } + + .headers-table, + .timeline-table { + overflow: auto; + border-radius: 4px; + border: 1px solid ${(props) => props.theme.console.border}; + max-height: 300px; + + table { + width: 100%; + border-collapse: collapse; + font-size: 12px; + background: ${(props) => props.theme.console.headerBg}; + + thead { + background: ${(props) => props.theme.console.dropdownHeaderBg}; + position: sticky; + top: 0; + z-index: 10; + + td { + padding: 8px 12px; + font-weight: 600; + color: ${(props) => props.theme.console.titleColor}; + text-transform: uppercase; + font-size: 11px; + letter-spacing: 0.5px; + border-bottom: 1px solid ${(props) => props.theme.console.border}; + } + } + + tbody { + tr { + border-bottom: 1px solid ${(props) => props.theme.console.border}; + + &:last-child { + border-bottom: none; + } + + &:nth-child(odd) { + background: ${(props) => props.theme.console.contentBg}; + } + + &:hover { + background: ${(props) => props.theme.console.logHoverBg}; + } + } + + td { + padding: 8px 12px; + vertical-align: top; + word-break: break-word; + } + } + } + } + + .header-name, + .timeline-phase { + color: ${(props) => props.theme.console.countColor}; + font-weight: 600; + font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; + min-width: 120px; + } + + .header-value, + .timeline-message { + color: ${(props) => props.theme.console.messageColor}; + font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; + word-break: break-all; + } + + .timeline-duration { + color: ${(props) => props.theme.console.timestampColor}; + font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; + text-align: right; + min-width: 80px; + } + + .code-block { + background: ${(props) => props.theme.console.headerBg}; + border: 1px solid ${(props) => props.theme.console.border}; + border-radius: 4px; + padding: 12px; + font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; + font-size: 11px; + line-height: 1.4; + color: ${(props) => props.theme.console.messageColor}; + overflow: auto; + white-space: pre-wrap; + word-break: break-word; + max-height: 400px; + margin: 0; + } + + .empty-state { + padding: 12px; + text-align: center; + color: ${(props) => props.theme.console.emptyColor}; + font-style: italic; + font-size: 12px; + background: ${(props) => props.theme.console.headerBg}; + border: 1px solid ${(props) => props.theme.console.border}; + border-radius: 4px; + } + + .response-body-container { + border: 1px solid ${(props) => props.theme.console.border}; + border-radius: 4px; + overflow: hidden; + background: ${(props) => props.theme.console.headerBg}; + height: 400px; + display: flex; + flex-direction: column; + + .w-full.h-full.relative.flex { + height: 100% !important; + width: 100% !important; + background: ${(props) => props.theme.console.headerBg} !important; + display: flex !important; + flex-direction: column !important; + } + + div[role="tablist"] { + background: ${(props) => props.theme.console.dropdownHeaderBg}; + padding: 8px 12px; + border-bottom: 1px solid ${(props) => props.theme.console.border}; + display: flex !important; + gap: 8px !important; + flex-wrap: wrap !important; + align-items: center !important; + min-height: 40px !important; + flex-shrink: 0 !important; + + > div { + color: ${(props) => props.theme.console.buttonColor}; + font-size: 12px !important; + padding: 6px 12px !important; + border-radius: 4px; + transition: all 0.2s ease; + cursor: pointer; + border: 1px solid ${(props) => props.theme.console.border}; + background: ${(props) => props.theme.console.contentBg}; + white-space: nowrap !important; + min-width: auto !important; + height: auto !important; + line-height: 1.2 !important; + font-weight: 500 !important; + + &:hover { + background: ${(props) => props.theme.console.buttonHoverBg}; + color: ${(props) => props.theme.console.buttonHoverColor}; + border-color: ${(props) => props.theme.console.buttonHoverBg}; + } + + &.active { + background: ${(props) => props.theme.console.checkboxColor}; + color: white; + border-color: ${(props) => props.theme.console.checkboxColor}; + } + } + } + .response-filter { + position: absolute !important; + bottom: 8px !important; + right: 8px !important; + left: 8px !important; + z-index: 10 !important; + } + } + + .network-logs-container { + border: 1px solid ${(props) => props.theme.console.border}; + border-radius: 4px; + overflow: hidden; + background: ${(props) => props.theme.console.headerBg}; + min-height: 200px; + max-height: 400px; + + .network-logs { + background: ${(props) => props.theme.console.contentBg} !important; + color: ${(props) => props.theme.console.messageColor} !important; + height: 100% !important; + max-height: 400px !important; + + pre { + color: ${(props) => props.theme.console.messageColor} !important; + font-size: 11px !important; + line-height: 1.4 !important; + padding: 12px !important; + } + } + } +`; + +export default StyledWrapper; \ No newline at end of file diff --git a/packages/bruno-app/src/components/Devtools/Console/RequestDetailsPanel/index.js b/packages/bruno-app/src/components/Devtools/Console/RequestDetailsPanel/index.js new file mode 100644 index 000000000..23439c959 --- /dev/null +++ b/packages/bruno-app/src/components/Devtools/Console/RequestDetailsPanel/index.js @@ -0,0 +1,242 @@ +import React, { useState } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { + IconX, + IconFileText, + IconArrowRight, + IconNetwork +} from '@tabler/icons'; +import { clearSelectedRequest } from 'providers/ReduxStore/slices/logs'; +import QueryResult from 'components/ResponsePane/QueryResult'; +import Network from 'components/ResponsePane/Timeline/TimelineItem/Network'; +import StyledWrapper from './StyledWrapper'; +import { uuid } from 'utils/common/index'; + +const RequestTab = ({ request, response }) => { + const formatHeaders = (headers) => { + if (!headers) return []; + if (Array.isArray(headers)) return headers; + return Object.entries(headers).map(([key, value]) => ({ name: key, value })); + }; + + const formatBody = (body) => { + if (!body) return 'No body'; + if (typeof body === 'string') return body; + return JSON.stringify(body, null, 2); + }; + + return ( +
+
+

General

+
+
+ Request URL: + {request?.url || 'N/A'} +
+
+ Request Method: + {request?.method || 'GET'} +
+
+
+ +
+

Request Headers

+ {formatHeaders(request?.headers).length > 0 ? ( +
+ + + + + + + + + {formatHeaders(request.headers).map((header, index) => ( + + + + + ))} + +
NameValue
{header.name}{header.value}
+
+ ) : ( +
No headers
+ )} +
+ + {request?.body && ( +
+

Request Body

+
{formatBody(request.body)}
+
+ )} +
+ ); +}; + +const ResponseTab = ({ response, request, collection }) => { + const formatHeaders = (headers) => { + if (!headers) return []; + if (Array.isArray(headers)) return headers; + return Object.entries(headers).map(([key, value]) => ({ name: key, value })); + }; + + return ( +
+
+

Response Headers

+ {formatHeaders(response?.headers).length > 0 ? ( +
+ + + + + + + + + {formatHeaders(response.headers).map((header, index) => ( + + + + + ))} + +
NameValue
{header.name}{header.value}
+
+ ) : ( +
No headers
+ )} +
+ +
+

Response Body

+
+ {response?.data || response?.dataBuffer ? ( + + ) : ( +
No response data
+ )} +
+
+
+ ); +}; + +const NetworkTab = ({ response }) => { + const timeline = response?.timeline || []; + + return ( +
+
+

Network Logs

+
+ {timeline.length > 0 ? ( + + ) : ( +
No network logs available
+ )} +
+
+
+ ); +}; + +const RequestDetailsPanel = () => { + const dispatch = useDispatch(); + const { selectedRequest } = useSelector(state => state.logs); + const collections = useSelector(state => state.collections.collections); + const [activeTab, setActiveTab] = useState('request'); + + if (!selectedRequest) return null; + + const { data } = selectedRequest; + const { request, response } = data; + + const collection = collections.find(c => c.uid === selectedRequest.collectionUid); + + const handleClose = () => { + dispatch(clearSelectedRequest()); + }; + + const formatTime = (timestamp) => { + const date = new Date(timestamp); + return date.toLocaleString(); + }; + + const getTabContent = () => { + switch (activeTab) { + case 'request': + return ; + case 'response': + return ; + case 'network': + return ; + default: + return ; + } + }; + + return ( + +
+
+ + Request Details + ({formatTime(selectedRequest.timestamp)}) +
+ + +
+ +
+ + + + + +
+ +
+ {getTabContent()} +
+
+ ); +}; + +export default RequestDetailsPanel; \ No newline at end of file diff --git a/packages/bruno-app/src/components/Devtools/Console/StyledWrapper.js b/packages/bruno-app/src/components/Devtools/Console/StyledWrapper.js new file mode 100644 index 000000000..674214577 --- /dev/null +++ b/packages/bruno-app/src/components/Devtools/Console/StyledWrapper.js @@ -0,0 +1,521 @@ +import styled from 'styled-components'; + +const StyledWrapper = styled.div` + width: 100%; + height: 100%; + background: ${(props) => props.theme.console.bg}; + border-top: 1px solid ${(props) => props.theme.console.border}; + display: flex; + flex-direction: column; + overflow: hidden; + + .console-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 16px; + background: ${(props) => props.theme.console.headerBg}; + border-bottom: 1px solid ${(props) => props.theme.console.border}; + flex-shrink: 0; + position: relative; + } + + .console-tabs { + display: flex; + align-items: center; + gap: 2px; + } + + .console-tab { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + background: transparent; + border: none; + border-bottom: 2px solid transparent; + color: ${(props) => props.theme.console.buttonColor}; + cursor: pointer; + transition: all 0.2s ease; + font-size: 12px; + font-weight: 500; + border-radius: 4px 4px 0 0; + + &:hover { + background: ${(props) => props.theme.console.buttonHoverBg}; + color: ${(props) => props.theme.console.buttonHoverColor}; + } + + &.active { + color: ${(props) => props.theme.console.checkboxColor}; + border-bottom-color: ${(props) => props.theme.console.checkboxColor}; + background: ${(props) => props.theme.console.contentBg}; + } + } + + .console-controls { + display: flex; + align-items: center; + gap: 4px; + } + + .console-content { + flex: 1; + overflow: hidden; + background: ${(props) => props.theme.console.contentBg}; + min-height: 0; + display: flex; + flex-direction: column; + } + + .tab-content { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; + } + + .tab-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 16px; + background: ${(props) => props.theme.console.headerBg}; + border-bottom: 1px solid ${(props) => props.theme.console.border}; + flex-shrink: 0; + } + + .tab-title { + display: flex; + align-items: center; + gap: 8px; + color: ${(props) => props.theme.console.titleColor}; + font-size: 13px; + font-weight: 500; + + .log-count { + color: ${(props) => props.theme.console.countColor}; + font-size: 12px; + font-weight: 400; + } + } + + .tab-controls { + display: flex; + align-items: center; + gap: 8px; + } + + .tab-content-area { + flex: 1; + overflow-y: auto; + background: ${(props) => props.theme.console.contentBg}; + min-height: 0; + } + + .network-with-details { + display: flex; + height: 100%; + overflow: hidden; + } + + .network-main { + flex: 1; + overflow: hidden; + display: flex; + flex-direction: column; + min-width: 0; + } + + .debug-with-details { + display: flex; + height: 100%; + overflow: hidden; + } + + .debug-main { + flex: 1; + overflow: hidden; + display: flex; + flex-direction: column; + min-width: 0; + } + + .filter-controls { + display: flex; + align-items: center; + gap: 4px; + margin-right: 8px; + padding-right: 8px; + border-right: 1px solid ${(props) => props.theme.console.border}; + } + + .action-controls { + display: flex; + align-items: center; + gap: 4px; + } + + .control-button { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + background: transparent; + border: none; + border-radius: 4px; + color: ${(props) => props.theme.console.buttonColor}; + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + background: ${(props) => props.theme.console.buttonHoverBg}; + color: ${(props) => props.theme.console.buttonHoverColor}; + } + + &.close-button:hover { + background: #e81123; + color: white; + } + } + + .filter-dropdown { + position: relative; + } + + .filter-dropdown-trigger { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 8px; + background: transparent; + border: 1px solid ${(props) => props.theme.console.border}; + border-radius: 4px; + color: ${(props) => props.theme.console.buttonColor}; + cursor: pointer; + transition: all 0.2s ease; + font-size: 12px; + + &:hover { + background: ${(props) => props.theme.console.buttonHoverBg}; + color: ${(props) => props.theme.console.buttonHoverColor}; + border-color: ${(props) => props.theme.console.border}; + } + + .filter-summary { + font-weight: 500; + min-width: 24px; + text-align: center; + } + } + + .filter-dropdown-menu { + position: absolute; + top: calc(100% + 4px); + left: 0; + min-width: 200px; + max-width: 250px; + background: ${(props) => props.theme.console.dropdownBg}; + border: 1px solid ${(props) => props.theme.console.border}; + border-radius: 6px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + z-index: 1000; + overflow: hidden; + + &.right { + left: auto; + right: 0; + } + } + + .filter-dropdown-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + background: ${(props) => props.theme.console.dropdownHeaderBg}; + border-bottom: 1px solid ${(props) => props.theme.console.border}; + font-size: 12px; + font-weight: 500; + color: ${(props) => props.theme.console.titleColor}; + } + + .filter-toggle-all { + background: transparent; + border: none; + color: ${(props) => props.theme.console.buttonColor}; + cursor: pointer; + font-size: 11px; + font-weight: 500; + padding: 2px 4px; + border-radius: 2px; + transition: all 0.2s ease; + + &:hover { + background: ${(props) => props.theme.console.buttonHoverBg}; + } + } + + .filter-dropdown-options { + padding: 4px 0; + } + + .filter-option { + display: flex; + align-items: center; + padding: 6px 12px; + cursor: pointer; + transition: background-color 0.2s ease; + + &:hover { + background: ${(props) => props.theme.console.optionHoverBg}; + } + + input[type="checkbox"] { + margin: 0 8px 0 0; + width: 14px; + height: 14px; + accent-color: ${(props) => props.theme.console.checkboxColor}; + } + } + + .filter-option-content { + display: flex; + align-items: center; + gap: 8px; + flex: 1; + } + + .filter-option-label { + color: ${(props) => props.theme.console.optionLabelColor}; + font-size: 12px; + font-weight: 400; + } + + .filter-option-count { + color: ${(props) => props.theme.console.optionCountColor}; + font-size: 11px; + font-weight: 400; + margin-left: auto; + } + + .console-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + color: ${(props) => props.theme.console.emptyColor}; + text-align: center; + gap: 8px; + padding: 40px 20px; + + p { + margin: 0; + font-size: 14px; + font-weight: 500; + } + + span { + font-size: 12px; + opacity: 0.7; + } + } + + .logs-container { + padding: 8px 0; + } + + .method-badge { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 2px 6px; + border-radius: 4px; + font-size: 10px; + font-weight: 600; + color: white; + text-transform: uppercase; + letter-spacing: 0.5px; + min-width: 45px; + } + + .log-entry { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 4px 16px; + font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; + font-size: 12px; + line-height: 1.4; + border-left: 2px solid transparent; + transition: background-color 0.1s ease; + + &:hover { + background: ${(props) => props.theme.console.logHoverBg}; + } + + &.error { + border-left-color: #f14c4c; + + .log-level { + background: #f14c4c; + color: white; + } + + .log-icon { + color: #f14c4c; + } + } + + &.warn { + border-left-color: #ffcc02; + + .log-level { + background: #ffcc02; + color: #000; + } + + .log-icon { + color: #ffcc02; + } + } + + &.info { + border-left-color: #0078d4; + + .log-level { + background: #0078d4; + color: white; + } + + .log-icon { + color: #0078d4; + } + } + + &.debug { + border-left-color: #9b59b6; + + .log-level { + background: #9b59b6; + color: white; + } + + .log-icon { + color: #9b59b6; + } + } + + &.log { + border-left-color: #6a6a6a; + + .log-level { + background: #6a6a6a; + color: white; + } + + .log-icon { + color: #6a6a6a; + } + } + } + + .log-meta { + display: flex; + align-items: center; + gap: 8px; + flex-shrink: 0; + min-width: 120px; + } + + .log-timestamp { + color: ${(props) => props.theme.console.timestampColor}; + font-size: 11px; + font-weight: 400; + } + + .log-level { + font-size: 9px; + font-weight: 600; + padding: 2px 4px; + border-radius: 2px; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .log-icon { + flex-shrink: 0; + } + + .log-message { + color: ${(props) => props.theme.console.messageColor}; + white-space: pre-wrap; + word-break: break-word; + flex: 1; + + .log-object { + margin: 4px 0; + padding: 8px; + background: ${(props) => props.theme.console.headerBg}; + border-radius: 4px; + border: 1px solid ${(props) => props.theme.console.border}; + + .react-json-view { + background: transparent !important; + + .object-key-val { + font-size: 12px !important; + } + + .object-key { + color: ${(props) => props.theme.console.messageColor} !important; + font-weight: 500 !important; + } + + .object-value { + color: ${(props) => props.theme.console.messageColor} !important; + } + + .string-value { + color: ${(props) => props.theme.colors?.text?.green || (props.theme.console.messageColor)} !important; + } + + .number-value { + color: ${(props) => props.theme.colors?.text?.purple || (props.theme.console.messageColor)} !important; + } + + .boolean-value { + color: ${(props) => props.theme.colors?.text?.yellow || (props.theme.console.messageColor)} !important; + } + + .null-value { + color: ${(props) => props.theme.colors?.text?.danger || (props.theme.console.messageColor)} !important; + } + + .object-size { + color: ${(props) => props.theme.console.timestampColor} !important; + } + + .brace, .bracket { + color: ${(props) => props.theme.console.messageColor} !important; + } + + .collapsed-icon, .expanded-icon { + color: ${(props) => props.theme.console.checkboxColor} !important; + } + + .icon-container { + color: ${(props) => props.theme.console.checkboxColor} !important; + } + + .click-to-expand, .click-to-collapse { + color: ${(props) => props.theme.console.checkboxColor} !important; + } + } + } + } +`; + +export default StyledWrapper; \ No newline at end of file diff --git a/packages/bruno-app/src/components/Devtools/Console/index.js b/packages/bruno-app/src/components/Devtools/Console/index.js new file mode 100644 index 000000000..bde9a8b1d --- /dev/null +++ b/packages/bruno-app/src/components/Devtools/Console/index.js @@ -0,0 +1,531 @@ +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, + IconFilter, + IconAlertTriangle, + IconAlertCircle, + IconBug, + IconCode, + IconChevronDown, + IconTerminal2, + IconNetwork +} from '@tabler/icons'; +import { + closeConsole, + clearLogs, + updateFilter, + toggleAllFilters, + setActiveTab, + clearDebugErrors, + updateNetworkFilter, + toggleAllNetworkFilters +} from 'providers/ReduxStore/slices/logs'; +import NetworkTab from './NetworkTab'; +import RequestDetailsPanel from './RequestDetailsPanel'; +import DebugTab from './DebugTab'; +import ErrorDetailsPanel from './ErrorDetailsPanel'; +import StyledWrapper from './StyledWrapper'; + +const LogIcon = ({ type }) => { + const iconProps = { size: 16, strokeWidth: 1.5 }; + + switch (type) { + case 'error': + return ; + case 'warn': + return ; + case 'info': + return ; + case 'debug': + return ; + default: + return ; + } +}; + +const LogTimestamp = ({ timestamp }) => { + const date = new Date(timestamp); + const time = date.toLocaleTimeString('en-US', { + hour12: false, + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + fractionalSecondDigits: 3 + }); + + return {time}; +}; + +const LogMessage = ({ message, args }) => { + const { displayedTheme } = useTheme(); + + const formatMessage = (msg, originalArgs) => { + if (originalArgs && originalArgs.length > 0) { + return originalArgs.map((arg, index) => { + if (typeof arg === 'object' && arg !== null) { + return ( +
+ +
+ ); + } + return String(arg); + }); + } + return msg; + }; + + const formattedMessage = formatMessage(message, args); + + return ( + + {Array.isArray(formattedMessage) ? formattedMessage.map((item, index) => ( + {item} + )) : formattedMessage} + + ); +}; + +const FilterDropdown = ({ filters, logCounts, onFilterToggle, onToggleAll }) => { + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + + const allFiltersEnabled = Object.values(filters).every(f => f); + const activeFilters = Object.entries(filters).filter(([_, enabled]) => enabled); + + useEffect(() => { + const handleClickOutside = (event) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { + setIsOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + return ( +
+ + + {isOpen && ( +
+
+ Filter by Type + +
+ +
+ {Object.entries(filters).map(([filterType, enabled]) => ( + + ))} +
+
+ )} +
+ ); +}; + +const NetworkFilterDropdown = ({ filters, requestCounts, onFilterToggle, onToggleAll }) => { + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + + const allFiltersEnabled = Object.values(filters).every(f => f); + const activeFilters = Object.entries(filters).filter(([_, enabled]) => enabled); + + const getMethodColor = (method) => { + switch (method?.toUpperCase()) { + case 'GET': return '#10b981'; + case 'POST': return '#8b5cf6'; + case 'PUT': return '#f59e0b'; + case 'DELETE': return '#ef4444'; + case 'PATCH': return '#06b6d4'; + case 'HEAD': return '#6b7280'; + case 'OPTIONS': return '#84cc16'; + default: return '#6b7280'; + } + }; + + useEffect(() => { + const handleClickOutside = (event) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { + setIsOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + return ( +
+ + + {isOpen && ( +
+
+ Filter by Method + +
+ +
+ {Object.entries(filters).map(([method, enabled]) => ( + + ))} +
+
+ )} +
+ ); +}; + +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) { + logsEndRef.current.scrollIntoView({ behavior: 'auto' }); + } + prevLogsCountRef.current = logs.length; + }, [logs]); + + const filteredLogs = logs.filter(log => filters[log.type]); + + return ( +
+
+ {filteredLogs.length === 0 ? ( +
+ +

No logs to display

+ Logs will appear here as your application runs +
+ ) : ( +
+ {filteredLogs.map((log) => ( +
+
+ + +
+ +
+ ))} +
+
+ )} +
+
+ ); +}; + +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 consoleRef = useRef(null); + + const logCounts = logs.reduce((counts, log) => { + counts[log.type] = (counts[log.type] || 0) + 1; + return counts; + }, {}); + + const allRequests = React.useMemo(() => { + const requests = []; + + collections.forEach(collection => { + if (collection.timeline) { + collection.timeline + .filter(entry => entry.type === 'request') + .forEach(entry => { + requests.push({ + ...entry, + collectionName: collection.name, + collectionUid: collection.uid + }); + }); + } + }); + + return requests.sort((a, b) => a.timestamp - b.timestamp); + }, [collections]); + + const filteredLogs = logs.filter(log => filters[log.type]); + const filteredRequests = allRequests.filter(request => { + const method = request.data?.request?.method?.toUpperCase() || 'GET'; + return networkFilters[method]; + }); + + const requestCounts = allRequests.reduce((counts, request) => { + const method = request.data?.request?.method?.toUpperCase() || 'GET'; + counts[method] = (counts[method] || 0) + 1; + return counts; + }, {}); + + const handleFilterToggle = (filterType, enabled) => { + dispatch(updateFilter({ filterType, enabled })); + }; + + const handleNetworkFilterToggle = (method, enabled) => { + dispatch(updateNetworkFilter({ method, enabled })); + }; + + const handleClearLogs = () => { + dispatch(clearLogs()); + }; + + const handleClearDebugErrors = () => { + dispatch(clearDebugErrors()); + }; + + const handlecloseConsole = () => { + dispatch(closeConsole()); + }; + + const handleToggleAllFilters = (enabled) => { + dispatch(toggleAllFilters(enabled)); + }; + + const handleToggleAllNetworkFilters = (enabled) => { + dispatch(toggleAllNetworkFilters(enabled)); + }; + + const handleTabChange = (tab) => { + dispatch(setActiveTab(tab)); + }; + + const renderTabContent = () => { + switch (activeTab) { + case 'console': + return ( + + ); + case 'network': + return ; + case 'debug': + return ; + default: + return ( + + ); + } + }; + + const renderTabControls = () => { + switch (activeTab) { + case 'console': + return ( +
+
+ +
+
+ +
+
+ ); + case 'network': + return ( +
+
+ +
+
+ ); + case 'debug': + return ( +
+
+ {debugErrors.length > 0 && ( + + )} +
+
+ ); + default: + return null; + } + }; + + + + return ( + +
+ +
+
+ + + + + +
+ +
+ {renderTabControls()} + +
+
+ +
+ {activeTab === 'network' && selectedRequest ? ( +
+
+ {renderTabContent()} +
+ +
+ ) : activeTab === 'debug' && selectedError ? ( +
+
+ {renderTabContent()} +
+ +
+ ) : ( + renderTabContent() + )} +
+ + ); +}; + +export default Console; \ No newline at end of file diff --git a/packages/bruno-app/src/components/Devtools/index.js b/packages/bruno-app/src/components/Devtools/index.js new file mode 100644 index 000000000..35c05d49a --- /dev/null +++ b/packages/bruno-app/src/components/Devtools/index.js @@ -0,0 +1,89 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; +import Console from './Console'; + +const MIN_DEVTOOLS_HEIGHT = 150; +const MAX_DEVTOOLS_HEIGHT = window.innerHeight * 0.7; +const DEFAULT_DEVTOOLS_HEIGHT = 300; + +const Devtools = ({ mainSectionRef }) => { + const isDevtoolsOpen = useSelector((state) => state.logs.isConsoleOpen); + const [devtoolsHeight, setDevtoolsHeight] = useState(DEFAULT_DEVTOOLS_HEIGHT); + const [isResizingDevtools, setIsResizingDevtools] = useState(false); + + const handleDevtoolsResizeStart = useCallback((e) => { + e.preventDefault(); + setIsResizingDevtools(true); + }, []); + + 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)); + setDevtoolsHeight(clampedHeight); + + // Update main section height + if (mainSectionRef.current) { + mainSectionRef.current.style.height = `calc(100vh - 22px - ${clampedHeight}px)`; + } + }, [isResizingDevtools, mainSectionRef]); + + const handleDevtoolsResizeEnd = useCallback(() => { + setIsResizingDevtools(false); + }, []); + + useEffect(() => { + if (isResizingDevtools) { + document.addEventListener('mousemove', handleDevtoolsResize); + document.addEventListener('mouseup', handleDevtoolsResizeEnd); + document.body.style.userSelect = 'none'; + + return () => { + document.removeEventListener('mousemove', handleDevtoolsResize); + document.removeEventListener('mouseup', handleDevtoolsResizeEnd); + document.body.style.userSelect = ''; + }; + } + }, [isResizingDevtools, handleDevtoolsResize, handleDevtoolsResizeEnd]); + + // Set initial height + useEffect(() => { + if (mainSectionRef.current && isDevtoolsOpen) { + mainSectionRef.current.style.height = `calc(100vh - 22px - ${devtoolsHeight}px)`; + } + }, [isDevtoolsOpen, devtoolsHeight, mainSectionRef]); + + if (!isDevtoolsOpen) { + return null; + } + + return ( + <> +
e.target.style.backgroundColor = '#0078d4'} + onMouseLeave={(e) => e.target.style.backgroundColor = isResizingDevtools ? '#0078d4' : 'transparent'} + /> +
+ +
+ + ); +}; + +export default Devtools; \ No newline at end of file diff --git a/packages/bruno-app/src/components/ErrorCapture/index.js b/packages/bruno-app/src/components/ErrorCapture/index.js new file mode 100644 index 000000000..8a811276d --- /dev/null +++ b/packages/bruno-app/src/components/ErrorCapture/index.js @@ -0,0 +1,147 @@ +import React, { Component, useEffect } from 'react'; +import { useDispatch } from 'react-redux'; +import { addDebugError } from 'providers/ReduxStore/slices/logs'; + +class ErrorBoundary extends Component { + constructor(props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error) { + return { hasError: true }; + } + + componentDidCatch(error, errorInfo) { + if (this.props.onError) { + this.props.onError({ + message: error.message, + stack: error.stack, + error: error, + timestamp: new Date().toISOString() + }); + } + + setTimeout(() => { + this.setState({ hasError: false }); + }, 100); + } + + render() { + return this.props.children; + } +} + +const serializeArgs = (args) => { + return args.map(arg => { + try { + if (arg === null) return 'null'; + if (arg === undefined) return 'undefined'; + if (typeof arg === 'string' || typeof arg === 'number' || typeof arg === 'boolean') { + return arg; + } + if (arg instanceof Error) { + return { + __type: 'Error', + name: arg.name, + message: arg.message, + stack: arg.stack + }; + } + if (typeof arg === 'object') { + try { + return JSON.parse(JSON.stringify(arg)); + } catch { + return String(arg); + } + } + return String(arg); + } catch (e) { + return '[Unserializable]'; + } + }); +}; + +// 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 { + filename: match[1], + lineno: parseInt(match[2]), + colno: parseInt(match[3]) + }; + } + } + } catch (e) { + // Ignore parsing errors + } + + return { filename: null, lineno: null, colno: null }; +}; + +const useGlobalErrorCapture = () => { + const dispatch = useDispatch(); + + useEffect(() => { + const originalConsoleError = console.error; + + console.error = (...args) => { + const currentStack = new Error().stack; + + originalConsoleError.apply(console, args); + + if (currentStack && currentStack.includes('useIpcEvents.js')) { + return; + } + + const errorMessage = args.join(' '); + if (errorMessage.includes('removeConsoleLogListener')) { + return; + } + + const { filename, lineno, colno } = extractFileInfo(currentStack); + + const serializedArgs = serializeArgs(args); + + dispatch(addDebugError({ + message: errorMessage, + stack: currentStack, + filename: filename, + lineno: lineno, + colno: colno, + args: serializedArgs, + timestamp: new Date().toISOString() + })); + }; + + return () => { + console.error = originalConsoleError; + }; + }, [dispatch]); +}; + +const ErrorCapture = ({ children }) => { + const dispatch = useDispatch(); + + useGlobalErrorCapture(); + + const handleReactError = (errorData) => { + dispatch(addDebugError(errorData)); + }; + + return ( + + {children} + + ); +}; + +export default ErrorCapture; \ No newline at end of file diff --git a/packages/bruno-app/src/components/Notifications/index.js b/packages/bruno-app/src/components/Notifications/index.js index 88a28afe1..0667a4146 100644 --- a/packages/bruno-app/src/components/Notifications/index.js +++ b/packages/bruno-app/src/components/Notifications/index.js @@ -2,6 +2,7 @@ import { IconBell } from '@tabler/icons'; import { useState } from 'react'; import StyledWrapper from './StyleWrapper'; import Modal from 'components/Modal/index'; +import Portal from 'components/Portal'; import { useEffect } from 'react'; import { useApp } from 'providers/App'; import { @@ -109,7 +110,7 @@ const Notifications = () => { > 0 ? 'bell' : ''}`} @@ -121,6 +122,7 @@ const Notifications = () => { {showNotificationsModal && ( + { )}
+ )} ); diff --git a/packages/bruno-app/src/components/Sidebar/Collections/index.js b/packages/bruno-app/src/components/Sidebar/Collections/index.js index b7c8317f4..119318819 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/index.js @@ -113,7 +113,7 @@ const Collections = () => { )}
-
+
{collections && collections.length ? collections.map((c) => { return ( diff --git a/packages/bruno-app/src/components/Sidebar/index.js b/packages/bruno-app/src/components/Sidebar/index.js index 9f476e3c8..1ba71b1ab 100644 --- a/packages/bruno-app/src/components/Sidebar/index.js +++ b/packages/bruno-app/src/components/Sidebar/index.js @@ -1,29 +1,20 @@ import TitleBar from './TitleBar'; import Collections from './Collections'; import StyledWrapper from './StyledWrapper'; -import Preferences from 'components/Preferences'; -import Cookies from 'components/Cookies'; -import ToolHint from 'components/ToolHint'; -import GoldenEdition from './GoldenEdition'; import { useApp } from 'providers/App'; import { useState, useEffect } from 'react'; import { useSelector, useDispatch } from 'react-redux'; -import { IconSettings, IconCookie, IconHeart } from '@tabler/icons'; -import { updateLeftSidebarWidth, updateIsDragging, showPreferences } from 'providers/ReduxStore/slices/app'; +import { updateLeftSidebarWidth, updateIsDragging } from 'providers/ReduxStore/slices/app'; import { useTheme } from 'providers/Theme'; -import Notifications from 'components/Notifications'; const MIN_LEFT_SIDEBAR_WIDTH = 221; const MAX_LEFT_SIDEBAR_WIDTH = 600; const Sidebar = () => { const leftSidebarWidth = useSelector((state) => state.app.leftSidebarWidth); - const preferencesOpen = useSelector((state) => state.app.showPreferences); - const [goldenEditionOpen, setGoldenEditionOpen] = useState(false); const { version } = useApp(); const [asideWidth, setAsideWidth] = useState(leftSidebarWidth); - const [cookiesOpen, setCookiesOpen] = useState(false); const { storedTheme } = useTheme(); @@ -81,112 +72,14 @@ const Sidebar = () => { }, [leftSidebarWidth]); return ( - + diff --git a/packages/bruno-app/src/components/StatusBar/StyledWrapper.js b/packages/bruno-app/src/components/StatusBar/StyledWrapper.js new file mode 100644 index 000000000..72c58cddc --- /dev/null +++ b/packages/bruno-app/src/components/StatusBar/StyledWrapper.js @@ -0,0 +1,85 @@ +import styled from 'styled-components'; + +const StyledWrapper = styled.div` + .status-bar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 16px; + height: 22px; + background: ${(props) => props.theme.sidebar.bg}; + border-top: 1px solid ${(props) => props.theme.sidebar.dragbar}; + color: ${(props) => props.theme.sidebar.color}; + font-size: 12px; + user-select: none; + position: relative; + z-index: 15; + } + + .status-bar-section { + display: flex; + align-items: center; + position: relative; + z-index: 1; + } + + .status-bar-group { + display: flex; + align-items: center; + gap: 2px; + } + + .status-bar-button { + display: flex; + align-items: center; + justify-content: center; + padding: 4px; + color: ${(props) => props.theme.sidebar.color}; + cursor: pointer; + opacity: 0.7; + position: relative; + outline: none; + } + + .console-button-content { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + position: relative; + } + + .console-label { + font-size: 11px; + font-weight: 500; + white-space: nowrap; + } + + .error-count-inline { + font-size: 10px; + font-weight: 600; + color: ${(props) => props.theme.colors.text.danger}; + background: ${(props) => props.theme.colors.bg.danger}20; + padding: 1px 4px; + border-radius: 4px; + } + + .status-bar-divider { + width: 1px; + height: 16px; + background: ${(props) => props.theme.sidebar.dragbar}; + margin: 0 8px; + opacity: 0.3; + } + + .status-bar-version { + display: flex; + align-items: center; + padding: 2px 6px; + font-size: 10px; + color: ${(props) => props.theme.sidebar.muted}; + font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; + } +`; + +export default StyledWrapper; \ No newline at end of file diff --git a/packages/bruno-app/src/components/StatusBar/index.js b/packages/bruno-app/src/components/StatusBar/index.js new file mode 100644 index 000000000..47d687f90 --- /dev/null +++ b/packages/bruno-app/src/components/StatusBar/index.js @@ -0,0 +1,124 @@ +import React, { useState } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { IconSettings, IconCookie, IconTool } from '@tabler/icons'; +import ToolHint from 'components/ToolHint'; +import Preferences from 'components/Preferences'; +import Cookies from 'components/Cookies'; +import Notifications from 'components/Notifications'; +import Portal from 'components/Portal'; +import { showPreferences } from 'providers/ReduxStore/slices/app'; +import { openConsole } from 'providers/ReduxStore/slices/logs'; +import { useApp } from 'providers/App'; +import StyledWrapper from './StyledWrapper'; + +const StatusBar = () => { + const dispatch = useDispatch(); + const preferencesOpen = useSelector((state) => state.app.showPreferences); + const logs = useSelector((state) => state.logs.logs); + const [cookiesOpen, setCookiesOpen] = useState(false); + const { version } = useApp(); + + const errorCount = logs.filter(log => log.type === 'error').length; + + const handleConsoleClick = () => { + dispatch(openConsole()); + }; + + return ( + + {preferencesOpen && ( + + { + dispatch(showPreferences(false)); + document.querySelector('[data-trigger="preferences"]').focus(); + }} + aria-modal="true" + role="dialog" + aria-labelledby="preferences-title" + aria-describedby="preferences-description" + /> + + )} + + {cookiesOpen && ( + + { + setCookiesOpen(false); + document.querySelector('[data-trigger="cookies"]').focus(); + }} + aria-modal="true" + role="dialog" + aria-labelledby="cookies-title" + aria-describedby="cookies-description" + /> + + )} + +
+
+
+ + + + +
+ +
+
+
+ +
+
+ + +
+ +
+ v{version} +
+
+
+
+
+ ); +}; + +export default StatusBar; \ No newline at end of file diff --git a/packages/bruno-app/src/pages/Bruno/StyledWrapper.js b/packages/bruno-app/src/pages/Bruno/StyledWrapper.js index 741978cdf..c570cbf0b 100644 --- a/packages/bruno-app/src/pages/Bruno/StyledWrapper.js +++ b/packages/bruno-app/src/pages/Bruno/StyledWrapper.js @@ -4,8 +4,7 @@ const Wrapper = styled.div` display: flex; width: 100%; height: 100%; - min-height: 100vh; - max-height: 100vh; + flex: 1; &.is-dragging { cursor: col-resize !important; diff --git a/packages/bruno-app/src/pages/Bruno/index.js b/packages/bruno-app/src/pages/Bruno/index.js index f758ce263..36ec4025a 100644 --- a/packages/bruno-app/src/pages/Bruno/index.js +++ b/packages/bruno-app/src/pages/Bruno/index.js @@ -1,14 +1,17 @@ -import React from 'react'; +import React, { useState, useCallback, useRef, useEffect } from 'react'; import classnames from 'classnames'; import Welcome from 'components/Welcome'; import RequestTabs from 'components/RequestTabs'; import RequestTabPanel from 'components/RequestTabPanel'; import Sidebar from 'components/Sidebar'; +import StatusBar from 'components/StatusBar'; +import ErrorCapture from 'components/ErrorCapture'; import { useSelector } from 'react-redux'; import StyledWrapper from './StyledWrapper'; import 'codemirror/theme/material.css'; import 'codemirror/theme/monokai.css'; import 'codemirror/addon/scroll/simplescrollbars.css'; +import Devtools from 'components/Devtools'; require('codemirror/mode/javascript/javascript'); require('codemirror/mode/xml/xml'); @@ -42,34 +45,49 @@ require('utils/codemirror/brunoVarInfo'); require('utils/codemirror/javascript-lint'); require('utils/codemirror/autocomplete'); +const MIN_CONSOLE_HEIGHT = 150; +const MAX_CONSOLE_HEIGHT = window.innerHeight * 0.7; +const DEFAULT_CONSOLE_HEIGHT = 300; + export default function Main() { const activeTabUid = useSelector((state) => state.tabs.activeTabUid); const isDragging = useSelector((state) => state.app.isDragging); const showHomePage = useSelector((state) => state.app.showHomePage); - - // Todo: write a better logging flow that can be used to log by turning on debug flag - // Enable for debugging. - // console.log(useSelector((state) => state.collections.collections)); + const isConsoleOpen = useSelector((state) => state.logs.isConsoleOpen); + const mainSectionRef = useRef(null); const className = classnames({ 'is-dragging': isDragging }); return ( -
- - -
- {showHomePage ? ( - - ) : ( - <> - - - - )} -
-
-
+ +
+
+ + +
+ {showHomePage ? ( + + ) : ( + <> + + + + )} +
+
+
+ + + +
+
); } diff --git a/packages/bruno-app/src/providers/App/useIpcEvents.js b/packages/bruno-app/src/providers/App/useIpcEvents.js index 1828a5890..583e31725 100644 --- a/packages/bruno-app/src/providers/App/useIpcEvents.js +++ b/packages/bruno-app/src/providers/App/useIpcEvents.js @@ -25,6 +25,7 @@ import { useDispatch } from 'react-redux'; import { isElectron } from 'utils/common/platform'; import { globalEnvironmentsUpdateEvent, updateGlobalEnvironments } from 'providers/ReduxStore/slices/global-environments'; import { collectionAddOauth2CredentialsByUrl } from 'providers/ReduxStore/slices/collections/index'; +import { addLog } from 'providers/ReduxStore/slices/logs'; const useIpcEvents = () => { const dispatch = useDispatch(); @@ -131,8 +132,13 @@ const useIpcEvents = () => { dispatch(processEnvUpdateEvent(val)); }); - const removeConsoleLogListener = ipcRenderer.on('main:console-log', (val) => { - console[val.type](...val.args); + const removeConsoleLogListener = ipcRenderer.on('main:console-log', (val) => { + console[val.type](...val.args); + dispatch(addLog({ + type: val.type, + args: val.args, + timestamp: new Date().toISOString() + })); }); const removeConfigUpdatesListener = ipcRenderer.on('main:bruno-config-update', (val) => diff --git a/packages/bruno-app/src/providers/ReduxStore/index.js b/packages/bruno-app/src/providers/ReduxStore/index.js index e02886582..8ed528073 100644 --- a/packages/bruno-app/src/providers/ReduxStore/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/index.js @@ -6,6 +6,7 @@ import collectionsReducer from './slices/collections'; import tabsReducer from './slices/tabs'; import notificationsReducer from './slices/notifications'; import globalEnvironmentsReducer from './slices/global-environments'; +import logsReducer from './slices/logs'; import { draftDetectMiddleware } from './middlewares/draft/middleware'; const isDevEnv = () => { @@ -23,7 +24,8 @@ export const store = configureStore({ collections: collectionsReducer, tabs: tabsReducer, notifications: notificationsReducer, - globalEnvironments: globalEnvironmentsReducer + globalEnvironments: globalEnvironmentsReducer, + logs: logsReducer }, middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(middleware) }); diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/logs.js b/packages/bruno-app/src/providers/ReduxStore/slices/logs.js new file mode 100644 index 000000000..a96f1ec90 --- /dev/null +++ b/packages/bruno-app/src/providers/ReduxStore/slices/logs.js @@ -0,0 +1,143 @@ +import { createSlice } from '@reduxjs/toolkit'; + +const initialState = { + logs: [], + debugErrors: [], + isConsoleOpen: false, + activeTab: 'console', + filters: { + info: true, + warn: true, + error: true, + debug: true, + log: true + }, + networkFilters: { + GET: true, + POST: true, + PUT: true, + DELETE: true, + PATCH: true, + HEAD: true, + OPTIONS: true + }, + selectedRequest: null, + selectedError: null, + maxLogs: 1000, + maxDebugErrors: 500 +}; + +export const logsSlice = createSlice({ + name: 'logs', + initialState, + reducers: { + addLog: (state, action) => { + const { type, args, timestamp } = action.payload; + const newLog = { + id: Date.now() + Math.random(), + type: type || 'log', + message: args ? args.join(' ') : '', + args: args || [], + timestamp: timestamp || new Date().toISOString() + }; + + state.logs.push(newLog); + + if (state.logs.length > state.maxLogs) { + state.logs = state.logs.slice(-state.maxLogs); + } + }, + addDebugError: (state, action) => { + const { message, stack, filename, lineno, colno, args, timestamp } = action.payload; + const newError = { + id: Date.now() + Math.random(), + message: message || 'Unknown error', + stack: stack, + filename: filename, + lineno: lineno, + colno: colno, + args: args || [], + timestamp: timestamp || new Date().toISOString() + }; + + state.debugErrors.push(newError); + + if (state.debugErrors.length > state.maxDebugErrors) { + state.debugErrors = state.debugErrors.slice(-state.maxDebugErrors); + } + }, + clearLogs: (state) => { + state.logs = []; + }, + clearDebugErrors: (state) => { + state.debugErrors = []; + }, + openConsole: (state) => { + state.isConsoleOpen = true; + }, + closeConsole: (state) => { + state.isConsoleOpen = false; + }, + setActiveTab: (state, action) => { + state.activeTab = action.payload; + if (action.payload !== 'network') { + state.selectedRequest = null; + } + if (action.payload !== 'debug') { + state.selectedError = null; + } + }, + updateFilter: (state, action) => { + const { filterType, enabled } = action.payload; + state.filters[filterType] = enabled; + }, + toggleAllFilters: (state, action) => { + const enabled = action.payload; + Object.keys(state.filters).forEach(key => { + state.filters[key] = enabled; + }); + }, + updateNetworkFilter: (state, action) => { + const { method, enabled } = action.payload; + state.networkFilters[method] = enabled; + }, + toggleAllNetworkFilters: (state, action) => { + const enabled = action.payload; + Object.keys(state.networkFilters).forEach(key => { + state.networkFilters[key] = enabled; + }); + }, + setSelectedRequest: (state, action) => { + state.selectedRequest = action.payload; + }, + clearSelectedRequest: (state) => { + state.selectedRequest = null; + }, + setSelectedError: (state, action) => { + state.selectedError = action.payload; + }, + clearSelectedError: (state) => { + state.selectedError = null; + } + } +}); + +export const { + addLog, + addDebugError, + clearLogs, + clearDebugErrors, + openConsole, + closeConsole, + setActiveTab, + updateFilter, + toggleAllFilters, + updateNetworkFilter, + toggleAllNetworkFilters, + setSelectedRequest, + clearSelectedRequest, + setSelectedError, + clearSelectedError +} = logsSlice.actions; + +export default logsSlice.reducer; \ No newline at end of file diff --git a/packages/bruno-app/src/themes/dark.js b/packages/bruno-app/src/themes/dark.js index ec2e8d212..b924af361 100644 --- a/packages/bruno-app/src/themes/dark.js +++ b/packages/bruno-app/src/themes/dark.js @@ -295,6 +295,33 @@ const darkTheme = { bg: '#1f1f1f', border: '#333333', boxShadow: '0 4px 12px rgba(0, 0, 0, 0.5)' + }, + + console: { + bg: '#1e1e1e', + headerBg: '#2d2d30', + contentBg: '#1e1e1e', + border: '#3c3c3c', + titleColor: '#cccccc', + countColor: '#858585', + buttonColor: '#cccccc', + buttonHoverBg: 'rgba(255, 255, 255, 0.1)', + buttonHoverColor: '#ffffff', + messageColor: '#cccccc', + timestampColor: '#858585', + emptyColor: '#858585', + logHoverBg: 'rgba(255, 255, 255, 0.05)', + resizeHandleHover: '#0078d4', + resizeHandleActive: '#0078d4', + dropdownBg: '#2d2d30', + dropdownHeaderBg: '#3c3c3c', + optionHoverBg: 'rgba(255, 255, 255, 0.05)', + optionLabelColor: '#cccccc', + optionCountColor: '#858585', + checkboxColor: '#0078d4', + scrollbarTrack: '#2d2d30', + scrollbarThumb: '#5a5a5a', + scrollbarThumbHover: '#6a6a6a' } }; diff --git a/packages/bruno-app/src/themes/light.js b/packages/bruno-app/src/themes/light.js index cdcb8de26..9279462db 100644 --- a/packages/bruno-app/src/themes/light.js +++ b/packages/bruno-app/src/themes/light.js @@ -296,6 +296,33 @@ const lightTheme = { bg: 'white', border: '#e0e0e0', boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)' + }, + + console: { + bg: '#f8f9fa', + headerBg: '#f8f9fa', + contentBg: '#ffffff', + border: '#dee2e6', + titleColor: '#212529', + countColor: '#6c757d', + buttonColor: '#495057', + buttonHoverBg: '#e9ecef', + buttonHoverColor: '#212529', + messageColor: '#212529', + timestampColor: '#6c757d', + emptyColor: '#6c757d', + logHoverBg: 'rgba(0, 0, 0, 0.03)', + resizeHandleHover: '#0d6efd', + resizeHandleActive: '#0d6efd', + dropdownBg: '#ffffff', + dropdownHeaderBg: '#f8f9fa', + optionHoverBg: '#f8f9fa', + optionLabelColor: '#212529', + optionCountColor: '#6c757d', + checkboxColor: '#0d6efd', + scrollbarTrack: '#f8f9fa', + scrollbarThumb: '#ced4da', + scrollbarThumbHover: '#adb5bd' } };