fix: theme within grpc timeline (#6581)

* fix: theme within grpc timeline

* fix: use font from the theme

* remove y padding to make timeline item more compact

* fix: font

* fix: padding

* fix: use fira code

* fix: icon spacing

* add border to the method  search

* show bg for message section within request
This commit is contained in:
sanish chirayath
2025-12-31 23:31:39 +05:30
committed by GitHub
parent 1ae05dfb0e
commit 1ec8f55a9e
4 changed files with 365 additions and 103 deletions

View File

@@ -41,6 +41,16 @@ const StyledWrapper = styled.div`
min-width: 15rem;
}
input#search-input {
border: 1px solid ${(props) => props.theme.input.border};
color: ${(props) => props.theme.text};
&:focus {
outline: none;
border-color: ${(props) => props.theme.input.focusBorder};
}
}
.method-dropdown-service-group {
margin-bottom: 0.5rem;
}

View File

@@ -153,7 +153,7 @@ const MethodDropdown = ({
onKeyDown={handleKeyDown}
onBlur={focusSearchInput}
onChange={handleSearchChange}
className="mt-2 mb-3 "
className="mt-2 mb-3"
data-testid="grpc-methods-search-input"
/>
<div ref={listRef} className="method-dropdown-list" data-testid="grpc-methods-list">

View File

@@ -0,0 +1,244 @@
import styled from 'styled-components';
import { rgba } from 'polished';
const StyledWrapper = styled.div`
font-size: ${(props) => props.theme.font.size.base};
/* Event type styles */
&.event-metadata {
border-left: 4px solid ${(props) => rgba(props.theme.request.methods.post, 0.2)};
}
&.event-response {
border-left: 4px solid ${(props) => rgba(props.theme.request.methods.get, 0.2)};
}
&.event-request,
&.event-message {
border-left: 4px solid ${(props) => rgba(props.theme.request.methods.put, 0.2)};
}
&.event-status {
border-left: 4px solid ${(props) => rgba(props.theme.colors.text.purple, 0.2)};
}
&.event-error {
border-left: 4px solid ${(props) => rgba(props.theme.colors.text.danger, 0.2)};
}
&.event-end {
border-left: 4px solid ${(props) => rgba(props.theme.colors.text.muted, 0.2)};
}
&.event-cancel {
border-left: 4px solid ${(props) => rgba(props.theme.colors.text.warning, 0.2)};
}
/* Event type icon colors */
.icon-metadata {
color: ${(props) => props.theme.request.methods.post};
}
.icon-response {
color: ${(props) => props.theme.request.methods.get};
}
.icon-request,
.icon-message {
color: ${(props) => props.theme.request.methods.put};
}
.icon-status {
color: ${(props) => props.theme.colors.text.purple};
}
.icon-error {
color: ${(props) => props.theme.colors.text.danger};
}
.icon-end {
color: ${(props) => props.theme.colors.text.muted};
}
.icon-cancel {
color: ${(props) => props.theme.colors.text.warning};
}
/* Event Header */
.event-header {
display: flex;
align-items: center;
gap: 0.375rem;
cursor: pointer;
.event-icon-container {
display: flex;
align-items: center;
justify-content: center;
width: 1.25rem;
flex-shrink: 0;
}
span:nth-of-type(1) {
font-weight: 500;
}
pre {
font-family: var(--font-family-mono);
font-size: ${(props) => props.theme.font.size.xs};
margin: 0;
}
.event-timestamp {
opacity: 0.7;
}
}
/* Common content container styles */
.content-request,
.content-message,
.content-metadata,
.content-response,
.content-status,
.content-error,
.content-end,
.content-cancel {
margin-top: 0.375rem;
padding: 0.375rem;
border-radius: ${(props) => props.theme.border.radius.base};
display: flex;
flex-direction: column;
gap: 0.5rem;
}
/* Request/Message content */
.content-request,
.content-message {
background-color: ${(props) => rgba(props.theme.request.methods.put, 0.1)};
}
.content-request-label,
.content-message-label {
color: ${(props) => props.theme.request.methods.put};
font-weight: 500;
font-size: ${(props) => props.theme.font.size.sm};
}
/* Metadata content */
.content-metadata {
background-color: ${(props) => rgba(props.theme.request.methods.post, 0.1)};
}
.content-metadata-label {
color: ${(props) => props.theme.request.methods.post};
font-weight: 500;
font-size: ${(props) => props.theme.font.size.sm};
}
/* Response content */
.content-response {
background-color: ${(props) => rgba(props.theme.request.methods.get, 0.1)};
}
.content-response-label {
color: ${(props) => props.theme.request.methods.get};
font-weight: 500;
font-size: ${(props) => props.theme.font.size.sm};
}
/* Status content */
.content-status {
background-color: ${(props) => rgba(props.theme.colors.text.purple, 0.1)};
}
.content-status-label {
color: ${(props) => props.theme.colors.text.purple};
font-weight: 500;
font-size: ${(props) => props.theme.font.size.sm};
}
/* Error content */
.content-error {
background-color: ${(props) => rgba(props.theme.colors.text.danger, 0.1)};
}
.content-error-label {
color: ${(props) => props.theme.colors.text.danger};
font-weight: 500;
font-size: ${(props) => props.theme.font.size.sm};
}
/* End content */
.content-end {
background-color: ${(props) => rgba(props.theme.colors.text.muted, 0.1)};
font-weight: 500;
font-size: ${(props) => props.theme.font.size.sm};
}
/* Cancel content */
.content-cancel {
background-color: ${(props) => rgba(props.theme.colors.text.warning, 0.1)};
}
.content-cancel-label {
color: ${(props) => props.theme.colors.text.warning};
font-weight: 500;
font-size: ${(props) => props.theme.font.size.sm};
}
/* Common content styles */
.content-box {
background-color: ${(props) => props.theme.bg};
border-radius: ${(props) => props.theme.border.radius.base};
padding: 0.375rem;
margin: 0;
&,
pre {
font-family: var(--font-family-mono);
}
pre {
margin: 0;
}
}
.empty-text {
color: ${(props) => props.theme.colors.text.muted};
font-style: italic;
font-size: ${(props) => props.theme.font.size.xs};
}
/* Method type badge */
.method-type-badge {
background-color: ${(props) => rgba(props.theme.request.methods.put, 0.15)};
color: ${(props) => props.theme.request.methods.put};
border-radius: ${(props) => props.theme.border.radius.base};
font-size: ${(props) => props.theme.font.size.xs};
font-weight: 500;
}
/* Timestamp and URL */
.timestamp-text {
color: ${(props) => props.theme.colors.text.muted};
font-size: ${(props) => props.theme.font.size.xs};
}
.url-text {
color: ${(props) => props.theme.colors.text.muted};
font-size: ${(props) => props.theme.font.size.xs};
margin-left: 1.5rem;
margin-top: 0.25rem;
}
.contents {
display: contents;
font-size: ${(props) => props.theme.font.size.xs};
div:first-child {
font-weight: 500;
}
}
`;
export default StyledWrapper;

View File

@@ -12,18 +12,7 @@ import {
IconX,
IconSend
} from '@tabler/icons';
// Icons for different event types
const EventTypeIcons = {
metadata: <IconServer size={16} strokeWidth={1.5} className="text-blue-500" />,
response: <IconSend style={{ transform: 'rotate(225deg)' }} size={16} strokeWidth={1.5} className="text-green-500" />,
request: <IconSend style={{ transform: 'rotate(45deg)' }} size={16} strokeWidth={1.5} className="text-orange-500" />,
message: <IconSend style={{ transform: 'rotate(45deg)' }} size={16} strokeWidth={1.5} className="text-orange-500" />,
status: <IconCircleCheck size={16} strokeWidth={1.5} className="text-purple-500" />,
error: <IconAlertCircle size={16} strokeWidth={1.5} className="text-red-500" />,
end: <IconX size={16} strokeWidth={1.5} className="text-gray-500" />,
cancel: <IconCircleX size={16} strokeWidth={1.5} className="text-amber-500" />
};
import StyledWrapper from './StyledWrapper';
// Event type display names
const EventTypeNames = {
@@ -37,18 +26,6 @@ const EventTypeNames = {
cancel: 'Cancelled'
};
// Colors for different event types
const EventTypeColors = {
metadata: 'border-blue-500/20',
response: 'border-green-500/20',
request: 'border-orange-500/20',
message: 'border-orange-500/20',
status: 'border-purple-500/20',
error: 'border-red-500/20',
end: 'border-gray-500/20',
cancel: 'border-amber-500/20'
};
const GrpcTimelineItem = ({ timestamp, request, response, eventType, eventData, item }) => {
const [isCollapsed, setIsCollapsed] = useState(true);
const toggleCollapse = () => setIsCollapsed((prev) => !prev);
@@ -60,10 +37,34 @@ const GrpcTimelineItem = ({ timestamp, request, response, eventType, eventData,
const { method, url = '' } = effectiveRequest;
const { statusCode, statusText, duration } = response || {};
// Get event-specific icon and color
const eventIcon = EventTypeIcons[eventType] || <IconDatabase size={16} strokeWidth={1.5} />;
const eventColor = EventTypeColors[eventType] || 'border-gray-500/50';
// Get event-specific icon and class names
const getEventIcon = () => {
const iconClass = `icon-${eventType}`;
switch (eventType) {
case 'metadata':
return <IconServer size={16} strokeWidth={1.5} className={iconClass} />;
case 'response':
return <IconSend style={{ transform: 'rotate(225deg)' }} size={16} strokeWidth={1.5} className={iconClass} />;
case 'request':
return <IconSend style={{ transform: 'rotate(45deg)' }} size={16} strokeWidth={1.5} className={iconClass} />;
case 'message':
return <IconSend style={{ transform: 'rotate(45deg)' }} size={16} strokeWidth={1.5} className={iconClass} />;
case 'status':
return <IconCircleCheck size={16} strokeWidth={1.5} className={iconClass} />;
case 'error':
return <IconAlertCircle size={16} strokeWidth={1.5} className={iconClass} />;
case 'end':
return <IconX size={16} strokeWidth={1.5} className={iconClass} />;
case 'cancel':
return <IconCircleX size={16} strokeWidth={1.5} className={iconClass} />;
default:
return <IconDatabase size={16} strokeWidth={1.5} />;
}
};
const eventIcon = getEventIcon();
const eventName = EventTypeNames[eventType] || 'Event';
const eventClass = `event-${eventType}`;
// Render appropriate content based on event type
const renderEventContent = () => {
@@ -72,16 +73,15 @@ const GrpcTimelineItem = ({ timestamp, request, response, eventType, eventData,
switch (eventType) {
case 'request':
return (
<div className="mt-2 bg-orange-50 dark:bg-orange-900/10 rounded p-2">
<div className="content-request">
{effectiveRequest.headers && Object.keys(effectiveRequest.headers).length > 0 && (
<div className="mb-3">
<div className="text-xs font-medium mb-1 text-orange-700 dark:text-orange-400">Metadata</div>
<div className="grid grid-cols-2 gap-1 bg-white dark:bg-gray-800 p-2 rounded">
<div>
<div className="content-request-label mb-1">Metadata</div>
<div className="content-box grid grid-cols-2 gap-1">
{Object.entries(effectiveRequest.headers).map(([key, value], idx) => (
<div key={idx} className="contents">
<div className="text-xs font-medium overflow-hidden text-ellipsis">{key}:</div>
<div className="text-xs overflow-hidden text-ellipsis">{typeof value === 'string' ? value : '[Buffer Buffer]'}</div>
<div className="overflow-hidden text-ellipsis">{key}:</div>
<div className="overflow-hidden text-ellipsis">{typeof value === 'string' ? value : '[Buffer Buffer]'}</div>
</div>
))}
</div>
@@ -91,13 +91,11 @@ const GrpcTimelineItem = ({ timestamp, request, response, eventType, eventData,
{/* gRPC Messages section */}
{!isClientStreaming && effectiveRequest.body?.mode === 'grpc' && effectiveRequest.body?.grpc?.length > 0 && (
<div>
<div className="text-xs font-medium mb-1 text-orange-700 dark:text-orange-400">
Message
</div>
<div className="space-y-2">
<div className="content-request-label mb-1">Message</div>
<div className="space-y-1">
{effectiveRequest.body.grpc.filter((_, index) => index === 0).map((message, idx) => (
<div key={idx} className="bg-white dark:bg-gray-800 p-2 rounded">
<pre className="text-xs overflow-auto max-h-[150px]">
<div key={idx} className="content-box">
<pre className="overflow-auto max-h-[150px]">
{typeof message.content === 'string'
? message.content
: JSON.stringify(message.content, null, 2)}
@@ -112,76 +110,82 @@ const GrpcTimelineItem = ({ timestamp, request, response, eventType, eventData,
case 'message':
return (
<div className="mt-2 bg-orange-50 dark:bg-orange-900/10 rounded p-2">
<div className="font-medium mb-1 text-orange-700 dark:text-orange-400">Message</div>
<pre className="text-xs bg-white dark:bg-gray-800 p-2 rounded overflow-auto max-h-[200px]">
{typeof eventData === 'string'
? eventData
: JSON.stringify(eventData, null, 2)}
</pre>
<div className="content-message">
<div>
<div className="content-message-label mb-1">Message</div>
<pre className="content-box overflow-auto max-h-[200px]">
{typeof eventData === 'string'
? eventData
: JSON.stringify(eventData, null, 2)}
</pre>
</div>
</div>
);
case 'metadata':
return (
<div className="mt-2 bg-blue-50 dark:bg-blue-900/10 rounded p-2">
<div className="font-medium mb-1 text-blue-700 dark:text-blue-400">Metadata Headers</div>
{response.metadata && response.metadata.length > 0 ? (
<div className="grid grid-cols-2 gap-1">
{response.metadata.map((header, idx) => (
<div key={idx} className="contents">
<div className="text-xs font-medium">{header.name}:</div>
<div className="text-xs">{header.value}</div>
</div>
))}
</div>
) : (
<div className="italic text-gray-500">No metadata headers</div>
)}
<div className="content-metadata">
<div>
<div className="content-metadata-label mb-1">Metadata Headers</div>
{response.metadata && response.metadata.length > 0 ? (
<div className="content-box grid grid-cols-2 gap-1">
{response.metadata.map((header, idx) => (
<div key={idx} className="contents">
<div>{header.name}:</div>
<div>{header.value}</div>
</div>
))}
</div>
) : (
<div className="empty-text">No metadata headers</div>
)}
</div>
</div>
);
case 'response':
// For message responses, show the response data
return (
<div className="mt-2 bg-green-50 dark:bg-green-900/10 rounded p-2">
<div className="font-medium mb-1 text-green-700 dark:text-green-400">
Response Message #{(response?.responses?.length) || 0}
<div className="content-response">
<div>
<div className="content-response-label mb-1">
Response Message #{(response?.responses?.length) || 0}
</div>
{response?.responses && response.responses.length > 0 ? (
<pre className="content-box overflow-auto max-h-[200px]">
{JSON.stringify(response.responses[response.responses.length - 1], null, 2)}
</pre>
) : (
<div className="empty-text">Empty message</div>
)}
</div>
{response?.responses && response.responses.length > 0 ? (
<pre className="text-xs bg-white dark:bg-gray-800 p-2 rounded overflow-auto max-h-[200px]">
{JSON.stringify(response.responses[response.responses.length - 1], null, 2)}
</pre>
) : (
<div className="italic text-gray-500">Empty message</div>
)}
</div>
);
case 'status':
// For status events, show status and trailers
return (
<div className="mt-2 bg-purple-50 dark:bg-purple-900/10 rounded p-2">
<div className="flex items-center gap-2 mb-1">
<div className="content-status">
<div className="flex items-center gap-2">
<Status statusCode={statusCode} statusText={statusText} />
</div>
{response.statusDescription && (
<div className="mb-2">{response.statusDescription}</div>
<div>{response.statusDescription}</div>
)}
{response.trailers && response.trailers.length > 0 && (
<>
<div className="font-medium mt-2 mb-1 text-purple-700 dark:text-purple-400">Trailers</div>
<div className="grid grid-cols-2 gap-1">
<div>
<div className="content-status-label mb-1">Trailers</div>
<div className="content-box grid grid-cols-2 gap-1">
{response.trailers.map((trailer, idx) => (
<div key={idx} className="contents">
<div className="text-xs font-medium">{trailer.name}:</div>
<div className="text-xs">{trailer.value || ''}</div>
<div>{trailer.name}:</div>
<div>{trailer.value || ''}</div>
</div>
))}
</div>
</>
</div>
)}
</div>
);
@@ -189,26 +193,28 @@ const GrpcTimelineItem = ({ timestamp, request, response, eventType, eventData,
case 'error':
// For error events, show error details
return (
<div className="mt-2 bg-red-50 dark:bg-red-900/10 rounded p-2">
<div className="font-medium mb-1 text-red-700 dark:text-red-400">Error</div>
<div className="mb-2">{response.error || 'Unknown error'}</div>
<div className="content-error">
<div>
<div className="content-error-label mb-1">Error</div>
<div>{response.error || 'Unknown error'}</div>
</div>
<div className="flex items-center gap-2">
<Status statusCode={statusCode} statusText={statusText} />
</div>
{response.trailers && response.trailers.length > 0 && (
<>
<div className="font-medium mt-2 mb-1 text-red-700 dark:text-red-400">Error Metadata</div>
<div className="grid grid-cols-2 gap-1">
<div>
<div className="content-error-label mb-1">Error Metadata</div>
<div className="content-box grid grid-cols-2 gap-1">
{response.trailers.map((trailer, idx) => (
<div key={idx} className="contents">
<div className="text-xs font-medium">{trailer.name}:</div>
<div className="text-xs">{trailer.value}</div>
<div>{trailer.name}:</div>
<div>{trailer.value}</div>
</div>
))}
</div>
</>
</div>
)}
</div>
);
@@ -216,8 +222,8 @@ const GrpcTimelineItem = ({ timestamp, request, response, eventType, eventData,
case 'end':
// For end events, show summary
return (
<div className="mt-2 bg-gray-50 dark:bg-gray-700/30 rounded p-2">
<div className="font-medium mb-1">Stream Ended</div>
<div className="content-end">
<div>Stream Ended</div>
<div>
Total messages: {(response?.responses?.length) || 0}
</div>
@@ -227,8 +233,8 @@ const GrpcTimelineItem = ({ timestamp, request, response, eventType, eventData,
case 'cancel':
// For cancel events, show cancellation info
return (
<div className="mt-2 bg-amber-50 dark:bg-amber-900/10 rounded p-2">
<div className="font-medium mb-1 text-amber-700 dark:text-amber-400">Stream Cancelled</div>
<div className="content-cancel">
<div className="content-cancel-label mb-1">Stream Cancelled</div>
<div>{response.statusDescription || 'The gRPC stream was cancelled'}</div>
</div>
);
@@ -239,13 +245,15 @@ const GrpcTimelineItem = ({ timestamp, request, response, eventType, eventData,
};
return (
<div className={`border-l-4 ${eventColor} pl-3 py-2 mb-3`}>
<div className="flex items-center gap-2 cursor-pointer" onClick={toggleCollapse}>
<StyledWrapper className={`${eventClass} pl-1 mb-3`}>
<div className="event-header" onClick={toggleCollapse}>
{isCollapsed ? <IconChevronRight size={16} strokeWidth={1.5} /> : <IconChevronDown size={16} strokeWidth={1.5} />}
{eventIcon}
<span className="font-medium">{eventName}</span>
<div className="event-icon-container">
{eventIcon}
</div>
<span>{eventName}</span>
{eventType === 'request' && effectiveRequest.methodType && (
<span className="px-2 py-0.5 text-xs rounded bg-orange-100 dark:bg-orange-800/30 text-orange-700 dark:text-orange-300">
<span className="method-type-badge px-2 py-0.5">
{effectiveRequest.methodType}
</span>
)}
@@ -254,18 +262,18 @@ const GrpcTimelineItem = ({ timestamp, request, response, eventType, eventData,
<Status statusCode={statusCode} statusText={statusText} />
</div>
)}
<pre className="text-xs opacity-70">[{new Date(timestamp).toISOString()}]</pre>
<span className="text-xs text-gray-500 ml-auto">
<pre className="event-timestamp">[{new Date(timestamp).toISOString()}]</pre>
<span className="timestamp-text ml-auto">
<RelativeTime timestamp={timestamp} />
</span>
</div>
{/* Always show the URL */}
<div className="text-xs text-gray-500 mt-1 ml-6">{url}</div>
<div className="url-text">{url}</div>
{/* Expanded content - only show for non-status items */}
{!isCollapsed && renderEventContent()}
</div>
</StyledWrapper>
);
};