From 578fa72dc89f80615fd2f82f0ccf7b42c89ade55 Mon Sep 17 00:00:00 2001 From: Abhishek S Lal Date: Thu, 8 Jan 2026 15:25:39 +0530 Subject: [PATCH] refactor: enhance GrpcRequestPane and GrpcResponsePane with ResponsiveTabs component (#6649) * refactor: enhance GrpcRequestPane and GrpcResponsePane with ResponsiveTabs component - Replaced custom tab implementation with ResponsiveTabs for better structure and usability. - Utilized useMemo and useCallback for performance optimizations in GrpcRequestPane. - Removed unused imports and simplified tab management logic. - Updated StyledWrapper to remove legacy tab styles, improving maintainability. * fix: handle optional chaining for auth mode in GrpcRequestPane * feat: enhance GrpcRequestPane and GrpcResponsePane with tab initialization and response count indicators * refactor: simplify GrpcResponsePane tab management and enhance ResponsiveTabs key handling - Removed unnecessary useMemo for tab initialization in GrpcResponsePane. - Updated tab comparison logic in ResponsiveTabs to use key arrays for improved performance. - Adjusted test locator for response tab count to use role-based selection for better accessibility. * feat: add support for 'none' auth mode in GrpcAuth and integrate GrpcAuthMode in GrpcRequestPane - Updated StyledWrapper in ApiKeyAuth, BasicAuth, BearerAuth, OAuth2, WsseAuth, and GrpcAuth components to remove unnecessary margin-top, ensuring a uniform appearance across authentication interfaces. - Adjusted margin in GrantTypeSelector and WSAuth components for better layout consistency. * refactor: update import statement and enhance error handling in GrpcRequestPane - Changed the import of 'find' from lodash to a direct import for better clarity. - Improved error handling by returning null during initialization when requestPaneTab is not set, ensuring smoother user experience. * refactor: integrate StyledWrapper in SearchInput for improved styling * refactor: update StyledWrapper color and adjust margin in GrpcTimelineItem for improved layout consistency --- .../RequestPane/Auth/ApiKeyAuth/index.js | 2 +- .../RequestPane/Auth/BasicAuth/index.js | 2 +- .../RequestPane/Auth/BearerAuth/index.js | 2 +- .../Auth/OAuth2/GrantTypeSelector/index.js | 2 +- .../RequestPane/Auth/OAuth2/index.js | 2 +- .../RequestPane/Auth/WsseAuth/index.js | 2 +- .../ProtoFileDropdown/StyledWrapper.js | 2 +- .../GrpcRequestPane/GrpcAuth/index.js | 10 +- .../GrpcRequestPane/StyledWrapper.js | 29 ---- .../RequestPane/GrpcRequestPane/index.js | 132 +++++++++++------- .../ResponsePane/GrpcResponsePane/index.js | 124 ++++++++-------- .../Timeline/GrpcTimelineItem/index.js | 2 +- .../components/SearchInput/StyledWrapper.js | 36 +++++ .../src/components/SearchInput/index.js | 7 +- tests/utils/page/locators.ts | 2 +- 15 files changed, 197 insertions(+), 159 deletions(-) create mode 100644 packages/bruno-app/src/components/SearchInput/StyledWrapper.js diff --git a/packages/bruno-app/src/components/RequestPane/Auth/ApiKeyAuth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/ApiKeyAuth/index.js index 7a2d36c9f..7d5d02f55 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/ApiKeyAuth/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/ApiKeyAuth/index.js @@ -61,7 +61,7 @@ const ApiKeyAuth = ({ item, collection, updateAuth, request, save }) => { }, [apikeyAuth]); return ( - +
{ }; return ( - +
{ }; return ( - +
{ return ( -
+
diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/index.js index f92fb4d3a..24c996d77 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/index.js @@ -47,7 +47,7 @@ const OAuth2 = ({ item, collection }) => { let request = item.draft ? get(item, 'draft.request', {}) : get(item, 'request', {}); return ( - + diff --git a/packages/bruno-app/src/components/RequestPane/Auth/WsseAuth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/WsseAuth/index.js index 7b01b207d..0d5f4f553 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/WsseAuth/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/WsseAuth/index.js @@ -52,7 +52,7 @@ const WsseAuth = ({ item, collection, updateAuth, request, save }) => { }; return ( - +
props.theme.overlay.overlay1}; + color: ${(props) => props.theme.colors.text.muted}; margin-bottom: 0.5rem; } `; diff --git a/packages/bruno-app/src/components/RequestPane/GrpcRequestPane/GrpcAuth/index.js b/packages/bruno-app/src/components/RequestPane/GrpcRequestPane/GrpcAuth/index.js index 1149e0af7..7ae205c52 100644 --- a/packages/bruno-app/src/components/RequestPane/GrpcRequestPane/GrpcAuth/index.js +++ b/packages/bruno-app/src/components/RequestPane/GrpcRequestPane/GrpcAuth/index.js @@ -76,6 +76,9 @@ const GrpcAuth = ({ item, collection }) => { const getAuthView = () => { switch (authMode) { + case 'none': { + return
No Auth
; + } case 'basic': { return ; } @@ -98,7 +101,7 @@ const GrpcAuth = ({ item, collection }) => { if (source && supportedGrpcAuthModes.includes(source.auth?.mode)) { return ( <> -
+
Auth inherited from {source.name}:
{humanizeRequestAuthMode(source.auth?.mode)}
@@ -107,7 +110,7 @@ const GrpcAuth = ({ item, collection }) => { } else { return ( <> -
+
Inherited auth not supported by gRPC. Using no auth instead.
@@ -122,9 +125,6 @@ const GrpcAuth = ({ item, collection }) => { return ( -
- -
{getAuthView()}
); diff --git a/packages/bruno-app/src/components/RequestPane/GrpcRequestPane/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/GrpcRequestPane/StyledWrapper.js index eb061b83b..2a193b181 100644 --- a/packages/bruno-app/src/components/RequestPane/GrpcRequestPane/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestPane/GrpcRequestPane/StyledWrapper.js @@ -1,35 +1,6 @@ import styled from 'styled-components'; const StyledWrapper = styled.div` - div.tabs { - div.tab { - padding: 6px 0px; - border: none; - border-bottom: solid 2px transparent; - margin-right: ${(props) => props.theme.tabs.marginRight}; - color: ${(props) => props.theme.colors.text.subtext0}; - cursor: pointer; - - &:focus, - &:active, - &:focus-within, - &:focus-visible, - &:target { - outline: none !important; - box-shadow: none !important; - } - - &.active { - font-weight: ${(props) => props.theme.tabs.active.fontWeight} !important; - color: ${(props) => props.theme.tabs.active.color} !important; - border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important; - } - - .content-indicator { - color: ${(props) => props.theme.text} - } - } - } `; export default StyledWrapper; diff --git a/packages/bruno-app/src/components/RequestPane/GrpcRequestPane/index.js b/packages/bruno-app/src/components/RequestPane/GrpcRequestPane/index.js index 366f692f9..7ea16f177 100644 --- a/packages/bruno-app/src/components/RequestPane/GrpcRequestPane/index.js +++ b/packages/bruno-app/src/components/RequestPane/GrpcRequestPane/index.js @@ -1,34 +1,37 @@ -import React from 'react'; -import classnames from 'classnames'; +import React, { useMemo, useCallback, useEffect, useRef } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { updateRequestPaneTab } from 'providers/ReduxStore/slices/tabs'; import RequestHeaders from 'components/RequestPane/RequestHeaders'; import GrpcBody from 'components/RequestPane/GrpcBody'; import GrpcAuth from './GrpcAuth/index'; +import GrpcAuthMode from './GrpcAuth/GrpcAuthMode/index'; import StatusDot from 'components/StatusDot/index'; import HeightBoundContainer from 'ui/HeightBoundContainer'; -import StyledWrapper from './StyledWrapper'; -import { find, get } from 'lodash'; +import find from 'lodash/find'; import Documentation from 'components/Documentation/index'; -import { useEffect } from 'react'; import { getPropertyFromDraftOrRequest } from 'utils/collections/index'; +import ResponsiveTabs from 'ui/ResponsiveTabs'; +import StyledWrapper from './StyledWrapper'; const GrpcRequestPane = ({ item, collection, handleRun }) => { const dispatch = useDispatch(); const tabs = useSelector((state) => state.tabs.tabs); const activeTabUid = useSelector((state) => state.tabs.activeTabUid); + const rightContentRef = useRef(null); + const focusedTab = find(tabs, (t) => t.uid === activeTabUid); + const requestPaneTab = focusedTab?.requestPaneTab; - const selectTab = (tab) => { + const selectTab = useCallback((tab) => { dispatch( updateRequestPaneTab({ uid: item.uid, requestPaneTab: tab }) ); - }; + }, [dispatch, item.uid]); - const getTabPanel = (tab) => { - switch (tab) { + const tabPanel = useMemo(() => { + switch (requestPaneTab) { case 'body': { return ; } @@ -45,22 +48,7 @@ const GrpcRequestPane = ({ item, collection, handleRun }) => { return
404 | Not found
; } } - }; - - if (!activeTabUid) { - return
Something went wrong
; - } - - const focusedTab = find(tabs, (t) => t.uid === activeTabUid); - if (!focusedTab || !focusedTab.uid || !focusedTab.requestPaneTab) { - return
An error occurred!
; - } - - const getTabClassname = (tabName) => { - return classnames(`tab select-none ${tabName}`, { - active: tabName === focusedTab.requestPaneTab - }); - }; + }, [requestPaneTab, item, collection, handleRun]); const body = getPropertyFromDraftOrRequest(item, 'request.body'); const headers = getPropertyFromDraftOrRequest(item, 'request.headers'); @@ -74,44 +62,80 @@ const GrpcRequestPane = ({ item, collection, handleRun }) => { const request = item.draft ? item.draft.request : item.request; const isClientStreaming = request.methodType === 'client-streaming' || request.methodType === 'bidi-streaming'; + const allTabs = useMemo(() => { + const getMessageIndicator = () => { + if (grpcMessagesCount > 0) { + return isClientStreaming ? ( + {grpcMessagesCount} + ) : ( + + ); + } + return null; + }; + + return [ + { + key: 'body', + label: 'Message', + indicator: getMessageIndicator() + }, + { + key: 'headers', + label: 'Metadata', + indicator: activeHeadersLength > 0 ? {activeHeadersLength} : null + }, + { + key: 'auth', + label: 'Auth', + indicator: auth?.mode && auth.mode !== 'none' ? : null + }, + { + key: 'docs', + label: 'Docs', + indicator: docs && docs.length > 0 ? : null + } + ]; + }, [grpcMessagesCount, isClientStreaming, activeHeadersLength, auth?.mode, docs]); + + // Initialize tab to 'body' if no tab is currently set useEffect(() => { - // Only set the tab to 'body' if no tab is currently set - if (!focusedTab?.requestPaneTab) { + if (activeTabUid && focusedTab?.uid && !requestPaneTab) { selectTab('body'); } - }, []); + }, [activeTabUid, focusedTab?.uid, requestPaneTab, selectTab]); + + // Return error for truly missing active/focused tabs + if (!activeTabUid || !focusedTab?.uid) { + return
An error occurred!
; + } + + // Return null during initialization while requestPaneTab is being set by useEffect + if (!requestPaneTab) { + return null; + } + + const rightContent = requestPaneTab === 'auth' ? ( +
+ +
+ ) : null; return ( -
-
selectTab('body')}> - Message - {grpcMessagesCount > 0 && ( - isClientStreaming ? ( - {grpcMessagesCount} - ) : ( - - ) - )} -
-
selectTab('headers')}> - Metadata - {activeHeadersLength > 0 && {activeHeadersLength}} -
-
selectTab('auth')}> - Auth - {auth.mode !== 'none' && } -
-
selectTab('docs')}> - Docs - {docs && docs.length > 0 && } -
-
+ +
- {getTabPanel(focusedTab.requestPaneTab)} + {tabPanel}
diff --git a/packages/bruno-app/src/components/ResponsePane/GrpcResponsePane/index.js b/packages/bruno-app/src/components/ResponsePane/GrpcResponsePane/index.js index e9e77ca17..be6a15aca 100644 --- a/packages/bruno-app/src/components/ResponsePane/GrpcResponsePane/index.js +++ b/packages/bruno-app/src/components/ResponsePane/GrpcResponsePane/index.js @@ -1,6 +1,5 @@ -import React, { useState, useEffect } from 'react'; +import React, { useRef } from 'react'; import find from 'lodash/find'; -import classnames from 'classnames'; import { useDispatch, useSelector } from 'react-redux'; import { updateResponsePaneTab } from 'providers/ReduxStore/slices/tabs'; import Overlay from '../Overlay'; @@ -15,13 +14,14 @@ import StyledWrapper from './StyledWrapper'; import ResponseTrailers from './ResponseTrailers'; import GrpcQueryResult from './GrpcQueryResult'; import ResponseLayoutToggle from '../ResponseLayoutToggle'; -import Tab from 'components/Tab'; +import ResponsiveTabs from 'ui/ResponsiveTabs'; const GrpcResponsePane = ({ item, collection }) => { const dispatch = useDispatch(); const tabs = useSelector((state) => state.tabs.tabs); const activeTabUid = useSelector((state) => state.tabs.activeTabUid); const isLoading = ['queued', 'sending'].includes(item.requestState); + const rightContentRef = useRef(null); const requestTimeline = [...(collection?.timeline || [])].filter((obj) => { if (obj.itemUid === item.uid) return true; @@ -38,6 +38,38 @@ const GrpcResponsePane = ({ item, collection }) => { const response = item.response || {}; + const metadataCount = Array.isArray(response.metadata) ? response.metadata.length : 0; + const trailersCount = Array.isArray(response.trailers) ? response.trailers.length : 0; + const responsesCount = Array.isArray(response.responses) ? response.responses.length : 0; + + const allTabs = [ + { + key: 'response', + label: 'Response', + indicator: + responsesCount > 0 ? ( + + {responsesCount} + + ) : null + }, + { + key: 'headers', + label: 'Metadata', + indicator: metadataCount > 0 ? {metadataCount} : null + }, + { + key: 'trailers', + label: 'Trailers', + indicator: trailersCount > 0 ? {trailersCount} : null + }, + { + key: 'timeline', + label: 'Timeline', + indicator: null + } + ]; + const getTabPanel = (tab) => { switch (tab) { case 'response': { @@ -83,66 +115,40 @@ const GrpcResponsePane = ({ item, collection }) => { return
An error occurred!
; } - const tabConfig = [ - { - name: 'response', - label: 'Response', - count: Array.isArray(response.responses) ? response.responses.length : 0 - }, - { - name: 'headers', - label: 'Metadata', - count: Array.isArray(response.metadata) ? response.metadata.length : 0 - }, - { - name: 'trailers', - label: 'Trailers', - count: Array.isArray(response.trailers) ? response.trailers.length : 0 - }, - { - name: 'timeline', - label: 'Timeline' - } - ]; + const rightContent = !isLoading ? ( +
+ {focusedTab?.responsePaneTab === 'timeline' ? ( + <> + + + + ) : item?.response ? ( + <> + + + + + + ) : null} +
+ ) : null; return ( -
- {tabConfig.map((tab) => ( - - ))} - {!isLoading ? ( -
- {focusedTab?.responsePaneTab === 'timeline' ? ( - <> - - - - ) : item?.response ? ( - <> - - - - - - ) : null} -
- ) : null} +
+
-
+
{isLoading ? : null} {!item?.response ? ( focusedTab?.responsePaneTab === 'timeline' && requestTimeline?.length ? ( 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 ed26153cb..0841b0977 100644 --- a/packages/bruno-app/src/components/ResponsePane/Timeline/GrpcTimelineItem/index.js +++ b/packages/bruno-app/src/components/ResponsePane/Timeline/GrpcTimelineItem/index.js @@ -245,7 +245,7 @@ const GrpcTimelineItem = ({ timestamp, request, response, eventType, eventData, }; return ( - +
{isCollapsed ? : }
diff --git a/packages/bruno-app/src/components/SearchInput/StyledWrapper.js b/packages/bruno-app/src/components/SearchInput/StyledWrapper.js new file mode 100644 index 000000000..1fc9e81cc --- /dev/null +++ b/packages/bruno-app/src/components/SearchInput/StyledWrapper.js @@ -0,0 +1,36 @@ +import styled from 'styled-components'; + +const StyledWrapper = styled.div` + position: relative; + + .search-icon { + color: ${(props) => props.theme.colors.text.muted}; + } + + .close-icon { + color: ${(props) => props.theme.colors.text.muted}; + cursor: pointer; + + &:hover { + color: ${(props) => props.theme.text}; + } + } + + input#search-input { + background-color: ${(props) => props.theme.input.bg}; + border: 1px solid ${(props) => props.theme.input.border}; + color: ${(props) => props.theme.text}; + + &:focus { + outline: none; + border-color: ${(props) => props.theme.input.focusBorder}; + } + + &::placeholder { + color: ${(props) => props.theme.input.placeholder.color}; + opacity: ${(props) => props.theme.input.placeholder.opacity}; + } + } +`; + +export default StyledWrapper; diff --git a/packages/bruno-app/src/components/SearchInput/index.js b/packages/bruno-app/src/components/SearchInput/index.js index cb0a4555e..b8610592b 100644 --- a/packages/bruno-app/src/components/SearchInput/index.js +++ b/packages/bruno-app/src/components/SearchInput/index.js @@ -1,5 +1,6 @@ import React from 'react'; import { IconSearch, IconX } from '@tabler/icons'; +import StyledWrapper from './StyledWrapper'; const SearchInput = ({ searchText, @@ -17,9 +18,9 @@ const SearchInput = ({ }; return ( -
+
- +
@@ -50,7 +51,7 @@ const SearchInput = ({
)} -
+ ); }; diff --git a/tests/utils/page/locators.ts b/tests/utils/page/locators.ts index e097e6a14..c4c827af8 100644 --- a/tests/utils/page/locators.ts +++ b/tests/utils/page/locators.ts @@ -198,7 +198,7 @@ export const buildGrpcCommonLocators = (page: Page) => ({ list: () => page.getByTestId('grpc-responses-list'), responseItem: (index: number) => page.getByTestId(`grpc-response-item-${index}`), responseItems: () => page.locator('[data-testid^="grpc-response-item-"]'), - tabCount: () => page.getByTestId('tab-response-count') + tabCount: () => page.getByRole('tab', { name: 'Response' }).getByTestId('grpc-tab-response-count') } });