added: status bar & console (#4922)

* added: status bar & console
This commit is contained in:
naman-bruno
2025-07-14 19:05:57 +05:30
committed by GitHub
parent 48989ceea9
commit b9da31d24e
26 changed files with 3907 additions and 138 deletions

206
package-lock.json generated
View File

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

View File

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

View File

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

View File

@@ -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 (
<div
className={`error-row ${isSelected ? 'selected' : ''}`}
onClick={onClick}
>
<div className="error-message" title={error.message}>
{getShortMessage(error.message)}
</div>
<div className="error-location" title={error.filename}>
{getLocation(error)}
</div>
<div className="error-time">
{formatTime(error.timestamp)}
</div>
</div>
);
};
const DebugTab = () => {
const dispatch = useDispatch();
const { debugErrors, selectedError } = useSelector(state => state.logs);
const handleErrorClick = (error) => {
dispatch(setSelectedError(error));
};
const handleClearErrors = () => {
dispatch(clearDebugErrors());
};
return (
<StyledWrapper>
<div className="debug-content">
{debugErrors.length === 0 ? (
<div className="debug-empty">
<IconBug size={48} strokeWidth={1} />
<p>No errors</p>
<span>console.error() calls will appear here</span>
</div>
) : (
<div className="errors-container">
<div className="errors-header">
<div className="header-message">Message</div>
<div className="header-location">Location</div>
<div className="header-time">Time</div>
</div>
<div className="errors-list">
{debugErrors.map((error, index) => (
<ErrorRow
key={error.id}
error={error}
isSelected={selectedError?.id === error.id}
onClick={() => handleErrorClick(error)}
/>
))}
</div>
</div>
)}
</div>
</StyledWrapper>
);
};
export default DebugTab;

View File

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

View File

@@ -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 (
<div className="tab-content">
<div className="section">
<h4>Error Information</h4>
<div className="info-grid">
<div className="info-item">
<label>Message:</label>
<span className="error-message-full">{error.message || 'No message available'}</span>
</div>
{error.filename && (
<div className="info-item">
<label>File:</label>
<span className="file-path">{error.filename}</span>
</div>
)}
{error.lineno && (
<div className="info-item">
<label>Line:</label>
<span>{error.lineno}{error.colno ? `:${error.colno}` : ''}</span>
</div>
)}
<div className="info-item">
<label>Timestamp:</label>
<span>{formatTimestamp(error.timestamp)}</span>
</div>
</div>
</div>
<div className="section">
<h4>Report Issue</h4>
<div className="report-section">
<p>Found a bug? Help us improve Bruno by reporting this error on GitHub.</p>
<button
className="report-button"
onClick={handleReportIssue}
title="Report this error on GitHub"
>
<IconBrandGithub size={16} strokeWidth={1.5} />
<span>Report Issue on GitHub</span>
</button>
</div>
</div>
</div>
);
};
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 (
<div className="tab-content">
<div className="section">
<h4>Stack Trace</h4>
<div className="stack-trace-container">
<pre className="stack-trace">
{formatStackTrace(error.stack)}
</pre>
</div>
</div>
</div>
);
};
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 (
<div className="tab-content">
<div className="section">
<h4>Arguments</h4>
<div className="arguments-container">
<pre className="arguments">
{formatArguments(error.args)}
</pre>
</div>
</div>
</div>
);
};
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 <ErrorInfoTab error={selectedError} />;
case 'stack':
return <StackTraceTab error={selectedError} />;
case 'args':
return <ArgumentsTab error={selectedError} />;
default:
return <ErrorInfoTab error={selectedError} />;
}
};
return (
<StyledWrapper>
<div className="panel-header">
<div className="panel-title">
<IconBug size={16} strokeWidth={1.5} />
<span>Error Details</span>
<span className="error-time">({formatTime(selectedError.timestamp)})</span>
</div>
<button
className="close-button"
onClick={handleClose}
title="Close details panel"
>
<IconX size={16} strokeWidth={1.5} />
</button>
</div>
<div className="panel-tabs">
<button
className={`tab-button ${activeTab === 'info' ? 'active' : ''}`}
onClick={() => setActiveTab('info')}
>
<IconFileText size={14} strokeWidth={1.5} />
Info
</button>
<button
className={`tab-button ${activeTab === 'stack' ? 'active' : ''}`}
onClick={() => setActiveTab('stack')}
>
<IconStack size={14} strokeWidth={1.5} />
Stack
</button>
<button
className={`tab-button ${activeTab === 'args' ? 'active' : ''}`}
onClick={() => setActiveTab('args')}
>
<IconCode size={14} strokeWidth={1.5} />
Args
</button>
</div>
<div className="panel-content">
{getTabContent()}
</div>
</StyledWrapper>
);
};
export default ErrorDetailsPanel;

