mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-27 06:34:06 +00:00
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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user