mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-08 14:35:03 +00:00
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user