View File

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

View File

@@ -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 (
<span
className="method-badge"
style={{ backgroundColor: getMethodColor(method) }}
>
{method?.toUpperCase() || 'GET'}
</span>
);
};
const StatusBadge = ({ status, statusCode }) => {
const getStatusColor = (code) => {
if (code >= 200 && code < 300) return '#10b981';
if (code >= 300 && code < 400) return '#f59e0b';
if (code >= 400 && code < 500) return '#ef4444';
if (code >= 500) return '#dc2626';
return '#6b7280';
};
const displayStatus = statusCode || status;
return (
<span
className="status-badge"
style={{ color: getStatusColor(statusCode) }}
>
{displayStatus}
</span>
);
};
const NetworkFilterDropdown = ({ filters, requestCounts, onFilterToggle, onToggleAll }) => {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null);
const allFiltersEnabled = Object.values(filters).every(f => f);
const activeFilters = Object.entries(filters).filter(([_, enabled]) => enabled);
useEffect(() => {
const handleClickOutside = (event) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
return (
<div className="filter-dropdown" ref={dropdownRef}>
<button
className="filter-dropdown-trigger"
onClick={() => setIsOpen(!isOpen)}
title="Filter requests by method"
>
<IconFilter size={16} strokeWidth={1.5} />
<span className="filter-summary">
{activeFilters.length === Object.keys(filters).length ? 'All' : `${activeFilters.length}/${Object.keys(filters).length}`}
</span>
<IconChevronDown size={14} strokeWidth={1.5} />
</button>
{isOpen && (
<div className={`filter-dropdown-menu right`}>
<div className="filter-dropdown-header">
<span>Filter by Method</span>
<button
className="filter-toggle-all"
onClick={() => onToggleAll(!allFiltersEnabled)}
>
{allFiltersEnabled ? 'Hide All' : 'Show All'}
</button>
</div>
<div className="filter-dropdown-options">
{Object.keys(filters).map(method => (
<label key={method} className="filter-option">
<input
type="checkbox"
checked={filters[method]}
onChange={(e) => onFilterToggle(method, e.target.checked)}
/>
<div className="filter-option-content">
<MethodBadge method={method} />
<span className="filter-option-label">{method}</span>
<span className="filter-option-count">({requestCounts[method] || 0})</span>
</div>
</label>
))}
</div>
</div>
)}
</div>
);
};
const RequestRow = ({ request, isSelected, onClick }) => {
const { data } = request;
const { request: req, response: res, timestamp } = data;
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 (
<div
className={`request-row ${isSelected ? 'selected' : ''}`}
onClick={onClick}
>
<div className="request-method">
<MethodBadge method={req?.method} />
</div>
<div className="request-status">
<StatusBadge status={res?.status} statusCode={res?.statusCode} />
</div>
<div className="request-domain" title={getDomain()}>
{getDomain()}
</div>
<div className="request-path" title={getPath()}>
{getPath()}
</div>
<div className="request-time">
{formatTime(timestamp)}
</div>
<div className="request-duration">
{formatDuration(res?.duration)}
</div>
<div className="request-size">
{formatSize(res?.size)}
</div>
</div>
);
};
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 (
<StyledWrapper>
<div className="network-content">
{filteredRequests.length === 0 ? (
<div className="network-empty">
<IconNetwork size={48} strokeWidth={1} />
<p>No network requests</p>
<span>Requests will appear here as you make API calls</span>
</div>
) : (
<div className="requests-container">
<div className="requests-header">
<div className="header-method">Method</div>
<div className="header-status">Status</div>
<div className="header-domain">Domain</div>
<div className="header-path">Path</div>
<div className="header-time">Time</div>
<div className="header-duration">Duration</div>
<div className="header-size">Size</div>
</div>
<div className="requests-list">
{filteredRequests.map((request, index) => (
<RequestRow
key={`${request.collectionUid}-${request.itemUid}-${request.timestamp}-${index}`}
request={request}
isSelected={selectedRequest?.timestamp === request.timestamp && selectedRequest?.itemUid === request.itemUid}
onClick={() => handleRequestClick(request)}
/>
))}
</div>
</div>
)}
</div>
</StyledWrapper>
);
};
export default NetworkTab;

