Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[INS-1703] Display WebSocket messages - first pass #5054

Merged
merged 2 commits into from
Aug 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions packages/insomnia/src/main/network/websocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,26 @@ export interface WebSocketConnection extends WebSocket {
export type WebsocketOpenEvent = Omit<OpenEvent, 'target'> & {
_id: string;
requestId: string;
type: 'open';
};

export type WebsocketMessageEvent = Omit<MessageEvent, 'target'> & {
_id: string;
requestId: string;
direction: 'OUTGOING' | 'INCOMING';
type: 'message';
};

export type WebsocketErrorEvent = Omit<ErrorEvent, 'target'> & {
_id: string;
requestId: string;
type: 'error';
};

export type WebsocketCloseEvent = Omit<CloseEvent, 'target'> & {
_id: string;
requestId: string;
type: 'close';
};

export type WebsocketEvent =
Expand Down Expand Up @@ -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]);
Expand All @@ -87,28 +91,28 @@ 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',
};

WebSocketEventLogs.set(options.requestId, [...msgs, messageEvent]);
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,
};

Expand Down
268 changes: 265 additions & 3 deletions packages/insomnia/src/ui/components/websocket-response-pane.tsx
Original file line number Diff line number Diff line change
@@ -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 <div>WebSocket Response</div>;
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<WebsocketEvent[]>([]);

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 (
<TableRow isActive={isActive} onClick={onClick}>
<td>{event.direction === 'OUTGOING' ? '⬆️' : '⬇️'}</td>
<td>{event.data.slice(0, 220)}</td>
</TableRow>
);
}
);

MessageEventTableRow.displayName = 'MessageEventTableRow';

export const CloseEventTableRow = memo(
(props: {
event: WebsocketCloseEvent;
isActive: boolean;
onClick: () => void;
}) => {
const { event, isActive, onClick } = props;
return (
<TableRow isActive={isActive} onClick={onClick}>
<td>
<SvgIcon icon="error" />
</td>
<td>
Connection closed. {event.reason && `Reason: ${event.reason}`}{' '}
{event.code && `Code: ${event.code}`}
</td>
</TableRow>
);
}
);

CloseEventTableRow.displayName = 'CloseEventTableRow';

export const OpenEventTableRow = memo(
(props: {
event: WebsocketOpenEvent;
isActive: boolean;
onClick: () => void;
}) => {
const { isActive, onClick } = props;
return (
<TableRow isActive={isActive} onClick={onClick}>
<td>
<SvgIcon icon="checkmark" />
</td>
<td>Connected successfully</td>
</TableRow>
);
}
);

OpenEventTableRow.displayName = 'OpenEventTableRow';

export const ErrorEventTableRow = memo(
(props: {
event: WebsocketErrorEvent;
isActive: boolean;
onClick: () => void;
}) => {
const { event, isActive, onClick } = props;
return (
<TableRow isActive={isActive} onClick={onClick}>
<td>
<SvgIcon icon="warning" />
</td>
<td>{event.message.slice(0, 50)}</td>
</TableRow>
);
}
);

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 (
<MessageEventTableRow
event={event}
isActive={isActive}
onClick={_onClick}
/>
);
}
case 'open': {
return (
<OpenEventTableRow
event={event}
isActive={isActive}
onClick={_onClick}
/>
);
}
case 'close': {
return (
<CloseEventTableRow
event={event}
isActive={isActive}
onClick={_onClick}
/>
);
}
case 'error': {
return (
<ErrorEventTableRow
event={event}
isActive={isActive}
onClick={_onClick}
/>
);
}
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<WebsocketEvent | null>(
null
);
const events = useWebSocketConnectionEvents({ requestId });

return (
<PaneContainer className={paneBodyClasses}>
<Section>
<div style={{ width: '100%' }}>
{events ? (
<div className="selectable scrollable" style={{ height: '100%' }}>
<table className="table--fancy table--striped table--compact">
<tbody>
{events.map(event => (
<EventTableRow
key={event._id}
event={event}
isActive={selectedEvent?._id === event._id}
onClick={setSelectedEvent}
/>
))}
</tbody>
</table>
</div>
) : null}
</div>
</Section>
<Separator />
<Section>
{selectedEvent && (
<CodeEditor
hideLineNumbers
mode={'text/plain'}
defaultValue={JSON.stringify(selectedEvent)}
uniquenessKey={selectedEvent?._id}
readOnly
/>
)}
</Section>
</PaneContainer>
);
};
2 changes: 1 addition & 1 deletion packages/insomnia/src/ui/components/wrapper-debug.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ export const WrapperDebug: FC<Props> = ({
/>
) : (
isWebSocketRequest(activeRequest) ? (
<WebSocketResponsePane />
<WebSocketResponsePane requestId={activeRequest._id} />
) : (
<ResponsePane
handleSetFilter={handleSetResponseFilter}
Expand Down