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:
sanish chirayath
2026-01-01 23:58:47 +05:30
committed by GitHub
parent 002a5d16eb
commit 3ccaf29ddd
13 changed files with 294 additions and 128 deletions

View File

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

View File

@@ -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} />
) : (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 */}

View File

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

View File

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

View File

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