From d3fcb42a8f3310aa6e87b9a231aa506613636b92 Mon Sep 17 00:00:00 2001 From: lohxt1 Date: Mon, 17 Mar 2025 14:09:36 +0530 Subject: [PATCH] timeline ui updates wip --- .../ResponsePane/QueryResult/index.js | 2 +- .../ResponsePane/Timeline/StyledWrapper.js | 8 +- .../TimelineItem/Common/Body/index.js | 37 ++ .../TimelineItem/Common/Headers/index.js | 54 ++ .../TimelineItem/Common/Method/index.js | 19 + .../TimelineItem/Common/Status/index.js | 26 + .../TimelineItem/Common/Time/index.js | 36 ++ .../Timeline/TimelineItem/Network/index.js | 46 ++ .../Timeline/TimelineItem/Request/index.js | 23 + .../Timeline/TimelineItem/Response/index.js | 26 + .../Timeline/TimelineItem/index.js | 79 +++ .../components/ResponsePane/Timeline/index.js | 560 ++---------------- .../ReduxStore/slices/collections/index.js | 2 + packages/bruno-app/src/utils/common/index.js | 2 +- .../ipc/network/authorize-user-in-window.js | 117 ++-- packages/bruno-electron/src/utils/oauth2.js | 89 ++- .../user_info_request-auth.bru | 6 +- 17 files changed, 487 insertions(+), 645 deletions(-) create mode 100644 packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Common/Body/index.js create mode 100644 packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Common/Headers/index.js create mode 100644 packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Common/Method/index.js create mode 100644 packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Common/Status/index.js create mode 100644 packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Common/Time/index.js create mode 100644 packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Network/index.js create mode 100644 packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Request/index.js create mode 100644 packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Response/index.js create mode 100644 packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/index.js diff --git a/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js b/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js index 78993a413..d706be1bb 100644 --- a/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js +++ b/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js @@ -139,7 +139,7 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven return ( diff --git a/packages/bruno-app/src/components/ResponsePane/Timeline/StyledWrapper.js b/packages/bruno-app/src/components/ResponsePane/Timeline/StyledWrapper.js index e430e9aa0..4b7cb28a7 100644 --- a/packages/bruno-app/src/components/ResponsePane/Timeline/StyledWrapper.js +++ b/packages/bruno-app/src/components/ResponsePane/Timeline/StyledWrapper.js @@ -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; } diff --git a/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Common/Body/index.js b/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Common/Body/index.js new file mode 100644 index 000000000..9feb49cc5 --- /dev/null +++ b/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Common/Body/index.js @@ -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 ( +
+
toggleBody(!isBodyCollapsed)}> +
+        
{isBodyCollapsed ? '▼' : '▶'}
Body +
+
+ {isBodyCollapsed && ( +
+ {data || dataBuffer ? ( +
+ +
+ ) : ( +
No Body found
+ )} +
+ )} +
+ ) +} + +export default BodyBlock; \ No newline at end of file diff --git a/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Common/Headers/index.js b/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Common/Headers/index.js new file mode 100644 index 000000000..91de2e5cc --- /dev/null +++ b/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Common/Headers/index.js @@ -0,0 +1,54 @@ +import { useState } from "react"; + +const HeadersBlock = ({ headers, type }) => { + const [areHeadersCollapsed, toggleHeaders] = useState(true); + + return ( +
+
toggleHeaders(!areHeadersCollapsed)}> +
+          
{areHeadersCollapsed ? '▼' : '▶'}
Headers + {headers && Object.keys(headers).length > 0 && +
({Object.keys(headers).length})
+ } +
+
+ {areHeadersCollapsed && ( +
+ {headers && Object.keys(headers).length > 0 + ? + :
No Headers found
+ } +
+ )} +
+ ) +}; + +const Headers = ({ headers, type }) => { + if (Array.isArray(headers)) { + return ( +
+ {headers.map((header, index) => ( +
+            {type === 'request' ? '>' : '<'} {header?.name}:
+            {String(header?.value)}
+          
+ ))} +
+ ); + } else { + return ( +
+ {Object.entries(headers).map(([key, value], index) => ( +
+            {type === 'request' ? '>' : '<'} {key}:
+            {String(value)}
+          
+ ))} +
+ ); + } +}; + +export default HeadersBlock; diff --git a/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Common/Method/index.js b/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Common/Method/index.js new file mode 100644 index 000000000..1e0b22d3a --- /dev/null +++ b/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Common/Method/index.js @@ -0,0 +1,19 @@ +const Method = ({ method }) => { + return ( + + {method?.toUpperCase()} + + ) +} + +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; \ No newline at end of file diff --git a/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Common/Status/index.js b/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Common/Status/index.js new file mode 100644 index 000000000..8c0094120 --- /dev/null +++ b/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Common/Status/index.js @@ -0,0 +1,26 @@ +const Status = ({ statusCode, statusText }) => { + return ( + + {statusCode}{' '} + {statusText || ''} + + ) +} + +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; \ No newline at end of file diff --git a/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Common/Time/index.js b/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Common/Time/index.js new file mode 100644 index 000000000..d4fd0ec07 --- /dev/null +++ b/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Common/Time/index.js @@ -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
{relativeTime}
; +}; diff --git a/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Network/index.js b/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Network/index.js new file mode 100644 index 000000000..373fd5ea6 --- /dev/null +++ b/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Network/index.js @@ -0,0 +1,46 @@ +const Network = ({ logs }) => { + return ( +
+
+        {logs.map((entry, index) => (
+          
+        ))}
+      
+
+ ) +} + +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 ( +
+
{message}
+
+ ); +}; + + +export default Network; \ No newline at end of file diff --git a/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Request/index.js b/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Request/index.js new file mode 100644 index 000000000..b81d3821a --- /dev/null +++ b/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Request/index.js @@ -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 ( +
+ {/* Method and URL */} +
+
{url}
+
+ + {/* Headers */} + + + {/* Body */} + +
+ ) +} + +export default Request; \ No newline at end of file diff --git a/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Response/index.js b/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Response/index.js new file mode 100644 index 000000000..5ee182c64 --- /dev/null +++ b/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Response/index.js @@ -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 ( +
+ {/* Status */} +
+ + {response.duration && {response.duration}ms} + {response.size && {response.size}B} +
+ + {/* Headers */} + + + {/* Body */} + +
+ ) +} + +export default Response; \ No newline at end of file diff --git a/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/index.js b/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/index.js new file mode 100644 index 000000000..3f8e7c2ef --- /dev/null +++ b/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/index.js @@ -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 ( +
+
+
+
+ + + {isOauth2 ?
[oauth2.0]
: null} +
[{new Date(timestamp).toISOString()}]
+
+ + + +
+
{url}
+
+ {isCollapsed && (
+ {/* Tabs */} +
+ + + {showNetworkLogs && ( + + )} +
+ + {/* Tab Content */} +
+ {/* Request Tab */} + {activeTab === 'request' && ( + + )} + + {/* Response Tab */} + {activeTab === 'response' && ( + + )} + + {/* Network Logs Tab */} + {activeTab === 'networkLogs' && showNetworkLogs && ( + + )} +
+
)} +
+ ); +}; + +export default TimelineItem; \ No newline at end of file diff --git a/packages/bruno-app/src/components/ResponsePane/Timeline/index.js b/packages/bruno-app/src/components/ResponsePane/Timeline/index.js index 50c79aad4..40fccf969 100644 --- a/packages/bruno-app/src/components/ResponsePane/Timeline/index.js +++ b/packages/bruno-app/src/components/ResponsePane/Timeline/index.js @@ -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 ( {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 ( -
-
-
-
-
- - {request.method?.toUpperCase()} - {' '} -
- - {response.status || request.statusCode}{' '} - {response.statusText || ''} - -
- {response.duration && ( - - {response.duration}ms - - )} - {response.size && ( - - {response.size}B - - )} - {response.state && ( - - {response.state} - - )} -
-
- - {new Date(timestamp).toLocaleString()} - -
-
{request.url}
-
- {isOpen && ( -
- -
- )} +
+
); } else if (event.type === 'oauth2') { - const { data } = event; - const { debugInfo, fetchedAt } = data; - const flattenedRequests = flattenRequests(debugInfo); + const { data, timestamp } = event; + const { debugInfo } = data; return ( -
-
+
+
- {isOpen ? '▼' : '▶'} - OAuth2.0 Calls + OAuth2.0 Calls
- {isOpen && ( -
- {flattenedRequests && flattenedRequests.length > 0 ? ( - flattenedRequests.map((data, idx) => ( - +
+ {debugInfo && debugInfo.length > 0 ? ( + debugInfo.map((data, idx) => ( +
+ +
)) ) : (
No debug information available.
)}
- )}
); } @@ -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 ( -
- {/* Tabs */} -
- - - {showNetworkLogs && ( - - )} -
- - {/* Tab Content */} -
- {/* Request Tab */} - {activeTab === 'request' && ( -
- {/* Method and URL */} -
- - {request.method.toUpperCase()} - {' '} - {request.url} -
- - {/* Headers */} -
-
setIsRequestHeadersOpen(!isRequestHeadersOpen)}> - - {isRequestHeadersOpen ? '▼' : '▶'} Headers - {filteredRequestHeaders && Object.keys(filteredRequestHeaders).length > 0 && - ({Object.keys(filteredRequestHeaders).length}) - } - -
- {isRequestHeadersOpen && ( -
- {filteredRequestHeaders && Object.keys(filteredRequestHeaders).length > 0 - ? renderHeaders(filteredRequestHeaders) - :
No Headers found
- } -
- )} -
- - {/* Cookies */} -
-
setIsRequestCookiesOpen(!isRequestCookiesOpen)}> - - {isRequestCookiesOpen ? '▼' : '▶'} Cookies - {requestCookies && Object.keys(requestCookies).length > 0 && - ({Object.keys(requestCookies).length}) - } - -
- {isRequestCookiesOpen && ( -
- {requestCookies && Object.keys(requestCookies).length > 0 - ? renderHeaders(requestCookies) - :
No Cookies found
- } -
- )} -
- - {/* Body */} -
-
setIsRequestBodyOpen(!isRequestBodyOpen)}> - - {isRequestBodyOpen ? '▼' : '▶'} Body - -
- {isRequestBodyOpen && ( -
- {request.data || request.dataBuffer || request?.requestBody ? ( -
- -
- ) : ( -
No Body found
- )} -
- )} -
-
- )} - - {/* Response Tab */} - {activeTab === 'response' && ( -
- {/* Status */} -
- - {response.status || request.statusCode} - {' '} - {response.statusText || request.statusText || ''} - {response.duration && {response.duration}ms} - {response.size && {response.size}B} -
- - {/* Headers */} -
-
setIsResponseHeadersOpen(!isResponseHeadersOpen)}> - - {isResponseHeadersOpen ? '▼' : '▶'} Headers - {filteredResponseHeaders && Object.keys(filteredResponseHeaders).length > 0 && - ({Object.keys(filteredResponseHeaders).length}) - } - -
- {isResponseHeadersOpen && ( -
- {filteredResponseHeaders && Object.keys(filteredResponseHeaders).length > 0 - ? renderHeaders(filteredResponseHeaders) - :
No Headers found
- } -
- )} -
- - {/* Cookies */} -
-
setIsResponseCookiesOpen(!isResponseCookiesOpen)}> - - {isResponseCookiesOpen ? '▼' : '▶'} Cookies - {responseCookies && Object.keys(responseCookies).length > 0 && - ({Object.keys(responseCookies).length}) - } - -
- {isResponseCookiesOpen && ( -
- {responseCookies && Object.keys(responseCookies).length > 0 - ? renderHeaders(responseCookies) - :
No Cookies found
- } -
- )} -
- - {/* Body */} -
-
setIsResponseBodyOpen(!isResponseBodyOpen)}> - - {isResponseBodyOpen ? '▼' : '▶'} Body - -
- {isResponseBodyOpen && ( -
- {response.data || response.dataBuffer ? ( -
- -
- ) : ( -
No Body found
- )} -
- )} -
-
- )} - - {/* Network Logs Tab */} - {activeTab === 'networkLogs' && showNetworkLogs && ( -
-
-              {response.timeline.map((entry, index) => (
-                
-              ))}
-            
-
- )} -
-
- ); -}; - -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 ( -
-
{message}
-
- ); -}; - -// Helper functions -const renderHeaders = (data) => { - if (Array.isArray(data)) { - return ( -
- {data.map((header, index) => ( -
-
{header.name}:
-
{String(header.value)}
-
- ))} -
- ); - } else { - return ( -
- {Object.entries(data).map(([key, value], index) => ( -
-
{key}:
-
{String(value)}
-
- ))} -
- ); - } -}; - -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 ( -
-
-
-
-
- - {request.method?.toUpperCase()} - {' '} -
- - {request.statusCode} - {request.statusText || ''} - -
- {isSubRequest && API Request} - {isImage && Image} - {request.duration && {request.duration}ms} - {request.size && {request.size}B} - {request.state && {request.state}} -
-
-
-
{request.url}
-
- {isOpen && ( -
- -
- )} -
- ); -}; - export default Timeline; \ No newline at end of file diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index 82691fd3e..1dd63f31e 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -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", diff --git a/packages/bruno-app/src/utils/common/index.js b/packages/bruno-app/src/utils/common/index.js index 518b1908b..14fd8805a 100644 --- a/packages/bruno-app/src/utils/common/index.js +++ b/packages/bruno-app/src/utils/common/index.js @@ -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]; } diff --git a/packages/bruno-electron/src/ipc/network/authorize-user-in-window.js b/packages/bruno-electron/src/ipc/network/authorize-user-in-window.js index 090862c28..7d2e23abc 100644 --- a/packages/bruno-electron/src/ipc/network/authorize-user-in-window.js +++ b/packages/bruno-electron/src/ipc/network/authorize-user-in-window.js @@ -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); diff --git a/packages/bruno-electron/src/utils/oauth2.js b/packages/bruno-electron/src/utils/oauth2.js index ae2317dec..bfc1f20e3 100644 --- a/packages/bruno-electron/src/utils/oauth2.js +++ b/packages/bruno-electron/src/utils/oauth2.js @@ -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 diff --git a/packages/bruno-tests/keycloak-authorization_code/user_info_request-auth.bru b/packages/bruno-tests/keycloak-authorization_code/user_info_request-auth.bru index a6d630210..72e5dbdec 100644 --- a/packages/bruno-tests/keycloak-authorization_code/user_info_request-auth.bru +++ b/packages/bruno-tests/keycloak-authorization_code/user_info_request-auth.bru @@ -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 }