diff --git a/packages/bruno-app/src/components/RequestPane/GrpcQueryUrl/MethodDropdown/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/GrpcQueryUrl/MethodDropdown/StyledWrapper.js
index e952cae27..9ca77e7d4 100644
--- a/packages/bruno-app/src/components/RequestPane/GrpcQueryUrl/MethodDropdown/StyledWrapper.js
+++ b/packages/bruno-app/src/components/RequestPane/GrpcQueryUrl/MethodDropdown/StyledWrapper.js
@@ -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;
}
diff --git a/packages/bruno-app/src/components/RequestPane/GrpcQueryUrl/MethodDropdown/index.js b/packages/bruno-app/src/components/RequestPane/GrpcQueryUrl/MethodDropdown/index.js
index 3b7ba763d..cd474a179 100644
--- a/packages/bruno-app/src/components/RequestPane/GrpcQueryUrl/MethodDropdown/index.js
+++ b/packages/bruno-app/src/components/RequestPane/GrpcQueryUrl/MethodDropdown/index.js
@@ -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"
/>
diff --git a/packages/bruno-app/src/components/ResponsePane/Timeline/GrpcTimelineItem/StyledWrapper.js b/packages/bruno-app/src/components/ResponsePane/Timeline/GrpcTimelineItem/StyledWrapper.js
new file mode 100644
index 000000000..02aa3cf3e
--- /dev/null
+++ b/packages/bruno-app/src/components/ResponsePane/Timeline/GrpcTimelineItem/StyledWrapper.js
@@ -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;
diff --git a/packages/bruno-app/src/components/ResponsePane/Timeline/GrpcTimelineItem/index.js b/packages/bruno-app/src/components/ResponsePane/Timeline/GrpcTimelineItem/index.js
index a42930777..86bc10034 100644
--- a/packages/bruno-app/src/components/ResponsePane/Timeline/GrpcTimelineItem/index.js
+++ b/packages/bruno-app/src/components/ResponsePane/Timeline/GrpcTimelineItem/index.js
@@ -12,18 +12,7 @@ import {
IconX,
IconSend
} from '@tabler/icons';
-
-// Icons for different event types
-const EventTypeIcons = {
- metadata:
,
- response:
,
- request:
,
- message:
,
- status:
,
- error:
,
- end:
,
- cancel:
-};
+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] ||
;
- 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
;
+ case 'response':
+ return
;
+ case 'request':
+ return
;
+ case 'message':
+ return
;
+ case 'status':
+ return
;
+ case 'error':
+ return
;
+ case 'end':
+ return
;
+ case 'cancel':
+ return
;
+ default:
+ return
;
+ }
+ };
+
+ 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 (
-
-
+
{effectiveRequest.headers && Object.keys(effectiveRequest.headers).length > 0 && (
-
-
Metadata
-
+
+
Metadata
+
{Object.entries(effectiveRequest.headers).map(([key, value], idx) => (
-
{key}:
-
{typeof value === 'string' ? value : '[Buffer Buffer]'}
+
{key}:
+
{typeof value === 'string' ? value : '[Buffer Buffer]'}
))}
@@ -91,13 +91,11 @@ const GrpcTimelineItem = ({ timestamp, request, response, eventType, eventData,
{/* gRPC Messages section */}
{!isClientStreaming && effectiveRequest.body?.mode === 'grpc' && effectiveRequest.body?.grpc?.length > 0 && (
-
- Message
-
-
+
Message
+
{effectiveRequest.body.grpc.filter((_, index) => index === 0).map((message, idx) => (
-
-
+
+
{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 (
-
-
Message
-
- {typeof eventData === 'string'
- ? eventData
- : JSON.stringify(eventData, null, 2)}
-
+
+
+
Message
+
+ {typeof eventData === 'string'
+ ? eventData
+ : JSON.stringify(eventData, null, 2)}
+
+
);
case 'metadata':
return (
-
-
Metadata Headers
- {response.metadata && response.metadata.length > 0 ? (
-
- {response.metadata.map((header, idx) => (
-
-
{header.name}:
-
{header.value}
-
- ))}
-
- ) : (
-
No metadata headers
- )}
+
+
+
Metadata Headers
+ {response.metadata && response.metadata.length > 0 ? (
+
+ {response.metadata.map((header, idx) => (
+
+
{header.name}:
+
{header.value}
+
+ ))}
+
+ ) : (
+
No metadata headers
+ )}
+
);
case 'response':
// For message responses, show the response data
return (
-
-
- Response Message #{(response?.responses?.length) || 0}
+
+
+
+ Response Message #{(response?.responses?.length) || 0}
+
+ {response?.responses && response.responses.length > 0 ? (
+
+ {JSON.stringify(response.responses[response.responses.length - 1], null, 2)}
+
+ ) : (
+
Empty message
+ )}
- {response?.responses && response.responses.length > 0 ? (
-
- {JSON.stringify(response.responses[response.responses.length - 1], null, 2)}
-
- ) : (
-
Empty message
- )}
);
case 'status':
// For status events, show status and trailers
return (
-
-
+
+
{response.statusDescription && (
-
{response.statusDescription}
+
{response.statusDescription}
)}
{response.trailers && response.trailers.length > 0 && (
- <>
-
Trailers
-
+
+
Trailers
+
{response.trailers.map((trailer, idx) => (
-
{trailer.name}:
-
{trailer.value || ''}
+
{trailer.name}:
+
{trailer.value || ''}
))}
- >
+
)}
);
@@ -189,26 +193,28 @@ const GrpcTimelineItem = ({ timestamp, request, response, eventType, eventData,
case 'error':
// For error events, show error details
return (
-
-
Error
-
{response.error || 'Unknown error'}
+
+
+
Error
+
{response.error || 'Unknown error'}
+
{response.trailers && response.trailers.length > 0 && (
- <>
-
Error Metadata
-
+
+
Error Metadata
+
{response.trailers.map((trailer, idx) => (
-
{trailer.name}:
-
{trailer.value}
+
{trailer.name}:
+
{trailer.value}
))}
- >
+
)}
);
@@ -216,8 +222,8 @@ const GrpcTimelineItem = ({ timestamp, request, response, eventType, eventData,
case 'end':
// For end events, show summary
return (
-
-
Stream Ended
+
+
Stream Ended
Total messages: {(response?.responses?.length) || 0}
@@ -227,8 +233,8 @@ const GrpcTimelineItem = ({ timestamp, request, response, eventType, eventData,
case 'cancel':
// For cancel events, show cancellation info
return (
-
-
Stream Cancelled
+
+
Stream Cancelled
{response.statusDescription || 'The gRPC stream was cancelled'}
);
@@ -239,13 +245,15 @@ const GrpcTimelineItem = ({ timestamp, request, response, eventType, eventData,
};
return (
-
-
+
+
{isCollapsed ?
:
}
- {eventIcon}
-
{eventName}
+
+ {eventIcon}
+
+
{eventName}
{eventType === 'request' && effectiveRequest.methodType && (
-
+
{effectiveRequest.methodType}
)}
@@ -254,18 +262,18 @@ const GrpcTimelineItem = ({ timestamp, request, response, eventType, eventData,
)}
- [{new Date(timestamp).toISOString()}]
-
+ [{new Date(timestamp).toISOString()}]
+
{/* Always show the URL */}
-
{url}
+
{url}
{/* Expanded content - only show for non-status items */}
{!isCollapsed && renderEventContent()}
-
+
);
};