mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-30 08:04:09 +00:00
340 lines
12 KiB
JavaScript
340 lines
12 KiB
JavaScript
import React, { useState, useEffect, useRef } from 'react';
|
|
import find from 'lodash/find';
|
|
import toast from 'react-hot-toast';
|
|
import { useSelector, useDispatch } from 'react-redux';
|
|
import GraphQLRequestPane from 'components/RequestPane/GraphQLRequestPane';
|
|
import HttpRequestPane from 'components/RequestPane/HttpRequestPane';
|
|
import GrpcRequestPane from 'components/RequestPane/GrpcRequestPane/index';
|
|
import ResponsePane from 'components/ResponsePane';
|
|
import GrpcResponsePane from 'components/ResponsePane/GrpcResponsePane';
|
|
import { findItemInCollection } from 'utils/collections';
|
|
import { cancelRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
|
import RequestNotFound from './RequestNotFound';
|
|
import QueryUrl from 'components/RequestPane/QueryUrl/index';
|
|
import GrpcQueryUrl from 'components/RequestPane/GrpcQueryUrl/index';
|
|
import NetworkError from 'components/ResponsePane/NetworkError';
|
|
import RunnerResults from 'components/RunnerResults';
|
|
import VariablesEditor from 'components/VariablesEditor';
|
|
import CollectionSettings from 'components/CollectionSettings';
|
|
import { DocExplorer } from '@usebruno/graphql-docs';
|
|
|
|
import StyledWrapper from './StyledWrapper';
|
|
import SecuritySettings from 'components/SecuritySettings';
|
|
import FolderSettings from 'components/FolderSettings';
|
|
import { getGlobalEnvironmentVariables, getGlobalEnvironmentVariablesMasked } from 'utils/collections/index';
|
|
import { produce } from 'immer';
|
|
import CollectionOverview from 'components/CollectionSettings/Overview';
|
|
import RequestNotLoaded from './RequestNotLoaded';
|
|
import RequestIsLoading from './RequestIsLoading';
|
|
import FolderNotFound from './FolderNotFound';
|
|
import ExampleNotFound from './ExampleNotFound';
|
|
import WsQueryUrl from 'components/RequestPane/WsQueryUrl';
|
|
import WSRequestPane from 'components/RequestPane/WSRequestPane';
|
|
import WSResponsePane from 'components/ResponsePane/WsResponsePane';
|
|
import { useTabPaneBoundaries } from 'hooks/useTabPaneBoundaries/index';
|
|
import ResponseExample from 'components/ResponseExample';
|
|
import WorkspaceHome from 'components/WorkspaceHome';
|
|
|
|
const MIN_LEFT_PANE_WIDTH = 300;
|
|
const MIN_RIGHT_PANE_WIDTH = 350;
|
|
const MIN_TOP_PANE_HEIGHT = 150;
|
|
const MIN_BOTTOM_PANE_HEIGHT = 150;
|
|
|
|
const RequestTabPanel = () => {
|
|
if (typeof window == 'undefined') {
|
|
return <div></div>;
|
|
}
|
|
const dispatch = useDispatch();
|
|
const tabs = useSelector((state) => state.tabs.tabs);
|
|
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
|
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
|
|
const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments);
|
|
const _collections = useSelector((state) => state.collections.collections);
|
|
const preferences = useSelector((state) => state.app.preferences);
|
|
const isVerticalLayout = preferences?.layout?.responsePaneOrientation === 'vertical';
|
|
|
|
// merge `globalEnvironmentVariables` into the active collection and rebuild `collections` immer proxy object
|
|
let collections = produce(_collections, (draft) => {
|
|
let collection = find(draft, (c) => c.uid === focusedTab?.collectionUid);
|
|
|
|
if (collection) {
|
|
// add selected global env variables to the collection object
|
|
const globalEnvironmentVariables = getGlobalEnvironmentVariables({
|
|
globalEnvironments,
|
|
activeGlobalEnvironmentUid
|
|
});
|
|
const globalEnvSecrets = getGlobalEnvironmentVariablesMasked({ globalEnvironments, activeGlobalEnvironmentUid });
|
|
collection.globalEnvironmentVariables = globalEnvironmentVariables;
|
|
collection.globalEnvSecrets = globalEnvSecrets;
|
|
}
|
|
});
|
|
|
|
let collection = find(collections, (c) => c.uid === focusedTab?.collectionUid);
|
|
const [dragging, setDragging] = useState(false);
|
|
const dragOffset = useRef({ x: 0, y: 0 });
|
|
const { left: leftPaneWidth, top: topPaneHeight, reset: resetPaneBoundaries, setTop: setTopPaneHeight, setLeft: setLeftPaneWidth } = useTabPaneBoundaries(activeTabUid);
|
|
|
|
// Not a recommended pattern here to have the child component
|
|
// make a callback to set state, but treating this as an exception
|
|
const docExplorerRef = useRef(null);
|
|
const mainSectionRef = useRef(null);
|
|
const [schema, setSchema] = useState(null);
|
|
const [showGqlDocs, setShowGqlDocs] = useState(false);
|
|
const onSchemaLoad = (schema) => setSchema(schema);
|
|
const toggleDocs = () => setShowGqlDocs((showGqlDocs) => !showGqlDocs);
|
|
const handleGqlClickReference = (reference) => {
|
|
if (docExplorerRef.current) {
|
|
docExplorerRef.current.showDocForReference(reference);
|
|
}
|
|
if (!showGqlDocs) {
|
|
setShowGqlDocs(true);
|
|
}
|
|
};
|
|
|
|
const handleMouseMove = (e) => {
|
|
if (dragging && mainSectionRef.current) {
|
|
e.preventDefault();
|
|
const mainRect = mainSectionRef.current.getBoundingClientRect();
|
|
|
|
if (isVerticalLayout) {
|
|
const newHeight = e.clientY - mainRect.top - dragOffset.current.y;
|
|
if (newHeight < MIN_TOP_PANE_HEIGHT || newHeight > mainRect.height - MIN_BOTTOM_PANE_HEIGHT) {
|
|
return;
|
|
}
|
|
|
|
setTopPaneHeight(newHeight);
|
|
} else {
|
|
const newWidth = e.clientX - mainRect.left - dragOffset.current.x;
|
|
if (newWidth < MIN_LEFT_PANE_WIDTH || newWidth > mainRect.width - MIN_RIGHT_PANE_WIDTH) {
|
|
return;
|
|
}
|
|
|
|
setLeftPaneWidth(newWidth);
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleMouseUp = (e) => {
|
|
if (dragging) {
|
|
e.preventDefault();
|
|
setDragging(false);
|
|
}
|
|
};
|
|
|
|
const handleDragbarMouseDown = (e) => {
|
|
e.preventDefault();
|
|
setDragging(true);
|
|
};
|
|
|
|
useEffect(() => {
|
|
document.addEventListener('mouseup', handleMouseUp);
|
|
document.addEventListener('mousemove', handleMouseMove);
|
|
|
|
return () => {
|
|
document.removeEventListener('mouseup', handleMouseUp);
|
|
document.removeEventListener('mousemove', handleMouseMove);
|
|
};
|
|
}, [dragging]);
|
|
|
|
if (!activeTabUid) {
|
|
return <WorkspaceHome />;
|
|
}
|
|
|
|
if (!focusedTab || !focusedTab.uid || !focusedTab.collectionUid) {
|
|
return <div className="pb-4 px-4">An error occurred!</div>;
|
|
}
|
|
|
|
if (!collection || !collection.uid) {
|
|
return <div className="pb-4 px-4">Collection not found!</div>;
|
|
}
|
|
|
|
if (focusedTab.type === 'response-example') {
|
|
const item = findItemInCollection(collection, focusedTab.itemUid);
|
|
const example = item?.examples?.find((ex) => ex.uid === focusedTab.uid);
|
|
|
|
if (!example) {
|
|
return <ExampleNotFound itemUid={focusedTab.itemUid} exampleUid={focusedTab.uid} />;
|
|
}
|
|
return <ResponseExample item={item} collection={collection} example={example} />;
|
|
}
|
|
|
|
const item = findItemInCollection(collection, activeTabUid);
|
|
const isGrpcRequest = item?.type === 'grpc-request';
|
|
const isWsRequest = item?.type === 'ws-request';
|
|
|
|
if (focusedTab.type === 'collection-runner') {
|
|
return <RunnerResults collection={collection} />;
|
|
}
|
|
|
|
if (focusedTab.type === 'variables') {
|
|
return <VariablesEditor collection={collection} />;
|
|
}
|
|
|
|
if (focusedTab.type === 'collection-settings') {
|
|
return <CollectionSettings collection={collection} />;
|
|
}
|
|
|
|
if (focusedTab.type === 'collection-overview') {
|
|
return <CollectionOverview collection={collection} />;
|
|
}
|
|
|
|
if (focusedTab.type === 'folder-settings') {
|
|
const folder = findItemInCollection(collection, focusedTab.folderUid);
|
|
if (!folder) {
|
|
return <FolderNotFound folderUid={focusedTab.folderUid} />;
|
|
}
|
|
|
|
return <FolderSettings collection={collection} folder={folder} />;
|
|
}
|
|
|
|
if (focusedTab.type === 'security-settings') {
|
|
return <SecuritySettings collection={collection} />;
|
|
}
|
|
|
|
if (!item || !item.uid) {
|
|
return <RequestNotFound itemUid={activeTabUid} />;
|
|
}
|
|
|
|
if (item?.partial) {
|
|
return <RequestNotLoaded item={item} collection={collection} />;
|
|
}
|
|
|
|
if (item?.loading) {
|
|
return <RequestIsLoading item={item} />;
|
|
}
|
|
|
|
const handleRun = async () => {
|
|
const isGrpcRequest = item?.type === 'grpc-request';
|
|
const isWsRequest = item?.type === 'ws-request';
|
|
const request = item.draft ? item.draft.request : item.request;
|
|
|
|
if (isGrpcRequest && !request.url) {
|
|
toast.error('Please enter a valid gRPC server URL');
|
|
return;
|
|
}
|
|
|
|
if (isGrpcRequest && !request.method) {
|
|
toast.error('Please select a gRPC method');
|
|
return;
|
|
}
|
|
|
|
if (isWsRequest && !request.url) {
|
|
toast.error('Please enter a valid WebSocket URL');
|
|
return;
|
|
}
|
|
|
|
if (item.response?.stream?.running) {
|
|
dispatch(cancelRequest(item.cancelTokenUid, item, collection)).catch((err) =>
|
|
toast.custom((t) => <NetworkError onClose={() => toast.dismiss(t.id)} />, {
|
|
duration: 5000
|
|
}));
|
|
} else if (item.requestState !== 'sending' && item.requestState !== 'queued') {
|
|
dispatch(sendRequest(item, collection.uid)).catch((err) =>
|
|
toast.custom((t) => <NetworkError onClose={() => toast.dismiss(t.id)} />, {
|
|
duration: 5000
|
|
}));
|
|
}
|
|
};
|
|
|
|
// TODO: reaper, improve selection of panes
|
|
return (
|
|
<StyledWrapper
|
|
className={`flex flex-col flex-grow relative ${dragging ? 'dragging' : ''} ${
|
|
isVerticalLayout ? 'vertical-layout' : ''
|
|
}`}
|
|
>
|
|
<div className="pt-3 pb-3 px-4">
|
|
{
|
|
isGrpcRequest
|
|
? <GrpcQueryUrl item={item} collection={collection} handleRun={handleRun} />
|
|
: isWsRequest
|
|
? <WsQueryUrl item={item} collection={collection} handleRun={handleRun} />
|
|
: <QueryUrl item={item} collection={collection} handleRun={handleRun} />
|
|
}
|
|
</div>
|
|
<section ref={mainSectionRef} className={`main flex ${isVerticalLayout ? 'flex-col' : ''} flex-grow pb-4 relative overflow-auto`}>
|
|
<section className="request-pane">
|
|
<div
|
|
className="px-4 h-full"
|
|
style={isVerticalLayout ? {
|
|
height: `${Math.max(topPaneHeight, MIN_TOP_PANE_HEIGHT)}px`,
|
|
minHeight: `${MIN_TOP_PANE_HEIGHT}px`,
|
|
width: '100%'
|
|
|
|
} : {
|
|
width: `${Math.max(leftPaneWidth, MIN_LEFT_PANE_WIDTH)}px`
|
|
}}
|
|
>
|
|
{item.type === 'graphql-request' ? (
|
|
<GraphQLRequestPane
|
|
item={item}
|
|
collection={collection}
|
|
onSchemaLoad={onSchemaLoad}
|
|
toggleDocs={toggleDocs}
|
|
handleGqlClickReference={handleGqlClickReference}
|
|
/>
|
|
) : null}
|
|
|
|
{item.type === 'http-request' ? (
|
|
<HttpRequestPane item={item} collection={collection} />
|
|
) : null}
|
|
|
|
{isGrpcRequest ? (
|
|
<GrpcRequestPane item={item} collection={collection} handleRun={handleRun} />
|
|
) : null}
|
|
|
|
{isWsRequest ? (
|
|
<WSRequestPane item={item} collection={collection} handleRun={handleRun} />
|
|
) : null}
|
|
</div>
|
|
</section>
|
|
|
|
<div
|
|
className="dragbar-wrapper"
|
|
onDoubleClick={(e) => {
|
|
e.preventDefault();
|
|
resetPaneBoundaries();
|
|
}}
|
|
onMouseDown={handleDragbarMouseDown}
|
|
>
|
|
<div className="dragbar-handle" />
|
|
</div>
|
|
|
|
<section className="response-pane flex-grow overflow-x-auto">
|
|
{item.type === 'grpc-request' ? (
|
|
<GrpcResponsePane
|
|
item={item}
|
|
collection={collection}
|
|
response={item.response}
|
|
/>
|
|
) : item.type === 'ws-request' ? (
|
|
<WSResponsePane
|
|
item={item}
|
|
collection={collection}
|
|
response={item.response}
|
|
/>
|
|
) : (
|
|
<ResponsePane
|
|
item={item}
|
|
collection={collection}
|
|
response={item.response}
|
|
/>
|
|
)}
|
|
</section>
|
|
</section>
|
|
|
|
{item.type === 'graphql-request' ? (
|
|
<div className={`graphql-docs-explorer-container ${showGqlDocs ? '' : 'hidden'}`}>
|
|
<DocExplorer schema={schema} ref={(r) => (docExplorerRef.current = r)}>
|
|
<button className="mr-2" onClick={toggleDocs} aria-label="Close Documentation Explorer">
|
|
{'\u2715'}
|
|
</button>
|
|
</DocExplorer>
|
|
</div>
|
|
) : null}
|
|
</StyledWrapper>
|
|
);
|
|
};
|
|
|
|
export default RequestTabPanel;
|