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

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;