diff --git a/packages/insomnia/package-lock.json b/packages/insomnia/package-lock.json index 31b8a794ce0..68d9d509039 100644 --- a/packages/insomnia/package-lock.json +++ b/packages/insomnia/package-lock.json @@ -141,6 +141,7 @@ "react-sortable-hoc": "^2.0.0", "react-tabs": "^3.2.3", "react-use": "^17.2.4", + "react-virtual": "2.10.4", "redux": "^4.1.2", "redux-mock-store": "^1.5.4", "redux-thunk": "^2.4.1", @@ -3164,6 +3165,12 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" }, + "node_modules/@reach/observe-rect": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@reach/observe-rect/-/observe-rect-1.2.0.tgz", + "integrity": "sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ==", + "dev": true + }, "node_modules/@repeaterjs/repeater": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@repeaterjs/repeater/-/repeater-3.0.4.tgz", @@ -17606,6 +17613,21 @@ "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", "dev": true }, + "node_modules/react-virtual": { + "version": "2.10.4", + "resolved": "https://registry.npmjs.org/react-virtual/-/react-virtual-2.10.4.tgz", + "integrity": "sha512-Ir6+oPQZTVHfa6+JL9M7cvMILstFZH/H3jqeYeKI4MSUX+rIruVwFC6nGVXw9wqAw8L0Kg2KvfXxI85OvYQdpQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/tannerlinsley" + ], + "dependencies": { + "@reach/observe-rect": "^1.1.0" + }, + "peerDependencies": { + "react": "^16.6.3 || ^17.0.0" + } + }, "node_modules/read-config-file": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-6.2.0.tgz", @@ -22981,6 +23003,12 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" }, + "@reach/observe-rect": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@reach/observe-rect/-/observe-rect-1.2.0.tgz", + "integrity": "sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ==", + "dev": true + }, "@repeaterjs/repeater": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@repeaterjs/repeater/-/repeater-3.0.4.tgz", @@ -34250,6 +34278,15 @@ } } }, + "react-virtual": { + "version": "2.10.4", + "resolved": "https://registry.npmjs.org/react-virtual/-/react-virtual-2.10.4.tgz", + "integrity": "sha512-Ir6+oPQZTVHfa6+JL9M7cvMILstFZH/H3jqeYeKI4MSUX+rIruVwFC6nGVXw9wqAw8L0Kg2KvfXxI85OvYQdpQ==", + "dev": true, + "requires": { + "@reach/observe-rect": "^1.1.0" + } + }, "read-config-file": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-6.2.0.tgz", diff --git a/packages/insomnia/package.json b/packages/insomnia/package.json index da17daf9998..a373e8ec7fb 100644 --- a/packages/insomnia/package.json +++ b/packages/insomnia/package.json @@ -196,6 +196,7 @@ "react-sortable-hoc": "^2.0.0", "react-tabs": "^3.2.3", "react-use": "^17.2.4", + "react-virtual": "2.10.4", "redux": "^4.1.2", "redux-mock-store": "^1.5.4", "redux-thunk": "^2.4.1", diff --git a/packages/insomnia/src/ui/components/websockets/event-log-table.tsx b/packages/insomnia/src/ui/components/websockets/event-log-table.tsx deleted file mode 100644 index 5f67fcf0c24..00000000000 --- a/packages/insomnia/src/ui/components/websockets/event-log-table.tsx +++ /dev/null @@ -1,242 +0,0 @@ -import { format } from 'date-fns'; -import { SvgIcon } from 'insomnia-components'; -import React, { FC, memo, useCallback } from 'react'; -import styled from 'styled-components'; - -import { - WebSocketCloseEvent, - WebSocketErrorEvent, - WebSocketEvent, - WebSocketMessageEvent, - WebSocketOpenEvent, -} from '../../../main/network/websocket'; - -const Table = styled.table({ - borderCollapse: 'collapse', - tableLayout: 'fixed', - width: '100%', - boxSizing: 'border-box', -}); -const TableCell = styled('td')({ - border: '1px solid var(--hl-md)', -}); -const TableRow = styled('tr')<{ isActive: boolean }>( - ({ isActive }) => ({ - zIndex: 1, - backgroundColor: isActive ? 'var(--hl-md)' : 'transparent', - }) -); -const TableCellTextWrapper = styled.div({ - width: 'inherit', - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', -}); -const TableCellIconWrapper = styled.div({ - display: 'flex', - justifyContent: 'center', - alignItems: 'center', -}); - -const Timestamp: FC<{ time: Date | number }> = ({ time }) => { - const date = format(time, 'HH:mm:ss'); - return <>{date}; -}; - -export const MessageEventTableRow = memo( - (props: { - event: WebSocketMessageEvent; - isActive: boolean; - onClick: () => void; - }) => { - const { event, isActive, onClick } = props; - return ( - - - - {event.direction === 'OUTGOING' ? : } - - - - - {event.data} - - - - - - - ); - } -); -MessageEventTableRow.displayName = 'MessageEventTableRow'; - -export const CloseEventTableRow = memo( - (props: { - event: WebSocketCloseEvent; - isActive: boolean; - onClick: () => void; - }) => { - const { event, isActive, onClick } = props; - return ( - - - - - - - - - Connection closed. {event.reason && `Reason: ${event.reason}`}{' '} - {event.code && `Code: ${event.code}`} - - - - - - - ); - } -); -CloseEventTableRow.displayName = 'CloseEventTableRow'; - -export const OpenEventTableRow = memo( - (props: { - event: WebSocketOpenEvent; - isActive: boolean; - onClick: () => void; - }) => { - const { isActive, onClick } = props; - return ( - - - - - - - - - Connected successfully - - - - - - - ); - } -); -OpenEventTableRow.displayName = 'OpenEventTableRow'; - -export const ErrorEventTableRow = memo( - (props: { - event: WebSocketErrorEvent; - isActive: boolean; - onClick: () => void; - }) => { - const { event, isActive, onClick } = props; - return ( - - - - - - - {event.message.slice(0, 50)} - - - - - ); - } -); -ErrorEventTableRow.displayName = 'ErrorEventTableRow'; - -export const EventTableRow = memo( - (props: { - event: WebSocketEvent; - isActive: boolean; - onClick: (event: WebSocketEvent) => void; - }) => { - const { event, isActive, onClick } = props; - const _onClick = useCallback(() => onClick(event), [event, onClick]); - - switch (event.type) { - case 'message': { - return ( - - ); - } - case 'open': { - return ( - - ); - } - case 'close': { - return ( - - ); - } - case 'error': { - return ( - - ); - } - default: { - return null; - } - } - } -); -EventTableRow.displayName = 'EventTableRow'; - -const TableHeadRow = styled('tr')({ - position: 'sticky', - backgroundColor: 'var(--color-bg)', - top: 'calc(-0.5rem - 1px)', -}); - -interface Props { - events: WebSocketEvent[]; - selectionId?: string; - onSelect: (event: WebSocketEvent) => void; -} -export const EventLogTable: FC = ({ events, onSelect, selectionId }) => { - return ( - - - - - - - - - {events.map(event => ( - - ))} - -
- DataTime
- ); -}; diff --git a/packages/insomnia/src/ui/components/websockets/event-log-view.tsx b/packages/insomnia/src/ui/components/websockets/event-log-view.tsx index db70d4d809b..bc5ba78b280 100644 --- a/packages/insomnia/src/ui/components/websockets/event-log-view.tsx +++ b/packages/insomnia/src/ui/components/websockets/event-log-view.tsx @@ -1,18 +1,200 @@ -import React, { FC } from 'react'; +import { format } from 'date-fns'; +import { SvgIcon, SvgIconProps } from 'insomnia-components'; +import React, { FC, useRef } from 'react'; +import { useMeasure } from 'react-use'; +import { useVirtual } from 'react-virtual'; +import styled from 'styled-components'; import { WebSocketEvent } from '../../../main/network/websocket'; -import { CodeEditor } from '../codemirror/code-editor'; + +const Timestamp: FC<{ time: Date | number }> = ({ time }) => { + const date = format(time, 'HH:mm:ss'); + return <>{date}; +}; + interface Props { - event: WebSocketEvent; + events: WebSocketEvent[]; + selectionId?: string; + onSelect: (event: WebSocketEvent) => void; +} + +const Divider = styled('div')({ + height: '100%', + width: '1px', + backgroundColor: 'var(--hl-md)', +}); + +const AutoSize = styled.div({ + flex: '1 0', + overflow: 'hidden', +}); + +const Scrollable = styled.div({ + overflowY: 'scroll', +}); + +const HeadingRow = styled('div')({ + flex: '0 0 30px', + display: 'flex', + width: '100%', + alignItems: 'center', + borderBottom: '1px solid var(--hl-md)', + paddingRight: 'var(--scrollbar-width)', + boxSizing: 'border-box', +}); + +const Row = styled('div')<{ isActive: boolean }>(({ isActive }) => ({ + position: 'absolute', + top: 0, + left: 0, + height: '30px', + display: 'flex', + width: '100%', + alignItems: 'center', + borderBottom: '1px solid var(--hl-md)', + boxSizing: 'border-box', + backgroundColor: isActive ? 'var(--hl-lg)' : 'transparent', +})); + +const List = styled('div')({ + width: '100%', + position: 'relative', +}); + +const EventLog = styled('div')({ + display: 'flex', + flexDirection: 'column', + width: '100%', + height: '100%', + overflow: 'hidden', + border: '1px solid var(--hl-md)', +}); + +const EventIconCell = styled('div')({ + flex: '0 0 15px', + height: '100%', + display: 'flex', + alignItems: 'center', + boxSizing: 'border-box', + padding: 'var(--padding-xs)', +}); + +function getIcon(event: WebSocketEvent): SvgIconProps['icon'] { + switch (event.type) { + case 'message': { + if (event.direction === 'OUTGOING') { + return 'sent'; + } else { + return 'receive'; + } + } + case 'open': { + return 'ellipsis-circle2'; + } + case 'close': { + return 'disconnected'; + } + case 'error': { + return 'error'; + } + default: { + return 'bug'; + } + } } -export const EventLogView: FC = ({ event }) => { + +const EventMessageCell = styled('div')({ + flex: '1 0', + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + padding: 'var(--padding-xs)', +}); + +const getMessage = (event: WebSocketEvent): string => { + switch (event.type) { + case 'message': { + return event.data.toString(); + } + case 'open': { + return 'Connected successfully'; + } + case 'close': { + return 'Disconnected'; + } + case 'error': { + return event.message; + } + default: { + return 'Unknown event'; + } + } +}; + +const EventTimestampCell = styled('div')({ + flex: '0 0 80px', + padding: 'var(--padding-xs)', +}); + +export const EventLogView: FC = ({ events, onSelect, selectionId }) => { + const parentRef = useRef(null); + const virtualizer = useVirtual({ + parentRef, + size: events.length, + estimateSize: React.useCallback(() => 30, []), + overscan: 30, + keyExtractor: index => events[index]._id, + }); + + const [autoSizeRef, { height }] = useMeasure(); + return ( - + + + +
+ + + Data + + Time + + + + + {virtualizer.virtualItems.map(item => { + const event = events[item.index]; + + return ( + onSelect(event)} + isActive={event._id === selectionId} + style={{ + height: `${item.size}px`, + transform: `translateY(${item.start}px)`, + }} + > + + + + + + {getMessage(event)} + + + + + + ); + })} + + + + ); }; diff --git a/packages/insomnia/src/ui/components/websockets/event-view.tsx b/packages/insomnia/src/ui/components/websockets/event-view.tsx new file mode 100644 index 00000000000..7a486976949 --- /dev/null +++ b/packages/insomnia/src/ui/components/websockets/event-view.tsx @@ -0,0 +1,18 @@ +import React, { FC } from 'react'; + +import { WebSocketEvent } from '../../../main/network/websocket'; +import { CodeEditor } from '../codemirror/code-editor'; +interface Props { + event: WebSocketEvent; +} +export const EventView: FC = ({ event }) => { + return ( + + ); +}; diff --git a/packages/insomnia/src/ui/components/websockets/websocket-response-pane.tsx b/packages/insomnia/src/ui/components/websockets/websocket-response-pane.tsx index 2336c49cd5c..821fb51d31b 100644 --- a/packages/insomnia/src/ui/components/websockets/websocket-response-pane.tsx +++ b/packages/insomnia/src/ui/components/websockets/websocket-response-pane.tsx @@ -18,32 +18,35 @@ import { ResponseCookiesViewer } from '../viewers/response-cookies-viewer'; import { ResponseErrorViewer } from '../viewers/response-error-viewer'; import { ResponseHeadersViewer } from '../viewers/response-headers-viewer'; import { ResponseTimelineViewer } from '../viewers/response-timeline-viewer'; -import { EventLogTable } from './event-log-table'; import { EventLogView } from './event-log-view'; +import { EventView } from './event-view'; const PaneHeader = styled(OriginalPaneHeader)({ '&&': { justifyContent: 'unset' }, }); + const EventLogTableWrapper = styled.div({ width: '100%', flex: 1, - overflowY: 'scroll', + overflow: 'hidden', padding: 'var(--padding-sm)', boxSizing: 'border-box', }); -const EventLogViewWrapper = styled.div({ + +const EventViewWrapper = styled.div({ flex: 1, borderTop: '1px solid var(--hl-md)', height: '100%', - boxSizing: 'content-box', padding: 'var(--padding-sm)', }); + const PaneBodyContent = styled.div({ height: '100%', - display: 'flex', - flexDirection: 'column', - flex: 1, + width: '100%', + display: 'grid', + gridTemplateRows: 'repeat(auto-fit, minmax(0, 1fr))', }); + export const WebSocketResponsePane: FC<{ requestId: string; response: Response | null; handleSetActiveResponse: (requestId: string, activeResponse: Response | null) => void }> = ({ requestId, @@ -144,7 +147,7 @@ const WebSocketActiveResponsePane: FC<{ requestId: string; response: Response; h : <> {Boolean(events?.length) && ( - )} {selectedEvent && ( - - - + + + )} }