View File

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

View File

@@ -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 (
<div className="tab-content">
<div className="section">
<h4>General</h4>
<div className="info-grid">
<div className="info-item">
<span className="label">Request URL:</span>
<span className="value">{request?.url || 'N/A'}</span>
</div>
<div className="info-item">
<span className="label">Request Method:</span>
<span className="value">{request?.method || 'GET'}</span>
</div>
</div>
</div>
<div className="section">
<h4>Request Headers</h4>
{formatHeaders(request?.headers).length > 0 ? (
<div className="headers-table">
<table>
<thead>
<tr>
<td>Name</td>
<td>Value</td>
</tr>
</thead>
<tbody>
{formatHeaders(request.headers).map((header, index) => (
<tr key={index}>
<td className="header-name">{header.name}</td>
<td className="header-value">{header.value}</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="empty-state">No headers</div>
)}
</div>
{request?.body && (
<div className="section">
<h4>Request Body</h4>
<pre className="code-block">{formatBody(request.body)}</pre>
</div>
)}
</div>
);
};
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 (
<div className="tab-content">
<div className="section">
<h4>Response Headers</h4>
{formatHeaders(response?.headers).length > 0 ? (
<div className="headers-table">
<table>
<thead>
<tr>
<td>Name</td>
<td>Value</td>
</tr>
</thead>
<tbody>
{formatHeaders(response.headers).map((header, index) => (
<tr key={index}>
<td className="header-name">{header.name}</td>
<td className="header-value">{header.value}</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="empty-state">No headers</div>
)}
</div>
<div className="section">
<h4>Response Body</h4>
<div className="response-body-container">
{response?.data || response?.dataBuffer ? (
<QueryResult
item={{ uid: uuid()}}
collection={collection}
data={response.data}
dataBuffer={response.dataBuffer}
headers={response.headers}
error={response.error}
disableRunEventListener={true}
/>
) : (
<div className="empty-state">No response data</div>
)}
</div>
</div>
</div>
);
};
const NetworkTab = ({ response }) => {
const timeline = response?.timeline || [];
return (
<div className="tab-content">
<div className="section">
<h4>Network Logs</h4>
<div className="network-logs-container">
{timeline.length > 0 ? (
<Network logs={timeline} />
) : (
<div className="empty-state">No network logs available</div>
)}
</div>
</div>
</div>
);
};
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 <RequestTab request={request} response={response} />;
case 'response':
return <ResponseTab response={response} request={request} collection={collection} />;
case 'network':
return <NetworkTab response={response} />;
default:
return <RequestTab request={request} response={response} />;
}
};
return (
<StyledWrapper>
<div className="panel-header">
<div className="panel-title">
<IconFileText size={16} strokeWidth={1.5} />
<span>Request Details</span>
<span className="request-time">({formatTime(selectedRequest.timestamp)})</span>
</div>
<button
className="close-button"
onClick={handleClose}
title="Close details panel"
>
<IconX size={16} strokeWidth={1.5} />
</button>
</div>
<div className="panel-tabs">
<button
className={`tab-button ${activeTab === 'request' ? 'active' : ''}`}
onClick={() => setActiveTab('request')}
>
<IconArrowRight size={14} strokeWidth={1.5} />
Request
</button>
<button
className={`tab-button ${activeTab === 'response' ? 'active' : ''}`}
onClick={() => setActiveTab('response')}
>
<IconFileText size={14} strokeWidth={1.5} />
Response
</button>
<button
className={`tab-button ${activeTab === 'network' ? 'active' : ''}`}
onClick={() => setActiveTab('network')}
>
<IconNetwork size={14} strokeWidth={1.5} />
Network
</button>
</div>
<div className="panel-content">
{getTabContent()}
</div>
</StyledWrapper>
);
};
export default RequestDetailsPanel;

View File

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

View File

@@ -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 <IconAlertCircle className="log-icon error" {...iconProps} />;
case 'warn':
return <IconAlertTriangle className="log-icon warn" {...iconProps} />;
case 'info':
return <IconAlertTriangle className="log-icon info" {...iconProps} />;
case 'debug':
return <IconBug className="log-icon debug" {...iconProps} />;
default:
return <IconCode className="log-icon log" {...iconProps} />;
}
};
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 <span className="log-timestamp">{time}</span>;
};
const LogMessage = ({ message, args }) => {
const { displayedTheme } = useTheme();
const formatMessage = (msg, originalArgs) => {
if (originalArgs && originalArgs.length > 0) {
return originalArgs.map((arg, index) => {
if (typeof arg === 'object' && arg !== null) {
return (
<div key={index} className="log-object">
<ReactJson
src={arg}
theme={displayedTheme === 'light' ? 'rjv-default' : 'monokai'}
iconStyle="triangle"
indentWidth={2}
collapsed={1}
displayDataTypes={false}
displayObjectSize={false}
enableClipboard={false}
name={false}
style={{
backgroundColor: 'transparent',
fontSize: '12px',
fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace'
}}
/>
</div>
);
}
return String(arg);
});
}
return msg;
};
const formattedMessage = formatMessage(message, args);
return (
<span className="log-message">
{Array.isArray(formattedMessage) ? formattedMessage.map((item, index) => (
<span key={index}>{item} </span>
)) : formattedMessage}
</span>
);
};
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 (
<div className="filter-dropdown" ref={dropdownRef}>
<button
className="filter-dropdown-trigger"
onClick={() => setIsOpen(!isOpen)}
title="Filter logs by type"
>
<IconFilter size={16} strokeWidth={1.5} />
<span className="filter-summary">
{activeFilters.length === Object.keys(filters).length ? 'All' : `${activeFilters.length}/${Object.keys(filters).length}`}
</span>
<IconChevronDown size={14} strokeWidth={1.5} />
</button>
{isOpen && (
<div className={`filter-dropdown-menu right`}>
<div className="filter-dropdown-header">
<span>Filter by Type</span>
<button
className="filter-toggle-all"
onClick={() => onToggleAll(!allFiltersEnabled)}
>
{allFiltersEnabled ? 'Hide All' : 'Show All'}
</button>
</div>
<div className="filter-dropdown-options">
{Object.entries(filters).map(([filterType, enabled]) => (
<label key={filterType} className="filter-option">
<input
type="checkbox"
checked={enabled}
onChange={(e) => onFilterToggle(filterType, e.target.checked)}
/>
<div className="filter-option-content">
<LogIcon type={filterType} />
<span className="filter-option-label">{filterType}</span>
<span className="filter-option-count">({logCounts[filterType] || 0})</span>
</div>
</label>
))}
</div>
</div>
)}
</div>
);
};
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 (
<div className="filter-dropdown" ref={dropdownRef}>
<button
className="filter-dropdown-trigger"
onClick={() => setIsOpen(!isOpen)}
title="Filter requests by method"
>
<IconFilter size={16} strokeWidth={1.5} />
<span className="filter-summary">
{activeFilters.length === Object.keys(filters).length ? 'All' : `${activeFilters.length}/${Object.keys(filters).length}`}
</span>
<IconChevronDown size={14} strokeWidth={1.5} />
</button>
{isOpen && (
<div className={`filter-dropdown-menu right`}>
<div className="filter-dropdown-header">
<span>Filter by Method</span>
<button
className="filter-toggle-all"
onClick={() => onToggleAll(!allFiltersEnabled)}
>
{allFiltersEnabled ? 'Hide All' : 'Show All'}
</button>
</div>
<div className="filter-dropdown-options">
{Object.entries(filters).map(([method, enabled]) => (
<label key={method} className="filter-option">
<input
type="checkbox"
checked={enabled}
onChange={(e) => onFilterToggle(method, e.target.checked)}
/>
<div className="filter-option-content">
<span className="method-badge" style={{ backgroundColor: getMethodColor(method) }}>
{method}
</span>
<span className="filter-option-label">{method}</span>
<span className="filter-option-count">({requestCounts[method] || 0})</span>
</div>
</label>
))}
</div>
</div>
)}
</div>
);
};
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 (
<div className="tab-content">
<div className="tab-content-area">
{filteredLogs.length === 0 ? (
<div className="console-empty">
<IconTerminal2 size={48} strokeWidth={1} />
<p>No logs to display</p>
<span>Logs will appear here as your application runs</span>
</div>
) : (
<div className="logs-container">
{filteredLogs.map((log) => (
<div key={log.id} className={`log-entry ${log.type}`}>
<div className="log-meta">
<LogTimestamp timestamp={log.timestamp} />
<LogIcon type={log.type} />
</div>
<LogMessage message={log.message} args={log.args} />
</div>
))}
<div ref={logsEndRef} />
</div>
)}
</div>
</div>
);
};
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 (
<ConsoleTab
logs={logs}
filters={filters}
logCounts={logCounts}
onFilterToggle={handleFilterToggle}
onToggleAll={handleToggleAllFilters}
onClearLogs={handleClearLogs}
/>
);
case 'network':
return <NetworkTab />;
case 'debug':
return <DebugTab />;
default:
return (
<ConsoleTab
logs={logs}
filters={filters}
logCounts={logCounts}
onFilterToggle={handleFilterToggle}
onToggleAll={handleToggleAllFilters}
onClearLogs={handleClearLogs}
/>
);
}
};
const renderTabControls = () => {
switch (activeTab) {
case 'console':
return (
<div className="tab-controls">
<div className="filter-controls">
<FilterDropdown
filters={filters}
logCounts={logCounts}
onFilterToggle={handleFilterToggle}
onToggleAll={handleToggleAllFilters}
/>
</div>
<div className="action-controls">
<button
className="control-button"
onClick={handleClearLogs}
title="Clear all logs"
>
<IconTrash size={16} strokeWidth={1.5} />
</button>
</div>
</div>
);
case 'network':
return (
<div className="tab-controls">
<div className="filter-controls">
<NetworkFilterDropdown
filters={networkFilters}
requestCounts={requestCounts}
onFilterToggle={handleNetworkFilterToggle}
onToggleAll={handleToggleAllNetworkFilters}
/>
</div>
</div>
);
case 'debug':
return (
<div className="tab-controls">
<div className="action-controls">
{debugErrors.length > 0 && (
<button
className="control-button"
onClick={handleClearDebugErrors}
title="Clear all errors"
>
<IconTrash size={16} strokeWidth={1.5} />
</button>
)}
</div>
</div>
);
default:
return null;
}
};
return (
<StyledWrapper ref={consoleRef}>
<div
className="console-resize-handle"
/>
<div className="console-header">
<div className="console-tabs">
<button
className={`console-tab ${activeTab === 'console' ? 'active' : ''}`}
onClick={() => handleTabChange('console')}
>
<IconTerminal2 size={16} strokeWidth={1.5} />
<span>Console</span>
</button>
<button
className={`console-tab ${activeTab === 'network' ? 'active' : ''}`}
onClick={() => handleTabChange('network')}
>
<IconNetwork size={16} strokeWidth={1.5} />
<span>Network</span>
</button>
<button
className={`console-tab ${activeTab === 'debug' ? 'active' : ''}`}
onClick={() => handleTabChange('debug')}
>
<IconBug size={16} strokeWidth={1.5} />
<span>Debug</span>
</button>
</div>
<div className="console-controls">
{renderTabControls()}
<button
className="control-button close-button"
onClick={handlecloseConsole}
title="Close console"
>
<IconX size={16} strokeWidth={1.5} />
</button>
</div>
</div>
<div className="console-content">
{activeTab === 'network' && selectedRequest ? (
<div className="network-with-details">
<div className="network-main">
{renderTabContent()}
</div>
<RequestDetailsPanel />
</div>
) : activeTab === 'debug' && selectedError ? (
<div className="debug-with-details">
<div className="debug-main">
{renderTabContent()}
</div>
<ErrorDetailsPanel />
</div>
) : (
renderTabContent()
)}
</div>
</StyledWrapper>
);
};
export default Console;

