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
This commit is contained in:
Abhishek S Lal
2026-01-08 15:25:39 +05:30
committed by GitHub
parent 4708e8e589
commit 578fa72dc8
15 changed files with 197 additions and 159 deletions

View File

@@ -61,7 +61,7 @@ const ApiKeyAuth = ({ item, collection, updateAuth, request, save }) => {
}, [apikeyAuth]);
return (
<StyledWrapper className="mt-2 w-full">
<StyledWrapper className="w-full">
<label className="block mb-1">Key</label>
<div className="single-line-editor-wrapper mb-3">
<SingleLineEditor

View File

@@ -52,7 +52,7 @@ const BasicAuth = ({ item, collection, updateAuth, request, save }) => {
};
return (
<StyledWrapper className="mt-2 w-full">
<StyledWrapper className="w-full">
<label className="block mb-1">Username</label>
<div className="single-line-editor-wrapper mb-3">
<SingleLineEditor

View File

@@ -38,7 +38,7 @@ const BearerAuth = ({ item, collection, updateAuth, request, save }) => {
};
return (
<StyledWrapper className="mt-2 w-full">
<StyledWrapper className="w-full">
<label className="block mb-1">Token</label>
<div className="single-line-editor-wrapper flex items-center">
<SingleLineEditor

View File

@@ -73,7 +73,7 @@ const GrantTypeSelector = ({ item = {}, request, updateAuth, collection }) => {
return (
<StyledWrapper>
<div className="flex items-center gap-2.5 my-4">
<div className="flex items-center gap-2.5 mb-4">
<div className="flex items-center px-2.5 py-1.5 oauth2-icon-container rounded-md">
<IconKey size={14} className="oauth2-icon" />
</div>

View File

@@ -47,7 +47,7 @@ const OAuth2 = ({ item, collection }) => {
let request = item.draft ? get(item, 'draft.request', {}) : get(item, 'request', {});
return (
<StyledWrapper className="mt-2 w-full">
<StyledWrapper className="w-full">
<GrantTypeSelector item={item} request={request} updateAuth={updateAuth} collection={collection} />
<GrantTypeComponentMap item={item} collection={collection} />
</StyledWrapper>

View File

@@ -52,7 +52,7 @@ const WsseAuth = ({ item, collection, updateAuth, request, save }) => {
};
return (
<StyledWrapper className="mt-2 w-full">
<StyledWrapper className="w-full">
<label className="block mb-1">Username</label>
<div className="single-line-editor-wrapper mb-3">
<SingleLineEditor

View File

@@ -60,7 +60,7 @@ const StyledWrapper = styled.div`
.proto-file-dropdown-reflection-message {
padding: 0.5rem 0.75rem;
color: ${(props) => props.theme.overlay.overlay1};
color: ${(props) => props.theme.colors.text.muted};
margin-bottom: 0.5rem;
}
`;

View File

@@ -76,6 +76,9 @@ const GrpcAuth = ({ item, collection }) => {
const getAuthView = () => {
switch (authMode) {
case 'none': {
return <div>No Auth</div>;
}
case 'basic': {
return <BasicAuth collection={collection} item={item} updateAuth={updateAuth} request={request} save={save} />;
}
@@ -98,7 +101,7 @@ const GrpcAuth = ({ item, collection }) => {
if (source && supportedGrpcAuthModes.includes(source.auth?.mode)) {
return (
<>
<div className="flex flex-row w-full mt-2 gap-2">
<div className="flex flex-row w-full gap-2">
<div>Auth inherited from {source.name}: </div>
<div className="inherit-mode-text">{humanizeRequestAuthMode(source.auth?.mode)}</div>
</div>
@@ -107,7 +110,7 @@ const GrpcAuth = ({ item, collection }) => {
} else {
return (
<>
<div className="flex flex-row w-full mt-2 gap-2">
<div className="flex flex-row w-full gap-2">
<div>Inherited auth not supported by gRPC. Using no auth instead.</div>
</div>
</>
@@ -122,9 +125,6 @@ const GrpcAuth = ({ item, collection }) => {
return (
<StyledWrapper className="w-full overflow-y-scroll">
<div className="flex flex-grow justify-start items-center">
<GrpcAuthMode item={item} collection={collection} />
</div>
{getAuthView()}
</StyledWrapper>
);

View File

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

View File

@@ -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 <GrpcBody item={item} collection={collection} hideModeSelector={true} hidePrettifyButton={true} handleRun={handleRun} />;
}
@@ -45,22 +48,7 @@ const GrpcRequestPane = ({ item, collection, handleRun }) => {
return <div className="mt-4">404 | Not found</div>;
}
}
};
if (!activeTabUid) {
return <div>Something went wrong</div>;
}
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
if (!focusedTab || !focusedTab.uid || !focusedTab.requestPaneTab) {
return <div className="pb-4 px-4">An error occurred!</div>;
}
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 ? (
<sup className="ml-[.125rem] font-medium">{grpcMessagesCount}</sup>
) : (
<StatusDot />
);
}
return null;
};
return [
{
key: 'body',
label: 'Message',
indicator: getMessageIndicator()
},
{
key: 'headers',
label: 'Metadata',
indicator: activeHeadersLength > 0 ? <sup className="ml-[.125rem] font-medium">{activeHeadersLength}</sup> : null
},
{
key: 'auth',
label: 'Auth',
indicator: auth?.mode && auth.mode !== 'none' ? <StatusDot type="default" /> : null
},
{
key: 'docs',
label: 'Docs',
indicator: docs && docs.length > 0 ? <StatusDot type="default" /> : 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 <div className="pb-4 px-4">An error occurred!</div>;
}
// Return null during initialization while requestPaneTab is being set by useEffect
if (!requestPaneTab) {
return null;
}
const rightContent = requestPaneTab === 'auth' ? (
<div ref={rightContentRef} className="flex flex-grow justify-start items-center">
<GrpcAuthMode item={item} collection={collection} />
</div>
) : null;
return (
<StyledWrapper className="flex flex-col h-full relative">
<div className="flex flex-wrap items-center tabs" role="tablist">
<div className={getTabClassname('body')} role="tab" onClick={() => selectTab('body')}>
Message
{grpcMessagesCount > 0 && (
isClientStreaming ? (
<sup className="ml-[.125rem] font-medium">{grpcMessagesCount}</sup>
) : (
<StatusDot />
)
)}
</div>
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
Metadata
{activeHeadersLength > 0 && <sup className="ml-[.125rem] font-medium">{activeHeadersLength}</sup>}
</div>
<div className={getTabClassname('auth')} role="tab" onClick={() => selectTab('auth')}>
Auth
{auth.mode !== 'none' && <StatusDot type="default" />}
</div>
<div className={getTabClassname('docs')} role="tab" onClick={() => selectTab('docs')}>
Docs
{docs && docs.length > 0 && <StatusDot type="default" />}
</div>
</div>
<ResponsiveTabs
tabs={allTabs}
activeTab={requestPaneTab}
onTabSelect={selectTab}
rightContent={rightContent}
rightContentRef={rightContent ? rightContentRef : null}
/>
<section
className={classnames('flex w-full flex-1 h-full mt-4')}
className="flex w-full flex-1 h-full mt-4"
>
<HeightBoundContainer>
{getTabPanel(focusedTab.requestPaneTab)}
{tabPanel}
</HeightBoundContainer>
</section>
</StyledWrapper>

View File

@@ -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 ? (
<sup data-testid="grpc-tab-response-count" className="ml-1 font-medium">
{responsesCount}
</sup>
) : null
},
{
key: 'headers',
label: 'Metadata',
indicator: metadataCount > 0 ? <sup className="ml-1 font-medium">{metadataCount}</sup> : null
},
{
key: 'trailers',
label: 'Trailers',
indicator: trailersCount > 0 ? <sup className="ml-1 font-medium">{trailersCount}</sup> : null
},
{
key: 'timeline',
label: 'Timeline',
indicator: null
}
];
const getTabPanel = (tab) => {
switch (tab) {
case 'response': {
@@ -83,66 +115,40 @@ const GrpcResponsePane = ({ item, collection }) => {
return <div className="pb-4 px-4">An error occurred!</div>;
}
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 ? (
<div ref={rightContentRef} className="flex items-center">
{focusedTab?.responsePaneTab === 'timeline' ? (
<>
<ResponseLayoutToggle />
<ClearTimeline item={item} collection={collection} />
</>
) : item?.response ? (
<>
<ResponseLayoutToggle />
<ResponseClear item={item} collection={collection} />
<GrpcStatusCode
status={response.statusCode}
text={response.statusText}
details={response.statusDescription}
/>
<ResponseTime duration={response.duration} />
</>
) : null}
</div>
) : null;
return (
<StyledWrapper className="flex flex-col h-full relative">
<div className="flex flex-wrap items-center pl-3 pr-4 tabs" role="tablist" data-testid="grpc-response-tabs">
{tabConfig.map((tab) => (
<Tab
key={tab.name}
name={tab.name}
label={tab.label}
isActive={focusedTab.responsePaneTab === tab.name}
onClick={selectTab}
count={tab.count}
/>
))}
{!isLoading ? (
<div className="flex flex-grow justify-end items-center">
{focusedTab?.responsePaneTab === 'timeline' ? (
<>
<ResponseLayoutToggle />
<ClearTimeline item={item} collection={collection} />
</>
) : item?.response ? (
<>
<ResponseLayoutToggle />
<ResponseClear item={item} collection={collection} />
<GrpcStatusCode
status={response.statusCode}
text={response.statusText}
details={response.statusDescription}
/>
<ResponseTime duration={response.duration} />
</>
) : null}
</div>
) : null}
<div className="px-4">
<ResponsiveTabs
tabs={allTabs}
activeTab={focusedTab.responsePaneTab}
onTabSelect={selectTab}
rightContent={rightContent}
rightContentRef={rightContentRef}
/>
</div>
<section
className="flex flex-col flex-grow pl-3 pr-4 h-0 mt-4"
>
<section className="flex flex-col flex-grow px-4 h-0 mt-4">
{isLoading ? <Overlay item={item} collection={collection} /> : null}
{!item?.response ? (
focusedTab?.responsePaneTab === 'timeline' && requestTimeline?.length ? (

View File

@@ -245,7 +245,7 @@ const GrpcTimelineItem = ({ timestamp, request, response, eventType, eventData,
};
return (
<StyledWrapper className={`${eventClass} pl-1`}>
<StyledWrapper className={`${eventClass} pl-1 mb-2`}>
<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

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

View File

@@ -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 (
<div className={`relative px-2 ${className}`}>
<StyledWrapper className={`px-2 ${className}`}>
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
<span className="text-gray-500">
<span className="search-icon">
<IconSearch size={16} strokeWidth={1.5} />
</span>
</div>
@@ -50,7 +51,7 @@ const SearchInput = ({
</span>
</div>
)}
</div>
</StyledWrapper>
);
};

View File

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