mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-29 15:44:13 +00:00
@@ -1,30 +0,0 @@
|
||||
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: var(--color-tab-inactive);
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
&:active,
|
||||
&:focus-within,
|
||||
&:focus-visible,
|
||||
&:target {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: ${(props) => props.theme.tabs.active.color} !important;
|
||||
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState, useCallback, useMemo, useRef } from 'react';
|
||||
import find from 'lodash/find';
|
||||
import get from 'lodash/get';
|
||||
import classnames from 'classnames';
|
||||
@@ -15,54 +15,86 @@ import Tests from 'components/RequestPane/Tests';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { updateRequestGraphqlQuery } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Documentation from 'components/Documentation/index';
|
||||
import GraphQLSchemaActions from '../GraphQLSchemaActions/index';
|
||||
import HeightBoundContainer from 'ui/HeightBoundContainer';
|
||||
import Settings from 'components/RequestPane/Settings';
|
||||
import RequestPaneTabs from 'components/RequestPane/RequestPaneTabs';
|
||||
|
||||
const MULTIPLE_CONTENT_TABS = new Set(['script', 'vars', 'auth', 'docs']);
|
||||
|
||||
const TAB_CONFIG = [
|
||||
{ key: 'query', label: 'Query' },
|
||||
{ key: 'variables', label: 'Variables' },
|
||||
{ key: 'headers', label: 'Headers' },
|
||||
{ key: 'auth', label: 'Auth' },
|
||||
{ key: 'vars', label: 'Vars' },
|
||||
{ key: 'script', label: 'Script' },
|
||||
{ key: 'assert', label: 'Assert' },
|
||||
{ key: 'tests', label: 'Tests' },
|
||||
{ key: 'docs', label: 'Docs' },
|
||||
{ key: 'settings', label: 'Settings' }
|
||||
];
|
||||
|
||||
const GraphQLRequestPane = ({ item, collection, onSchemaLoad, toggleDocs, handleGqlClickReference }) => {
|
||||
const dispatch = useDispatch();
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
|
||||
const query = item.draft
|
||||
? get(item, 'draft.request.body.graphql.query', '')
|
||||
: get(item, 'request.body.graphql.query', '');
|
||||
const variables = item.draft
|
||||
? get(item, 'draft.request.body.graphql.variables')
|
||||
: get(item, 'request.body.graphql.variables');
|
||||
|
||||
const { displayedTheme } = useTheme();
|
||||
const [schema, setSchema] = useState(null);
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const schemaActionsRef = useRef(null);
|
||||
|
||||
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
const requestPaneTab = focusedTab?.requestPaneTab;
|
||||
|
||||
useEffect(() => {
|
||||
onSchemaLoad(schema);
|
||||
}, [schema]);
|
||||
}, [schema, onSchemaLoad]);
|
||||
|
||||
const onQueryChange = (value) => {
|
||||
dispatch(
|
||||
updateRequestGraphqlQuery({
|
||||
query: value,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
const onRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
const onQueryChange = useCallback(
|
||||
(value) => {
|
||||
dispatch(
|
||||
updateRequestGraphqlQuery({
|
||||
query: value,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch, item.uid, collection.uid]
|
||||
);
|
||||
|
||||
const selectTab = (tab) => {
|
||||
dispatch(
|
||||
updateRequestPaneTab({
|
||||
uid: item.uid,
|
||||
requestPaneTab: tab
|
||||
})
|
||||
);
|
||||
};
|
||||
const onRun = useCallback(
|
||||
() => dispatch(sendRequest(item, collection.uid)),
|
||||
[dispatch, item, collection.uid]
|
||||
);
|
||||
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
case 'query': {
|
||||
const onSave = useCallback(
|
||||
() => dispatch(saveRequest(item.uid, collection.uid)),
|
||||
[dispatch, item.uid, collection.uid]
|
||||
);
|
||||
|
||||
const selectTab = useCallback(
|
||||
(tabKey) => {
|
||||
dispatch(updateRequestPaneTab({ uid: item.uid, requestPaneTab: tabKey }));
|
||||
},
|
||||
[dispatch, item.uid]
|
||||
);
|
||||
|
||||
const allTabs = useMemo(() => TAB_CONFIG.map(({ key, label }) => ({ key, label })), []);
|
||||
|
||||
const tabPanel = useMemo(() => {
|
||||
switch (requestPaneTab) {
|
||||
case 'query':
|
||||
return (
|
||||
<QueryEditor
|
||||
collection={collection}
|
||||
@@ -77,94 +109,55 @@ const GraphQLRequestPane = ({ item, collection, onSchemaLoad, toggleDocs, handle
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'variables': {
|
||||
case 'variables':
|
||||
return <GraphQLVariables item={item} variables={variables} collection={collection} />;
|
||||
}
|
||||
case 'headers': {
|
||||
case 'headers':
|
||||
return <RequestHeaders item={item} collection={collection} />;
|
||||
}
|
||||
case 'auth': {
|
||||
case 'auth':
|
||||
return <Auth item={item} collection={collection} />;
|
||||
}
|
||||
case 'vars': {
|
||||
case 'vars':
|
||||
return <Vars item={item} collection={collection} />;
|
||||
}
|
||||
case 'assert': {
|
||||
case 'assert':
|
||||
return <Assertions item={item} collection={collection} />;
|
||||
}
|
||||
case 'script': {
|
||||
case 'script':
|
||||
return <Script item={item} collection={collection} />;
|
||||
}
|
||||
case 'tests': {
|
||||
case 'tests':
|
||||
return <Tests item={item} collection={collection} />;
|
||||
}
|
||||
case 'docs': {
|
||||
case 'docs':
|
||||
return <Documentation item={item} collection={collection} />;
|
||||
}
|
||||
case 'settings': {
|
||||
case 'settings':
|
||||
return <Settings item={item} collection={collection} />;
|
||||
}
|
||||
default: {
|
||||
default:
|
||||
return <div className="mt-4">404 | Not found</div>;
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [requestPaneTab, item, collection, displayedTheme, schema, onSave, query, onRun, onQueryChange, handleGqlClickReference, preferences, variables]);
|
||||
|
||||
if (!activeTabUid) {
|
||||
return <div>Something went wrong</div>;
|
||||
}
|
||||
|
||||
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
if (!focusedTab || !focusedTab.uid || !focusedTab.requestPaneTab) {
|
||||
if (!activeTabUid || !focusedTab?.uid || !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
|
||||
});
|
||||
};
|
||||
const isMultipleContentTab = MULTIPLE_CONTENT_TABS.has(requestPaneTab);
|
||||
|
||||
const rightContent = (
|
||||
<div ref={schemaActionsRef}>
|
||||
<GraphQLSchemaActions item={item} collection={collection} onSchemaLoad={setSchema} toggleDocs={toggleDocs} />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
<div className="flex flex-wrap items-center tabs" role="tablist">
|
||||
<div className={getTabClassname('query')} role="tab" onClick={() => selectTab('query')}>
|
||||
Query
|
||||
</div>
|
||||
<div className={getTabClassname('variables')} role="tab" onClick={() => selectTab('variables')}>
|
||||
Variables
|
||||
</div>
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
||||
Headers
|
||||
</div>
|
||||
<div className={getTabClassname('auth')} role="tab" onClick={() => selectTab('auth')}>
|
||||
Auth
|
||||
</div>
|
||||
<div className={getTabClassname('vars')} role="tab" onClick={() => selectTab('vars')}>
|
||||
Vars
|
||||
</div>
|
||||
<div className={getTabClassname('script')} role="tab" onClick={() => selectTab('script')}>
|
||||
Script
|
||||
</div>
|
||||
<div className={getTabClassname('assert')} role="tab" onClick={() => selectTab('assert')}>
|
||||
Assert
|
||||
</div>
|
||||
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
|
||||
Tests
|
||||
</div>
|
||||
<div className={getTabClassname('docs')} role="tab" onClick={() => selectTab('docs')}>
|
||||
Docs
|
||||
</div>
|
||||
<div className={getTabClassname('settings')} role="tab" onClick={() => selectTab('settings')}>
|
||||
Settings
|
||||
</div>
|
||||
<GraphQLSchemaActions item={item} collection={collection} onSchemaLoad={setSchema} toggleDocs={toggleDocs} />
|
||||
</div>
|
||||
<section className="flex w-full mt-5 flex-1 relative">
|
||||
<HeightBoundContainer>{getTabPanel(focusedTab.requestPaneTab)}</HeightBoundContainer>
|
||||
<div className="flex flex-col h-full relative">
|
||||
<RequestPaneTabs
|
||||
tabs={allTabs}
|
||||
activeTab={requestPaneTab}
|
||||
onTabSelect={selectTab}
|
||||
rightContent={rightContent}
|
||||
rightContentRef={schemaActionsRef}
|
||||
/>
|
||||
|
||||
<section className={classnames('flex w-full flex-1', { 'mt-5': !isMultipleContentTab })}>
|
||||
<HeightBoundContainer>{tabPanel}</HeightBoundContainer>
|
||||
</section>
|
||||
</StyledWrapper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useRef, useCallback, useMemo } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { find, get } from 'lodash';
|
||||
import { updateRequestPaneTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import QueryParams from 'components/RequestPane/QueryParams';
|
||||
import RequestHeaders from 'components/RequestPane/RequestHeaders';
|
||||
@@ -11,178 +12,144 @@ import Vars from 'components/RequestPane/Vars';
|
||||
import Assertions from 'components/RequestPane/Assertions';
|
||||
import Script from 'components/RequestPane/Script';
|
||||
import Tests from 'components/RequestPane/Tests';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { find, get } from 'lodash';
|
||||
import Documentation from 'components/Documentation/index';
|
||||
import HeightBoundContainer from 'ui/HeightBoundContainer';
|
||||
import { useEffect } from 'react';
|
||||
import StatusDot from 'components/StatusDot';
|
||||
import Settings from 'components/RequestPane/Settings';
|
||||
import Documentation from 'components/Documentation/index';
|
||||
import StatusDot from 'components/StatusDot';
|
||||
import RequestPaneTabs from 'components/RequestPane/RequestPaneTabs';
|
||||
import HeightBoundContainer from 'ui/HeightBoundContainer';
|
||||
|
||||
const MULTIPLE_CONTENT_TABS = new Set(['params', 'script', 'vars', 'auth', 'docs']);
|
||||
|
||||
const TAB_CONFIG = [
|
||||
{ key: 'params', label: 'Params' },
|
||||
{ key: 'body', label: 'Body' },
|
||||
{ key: 'headers', label: 'Headers' },
|
||||
{ key: 'auth', label: 'Auth' },
|
||||
{ key: 'vars', label: 'Vars' },
|
||||
{ key: 'script', label: 'Script' },
|
||||
{ key: 'assert', label: 'Assert' },
|
||||
{ key: 'tests', label: 'Tests' },
|
||||
{ key: 'docs', label: 'Docs' },
|
||||
{ key: 'settings', label: 'Settings' }
|
||||
];
|
||||
|
||||
const TAB_PANELS = {
|
||||
params: QueryParams,
|
||||
body: RequestBody,
|
||||
headers: RequestHeaders,
|
||||
auth: Auth,
|
||||
vars: Vars,
|
||||
assert: Assertions,
|
||||
script: Script,
|
||||
tests: Tests,
|
||||
docs: Documentation,
|
||||
settings: Settings
|
||||
};
|
||||
|
||||
const HttpRequestPane = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
|
||||
const selectTab = (tab) => {
|
||||
dispatch(
|
||||
updateRequestPaneTab({
|
||||
uid: item.uid,
|
||||
requestPaneTab: tab
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
case 'params': {
|
||||
return <QueryParams item={item} collection={collection} />;
|
||||
}
|
||||
case 'body': {
|
||||
return <RequestBody item={item} collection={collection} />;
|
||||
}
|
||||
case 'headers': {
|
||||
return <RequestHeaders item={item} collection={collection} />;
|
||||
}
|
||||
case 'auth': {
|
||||
return <Auth item={item} collection={collection} />;
|
||||
}
|
||||
case 'vars': {
|
||||
return <Vars item={item} collection={collection} />;
|
||||
}
|
||||
case 'assert': {
|
||||
return <Assertions item={item} collection={collection} />;
|
||||
}
|
||||
case 'script': {
|
||||
return <Script item={item} collection={collection} />;
|
||||
}
|
||||
case 'tests': {
|
||||
return <Tests item={item} collection={collection} />;
|
||||
}
|
||||
case 'docs': {
|
||||
return <Documentation item={item} collection={collection} />;
|
||||
}
|
||||
case 'settings': {
|
||||
return <Settings item={item} collection={collection} />;
|
||||
}
|
||||
default: {
|
||||
return <div className="mt-4">404 | Not found</div>;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!activeTabUid) {
|
||||
return <div>Something went wrong</div>;
|
||||
}
|
||||
const bodyModeRef = useRef(null);
|
||||
const initialAutoSelectDone = useRef(false);
|
||||
|
||||
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
if (!focusedTab || !focusedTab.uid || !focusedTab.requestPaneTab) {
|
||||
const requestPaneTab = focusedTab?.requestPaneTab;
|
||||
|
||||
const getProperty = useCallback(
|
||||
(key) => (item.draft ? get(item, `draft.${key}`, []) : get(item, key, [])),
|
||||
[item.draft, item]
|
||||
);
|
||||
|
||||
const params = getProperty('request.params');
|
||||
const body = getProperty('request.body');
|
||||
const headers = getProperty('request.headers');
|
||||
const script = getProperty('request.script');
|
||||
const assertions = getProperty('request.assertions');
|
||||
const tests = getProperty('request.tests');
|
||||
const docs = getProperty('request.docs');
|
||||
const requestVars = getProperty('request.vars.req');
|
||||
const responseVars = getProperty('request.vars.res');
|
||||
const auth = getProperty('request.auth');
|
||||
const tags = getProperty('tags');
|
||||
|
||||
const activeCounts = useMemo(() => ({
|
||||
params: params.filter((p) => p.enabled).length,
|
||||
headers: headers.filter((h) => h.enabled).length,
|
||||
assertions: assertions.filter((a) => a.enabled).length,
|
||||
vars: requestVars.filter((r) => r.enabled).length + responseVars.filter((r) => r.enabled).length
|
||||
}), [params, headers, assertions, requestVars, responseVars]);
|
||||
|
||||
const selectTab = useCallback(
|
||||
(tabKey) => {
|
||||
dispatch(updateRequestPaneTab({ uid: item.uid, requestPaneTab: tabKey }));
|
||||
},
|
||||
[dispatch, item.uid]
|
||||
);
|
||||
|
||||
const indicators = useMemo(() => {
|
||||
const hasScriptError = item.preRequestScriptErrorMessage || item.postResponseScriptErrorMessage;
|
||||
const hasTestError = item.testScriptErrorMessage;
|
||||
|
||||
return {
|
||||
params: activeCounts.params > 0 ? <sup className="font-medium">{activeCounts.params}</sup> : null,
|
||||
body: body.mode !== 'none' ? <StatusDot /> : null,
|
||||
headers: activeCounts.headers > 0 ? <sup className="font-medium">{activeCounts.headers}</sup> : null,
|
||||
auth: auth.mode !== 'none' ? <StatusDot /> : null,
|
||||
vars: activeCounts.vars > 0 ? <sup className="font-medium">{activeCounts.vars}</sup> : null,
|
||||
script: (script.req || script.res) ? (hasScriptError ? <StatusDot type="error" /> : <StatusDot />) : null,
|
||||
assert: activeCounts.assertions > 0 ? <sup className="font-medium">{activeCounts.assertions}</sup> : null,
|
||||
tests: tests?.length > 0 ? (hasTestError ? <StatusDot type="error" /> : <StatusDot />) : null,
|
||||
docs: docs?.length > 0 ? <StatusDot /> : null,
|
||||
settings: tags?.length > 0 ? <StatusDot /> : null
|
||||
};
|
||||
}, [activeCounts, body.mode, auth.mode, script, item.preRequestScriptErrorMessage, item.postResponseScriptErrorMessage, item.testScriptErrorMessage, tests, docs, tags]);
|
||||
|
||||
const allTabs = useMemo(
|
||||
() => TAB_CONFIG.map(({ key, label }) => ({ key, label, indicator: indicators[key] })),
|
||||
[indicators]
|
||||
);
|
||||
|
||||
const tabPanel = useMemo(() => {
|
||||
const Component = TAB_PANELS[requestPaneTab];
|
||||
return Component ? <Component item={item} collection={collection} /> : <div className="mt-4">404 | Not found</div>;
|
||||
}, [requestPaneTab, item, collection]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialAutoSelectDone.current && activeCounts.params === 0 && body.mode !== 'none') {
|
||||
selectTab('body');
|
||||
}
|
||||
initialAutoSelectDone.current = true;
|
||||
}, [activeCounts.params, body.mode, selectTab]);
|
||||
|
||||
if (!activeTabUid || !focusedTab?.uid || !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
|
||||
});
|
||||
};
|
||||
const isMultipleContentTab = MULTIPLE_CONTENT_TABS.has(requestPaneTab);
|
||||
|
||||
const isMultipleContentTab = ['params', 'script', 'vars', 'auth', 'docs'].includes(focusedTab.requestPaneTab);
|
||||
|
||||
// get the length of active params, headers, asserts and vars as well as the contents of the body, tests and script
|
||||
const getPropertyFromDraftOrRequest = (propertyKey) =>
|
||||
item.draft ? get(item, `draft.${propertyKey}`, []) : get(item, propertyKey, []);
|
||||
const params = getPropertyFromDraftOrRequest('request.params');
|
||||
const body = getPropertyFromDraftOrRequest('request.body');
|
||||
const headers = getPropertyFromDraftOrRequest('request.headers');
|
||||
const script = getPropertyFromDraftOrRequest('request.script');
|
||||
const assertions = getPropertyFromDraftOrRequest('request.assertions');
|
||||
const tests = getPropertyFromDraftOrRequest('request.tests');
|
||||
const docs = getPropertyFromDraftOrRequest('request.docs');
|
||||
const requestVars = getPropertyFromDraftOrRequest('request.vars.req');
|
||||
const responseVars = getPropertyFromDraftOrRequest('request.vars.res');
|
||||
const auth = getPropertyFromDraftOrRequest('request.auth');
|
||||
const tags = getPropertyFromDraftOrRequest('tags');
|
||||
|
||||
const activeParamsLength = params.filter((param) => param.enabled).length;
|
||||
const activeHeadersLength = headers.filter((header) => header.enabled).length;
|
||||
const activeAssertionsLength = assertions.filter((assertion) => assertion.enabled).length;
|
||||
const activeVarsLength
|
||||
= requestVars.filter((request) => request.enabled).length
|
||||
+ responseVars.filter((response) => response.enabled).length;
|
||||
|
||||
useEffect(() => {
|
||||
if (activeParamsLength === 0 && body.mode !== 'none') {
|
||||
selectTab('body');
|
||||
}
|
||||
}, []);
|
||||
const rightContent = requestPaneTab === 'body' ? (
|
||||
<div ref={bodyModeRef}>
|
||||
<RequestBodyMode 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('params')} role="tab" onClick={() => selectTab('params')}>
|
||||
Params
|
||||
{activeParamsLength > 0 && <sup className="ml-1 font-medium">{activeParamsLength}</sup>}
|
||||
</div>
|
||||
<div className={getTabClassname('body')} role="tab" onClick={() => selectTab('body')}>
|
||||
Body
|
||||
{body.mode !== 'none' && <StatusDot />}
|
||||
</div>
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
||||
Headers
|
||||
{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 />}
|
||||
</div>
|
||||
<div className={getTabClassname('vars')} role="tab" onClick={() => selectTab('vars')}>
|
||||
Vars
|
||||
{activeVarsLength > 0 && <sup className="ml-1 font-medium">{activeVarsLength}</sup>}
|
||||
</div>
|
||||
<div className={getTabClassname('script')} role="tab" onClick={() => selectTab('script')}>
|
||||
Script
|
||||
{(script.req || script.res) && (
|
||||
item.preRequestScriptErrorMessage || item.postResponseScriptErrorMessage
|
||||
? <StatusDot type="error" />
|
||||
: <StatusDot />
|
||||
)}
|
||||
</div>
|
||||
<div className={getTabClassname('assert')} role="tab" onClick={() => selectTab('assert')}>
|
||||
Assert
|
||||
{activeAssertionsLength > 0 && <sup className="ml-1 font-medium">{activeAssertionsLength}</sup>}
|
||||
</div>
|
||||
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
|
||||
Tests
|
||||
{tests && tests.length > 0 && (
|
||||
item.testScriptErrorMessage
|
||||
? <StatusDot type="error" />
|
||||
: <StatusDot />
|
||||
)}
|
||||
</div>
|
||||
<div className={getTabClassname('docs')} role="tab" onClick={() => selectTab('docs')}>
|
||||
Docs
|
||||
{docs && docs.length > 0 && <StatusDot />}
|
||||
</div>
|
||||
<div className={getTabClassname('settings')} role="tab" onClick={() => selectTab('settings')}>
|
||||
Settings
|
||||
{tags && tags.length > 0 && <StatusDot />}
|
||||
</div>
|
||||
{focusedTab.requestPaneTab === 'body' ? (
|
||||
<div className="flex flex-grow justify-end items-center">
|
||||
<RequestBodyMode item={item} collection={collection} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<section
|
||||
className={classnames('flex w-full flex-1', {
|
||||
'mt-5': !isMultipleContentTab
|
||||
})}
|
||||
>
|
||||
<HeightBoundContainer>
|
||||
{getTabPanel(focusedTab.requestPaneTab)}
|
||||
</HeightBoundContainer>
|
||||
<div className="flex flex-col h-full relative">
|
||||
<RequestPaneTabs
|
||||
tabs={allTabs}
|
||||
activeTab={requestPaneTab}
|
||||
onTabSelect={selectTab}
|
||||
rightContent={rightContent}
|
||||
rightContentRef={bodyModeRef}
|
||||
delayedTabs={['body']}
|
||||
/>
|
||||
|
||||
<section className={classnames('flex w-full flex-1', { 'mt-3': !isMultipleContentTab })}>
|
||||
<HeightBoundContainer>{tabPanel}</HeightBoundContainer>
|
||||
</section>
|
||||
</StyledWrapper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
min-width: 125px;
|
||||
|
||||
.body-mode-selector {
|
||||
background: transparent;
|
||||
|
||||
@@ -1,14 +1,28 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.tabs {
|
||||
&.tabs {
|
||||
div.more-tabs {
|
||||
color: ${(props) => props.theme.text} !important;
|
||||
background-color: ${(props) => props.theme.requestTabs.bg} !important;
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.requestTabs.icon.hoverBg} !important;
|
||||
}
|
||||
}
|
||||
|
||||
div.tab {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 6px 0px;
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: ${(props) => props.theme.tabs.marginRight};
|
||||
color: var(--color-tab-inactive);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:focus,
|
||||
&:active,
|
||||
@@ -25,7 +39,15 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
.content-indicator {
|
||||
color: ${(props) => props.theme.text}
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
|
||||
sup {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
line-height: 1;
|
||||
vertical-align: baseline;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import React, { useEffect, useState, useRef, useCallback, useMemo } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { IconDots } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const DROPDOWN_WIDTH = 60;
|
||||
const CALCULATION_DELAY_DEFAULT = 20;
|
||||
const CALCULATION_DELAY_EXTENDED = 150;
|
||||
|
||||
const RequestPaneTabs = ({
|
||||
tabs,
|
||||
activeTab,
|
||||
onTabSelect,
|
||||
rightContent,
|
||||
rightContentRef,
|
||||
delayedTabs = []
|
||||
}) => {
|
||||
const [visibleTabs, setVisibleTabs] = useState([]);
|
||||
const [overflowTabs, setOverflowTabs] = useState([]);
|
||||
|
||||
const tabsContainerRef = useRef(null);
|
||||
const tabRefsMap = useRef({});
|
||||
const dropdownTippyRef = useRef(null);
|
||||
|
||||
const handleTabSelect = useCallback(
|
||||
(tabKey) => {
|
||||
onTabSelect(tabKey);
|
||||
dropdownTippyRef.current?.hide();
|
||||
},
|
||||
[onTabSelect]
|
||||
);
|
||||
|
||||
const calculateTabVisibility = useCallback(() => {
|
||||
const container = tabsContainerRef.current;
|
||||
if (!container || !tabs.length) return;
|
||||
|
||||
const containerWidth = container.offsetWidth;
|
||||
const rightContentWidth = rightContentRef?.current
|
||||
? rightContentRef.current.offsetWidth + 20
|
||||
: 0;
|
||||
|
||||
const availableWidth = containerWidth - rightContentWidth - DROPDOWN_WIDTH;
|
||||
const visible = [];
|
||||
const overflow = [];
|
||||
let currentWidth = 0;
|
||||
|
||||
for (const tab of tabs) {
|
||||
const tabElement = tabRefsMap.current[tab.key];
|
||||
const tabWidth = tabElement ? tabElement.offsetWidth + 20 : 100;
|
||||
|
||||
if (currentWidth + tabWidth <= availableWidth && !overflow.length) {
|
||||
visible.push(tab);
|
||||
currentWidth += tabWidth;
|
||||
} else {
|
||||
overflow.push(tab);
|
||||
}
|
||||
}
|
||||
|
||||
if (!visible.some((t) => t.key === activeTab) && overflow.length) {
|
||||
const activeTabIndex = overflow.findIndex((t) => t.key === activeTab);
|
||||
if (activeTabIndex !== -1) {
|
||||
const [activeTabItem] = overflow.splice(activeTabIndex, 1);
|
||||
const lastVisible = visible.pop();
|
||||
if (lastVisible) overflow.unshift(lastVisible);
|
||||
visible.push(activeTabItem);
|
||||
}
|
||||
}
|
||||
|
||||
setVisibleTabs(visible);
|
||||
setOverflowTabs(overflow);
|
||||
}, [tabs, activeTab, rightContentRef]);
|
||||
|
||||
const renderTab = useCallback(
|
||||
(tab, isInDropdown = false) => {
|
||||
const isActive = tab.key === activeTab;
|
||||
|
||||
if (isInDropdown) {
|
||||
return (
|
||||
<div
|
||||
key={tab.key}
|
||||
className={classnames('dropdown-item', { active: isActive })}
|
||||
role="tab"
|
||||
onClick={() => handleTabSelect(tab.key)}
|
||||
>
|
||||
<span className="flex items-center gap-1">
|
||||
{tab.label}
|
||||
{tab.indicator}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={tab.key}
|
||||
className={classnames('tab select-none', tab.key, { active: isActive })}
|
||||
role="tab"
|
||||
onClick={() => handleTabSelect(tab.key)}
|
||||
ref={(el) => el && (tabRefsMap.current[tab.key] = el)}
|
||||
>
|
||||
{tab.label}
|
||||
{tab.indicator}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[activeTab, handleTabSelect]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const delay = delayedTabs.includes(activeTab) ? CALCULATION_DELAY_EXTENDED : CALCULATION_DELAY_DEFAULT;
|
||||
const timeoutId = setTimeout(() => requestAnimationFrame(calculateTabVisibility), delay);
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [calculateTabVisibility, activeTab, delayedTabs]);
|
||||
|
||||
useEffect(() => {
|
||||
let frameId = null;
|
||||
const observer = new ResizeObserver(() => {
|
||||
if (frameId) cancelAnimationFrame(frameId);
|
||||
frameId = requestAnimationFrame(calculateTabVisibility);
|
||||
});
|
||||
|
||||
if (tabsContainerRef.current) observer.observe(tabsContainerRef.current);
|
||||
if (rightContentRef?.current) observer.observe(rightContentRef.current);
|
||||
|
||||
return () => {
|
||||
if (frameId) cancelAnimationFrame(frameId);
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [calculateTabVisibility, rightContentRef]);
|
||||
|
||||
const hiddenStyle = useMemo(
|
||||
() => ({ visibility: 'hidden', position: 'absolute', display: 'flex', pointerEvents: 'none' }),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledWrapper ref={tabsContainerRef} className="flex items-center tabs" role="tablist">
|
||||
<div style={hiddenStyle}>
|
||||
{tabs.map((tab) => (
|
||||
<div
|
||||
key={tab.key}
|
||||
className={classnames('tab select-none', tab.key, { active: tab.key === activeTab })}
|
||||
ref={(el) => el && (tabRefsMap.current[tab.key] = el)}
|
||||
>
|
||||
{tab.label}
|
||||
{tab.indicator}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{visibleTabs.map((tab) => renderTab(tab))}
|
||||
|
||||
{overflowTabs.length > 0 && (
|
||||
<Dropdown
|
||||
icon={(
|
||||
<div className="tab more-tabs select-none flex items-center cursor-pointer rounded-md" style={{ padding: '2px 8px' }}>
|
||||
<IconDots size={18} />
|
||||
</div>
|
||||
)}
|
||||
placement="bottom-start"
|
||||
onCreate={(instance) => (dropdownTippyRef.current = instance)}
|
||||
>
|
||||
<div style={{ minWidth: '150px' }}>{overflowTabs.map((tab) => renderTab(tab, true))}</div>
|
||||
</Dropdown>
|
||||
)}
|
||||
|
||||
{rightContent && (
|
||||
<div className="flex flex-grow justify-end items-center">
|
||||
{rightContent}
|
||||
</div>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default RequestPaneTabs;
|
||||
@@ -15,6 +15,7 @@ const Wrapper = styled.div`
|
||||
|
||||
section.request-pane,
|
||||
section.response-pane {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { test, expect } from '../../../playwright';
|
||||
import { closeAllCollections, createUntitledRequest } from '../../utils/page';
|
||||
import { closeAllCollections, createUntitledRequest, selectRequestPaneTab } from '../../utils/page';
|
||||
|
||||
test.describe('Tag persistence', () => {
|
||||
test.afterEach(async ({ page }) => {
|
||||
@@ -62,7 +62,7 @@ test.describe('Tag persistence', () => {
|
||||
// Click on the moved request (now first) to verify the tag persisted after the move
|
||||
await untitledRequests.first().click();
|
||||
await page.locator('.request-tab.active').waitFor({ state: 'visible' });
|
||||
await page.getByRole('tab', { name: 'Settings' }).click();
|
||||
await selectRequestPaneTab(page, 'Settings');
|
||||
await page.waitForTimeout(200);
|
||||
// Verify the tag is still present after the move
|
||||
await expect(page.locator('.tag-item', { hasText: 'smoke' })).toBeVisible();
|
||||
@@ -116,11 +116,11 @@ test.describe('Tag persistence', () => {
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
// Add a tag to the request
|
||||
await page.getByRole('tab', { name: 'Settings' }).click();
|
||||
await selectRequestPaneTab(page, 'Settings');
|
||||
await page.waitForTimeout(200);
|
||||
const tagInput = await page.getByTestId('tag-input').getByRole('textbox');
|
||||
await tagInput.fill('smoke');
|
||||
await tagInput.press('Enter');
|
||||
const tagInput2 = await page.getByTestId('tag-input').getByRole('textbox');
|
||||
await tagInput2.fill('smoke');
|
||||
await tagInput2.press('Enter');
|
||||
await page.waitForTimeout(200);
|
||||
await expect(page.locator('.tag-item', { hasText: 'smoke' })).toBeVisible();
|
||||
await page.keyboard.press('Meta+s');
|
||||
@@ -157,7 +157,7 @@ test.describe('Tag persistence', () => {
|
||||
// Click on request-2 to verify the tag persisted after the move
|
||||
await page.locator('.collection-item-name').filter({ hasText: 'request-2' }).click();
|
||||
await page.locator('.request-tab.active').filter({ hasText: 'request-2' }).waitFor({ state: 'visible' });
|
||||
await page.getByRole('tab', { name: 'Settings' }).click();
|
||||
await selectRequestPaneTab(page, 'Settings');
|
||||
await page.waitForTimeout(200);
|
||||
await expect(page.locator('.tag-item', { hasText: 'smoke' })).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { test, expect } from '../../../playwright';
|
||||
import { selectRequestPaneTab } from '../../utils/page';
|
||||
|
||||
test.describe('Max Redirects Settings Tests', () => {
|
||||
test('should configure and test max redirects settings', async ({
|
||||
@@ -13,7 +14,7 @@ test.describe('Max Redirects Settings Tests', () => {
|
||||
await page.getByRole('complementary').getByText('max-redirects').click();
|
||||
|
||||
// Go to Settings tab
|
||||
await page.getByRole('tab', { name: 'Settings' }).click();
|
||||
await selectRequestPaneTab(page, 'Settings');
|
||||
|
||||
// Test Max Redirects Settings
|
||||
const maxRedirectsInput = page.locator('input[id="maxRedirects"]');
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { test, expect } from '../../../playwright';
|
||||
import { selectRequestPaneTab } from '../../utils/page';
|
||||
|
||||
test.describe('No Redirects Settings Tests', () => {
|
||||
test('should configure and test no redirects settings', async ({
|
||||
@@ -13,7 +14,7 @@ test.describe('No Redirects Settings Tests', () => {
|
||||
await page.getByRole('complementary').getByText('no-redirects').click();
|
||||
|
||||
// Go to Settings tab
|
||||
await page.getByRole('tab', { name: 'Settings' }).click();
|
||||
await selectRequestPaneTab(page, 'Settings');
|
||||
|
||||
// Test No Redirects Settings
|
||||
const maxRedirectsInput = page.locator('input[id="maxRedirects"]');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { test, expect } from '../../../playwright';
|
||||
import { closeAllCollections } from '../../utils/page';
|
||||
import { closeAllCollections, selectRequestPaneTab } from '../../utils/page';
|
||||
|
||||
test.describe('Timeout Settings Tests', () => {
|
||||
test('should configure and test timeout settings', async ({
|
||||
@@ -13,7 +13,7 @@ test.describe('Timeout Settings Tests', () => {
|
||||
await page.getByRole('complementary').getByText('timeout-test').click();
|
||||
|
||||
// Go to Settings tab
|
||||
await page.getByRole('tab', { name: 'Settings' }).click();
|
||||
await selectRequestPaneTab(page, 'Settings');
|
||||
|
||||
// Test Timeout Settings with custom value
|
||||
const timeoutInput = page.locator('input[id="timeout"]');
|
||||
|
||||
@@ -132,7 +132,7 @@ const createUntitledRequest = async (
|
||||
|
||||
// Add tag if provided
|
||||
if (tag) {
|
||||
await page.getByRole('tab', { name: 'Settings' }).click();
|
||||
await selectRequestPaneTab(page, 'Settings');
|
||||
await page.waitForTimeout(200);
|
||||
const tagInput = await page.getByTestId('tag-input').getByRole('textbox');
|
||||
await tagInput.fill(tag);
|
||||
@@ -563,6 +563,33 @@ const getResponseBody = async (page: Page): Promise<string> => {
|
||||
return await page.locator('.response-pane').innerText();
|
||||
};
|
||||
|
||||
const selectRequestPaneTab = async (page: Page, tabName: string) => {
|
||||
await test.step(`Select request pane tab "${tabName}"`, async () => {
|
||||
const visibleTab = page.locator('.tabs').getByRole('tab', { name: tabName });
|
||||
const overflowButton = page.locator('.tabs .more-tabs');
|
||||
|
||||
// Check if tab is directly visible
|
||||
if (await visibleTab.isVisible()) {
|
||||
await visibleTab.click();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if there's an overflow dropdown
|
||||
if (await overflowButton.isVisible()) {
|
||||
await overflowButton.click();
|
||||
|
||||
// Wait for dropdown to appear and click the tab
|
||||
const dropdownTab = page.locator('.tippy-content').getByRole('tab', { name: tabName });
|
||||
await expect(dropdownTab).toBeVisible();
|
||||
await dropdownTab.click();
|
||||
return;
|
||||
}
|
||||
|
||||
// If neither found, fail with a helpful message
|
||||
throw new Error(`Tab "${tabName}" not found in visible tabs or overflow dropdown`);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Verify response contains specific text
|
||||
* @param page - The page object
|
||||
@@ -599,7 +626,8 @@ export {
|
||||
openRequest,
|
||||
openFolderRequest,
|
||||
getResponseBody,
|
||||
expectResponseContains
|
||||
expectResponseContains,
|
||||
selectRequestPaneTab
|
||||
};
|
||||
|
||||
export type { SandboxMode, EnvironmentType, EnvironmentVariable, CreateCollectionOptions, ImportCollectionOptions, CreateRequestOptions, CreateUntitledRequestOptions };
|
||||
|
||||
Reference in New Issue
Block a user