View File

@@ -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 (
<>
<div
className="devtools-resize-handle"
onMouseDown={handleDevtoolsResizeStart}
style={{
height: '4px',
cursor: 'row-resize',
backgroundColor: isResizingDevtools ? '#0078d4' : 'transparent',
transition: 'background-color 0.2s ease',
zIndex: 20,
position: 'relative'
}}
onMouseEnter={(e) => e.target.style.backgroundColor = '#0078d4'}
onMouseLeave={(e) => e.target.style.backgroundColor = isResizingDevtools ? '#0078d4' : 'transparent'}
/>
<div style={{ height: `${devtoolsHeight}px`, overflow: 'hidden', zIndex: 20, position: 'relative' }}>
<Console />
</div>
</>
);
};
export default Devtools;

View File

@@ -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 (
<ErrorBoundary onError={handleReactError}>
{children}
</ErrorBoundary>
);
};
export default ErrorCapture;

View File

@@ -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 = () => {
>
<ToolHint text="Notifications" toolhintId="Notifications" offset={8}>
<IconBell
size={18}
size={16}
aria-hidden
strokeWidth={1.5}
className={`mr-2 ${unreadNotifications?.length > 0 ? 'bell' : ''}`}
@@ -121,6 +122,7 @@ const Notifications = () => {
</a>
{showNotificationsModal && (
<Portal>
<Modal
size="lg"
title="Notifications"
@@ -203,6 +205,7 @@ const Notifications = () => {
)}
</div>
</Modal>
</Portal>
)}
</StyledWrapper>
);

