diff --git a/packages/insomnia/src/main/network/websocket.ts b/packages/insomnia/src/main/network/websocket.ts index d05bcd3740e..b5cace94955 100644 --- a/packages/insomnia/src/main/network/websocket.ts +++ b/packages/insomnia/src/main/network/websocket.ts @@ -18,22 +18,26 @@ export interface WebSocketConnection extends WebSocket { export type WebsocketOpenEvent = Omit & { _id: string; requestId: string; + type: 'open'; }; export type WebsocketMessageEvent = Omit & { _id: string; requestId: string; direction: 'OUTGOING' | 'INCOMING'; + type: 'message'; }; export type WebsocketErrorEvent = Omit & { _id: string; requestId: string; + type: 'error'; }; export type WebsocketCloseEvent = Omit & { _id: string; requestId: string; + type: 'close'; }; export type WebsocketEvent = @@ -74,11 +78,11 @@ async function createWebSocketConnection( event.sender.send(readyStateChannel, ws.readyState); WebSocketConnections.set(options.requestId, ws); - ws.addEventListener('open', ({ type }) => { + ws.addEventListener('open', () => { const openEvent: WebsocketOpenEvent = { _id: uuidV4(), requestId: options.requestId, - type, + type: 'open', }; WebSocketEventLogs.set(options.requestId, [openEvent]); @@ -87,13 +91,13 @@ async function createWebSocketConnection( event.sender.send(readyStateChannel, ws.readyState); }); - ws.addEventListener('message', ({ data, type }) => { + ws.addEventListener('message', ({ data }) => { const msgs = WebSocketEventLogs.get(options.requestId) || []; const messageEvent: WebsocketMessageEvent = { _id: uuidV4(), requestId: options.requestId, data, - type, + type: 'message', direction: 'INCOMING', }; @@ -101,14 +105,14 @@ async function createWebSocketConnection( event.sender.send(eventChannel, messageEvent); }); - ws.addEventListener('close', ({ code, reason, type, wasClean }) => { + ws.addEventListener('close', ({ code, reason, wasClean }) => { const msgs = WebSocketEventLogs.get(options.requestId) || []; const closeEvent: WebsocketCloseEvent = { _id: uuidV4(), requestId: options.requestId, code, reason, - type, + type: 'close', wasClean, }; diff --git a/packages/insomnia/src/ui/components/websocket-response-pane.tsx b/packages/insomnia/src/ui/components/websocket-response-pane.tsx index 51dde993fc2..5fefd34f34e 100644 --- a/packages/insomnia/src/ui/components/websocket-response-pane.tsx +++ b/packages/insomnia/src/ui/components/websocket-response-pane.tsx @@ -1,5 +1,267 @@ -import React, { FC } from 'react'; +import { SvgIcon } from 'insomnia-components'; +import React, { FC, memo, useCallback, useState } from 'react'; +import styled from 'styled-components'; -export const WebSocketResponsePane: FC = () => { - return
WebSocket Response
; +import { + WebsocketCloseEvent, + WebsocketErrorEvent, + WebsocketEvent, + WebsocketMessageEvent, + WebsocketOpenEvent, +} from '../../main/network/websocket'; +import { CodeEditor } from './codemirror/code-editor'; +import { paneBodyClasses } from './panes/pane'; + +function useWebSocketConnectionEvents({ requestId }: { requestId: string }) { + // @TODO - This list can grow to thousands of events in a chatty websocket connection. + // It's worth investigating an LRU cache that keeps the last X number of messages. + // We'd also need to expand the findMany API to support pagination. + const [events, setEvents] = React.useState([]); + + React.useEffect(() => { + let isMounted = true; + let unsubscribe = () => {}; + + // @TODO - There is a possible race condition here. + // Subscribe should probably ask for events after a given event.id so we can make sure + // we don't lose any. + async function fetchAndSubscribeToEvents() { + // Fetch all existing events for this connection + const allEvents = await window.main.webSocketConnection.event.findMany({ + requestId, + }); + if (isMounted) { + setEvents(allEvents); + } + // Subscribe to new events and update the state. + unsubscribe = window.main.webSocketConnection.event.subscribe( + { requestId }, + event => { + if (isMounted) { + setEvents(events => events.concat([event])); + } + } + ); + } + + fetchAndSubscribeToEvents(); + + return () => { + isMounted = false; + unsubscribe(); + }; + }, [requestId]); + + return events; +} + +const TableRow = styled('tr')<{ isActive: boolean }>( + { + display: 'flex', + gap: '0.5rem', + }, + ({ isActive }) => ({ + zIndex: 1, + borderStyle: 'solid', + borderColor: isActive ? 'var(--hl-md)' : 'transparent', + borderWidth: '1px', + }) +); + +export const MessageEventTableRow = memo( + (props: { + event: WebsocketMessageEvent; + isActive: boolean; + onClick: () => void; + }) => { + const { event, isActive, onClick } = props; + return ( + + {event.direction === 'OUTGOING' ? '⬆️' : '⬇️'} + {event.data.slice(0, 220)} + + ); + } +); + +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 PaneContainer = styled('div')({ + display: 'flex', + flexDirection: 'column', + height: '100%', +}); + +const Separator = styled('div')({ + height: '1px', + background: 'var(--hl-md)', +}); + +const Section = styled('div')({ + height: '50%', + display: 'flex', +}); + +export const WebSocketResponsePane: FC<{ requestId: string }> = ({ + requestId, +}) => { + const [selectedEvent, setSelectedEvent] = useState( + null + ); + const events = useWebSocketConnectionEvents({ requestId }); + + return ( + +
+
+ {events ? ( +
+ + + {events.map(event => ( + + ))} + +
+
+ ) : null} +
+
+ +
+ {selectedEvent && ( + + )} +
+
+ ); }; diff --git a/packages/insomnia/src/ui/components/wrapper-debug.tsx b/packages/insomnia/src/ui/components/wrapper-debug.tsx index 9dbe6f3292d..9f07facf277 100644 --- a/packages/insomnia/src/ui/components/wrapper-debug.tsx +++ b/packages/insomnia/src/ui/components/wrapper-debug.tsx @@ -157,7 +157,7 @@ export const WrapperDebug: FC = ({ /> ) : ( isWebSocketRequest(activeRequest) ? ( - + ) : (