timeline ui updates wip

This commit is contained in:
lohxt1
2025-03-17 14:09:36 +05:30
parent 51be153527
commit d3fcb42a8f
17 changed files with 487 additions and 645 deletions

View File

@@ -139,7 +139,7 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
return (
<StyledWrapper
className="w-full h-full relative"
className="w-full h-full relative flex"
style={{ maxWidth: width }}
queryFilterEnabled={queryFilterEnabled}
>

View File

@@ -2,13 +2,11 @@ import styled from 'styled-components';
const StyledWrapper = styled.div`
.timeline-event {
border-bottom: 1px solid ${(props) => props.theme.colors.text.muted};
padding: 8px 0;
padding: 8px 0 0 0;
cursor: pointer;
}
.timeline-event-content {
background: ${(props) => props.theme.requestTabs.bg};
border-radius: 4px;
padding: 12px;
margin-top: 0.5rem;
@@ -83,13 +81,10 @@ const StyledWrapper = styled.div`
background: ${(props) => props.theme.codemirror.bg};
color: ${(props) => props.theme.text};
border-radius: 4px;
padding: 8px;
}
.oauth-request-item-content {
background: ${(props) => props.theme.requestTabs.bg};
border-radius: 4px;
padding: 12px;
margin-top: 0.5rem;
}
@@ -98,7 +93,6 @@ const StyledWrapper = styled.div`
.section-header {
cursor: pointer;
padding: 8px 0;
&:hover {
opacity: 0.8;
}

View File

@@ -0,0 +1,37 @@
import QueryResult from "components/ResponsePane/QueryResult/index";
import { useState } from "react";
const BodyBlock = ({ collection, data, dataBuffer, headers, error, item, width }) => {
const [isBodyCollapsed, toggleBody] = useState(true);
return (
<div className="collapsible-section">
<div className="section-header" onClick={() => toggleBody(!isBodyCollapsed)}>
<pre className="flex flex-row items-center text-lg text-indigo-500/80 dark:text-indigo-500/80">
<div className="opacity-70">{isBodyCollapsed ? '▼' : '▶'}</div> Body
</pre>
</div>
{isBodyCollapsed && (
<div className="mt-2">
{data || dataBuffer ? (
<div className="h-96 overflow-auto">
<QueryResult
item={item}
collection={collection}
width={width}
data={data}
dataBuffer={dataBuffer}
headers={headers}
error={error}
key={item?.uid}
/>
</div>
) : (
<div className="text-gray-500">No Body found</div>
)}
</div>
)}
</div>
)
}
export default BodyBlock;

View File

@@ -0,0 +1,54 @@
import { useState } from "react";
const HeadersBlock = ({ headers, type }) => {
const [areHeadersCollapsed, toggleHeaders] = useState(true);
return (
<div className="collapsible-section mt-2">
<div className="section-header" onClick={() => toggleHeaders(!areHeadersCollapsed)}>
<pre className="flex flex-row items-center text-lg text-indigo-500/80 dark:text-indigo-500/80">
<div className="opacity-70">{areHeadersCollapsed ? '▼' : '▶'}</div> Headers
{headers && Object.keys(headers).length > 0 &&
<div className="ml-1">({Object.keys(headers).length})</div>
}
</pre>
</div>
{areHeadersCollapsed && (
<div className="mt-1">
{headers && Object.keys(headers).length > 0
? <Headers headers={headers} type={type} />
: <div className="text-gray-500">No Headers found</div>
}
</div>
)}
</div>
)
};
const Headers = ({ headers, type }) => {
if (Array.isArray(headers)) {
return (
<div className="mt-1 text-sm">
{headers.map((header, index) => (
<pre key={index} className="mb-1 whitespace-pre-wrap">
{type === 'request' ? '>' : '<'}&nbsp;<span className="opacity-60">{header?.name}:</span>
<span className="whitespace-pre-wrap">{String(header?.value)}</span>
</pre>
))}
</div>
);
} else {
return (
<div className="mt-1 text-sm">
{Object.entries(headers).map(([key, value], index) => (
<pre key={index} className="mb-1 whitespace-pre-wrap">
{type === 'request' ? '>' : '<'}&nbsp;<span className="opacity-60">{key}:</span>
<span>{String(value)}</span>
</pre>
))}
</div>
);
}
};
export default HeadersBlock;

View File

@@ -0,0 +1,19 @@
const Method = ({ method }) => {
return (
<span className={`${methodColors[method?.toUpperCase()] || 'text-white'} font-bold`}>
{method?.toUpperCase()}
</span>
)
}
const methodColors = {
GET: 'text-green-500',
POST: 'text-blue-500',
PUT: 'text-yellow-500',
DELETE: 'text-red-500',
PATCH: 'text-purple-500',
OPTIONS: 'text-gray-500',
HEAD: 'text-gray-500',
};
export default Method;

View File

@@ -0,0 +1,26 @@
const Status = ({ statusCode, statusText }) => {
return (
<span
className={`${
statusColor(statusCode) || 'text-white'
} font-bold`}
>
{statusCode}{' '}
{statusText || ''}
</span>
)
}
const statusColor = (statusCode) => {
if (statusCode >= 200 && statusCode < 300) {
return 'text-green-500';
} else if (statusCode >= 300 && statusCode < 400) {
return 'text-yellow-500';
} else if (statusCode >= 400 && statusCode < 600) {
return 'text-red-500';
} else {
return 'text-gray-500';
}
};
export default Status;

View File

@@ -0,0 +1,36 @@
import { useState, useEffect } from "react";
const getRelativeTime = (date) => {
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
const diff = (date - new Date()) / 1000;
const timeUnits = [
{ unit: 'year', seconds: 31536000 },
{ unit: 'month', seconds: 2592000 },
{ unit: 'week', seconds: 604800 },
{ unit: 'day', seconds: 86400 },
{ unit: 'hour', seconds: 3600 },
{ unit: 'minute', seconds: 60 },
{ unit: 'second', seconds: 1 }
];
for (const { unit, seconds } of timeUnits) {
if (Math.abs(diff) >= seconds || unit === 'second') {
return rtf.format(Math.round(diff / seconds), unit);
}
}
};
export const RelativeTime = ({ timestamp }) => {
const [relativeTime, setRelativeTime] = useState(getRelativeTime(new Date(timestamp)));
useEffect(() => {
const interval = setInterval(() => {
setRelativeTime(getRelativeTime(new Date(timestamp)));
}, 1000);
return () => clearInterval(interval);
}, [timestamp]);
return <pre className="text-xs">{relativeTime}</pre>;
};

View File

@@ -0,0 +1,46 @@
const Network = ({ logs }) => {
return (
<div className="bg-black/5 text-white network-logs rounded overflow-auto h-96">
<pre className="whitespace-pre-wrap">
{logs.map((entry, index) => (
<NetworkLogsEntry key={index} entry={entry} />
))}
</pre>
</div>
)
}
const NetworkLogsEntry = ({ entry }) => {
const { type, message } = entry;
let className = '';
switch (type) {
case 'request':
className = 'text-blue-500';
break;
case 'response':
className = 'text-green-500';
break;
case 'error':
className = 'text-red-500';
break;
case 'tls':
className = 'text-purple-500';
break;
case 'info':
className = 'text-yellow-500';
break;
default:
className = 'text-gray-400';
break;
}
return (
<div className={`${className}`}>
<div>{message}</div>
</div>
);
};
export default Network;

View File

@@ -0,0 +1,23 @@
import Headers from "../Common/Headers/index";
import BodyBlock from "../Common/Body/index";
const Request = ({ collection, request, item, width }) => {
const { url, headers, data, dataBuffer, error } = request || {};
return (
<div>
{/* Method and URL */}
<div className="mb-1 flex gap-2">
<pre className="whitespace-pre-wrap">{url}</pre>
</div>
{/* Headers */}
<Headers headers={headers} type={'request'} />
{/* Body */}
<BodyBlock collection={collection} data={data} dataBuffer={dataBuffer} error={error} headers={headers} item={item} width={width} />
</div>
)
}
export default Request;

View File

@@ -0,0 +1,26 @@
import BodyBlock from "../Common/Body/index";
import Headers from "../Common/Headers/index";
import Status from "../Common/Status/index";
const Response = ({ collection, response, item, width }) => {
const { status, statusCode, statusText, headers, data, dataBuffer, error } = response || {};
return (
<div>
{/* Status */}
<div className="mb-1">
<Status statusCode={status || statusCode} statusText={statusText} />
{response.duration && <span className="text-sm text-gray-400 ml-2">{response.duration}ms</span>}
{response.size && <span className="text-sm text-gray-400 ml-2">{response.size}B</span>}
</div>
{/* Headers */}
<Headers headers={headers} type={'response'} />
{/* Body */}
<BodyBlock collection={collection} data={data} dataBuffer={dataBuffer} error={error} headers={headers} item={item} width={width} />
</div>
)
}
export default Response;

View File

@@ -0,0 +1,79 @@
import { useState } from "react";
import Network from "./Network/index";
import Request from "./Request/index";
import Response from "./Response/index";
import Method from "./Common/Method/index";
import Status from "./Common/Status/index";
import { RelativeTime } from "./Common/Time/index";
const TimelineItem = ({ timestamp, request, response, item, collection, width, isOauth2 }) => {
const [isCollapsed, _toggleCollapse] = useState(false);
const [activeTab, setActiveTab] = useState('request');
const toggleCollapse = () => _toggleCollapse(prev => !prev);
const { method, status, statusCode, statusText, url = '' } = request || {};
const showNetworkLogs = response.timeline && response.timeline.length > 0;
return (
<div className={`border-b-2 ${isOauth2 ? 'border-indigo-700/50' : 'border-amber-700/50' } py-2`}>
<div className="oauth-request-item-header cursor-pointer" onClick={toggleCollapse}>
<div className="flex justify-between items-center min-w-0">
<div className="flex items-center space-x-2 min-w-0">
<Method method={method} />
<Status statusCode={status || statusCode} statusText={statusText} />
{isOauth2 ? <pre className="opacity-50">[oauth2.0]</pre> : null}
<pre className="opacity-70">[{new Date(timestamp).toISOString()}]</pre>
</div>
<span className="text-sm text-gray-400 flex-shrink-0 overflow-hidden text-ellipsis whitespace-nowrap">
<RelativeTime timestamp={timestamp} />
</span>
</div>
<div className="truncate text-sm mt-1">{url}</div>
</div>
{isCollapsed && (<div className="text-sm overflow-hidden">
{/* Tabs */}
<div className="tabs-switcher flex mb-4">
<button
className={`mr-4 ${activeTab === 'request' ? 'active' : 'text-gray-400'}`}
onClick={() => setActiveTab('request')}
>
Request
</button>
<button
className={`mr-4 ${activeTab === 'response' ? 'active' : 'text-gray-400'}`}
onClick={() => setActiveTab('response')}
>
Response
</button>
{showNetworkLogs && (
<button
className={`${activeTab === 'networkLogs' ? 'active' : 'text-gray-400'}`}
onClick={() => setActiveTab('networkLogs')}
>
Network Logs
</button>
)}
</div>
{/* Tab Content */}
<div className="tab-content">
{/* Request Tab */}
{activeTab === 'request' && (
<Request request={request} item={item} collection={collection} width={width} />
)}
{/* Response Tab */}
{activeTab === 'response' && (
<Response response={response} item={item} collection={collection} width={width} />
)}
{/* Network Logs Tab */}
{activeTab === 'networkLogs' && showNetworkLogs && (
<Network logs={response?.timeline} />
)}
</div>
</div>)}
</div>
);
};
export default TimelineItem;

View File

@@ -1,31 +1,8 @@
import React, { useState } from 'react';
import StyledWrapper from './StyledWrapper';
import QueryResult from '../QueryResult/index';
import { findItemInCollection, findParentItemInCollection } from 'utils/collections/index';
import { get } from 'lodash';
const iconv = require('iconv-lite');
const methodColors = {
GET: 'text-green-500',
POST: 'text-blue-500',
PUT: 'text-yellow-500',
DELETE: 'text-red-500',
PATCH: 'text-purple-500',
OPTIONS: 'text-gray-500',
HEAD: 'text-gray-500',
};
const statusColor = (statusCode) => {
if (statusCode >= 200 && statusCode < 300) {
return 'text-green-500';
} else if (statusCode >= 300 && statusCode < 400) {
return 'text-yellow-500';
} else if (statusCode >= 400 && statusCode < 600) {
return 'text-red-500';
} else {
return 'text-gray-500';
}
};
import TimelineItem from './TimelineItem/index';
const getEffectiveAuthSource = (collection, item) => {
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
@@ -82,121 +59,57 @@ const Timeline = ({ collection, item, width }) => {
return false;
}).sort((a, b) => b.timestamp - a.timestamp);
const [openSections, setOpenSections] = useState(() =>
combinedTimeline.map((_, index) => index === 0)
);
return (
<StyledWrapper
className="pb-4 w-full"
style={{ maxWidth: width - 40, overflowWrap: 'break-word' }}
className="pb-4 w-full flex flex-grow flex-col"
style={{ maxWidth: width - 60, overflowWrap: 'break-word' }}
>
{combinedTimeline.map((event, index) => {
const isOpen = openSections[index];
const toggleOpen = () => {
setOpenSections((prevState) => {
const newState = [...prevState];
newState[index] = !newState[index];
return newState;
});
};
if (event.type === 'request') {
const { request, response, timestamp } = event.data;
const { data, timestamp } = event;
const { request, response } = data;
return (
<div key={index} className="timeline-event border-b border-gray-700 py-2">
<div
className="timeline-event-header cursor-pointer"
onClick={toggleOpen}
>
<div className="flex justify-between items-center min-w-0 gap-2">
<div className="flex items-center space-x-2 min-w-0 flex-1">
<div className="flex items-center flex-shrink-0">
<span
className={`${
methodColors[request.method?.toUpperCase()] || 'text-white'
} font-bold`}
>
{request.method?.toUpperCase()}
</span>{' '}
</div>
<span
className={`${
statusColor(response.status || request.statusCode) || 'text-white'
} font-bold`}
>
{response.status || request.statusCode}{' '}
{response.statusText || ''}
</span>
<div className="flex items-center flex-shrink-0 space-x-2">
{response.duration && (
<span className="text-sm text-gray-400">
{response.duration}ms
</span>
)}
{response.size && (
<span className="text-sm text-gray-400">
{response.size}B
</span>
)}
{response.state && (
<span className="text-sm text-gray-400">
{response.state}
</span>
)}
</div>
</div>
<span className="text-sm text-gray-400 flex-shrink-0 overflow-hidden text-ellipsis whitespace-nowrap" style={{ minWidth: '120px', maxWidth: '200px' }}>
{new Date(timestamp).toLocaleString()}
</span>
</div>
<div className="truncate text-sm text-[#9b9b9b] mt-1">{request.url}</div>
</div>
{isOpen && (
<div className="timeline-event-content ml-4 mt-2">
<RenderRequestResponse
request={request}
response={response}
item={item}
collection={collection}
width={width}
/>
</div>
)}
<div key={index} className="timeline-event mb-2">
<TimelineItem
timestamp={timestamp}
request={request}
response={response}
item={item}
collection={collection}
width={width}
/>
</div>
);
} else if (event.type === 'oauth2') {
const { data } = event;
const { debugInfo, fetchedAt } = data;
const flattenedRequests = flattenRequests(debugInfo);
const { data, timestamp } = event;
const { debugInfo } = data;
return (
<div key={index} className="timeline-event border-b border-gray-700 py-2">
<div
className="timeline-event-header cursor-pointer flex items-center"
onClick={toggleOpen}
>
<div key={index} className="timeline-event">
<div className="timeline-event-header cursor-pointer flex items-center">
<div className="flex items-center">
<span>{isOpen ? '▼' : '▶'}</span>
<span className="ml-2 font-bold">OAuth2.0 Calls</span>
<span className="font-bold">OAuth2.0 Calls</span>
</div>
</div>
{isOpen && (
<div className="ml-4 mt-2">
{flattenedRequests && flattenedRequests.length > 0 ? (
flattenedRequests.map((data, idx) => (
<OAuthRequestItem
key={idx}
request={data}
item={item}
collection={collection}
width={width - 50}
/>
<div className="mt-2">
{debugInfo && debugInfo.length > 0 ? (
debugInfo.map((data, idx) => (
<div className='ml-4'>
<TimelineItem
key={idx}
timestamp={timestamp}
request={data?.request}
response={data?.response}
item={item}
collection={collection}
width={width - 50}
isOauth2={true}
/>
</div>
))
) : (
<div>No debug information available.</div>
)}
</div>
)}
</div>
);
}
@@ -207,409 +120,4 @@ const Timeline = ({ collection, item, width }) => {
);
};
const flattenRequests = (requests, level = 0) => {
let flatList = [];
requests.forEach((request) => {
flatList.push({ ...request, isSubRequest: level > 0 });
if (request.requests && request.requests.length > 0) {
flatList = flatList.concat(flattenRequests(request.requests, level + 1));
}
});
return flatList;
};
// Helper function to process dataBuffer
const processDataBuffer = (dataBuffer) => {
if (dataBuffer) {
try {
let buffer;
if (Buffer.isBuffer(dataBuffer)) {
buffer = dataBuffer;
} else if (typeof dataBuffer === 'string') {
buffer = Buffer.from(dataBuffer, 'base64');
} else if (dataBuffer instanceof Uint8Array || Array.isArray(dataBuffer)) {
buffer = Buffer.from(dataBuffer);
} else {
return JSON.stringify(dataBuffer);
}
const dataRaw = iconv.decode(buffer, 'utf-8');
const formattedData = dataRaw.replace(/^\uFEFF/, '');
return formattedData;
} catch (error) {
console.error('Error processing dataBuffer:', error);
return '';
}
}
return null;
};
const RenderRequestResponse = ({ request, response, item, collection, width }) => {
const [activeTab, setActiveTab] = useState('request');
const [isRequestHeadersOpen, setIsRequestHeadersOpen] = useState(false);
const [isResponseHeadersOpen, setIsResponseHeadersOpen] = useState(false);
const [isRequestCookiesOpen, setIsRequestCookiesOpen] = useState(false);
const [isResponseCookiesOpen, setIsResponseCookiesOpen] = useState(false);
const [isRequestBodyOpen, setIsRequestBodyOpen] = useState(false);
const [isResponseBodyOpen, setIsResponseBodyOpen] = useState(false);
const requestHeaders = request.headers || request.requestHeaders || {};
const responseHeaders = response.headers || response.responseHeaders || {};
const {
cookies: requestCookies,
headers: filteredRequestHeaders,
} = separateCookiesAndHeaders(requestHeaders);
const {
cookies: responseCookies,
headers: filteredResponseHeaders,
} = separateCookiesAndHeaders(responseHeaders);
const showNetworkLogs = response.timeline && response.timeline.length > 0;
return (
<div className="text-sm overflow-hidden">
{/* Tabs */}
<div className="tabs-switcher flex mb-4">
<button
className={`mr-4 ${activeTab === 'request' ? 'active' : 'text-gray-400'}`}
onClick={() => setActiveTab('request')}
>
Request
</button>
<button
className={`mr-4 ${activeTab === 'response' ? 'active' : 'text-gray-400'}`}
onClick={() => setActiveTab('response')}
>
Response
</button>
{showNetworkLogs && (
<button
className={`${activeTab === 'networkLogs' ? 'active' : 'text-gray-400'}`}
onClick={() => setActiveTab('networkLogs')}
>
Network Logs
</button>
)}
</div>
{/* Tab Content */}
<div className="tab-content">
{/* Request Tab */}
{activeTab === 'request' && (
<div>
{/* Method and URL */}
<div className="mb-4">
<span className={`${methodColors[request.method.toUpperCase()] || 'text-white'} font-bold`}>
{request.method.toUpperCase()}
</span>{' '}
<span>{request.url}</span>
</div>
{/* Headers */}
<div className="collapsible-section">
<div className="section-header" onClick={() => setIsRequestHeadersOpen(!isRequestHeadersOpen)}>
<span className="font-bold">
{isRequestHeadersOpen ? '▼' : '▶'} Headers
{filteredRequestHeaders && Object.keys(filteredRequestHeaders).length > 0 &&
<sup className="ml-1 font-medium">({Object.keys(filteredRequestHeaders).length})</sup>
}
</span>
</div>
{isRequestHeadersOpen && (
<div className="mt-2">
{filteredRequestHeaders && Object.keys(filteredRequestHeaders).length > 0
? renderHeaders(filteredRequestHeaders)
: <div className="text-gray-500">No Headers found</div>
}
</div>
)}
</div>
{/* Cookies */}
<div className="collapsible-section">
<div className="section-header" onClick={() => setIsRequestCookiesOpen(!isRequestCookiesOpen)}>
<span className="font-bold">
{isRequestCookiesOpen ? '▼' : '▶'} Cookies
{requestCookies && Object.keys(requestCookies).length > 0 &&
<sup className="ml-1 font-medium">({Object.keys(requestCookies).length})</sup>
}
</span>
</div>
{isRequestCookiesOpen && (
<div className="mt-2">
{requestCookies && Object.keys(requestCookies).length > 0
? renderHeaders(requestCookies)
: <div className="text-gray-500">No Cookies found</div>
}
</div>
)}
</div>
{/* Body */}
<div className="collapsible-section">
<div className="section-header" onClick={() => setIsRequestBodyOpen(!isRequestBodyOpen)}>
<span className="font-bold">
{isRequestBodyOpen ? '▼' : '▶'} Body
</span>
</div>
{isRequestBodyOpen && (
<div className="mt-2">
{request.data || request.dataBuffer || request?.requestBody ? (
<div className="h-96 overflow-auto">
<QueryResult
item={item}
collection={collection}
width={width}
data={request?.requestBody || request.data}
dataBuffer={request.dataBuffer}
headers={request.headers}
error={request.error}
key={item.filename}
/>
</div>
) : (
<div className="text-gray-500">No Body found</div>
)}
</div>
)}
</div>
</div>
)}
{/* Response Tab */}
{activeTab === 'response' && (
<div>
{/* Status */}
<div className="mb-4">
<span className={`${statusColor(response.status || request.statusCode) || 'text-white'} font-bold`}>
{response.status || request.statusCode}
</span>{' '}
<span>{response.statusText || request.statusText || ''}</span>
{response.duration && <span className="text-sm text-gray-400 ml-2">{response.duration}ms</span>}
{response.size && <span className="text-sm text-gray-400 ml-2">{response.size}B</span>}
</div>
{/* Headers */}
<div className="collapsible-section">
<div className="section-header" onClick={() => setIsResponseHeadersOpen(!isResponseHeadersOpen)}>
<span className="font-bold">
{isResponseHeadersOpen ? '▼' : '▶'} Headers
{filteredResponseHeaders && Object.keys(filteredResponseHeaders).length > 0 &&
<sup className="ml-1 font-medium">({Object.keys(filteredResponseHeaders).length})</sup>
}
</span>
</div>
{isResponseHeadersOpen && (
<div className="mt-2">
{filteredResponseHeaders && Object.keys(filteredResponseHeaders).length > 0
? renderHeaders(filteredResponseHeaders)
: <div className="text-gray-500">No Headers found</div>
}
</div>
)}
</div>
{/* Cookies */}
<div className="collapsible-section">
<div className="section-header" onClick={() => setIsResponseCookiesOpen(!isResponseCookiesOpen)}>
<span className="font-bold">
{isResponseCookiesOpen ? '▼' : '▶'} Cookies
{responseCookies && Object.keys(responseCookies).length > 0 &&
<sup className="ml-1 font-medium">({Object.keys(responseCookies).length})</sup>
}
</span>
</div>
{isResponseCookiesOpen && (
<div className="mt-2">
{responseCookies && Object.keys(responseCookies).length > 0
? renderHeaders(responseCookies)
: <div className="text-gray-500">No Cookies found</div>
}
</div>
)}
</div>
{/* Body */}
<div className="collapsible-section">
<div className="section-header" onClick={() => setIsResponseBodyOpen(!isResponseBodyOpen)}>
<span className="font-bold">
{isResponseBodyOpen ? '▼' : '▶'} Body
</span>
</div>
{isResponseBodyOpen && (
<div className="mt-2">
{response.data || response.dataBuffer ? (
<div className="h-96 overflow-auto">
<QueryResult
item={item}
collection={collection}
width={width}
data={response.data}
dataBuffer={response.dataBuffer}
headers={response.headers}
error={response.error}
key={item.filename}
/>
</div>
) : (
<div className="text-gray-500">No Body found</div>
)}
</div>
)}
</div>
</div>
)}
{/* Network Logs Tab */}
{activeTab === 'networkLogs' && showNetworkLogs && (
<div className="bg-black/5 text-white p-2 network-logs rounded overflow-auto h-96">
<pre className="whitespace-pre-wrap">
{response.timeline.map((entry, index) => (
<NetworkLogsEntry key={index} entry={entry} />
))}
</pre>
</div>
)}
</div>
</div>
);
};
const NetworkLogsEntry = ({ entry }) => {
const { type, message } = entry;
let className = '';
switch (type) {
case 'request':
className = 'text-blue-500';
break;
case 'response':
className = 'text-green-500';
break;
case 'error':
className = 'text-red-500';
break;
case 'tls':
className = 'text-purple-500';
break;
case 'info':
className = 'text-yellow-500';
break;
default:
className = 'text-gray-400';
break;
}
return (
<div className={`${className}`}>
<div>{message}</div>
</div>
);
};
// Helper functions
const renderHeaders = (data) => {
if (Array.isArray(data)) {
return (
<div className="mt-2 text-sm">
{data.map((header, index) => (
<div key={index} className="flex mb-2">
<div className="w-1/3 font-bold">{header.name}:</div>
<div className="w-2/3">{String(header.value)}</div>
</div>
))}
</div>
);
} else {
return (
<div className="mt-2 text-sm">
{Object.entries(data).map(([key, value], index) => (
<div key={index} className="flex mb-2">
<div className="w-1/3 font-bold">{key}:</div>
<div className="w-2/3">{String(value)}</div>
</div>
))}
</div>
);
}
};
const separateCookiesAndHeaders = (headers) => {
const cookies = {};
let filteredHeaders = {};
if (Array.isArray(headers)) {
filteredHeaders = headers.filter((header) => header.enabled !== false);
filteredHeaders.forEach((header) => {
if (
header.name.toLowerCase() === 'cookie' ||
header.name.toLowerCase() === 'set-cookie'
) {
cookies[header.name] = header.value;
}
});
} else {
for (const [key, value] of Object.entries(headers)) {
if (key.toLowerCase() === 'cookie' || key.toLowerCase() === 'set-cookie') {
cookies[key] = value;
} else {
filteredHeaders[key] = value;
}
}
}
return { cookies, headers: filteredHeaders };
};
const OAuthRequestItem = ({ request, item, collection, width }) => {
const [isOpen, setIsOpen] = useState(false);
const toggleOpen = () => {
setIsOpen((prev) => !prev);
};
const { isSubRequest } = request;
const url = request.url || '';
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'];
const isImage = imageExtensions.some((ext) => url.toLowerCase().endsWith(ext));
return (
<div className="border-b border-gray-700 py-2">
<div className="oauth-request-item-header cursor-pointer" onClick={toggleOpen}>
<div className="flex justify-between items-center min-w-0">
<div className="flex items-center space-x-2 min-w-0">
<div className="flex items-center flex-shrink-0">
<span className={`${methodColors[request.method?.toUpperCase()] || 'text-white'} font-bold`}>
{request.method?.toUpperCase()}
</span>{' '}
</div>
<span className={`${statusColor(request.statusCode) || 'text-white'} font-bold`}>
{request.statusCode}
{request.statusText || ''}
</span>
<div className="flex items-center flex-shrink-0 space-x-2">
{isSubRequest && <span className="request-label">API Request</span>}
{isImage && <span className="request-label">Image</span>}
{request.duration && <span className="text-sm text-gray-400">{request.duration}ms</span>}
{request.size && <span className="text-sm text-gray-400">{request.size}B</span>}
{request.state && <span className="text-sm text-gray-400">{request.state}</span>}
</div>
</div>
</div>
<div className="truncate text-sm text-[#9b9b9b] mt-1">{request.url}</div>
</div>
{isOpen && (
<div className="oauth-request-item-content mt-2 text-sm">
<RenderRequestResponse
request={request}
response={request}
item={item}
collection={collection}
width={width}
/>
</div>
)}
</div>
);
};
export default Timeline;

View File

@@ -298,6 +298,8 @@ export const collectionsSlice = createSlice({
? item.requestSent.timestamp.getTime()
: item?.requestSent?.timestamp || Date.now();
console.log("response reieved", JSON.stringify(item), JSON.stringify(item.requestSent));
// Append the new timeline entry with numeric timestamp
collection.timeline.push({
type: "request",

View File

@@ -187,6 +187,6 @@ export const stringifyIfNot = v => typeof v === 'string' ? v : String(v);
export const getEncoding = (headers) => {
// Parse the charset from content type: https://stackoverflow.com/a/33192813
const charsetMatch = /charset=([^()<>@,;:"/[\]?.=\s]*)/i.exec(headers['content-type'] || '');
const charsetMatch = /charset=([^()<>@,;:"/[\]?.=\s]*)/i.exec(headers?.['content-type'] || '');
return charsetMatch?.[1];
}

View File

@@ -38,114 +38,78 @@ const authorizeUserInWindow = ({ authorizeUrl, callbackUrl, session }) => {
const { session: webSession } = window.webContents;
// Map to store request data using requestId as the key
const requestMap = {};
// Intercept request events and gather data
webSession.webRequest.onBeforeRequest((details, callback) => {
const { id: requestId, url, method, resourceType, frameId } = details;
const request = {
requestId,
url,
method,
resourceType,
frameId,
timestamp: Date.now(),
requestHeaders: {},
responseHeaders: {},
statusCode: null,
error: null,
fromCache: null,
completed: false,
};
requestMap[requestId] = request;
if (resourceType === 'mainFrame') {
// This is a main frame request
currentMainRequest = {
requestId,
url,
method,
timestamp: request.timestamp,
requestHeaders: {},
responseHeaders: {},
statusCode: null,
error: null,
fromCache: null,
completed: false,
requests: [], // To hold sub-resource requests
resourceType,
frameId,
request: {
url,
method,
headers: {},
error: null
},
response: {
headers: {},
status: null,
statusText: null,
error: null
},
fromCache: false,
completed: true,
requests: [], // No sub-requests in this context
};
// Add to mainRequests
// pushing the currentMainRequest to debugInfo
// the currentMainRequest will be further updated by object reference
debugInfo.data.push(currentMainRequest);
} else if (currentMainRequest) {
// Associate sub-resource request with current main request
currentMainRequest.requests.push(request);
}
callback({ cancel: false });
});
webSession.webRequest.onBeforeSendHeaders((details, callback) => {
const { id: requestId, requestHeaders } = details;
if (requestMap[requestId]) {
requestMap[requestId].requestHeaders = requestHeaders;
const { id: requestId, requestHeaders, method, url } = details;
if (currentMainRequest?.requestId === requestId) {
currentMainRequest.request = {
url,
headers: requestHeaders,
method
};
}
if (requestMap[requestId]?.resourceType === 'mainFrame') {
if (currentMainRequest?.requestId === requestId) {
currentMainRequest.requestHeaders = requestHeaders;
}
}
callback({ cancel: false, requestHeaders });
});
webSession.webRequest.onHeadersReceived((details, callback) => {
const { id: requestId, statusCode, responseHeaders } = details;
if (requestMap[requestId]) {
requestMap[requestId].statusCode = statusCode;
requestMap[requestId].responseHeaders = responseHeaders;
const { id: requestId, url, statusCode, responseHeaders, method } = details;
if (currentMainRequest?.requestId === requestId) {
currentMainRequest.response = {
url,
method,
status: statusCode,
headers: responseHeaders
};
}
if (requestMap[requestId]?.resourceType === 'mainFrame') {
if (currentMainRequest?.requestId === requestId) {
currentMainRequest.statusCode = statusCode;
currentMainRequest.responseHeaders = responseHeaders;
}
}
callback({ cancel: false, responseHeaders });
});
webSession.webRequest.onCompleted((details) => {
const { id: requestId, fromCache } = details;
if (requestMap[requestId]) {
requestMap[requestId].completed = true;
requestMap[requestId].fromCache = fromCache;
}
// If this is a mainFrame request, update currentMainRequest
if (requestMap[requestId]?.resourceType === 'mainFrame') {
if (currentMainRequest?.requestId === requestId) {
currentMainRequest.completed = true;
currentMainRequest.fromCache = fromCache;
}
if (currentMainRequest?.requestId === requestId) {
currentMainRequest.completed = true;
currentMainRequest.fromCache = fromCache;
}
});
webSession.webRequest.onErrorOccurred((details) => {
const { id: requestId, error } = details;
if (requestMap[requestId]) {
requestMap[requestId].error = error;
}
// If this is a mainFrame request, update currentMainRequest
if (requestMap[requestId]?.resourceType === 'mainFrame') {
if (currentMainRequest?.requestId === requestId) {
currentMainRequest.error = error;
}
if (currentMainRequest?.requestId === requestId) {
currentMainRequest.response.error = error;
}
});
@@ -204,7 +168,6 @@ const authorizeUserInWindow = ({ authorizeUrl, callbackUrl, session }) => {
try {
const callbackUrlWithCode = new URL(finalUrl);
const authorizationCode = callbackUrlWithCode.searchParams.get('code');
return resolve({ authorizationCode, debugInfo });
} catch (error) {
return reject(error);

View File

@@ -132,6 +132,7 @@ const getOAuth2TokenUsingAuthorizationCode = async ({ request, collectionUid, fo
}
requestCopy.data = data;
requestCopy.url = url;
requestCopy.responseType = 'arraybuffer';
// Initialize variables to hold request and response data for debugging
let axiosRequestInfo = null;
@@ -154,6 +155,7 @@ const getOAuth2TokenUsingAuthorizationCode = async ({ request, collectionUid, fo
// Interceptor to capture response data
axiosInstance.interceptors.response.use((response) => {
axiosResponseInfo = {
url: response?.url,
status: response.status,
statusText: response.statusText,
headers: response.headers,
@@ -164,6 +166,7 @@ const getOAuth2TokenUsingAuthorizationCode = async ({ request, collectionUid, fo
}, (error) => {
if (error.response) {
axiosResponseInfo = {
url: error?.response?.url,
status: error.response.status,
statusText: error.response.statusText,
headers: error.response.headers,
@@ -187,17 +190,23 @@ const getOAuth2TokenUsingAuthorizationCode = async ({ request, collectionUid, fo
// Add the axios request and response info as a main request in debugInfo
const axiosMainRequest = {
requestId: Date.now().toString(), // Generate a unique requestId
url: axiosRequestInfo.url,
method: axiosRequestInfo.method,
timestamp: axiosRequestInfo.timestamp,
requestHeaders: axiosRequestInfo.headers || {},
requestBody: axiosRequestInfo.data,
responseHeaders: axiosResponseInfo.headers || {},
data: parsedResponseData,
statusCode: axiosResponseInfo.status || null,
statusMessage: axiosResponseInfo.statusText || null,
error: null,
requestId: Date.now().toString(),
request: {
url: axiosRequestInfo?.url,
method: axiosRequestInfo?.method,
headers: axiosRequestInfo?.headers || {},
body: axiosRequestInfo?.data,
error: null
},
response: {
url: axiosResponseInfo?.url,
headers: axiosResponseInfo?.headers,
data: parsedResponseData,
dataBuffer: axiosResponseInfo?.data,
status: axiosResponseInfo?.status,
statusText: axiosResponseInfo?.statusText,
error: null
},
fromCache: false,
completed: true,
requests: [], // No sub-requests in this context
@@ -335,6 +344,7 @@ const getOAuth2TokenUsingClientCredentials = async ({ request, collectionUid, fo
}
requestCopy.data = data;
requestCopy.url = url;
requestCopy.responseType = 'arraybuffer';
// Initialize variables to hold request and response data for debugging
let axiosRequestInfo = null;
@@ -358,6 +368,7 @@ const getOAuth2TokenUsingClientCredentials = async ({ request, collectionUid, fo
// Interceptor to capture response data
axiosInstance.interceptors.response.use((response) => {
axiosResponseInfo = {
url: response.url,
status: response.status,
statusText: response.statusText,
headers: response.headers,
@@ -368,6 +379,7 @@ const getOAuth2TokenUsingClientCredentials = async ({ request, collectionUid, fo
}, (error) => {
if (error.response) {
axiosResponseInfo = {
url: error?.response?.url,
status: error.response.status,
statusText: error.response.statusText,
headers: error.response.headers,
@@ -385,16 +397,22 @@ const getOAuth2TokenUsingClientCredentials = async ({ request, collectionUid, fo
// Add the axios request and response info as a main request in debugInfo
const axiosMainRequest = {
requestId: Date.now().toString(),
url: axiosRequestInfo.url,
method: axiosRequestInfo.method,
timestamp: axiosRequestInfo.timestamp,
requestHeaders: axiosRequestInfo.headers || {},
requestBody: axiosRequestInfo.data,
responseHeaders: axiosResponseInfo.headers || {},
responseBody: parsedResponseData,
statusCode: axiosResponseInfo.status || null,
statusMessage: axiosResponseInfo.statusText || null,
error: null,
request: {
url: axiosRequestInfo?.url,
method: axiosRequestInfo?.method,
headers: axiosRequestInfo?.headers || {},
body: axiosRequestInfo?.data,
error: null
},
response: {
url: axiosResponseInfo.url,
headers: axiosResponseInfo?.headers,
data: parsedResponseData,
dataBuffer: axiosResponseInfo?.data?.toString('base64'),
status: axiosResponseInfo?.status,
statusText: axiosResponseInfo?.statusText,
error: null
},
fromCache: false,
completed: true,
requests: [], // No sub-requests in this context
@@ -500,6 +518,7 @@ const getOAuth2TokenUsingPasswordCredentials = async ({ request, collectionUid,
}
requestCopy.data = data;
requestCopy.url = url;
requestCopy.responseType = 'arraybuffer';
// Initialize variables to hold request and response data for debugging
let axiosRequestInfo = null;
@@ -523,6 +542,7 @@ const getOAuth2TokenUsingPasswordCredentials = async ({ request, collectionUid,
// Interceptor to capture response data
axiosInstance.interceptors.response.use((response) => {
axiosResponseInfo = {
url: response.url,
status: response.status,
statusText: response.statusText,
headers: response.headers,
@@ -533,6 +553,7 @@ const getOAuth2TokenUsingPasswordCredentials = async ({ request, collectionUid,
}, (error) => {
if (error.response) {
axiosResponseInfo = {
url: error?.response?.url,
status: error.response.status,
statusText: error.response.statusText,
headers: error.response.headers,
@@ -550,16 +571,22 @@ const getOAuth2TokenUsingPasswordCredentials = async ({ request, collectionUid,
// Add the axios request and response info as a main request in debugInfo
const axiosMainRequest = {
requestId: Date.now().toString(),
url: axiosRequestInfo.url,
method: axiosRequestInfo.method,
timestamp: axiosRequestInfo.timestamp,
requestHeaders: axiosRequestInfo.headers || {},
requestBody: axiosRequestInfo.data,
responseHeaders: axiosResponseInfo.headers || {},
responseBody: parsedResponseData,
statusCode: axiosResponseInfo.status || null,
statusMessage: axiosResponseInfo.statusText || null,
error: null,
request: {
url: axiosRequestInfo?.url,
method: axiosRequestInfo?.method,
headers: axiosRequestInfo?.headers || {},
body: axiosRequestInfo?.data,
error: null
},
response: {
url: axiosResponseInfo?.url,
headers: axiosResponseInfo?.headers,
data: parsedResponseData,
dataBuffer: axiosResponseInfo?.data?.toString('base64'),
status: axiosResponseInfo?.status,
statusText: axiosResponseInfo?.statusText,
error: null
},
fromCache: false,
completed: true,
requests: [], // No sub-requests in this context

View File

@@ -6,7 +6,7 @@ meta {
get {
url: {{key-host}}/realms/bruno/protocol/openid-connect/userinfo
body: none
body: json
auth: oauth2
}
@@ -15,6 +15,7 @@ auth:oauth2 {
callback_url: {{key-host}}/realms/bruno/account
authorization_url: {{key-host}}/realms/bruno/protocol/openid-connect/auth
access_token_url: {{key-host}}/realms/bruno/protocol/openid-connect/token
refresh_url:
client_id: account
client_secret: Lh3NkRikMZpO12rwSBwVimde9v89B5Rw
scope: openid
@@ -24,5 +25,6 @@ auth:oauth2 {
credentials_id: credentials
token_placement: header
token_header_prefix: Bearer
reuse_token:
auto_fetch_token: true
auto_refresh_token: true
}