View File

@@ -113,7 +113,7 @@ const Collections = () => {
)}
</div>
<div className="mt-4 flex flex-col overflow-hidden hover:overflow-y-auto absolute top-32 bottom-10 left-0 right-0">
<div className="mt-4 flex flex-col overflow-hidden hover:overflow-y-auto absolute top-32 bottom-0 left-0 right-0">
{collections && collections.length
? collections.map((c) => {
return (

View File

@@ -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 (
<StyledWrapper className="flex relative h-screen">
<StyledWrapper className="flex relative h-full">
<aside>
{goldenEditionOpen && (
<GoldenEdition
onClose={() => {
setGoldenEditionOpen(false);
document.querySelector('[data-trigger="golden-edition"]').focus();
}}
aria-modal="true"
role="dialog"
aria-labelledby="golden-edition-title"
aria-describedby="golden-edition-description"
/>
)}
<div className="flex flex-row h-screen w-full">
{preferencesOpen && (
<Preferences
onClose={() => {
dispatch(showPreferences(false));
document.querySelector('[data-trigger="preferences"]').focus();
}}
aria-modal="true"
role="dialog"
aria-labelledby="preferences-title"
aria-describedby="preferences-description"
/>
)}
{cookiesOpen && (
<Cookies
onClose={() => {
setCookiesOpen(false);
document.querySelector('[data-trigger="cookies"]').focus();
}}
aria-modal="true"
role="dialog"
aria-labelledby="cookies-title"
aria-describedby="cookies-description"
/>
)}
<div className="flex flex-row h-full w-full">
<div className="flex flex-col w-full" style={{ width: asideWidth }}>
<div className="flex flex-col flex-grow">
<TitleBar />
<Collections />
</div>
<div className="footer flex px-1 py-2 absolute bottom-0 left-0 right-0 items-center select-none">
<div className="flex items-center ml-1 text-xs ">
<ul role="menubar" className="flex space-x-2">
<li role="menuitem">
<a
className="cursor-pointer"
data-trigger="preferences"
onClick={() => dispatch(showPreferences(true))}
tabIndex={0}
aria-label="Open Preferences"
>
<ToolHint text="Preferences" toolhintId="Preferences" effect="float" place="top-start" offset={8}>
<IconSettings size={18} strokeWidth={1.5} aria-hidden="true" />
</ToolHint>
</a>
</li>
<li role="menuitem">
<a
className="cursor-pointer"
data-trigger="cookies"
onClick={() => setCookiesOpen(true)}
tabIndex={0}
aria-label="Open Cookies Settings"
>
<ToolHint text="Cookies" toolhintId="Cookies" offset={8}>
<IconCookie size={18} strokeWidth={1.5} aria-hidden="true" />
</ToolHint>
</a>
</li>
<li role="menuitem">
<a
className="cursor-pointer"
data-trigger="golden-edition"
onClick={() => setGoldenEditionOpen(true)}
tabIndex={0}
aria-label="Open Golden Edition"
>
<ToolHint text="Golden Edition" toolhintId="Golden Edition" offset={8}>
<IconHeart size={18} strokeWidth={1.5} aria-hidden="true" />
</ToolHint>
</a>
</li>
<li role="menuitem">
<Notifications />
</li>
</ul>
</div>
<div className="pl-1" style={{ position: 'relative', top: '3px' }}>
{/* This will get moved to home page */}
{/* <GitHubButton
href="https://github.com/usebruno/bruno"
data-color-scheme={storedTheme}
data-show-count="true"
aria-label="Star usebruno/bruno on GitHub"
>
Star
</GitHubButton> */}
</div>
<div className="flex flex-grow items-center justify-end text-xs mr-2">v{version}</div>
</div>
</div>
</div>
</aside>

View File

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

View File

@@ -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 (
<StyledWrapper>
{preferencesOpen && (
<Portal>
<Preferences
onClose={() => {
dispatch(showPreferences(false));
document.querySelector('[data-trigger="preferences"]').focus();
}}
aria-modal="true"
role="dialog"
aria-labelledby="preferences-title"
aria-describedby="preferences-description"
/>
</Portal>
)}
{cookiesOpen && (
<Portal>
<Cookies
onClose={() => {
setCookiesOpen(false);
document.querySelector('[data-trigger="cookies"]').focus();
}}
aria-modal="true"
role="dialog"
aria-labelledby="cookies-title"
aria-describedby="cookies-description"
/>
</Portal>
)}
<div className="status-bar">
<div className="status-bar-section">
<div className="status-bar-group">
<button
className="status-bar-button"
data-trigger="preferences"
onClick={() => dispatch(showPreferences(true))}
tabIndex={0}
aria-label="Open Preferences"
>
<ToolHint text="Pref." toolhintId="Preferences" place="top" offset={10}>
<IconSettings size={16} strokeWidth={1.5} aria-hidden="true" />
</ToolHint>
</button>
<button
className="status-bar-button"
data-trigger="cookies"
onClick={() => setCookiesOpen(true)}
tabIndex={0}
aria-label="Open Cookies Settings"
>
<ToolHint text="Cookies" toolhintId="Cookies" place="top" offset={10}>
<IconCookie size={16} strokeWidth={1.5} aria-hidden="true" />
</ToolHint>
</button>
<div className="status-bar-notifications">
<Notifications />
</div>
</div>
</div>
<div className="status-bar-section">
<div className="status-bar-group">
<button
className={`status-bar-button ${errorCount > 0 ? 'has-errors' : ''}`}
data-trigger="console"
onClick={handleConsoleClick}
tabIndex={0}
aria-label={`Open Console${errorCount > 0 ? ` (${errorCount} errors)` : ''}`}
>
<ToolHint text={`Console${errorCount > 0 ? ` (${errorCount} errors)` : ''}`} toolhintId="Console" place="top" offset={10}>
<div className="console-button-content">
<IconTool size={16} strokeWidth={1.5} aria-hidden="true" />
<span className="console-label">Console</span>
{errorCount > 0 && (
<span className="error-count-inline">{errorCount}</span>
)}
</div>
</ToolHint>
</button>
<div className="status-bar-divider"></div>
<div className="status-bar-version">
v{version}
</div>
</div>
</div>
</div>
</StyledWrapper>
);
};
export default StatusBar;

View File

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

View File

@@ -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 (
<div>
<StyledWrapper className={className}>
<Sidebar />
<section className="flex flex-grow flex-col overflow-auto">
{showHomePage ? (
<Welcome />
) : (
<>
<RequestTabs />
<RequestTabPanel key={activeTabUid} />
</>
)}
</section>
</StyledWrapper>
</div>
<ErrorCapture>
<div className="flex flex-col h-screen max-h-screen overflow-hidden">
<div
ref={mainSectionRef}
className="flex-1 min-h-0 flex"
style={{
height: isConsoleOpen ? `calc(100vh - 22px - ${isConsoleOpen ? '300px' : '0px'})` : 'calc(100vh - 22px)'
}}
>
<StyledWrapper className={className} style={{ height: '100%', zIndex: 1 }}>
<Sidebar />
<section className="flex flex-grow flex-col overflow-hidden">
{showHomePage ? (
<Welcome />
) : (
<>
<RequestTabs />
<RequestTabPanel key={activeTabUid} />
</>
)}
</section>
</StyledWrapper>
</div>
<Devtools mainSectionRef={mainSectionRef} />
<StatusBar />
</div>
</ErrorCapture>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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