mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-16 04:11:29 +00:00
fix: use theme styling within timeline (#6604)
* fix: use theme styling within timeline * fix: remove inline styling and use css classes * fix: network logs within dev tools * compact timeline for grpc * refactor: standardize CSS class naming in StyledWrapper components for better readability * remove styling configuration from Network component * fix: update colors * update colors * fix: color
This commit is contained in:
@@ -305,7 +305,7 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
}
|
||||
|
||||
.network-logs-container {
|
||||
.network-logs-wrapper {
|
||||
border: 1px solid ${(props) => props.theme.console.border};
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
@@ -313,17 +313,17 @@ const StyledWrapper = styled.div`
|
||||
min-height: 200px;
|
||||
max-height: 400px;
|
||||
|
||||
.network-logs {
|
||||
.network-logs-container {
|
||||
background: ${(props) => props.theme.console.contentBg} !important;
|
||||
color: ${(props) => props.theme.console.messageColor} !important;
|
||||
height: 100% !important;
|
||||
max-height: 400px !important;
|
||||
padding: 0.5rem !important;
|
||||
|
||||
pre {
|
||||
.network-logs-pre {
|
||||
color: ${(props) => props.theme.console.messageColor} !important;
|
||||
font-size: ${(props) => props.theme.font.size.xs} !important;
|
||||
line-height: 1.4 !important;
|
||||
padding: 12px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ const NetworkTab = ({ response }) => {
|
||||
<div className="tab-content">
|
||||
<div className="section">
|
||||
<h4>Network Logs</h4>
|
||||
<div className="network-logs-container">
|
||||
<div className="network-logs-wrapper">
|
||||
{timeline.length > 0 ? (
|
||||
<Network logs={timeline} />
|
||||
) : (
|
||||
|
||||
@@ -245,7 +245,7 @@ const GrpcTimelineItem = ({ timestamp, request, response, eventType, eventData,
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className={`${eventClass} pl-1 mb-3`}>
|
||||
<StyledWrapper className={`${eventClass} pl-1`}>
|
||||
<div className="event-header" onClick={toggleCollapse}>
|
||||
{isCollapsed ? <IconChevronRight size={16} strokeWidth={1.5} /> : <IconChevronDown size={16} strokeWidth={1.5} />}
|
||||
<div className="event-icon-container">
|
||||
|
||||
@@ -6,7 +6,7 @@ const BodyBlock = ({ collection, data, dataBuffer, headers, error, item, type })
|
||||
return (
|
||||
<div className="collapsible-section">
|
||||
<div className="section-header" onClick={() => toggleBody(!isBodyCollapsed)}>
|
||||
<pre className="flex flex-row items-center text-indigo-500/80 dark:text-indigo-500/80">
|
||||
<pre className="flex flex-row items-center">
|
||||
<div className="opacity-70">{isBodyCollapsed ? '▼' : '▶'}</div> Body
|
||||
</pre>
|
||||
</div>
|
||||
@@ -26,7 +26,7 @@ const BodyBlock = ({ collection, data, dataBuffer, headers, error, item, type })
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-gray-500">No Body found</div>
|
||||
<div className="timeline-item-timestamp">No Body found</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -6,7 +6,7 @@ const HeadersBlock = ({ headers, type }) => {
|
||||
return (
|
||||
<div className="collapsible-section mt-2">
|
||||
<div className="section-header" onClick={() => toggleHeaders(!areHeadersCollapsed)}>
|
||||
<pre className="flex flex-row items-center text-indigo-500/80 dark:text-indigo-500/80">
|
||||
<pre className="flex flex-row items-center">
|
||||
<div className="opacity-70">{areHeadersCollapsed ? '▼' : '▶'}</div> Headers
|
||||
{headers && Object.keys(headers).length > 0
|
||||
&& <div className="ml-1">({Object.keys(headers).length})</div>}
|
||||
@@ -16,7 +16,7 @@ const HeadersBlock = ({ headers, type }) => {
|
||||
<div className="mt-1">
|
||||
{headers && Object.keys(headers).length > 0
|
||||
? <Headers headers={headers} type={type} />
|
||||
: <div className="text-gray-500">No Headers found</div>}
|
||||
: <div className="timeline-item-timestamp">No Headers found</div>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
import { useTheme } from 'providers/Theme';
|
||||
|
||||
const Method = ({ method }) => {
|
||||
const { theme } = useTheme();
|
||||
const methodUpper = method?.toUpperCase();
|
||||
const methodColor = theme.request.methods[methodUpper?.toLowerCase()] || theme.text;
|
||||
|
||||
return (
|
||||
<span className={`${methodColors[method?.toUpperCase()] || 'text-white'} font-bold`}>
|
||||
{method?.toUpperCase()}
|
||||
<span className="timeline-method" style={{ color: methodColor, fontWeight: 'bold' }}>
|
||||
{methodUpper}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const methodColors = {
|
||||
GET: 'text-green-500',
|
||||
POST: 'text-blue-500',
|
||||
PUT: 'text-yellow-500',
|
||||
DELETE: 'text-red-500',
|
||||
PATCH: 'text-purple-500',
|
||||
OPTIONS: 'text-gray-500',
|
||||
HEAD: 'text-gray-500'
|
||||
};
|
||||
|
||||
export default Method;
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
import { useTheme } from 'providers/Theme';
|
||||
|
||||
const Status = ({ statusCode, statusText }) => {
|
||||
const { theme } = useTheme();
|
||||
|
||||
let statusColor = theme.colors.text.muted;
|
||||
if (statusCode >= 200 && statusCode < 300) {
|
||||
statusColor = theme.requestTabPanel.responseOk;
|
||||
} else if (statusCode >= 300 && statusCode < 400) {
|
||||
statusColor = theme.colors.text.warning;
|
||||
} else if (statusCode >= 400 && statusCode < 600) {
|
||||
statusColor = theme.requestTabPanel.responseError;
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`${
|
||||
statusColor(statusCode) || 'text-white'
|
||||
} font-bold`}
|
||||
>
|
||||
<span className="timeline-status" style={{ color: statusColor, fontWeight: 'bold' }}>
|
||||
{statusCode}{' '}
|
||||
{statusText || ''}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const statusColor = (statusCode) => {
|
||||
if (statusCode >= 200 && statusCode < 300) {
|
||||
return 'text-green-500';
|
||||
} else if (statusCode >= 300 && statusCode < 400) {
|
||||
return 'text-yellow-500';
|
||||
} else if (statusCode >= 400 && statusCode < 600) {
|
||||
return 'text-red-500';
|
||||
} else {
|
||||
return 'text-gray-500';
|
||||
}
|
||||
};
|
||||
|
||||
export default Status;
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.network-logs-container {
|
||||
background: ${(props) => props.theme.codemirror.bg};
|
||||
color: ${(props) => props.theme.text};
|
||||
border-radius: 4px;
|
||||
overflow: auto;
|
||||
height: 24rem;
|
||||
}
|
||||
|
||||
.network-logs-pre {
|
||||
white-space: pre-wrap;
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
font-family: var(--font-family-mono);
|
||||
}
|
||||
|
||||
.network-logs-entry {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
|
||||
&--request {
|
||||
color: ${(props) => props.theme.textLink};
|
||||
}
|
||||
|
||||
&--response {
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
}
|
||||
|
||||
&--error {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
|
||||
&--tls {
|
||||
color: ${(props) => props.theme.colors.text.purple};
|
||||
}
|
||||
|
||||
&--info {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
}
|
||||
|
||||
.network-logs-separator {
|
||||
border-top: 2px solid ${(props) => props.theme.border.border1};
|
||||
width: 100%;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.network-logs-spacing {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,52 +1,56 @@
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Network = ({ logs }) => {
|
||||
return (
|
||||
<div className="bg-black/5 text-white network-logs rounded overflow-auto h-96">
|
||||
<pre className="whitespace-pre-wrap">
|
||||
{logs.map((currentLog, index) => {
|
||||
if (index > 0 && currentLog?.type === 'separator') {
|
||||
return <div className="border-t-2 border-gray-500 w-full my-2" key={index} />;
|
||||
}
|
||||
const nextLog = logs[index + 1];
|
||||
const isSameLogType = nextLog?.type === currentLog?.type;
|
||||
return (
|
||||
<>
|
||||
<NetworkLogsEntry key={index} entry={currentLog} />
|
||||
{!isSameLogType && <div className="mt-4" />}
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</pre>
|
||||
</div>
|
||||
<StyledWrapper>
|
||||
<div className="network-logs-container">
|
||||
<pre className="network-logs-pre">
|
||||
{logs.map((currentLog, index) => {
|
||||
if (index > 0 && currentLog?.type === 'separator') {
|
||||
return <div className="network-logs-separator" key={index} />;
|
||||
}
|
||||
const nextLog = logs[index + 1];
|
||||
const isSameLogType = nextLog?.type === currentLog?.type;
|
||||
return (
|
||||
<div key={index}>
|
||||
<NetworkLogsEntry entry={currentLog} />
|
||||
{!isSameLogType && <div className="network-logs-spacing" />}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</pre>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const NetworkLogsEntry = ({ entry }) => {
|
||||
const { type, message } = entry;
|
||||
let className = '';
|
||||
let className = 'network-logs-entry';
|
||||
|
||||
switch (type) {
|
||||
case 'request':
|
||||
className = 'text-blue-500';
|
||||
className = 'network-logs-entry network-logs-entry--request';
|
||||
break;
|
||||
case 'response':
|
||||
className = 'text-green-500';
|
||||
className = 'network-logs-entry network-logs-entry--response';
|
||||
break;
|
||||
case 'error':
|
||||
className = 'text-red-500';
|
||||
className = 'network-logs-entry network-logs-entry--error';
|
||||
break;
|
||||
case 'tls':
|
||||
className = 'text-purple-500';
|
||||
className = 'network-logs-entry network-logs-entry--tls';
|
||||
break;
|
||||
case 'info':
|
||||
className = 'text-yellow-500';
|
||||
className = 'network-logs-entry network-logs-entry--info';
|
||||
break;
|
||||
default:
|
||||
className = 'text-gray-400';
|
||||
className = 'network-logs-entry';
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${className}`}>
|
||||
<div className={className}>
|
||||
<div>{message}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -27,8 +27,8 @@ const Response = ({ collection, response, item }) => {
|
||||
{/* Status */}
|
||||
<div className="mb-1">
|
||||
<Status statusCode={status || statusCode} statusText={statusText} />
|
||||
{response.duration && <span className="text-gray-400 ml-2">{response.duration}ms</span>}
|
||||
{response.size && <span className="text-gray-400 ml-2">{response.size}B</span>}
|
||||
{response.duration && <span className="timeline-item-metadata">{response.duration}ms</span>}
|
||||
{response.size && <span className="timeline-item-metadata">{response.size}B</span>}
|
||||
</div>
|
||||
|
||||
{/* Headers */}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import styled from 'styled-components';
|
||||
import { rgba } from 'polished';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.timeline-item {
|
||||
border-bottom: 2px solid ${(props) => rgba(props.theme.colors.text.warning, 0.5)};
|
||||
padding: 0.5rem 0;
|
||||
|
||||
&--oauth2 {
|
||||
border-bottom: 2px solid ${(props) => rgba(props.theme.primary.solid, 0.5)};
|
||||
}
|
||||
}
|
||||
|
||||
.timeline-item-header {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.timeline-item-header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.timeline-item-header-items {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.timeline-item-url {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-top: 0.25rem;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
.timeline-item-timestamp {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.timeline-item-timestamp-iso {
|
||||
opacity: 0.7;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
.timeline-item-oauth-label {
|
||||
opacity: 0.5;
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
|
||||
.timeline-item-content {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.timeline-item-tabs {
|
||||
display: flex;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.timeline-item-tab {
|
||||
margin-right: 1rem;
|
||||
position: relative;
|
||||
padding: 0.5rem 1rem;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
|
||||
&--active {
|
||||
color: ${(props) => props.theme.tabs.active.color};
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: ${(props) => props.theme.tabs.active.border};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.timeline-item-tab-content {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.timeline-item-metadata {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
margin-left: 0.5rem;
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
}
|
||||
|
||||
.collapsible-section {
|
||||
.section-header {
|
||||
cursor: pointer;
|
||||
pre {
|
||||
color: ${(props) => rgba(props.theme.primary.solid, 0.8)};
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -5,6 +5,7 @@ import Response from './Response/index';
|
||||
import Method from './Common/Method/index';
|
||||
import Status from './Common/Status/index';
|
||||
import { RelativeTime } from './Common/Time/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const TimelineItem = ({ timestamp, request, response, item, collection, isOauth2, hideTimestamp = false }) => {
|
||||
const [isCollapsed, _toggleCollapse] = useState(false);
|
||||
@@ -15,72 +16,74 @@ const TimelineItem = ({ timestamp, request, response, item, collection, isOauth2
|
||||
const showNetworkLogs = response.timeline && response.timeline.length > 0;
|
||||
|
||||
return (
|
||||
<div className={`border-b-2 ${isOauth2 ? 'border-indigo-700/50' : 'border-amber-700/50'} py-2`}>
|
||||
<div className="oauth-request-item-header relative cursor-pointer" onClick={toggleCollapse}>
|
||||
<div className="flex justify-between items-center min-w-0">
|
||||
<div className="flex items-center space-x-2 min-w-0">
|
||||
<Status statusCode={responseStatus || responseStatusCode} statusText={responseStatusText} />
|
||||
<Method method={method} />
|
||||
<Status statusCode={status || statusCode} statusText={statusText} />
|
||||
{isOauth2 ? <pre className="opacity-50">[oauth2.0]</pre> : null}
|
||||
{!hideTimestamp && (
|
||||
<>
|
||||
<pre className="opacity-70">[{new Date(timestamp).toISOString()}]</pre>
|
||||
<span className="text-gray-400 flex-shrink-0 overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<RelativeTime timestamp={timestamp} />
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<StyledWrapper>
|
||||
<div className={`timeline-item ${isOauth2 ? 'timeline-item--oauth2' : ''}`}>
|
||||
<div className="timeline-item-header" onClick={toggleCollapse}>
|
||||
<div className="timeline-item-header-content">
|
||||
<div className="timeline-item-header-items">
|
||||
<Status statusCode={responseStatus || responseStatusCode} statusText={responseStatusText} />
|
||||
<Method method={method} />
|
||||
<Status statusCode={status || statusCode} statusText={statusText} />
|
||||
{isOauth2 && <pre className="timeline-item-oauth-label">[oauth2.0]</pre>}
|
||||
{!hideTimestamp && (
|
||||
<>
|
||||
<pre className="timeline-item-timestamp-iso">[{new Date(timestamp).toISOString()}]</pre>
|
||||
<span className="timeline-item-timestamp">
|
||||
<RelativeTime timestamp={timestamp} />
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="timeline-item-url">{url}</div>
|
||||
</div>
|
||||
<div className="truncate mt-1">{url}</div>
|
||||
</div>
|
||||
{isCollapsed && (
|
||||
<div className="overflow-hidden">
|
||||
{/* Tabs */}
|
||||
<div className="tabs-switcher flex mb-4">
|
||||
<button
|
||||
className={`mr-4 ${activeTab === 'request' ? 'active' : 'text-gray-400'}`}
|
||||
onClick={() => setActiveTab('request')}
|
||||
>
|
||||
Request
|
||||
</button>
|
||||
<button
|
||||
className={`mr-4 ${activeTab === 'response' ? 'active' : 'text-gray-400'}`}
|
||||
onClick={() => setActiveTab('response')}
|
||||
>
|
||||
Response
|
||||
</button>
|
||||
{showNetworkLogs && (
|
||||
{isCollapsed && (
|
||||
<div className="timeline-item-content">
|
||||
{/* Tabs */}
|
||||
<div className="timeline-item-tabs">
|
||||
<button
|
||||
className={`${activeTab === 'networkLogs' ? 'active' : 'text-gray-400'}`}
|
||||
onClick={() => setActiveTab('networkLogs')}
|
||||
className={`timeline-item-tab ${activeTab === 'request' ? 'timeline-item-tab--active' : ''}`}
|
||||
onClick={() => setActiveTab('request')}
|
||||
>
|
||||
Network Logs
|
||||
Request
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className={`timeline-item-tab ${activeTab === 'response' ? 'timeline-item-tab--active' : ''}`}
|
||||
onClick={() => setActiveTab('response')}
|
||||
>
|
||||
Response
|
||||
</button>
|
||||
{showNetworkLogs && (
|
||||
<button
|
||||
className={`timeline-item-tab ${activeTab === 'networkLogs' ? 'timeline-item-tab--active' : ''}`}
|
||||
onClick={() => setActiveTab('networkLogs')}
|
||||
>
|
||||
Network Logs
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
<div className="timeline-item-tab-content">
|
||||
{/* Request Tab */}
|
||||
{activeTab === 'request' && (
|
||||
<Request request={request} item={item} collection={collection} />
|
||||
)}
|
||||
|
||||
{/* Response Tab */}
|
||||
{activeTab === 'response' && (
|
||||
<Response response={response} item={item} collection={collection} />
|
||||
)}
|
||||
|
||||
{/* Network Logs Tab */}
|
||||
{activeTab === 'networkLogs' && showNetworkLogs && (
|
||||
<Network logs={response?.timeline} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
<div className="tab-content break-all">
|
||||
{/* Request Tab */}
|
||||
{activeTab === 'request' && (
|
||||
<Request request={request} item={item} collection={collection} />
|
||||
)}
|
||||
|
||||
{/* Response Tab */}
|
||||
{activeTab === 'response' && (
|
||||
<Response response={response} item={item} collection={collection} />
|
||||
)}
|
||||
|
||||
{/* Network Logs Tab */}
|
||||
{activeTab === 'networkLogs' && showNetworkLogs && (
|
||||
<Network logs={response?.timeline} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ const Timeline = ({ collection, item }) => {
|
||||
|
||||
if (isGrpcRequest) {
|
||||
return (
|
||||
<div key={index} className="timeline-event mb-2">
|
||||
<div key={index} className="timeline-event">
|
||||
<GrpcTimelineItem
|
||||
timestamp={eventTimestamp}
|
||||
request={request}
|
||||
@@ -94,7 +94,7 @@ const Timeline = ({ collection, item }) => {
|
||||
|
||||
// Regular HTTP request
|
||||
return (
|
||||
<div key={index} className="timeline-event mb-2">
|
||||
<div key={index} className="timeline-event">
|
||||
<TimelineItem
|
||||
timestamp={timestamp}
|
||||
request={request}
|
||||
|
||||
Reference in New Issue
Block a user