squishy squash connect and send (#5204)

Co-authored-by: jackkav <jackkav@gmail.com>
This commit is contained in:
Mark Kim 2022-09-23 07:47:20 -04:00 committed by GitHub
parent 883753a7df
commit 07833abcb5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 99 additions and 74 deletions

View File

@ -91,17 +91,18 @@ const parseResponseAndBuildTimeline = (url: string, incomingMessage: IncomingMes
]; ];
return { timeline, responseHeaders, statusCode, statusMessage, httpVersion }; return { timeline, responseHeaders, statusCode, statusMessage, httpVersion };
}; };
interface OpenWebSocketRequestOptions {
const createWebSocketConnection = async ( requestId: string;
workspaceId: string;
url: string;
headers: RequestHeader[];
authentication: RequestAuthentication;
cookieJar: CookieJar;
initialPayload?: string;
}
const openWebSocketConnection = async (
event: Electron.IpcMainInvokeEvent, event: Electron.IpcMainInvokeEvent,
options: { options: OpenWebSocketRequestOptions
requestId: string;
workspaceId: string;
url: string;
headers: RequestHeader[];
authentication: RequestAuthentication;
cookieJar: CookieJar;
}
): Promise<void> => { ): Promise<void> => {
const existingConnection = WebSocketConnections.get(options.requestId); const existingConnection = WebSocketConnections.get(options.requestId);
@ -282,6 +283,10 @@ const createWebSocketConnection = async (
eventLogFileStreams.get(options.requestId)?.write(JSON.stringify(openEvent) + '\n'); eventLogFileStreams.get(options.requestId)?.write(JSON.stringify(openEvent) + '\n');
timelineFileStreams.get(options.requestId)?.write(JSON.stringify({ value: 'WebSocket connection established', name: 'Text', timestamp: Date.now() }) + '\n'); timelineFileStreams.get(options.requestId)?.write(JSON.stringify({ value: 'WebSocket connection established', name: 'Text', timestamp: Date.now() }) + '\n');
event.sender.send(readyStateChannel, ws.readyState); event.sender.send(readyStateChannel, ws.readyState);
if (options.initialPayload) {
sendPayload(ws, { requestId: options.requestId, payload: options.initialPayload });
}
}); });
ws.addEventListener('message', ({ data }: MessageEvent) => { ws.addEventListener('message', ({ data }: MessageEvent) => {
@ -369,17 +374,8 @@ const getWebSocketReadyState = async (
return WebSocketConnections.get(options.requestId)?.readyState ?? 0; return WebSocketConnections.get(options.requestId)?.readyState ?? 0;
}; };
const sendWebSocketEvent = async ( const sendPayload = async (ws: WebSocket, options: { payload: string; requestId: string }): Promise<void> => {
options: { message: string; requestId: string } ws.send(options.payload, error => {
): Promise<void> => {
const ws = WebSocketConnections.get(options.requestId);
if (!ws) {
console.warn('No websocket found for requestId: ' + options.requestId);
return;
}
ws.send(options.message, error => {
// @TODO: We might want to set a status in the WebSocketMessageEvent // @TODO: We might want to set a status in the WebSocketMessageEvent
// and update it here based on the error. e.g. status = 'sending' | 'sent' | 'error' // and update it here based on the error. e.g. status = 'sending' | 'sent' | 'error'
if (error) { if (error) {
@ -392,7 +388,7 @@ const sendWebSocketEvent = async (
const lastMessage: WebSocketMessageEvent = { const lastMessage: WebSocketMessageEvent = {
_id: uuidV4(), _id: uuidV4(),
requestId: options.requestId, requestId: options.requestId,
data: options.message, data: options.payload,
direction: 'OUTGOING', direction: 'OUTGOING',
type: 'message', type: 'message',
timestamp: Date.now(), timestamp: Date.now(),
@ -406,6 +402,19 @@ const sendWebSocketEvent = async (
} }
}; };
const sendWebSocketEvent = async (
options: { payload: string; requestId: string }
): Promise<void> => {
const ws = WebSocketConnections.get(options.requestId);
if (!ws) {
console.warn('No websocket found for requestId: ' + options.requestId);
return;
}
sendPayload(ws, options);
};
const closeWebSocketConnection = async ( const closeWebSocketConnection = async (
options: { requestId: string } options: { requestId: string }
): Promise<void> => { ): Promise<void> => {
@ -436,14 +445,7 @@ const findMany = async (
}; };
export interface WebSocketBridgeAPI { export interface WebSocketBridgeAPI {
create: (options: { open: (options: OpenWebSocketRequestOptions) => void;
requestId: string;
workspaceId: string;
url: string;
headers: RequestHeader[];
authentication: RequestAuthentication;
cookieJar: CookieJar;
}) => void;
close: typeof closeWebSocketConnection; close: typeof closeWebSocketConnection;
closeAll: typeof closeAllWebSocketConnections; closeAll: typeof closeAllWebSocketConnections;
readyState: { readyState: {
@ -451,11 +453,11 @@ export interface WebSocketBridgeAPI {
}; };
event: { event: {
findMany: typeof findMany; findMany: typeof findMany;
send: (options: { requestId: string; message: string }) => void; send: typeof sendWebSocketEvent;
}; };
} }
export const registerWebSocketHandlers = () => { export const registerWebSocketHandlers = () => {
ipcMain.handle('webSocket.create', createWebSocketConnection); ipcMain.handle('webSocket.open', openWebSocketConnection);
ipcMain.handle('webSocket.event.send', (_, options: Parameters<typeof sendWebSocketEvent>[0]) => sendWebSocketEvent(options)); ipcMain.handle('webSocket.event.send', (_, options: Parameters<typeof sendWebSocketEvent>[0]) => sendWebSocketEvent(options));
ipcMain.handle('webSocket.close', (_, options: Parameters<typeof closeWebSocketConnection>[0]) => closeWebSocketConnection(options)); ipcMain.handle('webSocket.close', (_, options: Parameters<typeof closeWebSocketConnection>[0]) => closeWebSocketConnection(options));
ipcMain.handle('webSocket.closeAll', closeAllWebSocketConnections); ipcMain.handle('webSocket.closeAll', closeAllWebSocketConnections);

View File

@ -3,7 +3,7 @@ import { contextBridge, ipcRenderer } from 'electron';
import type { WebSocketBridgeAPI } from './main/network/websocket'; import type { WebSocketBridgeAPI } from './main/network/websocket';
const webSocket: WebSocketBridgeAPI = { const webSocket: WebSocketBridgeAPI = {
create: options => ipcRenderer.invoke('webSocket.create', options), open: options => ipcRenderer.invoke('webSocket.open', options),
close: options => ipcRenderer.invoke('webSocket.close', options), close: options => ipcRenderer.invoke('webSocket.close', options),
closeAll: () => ipcRenderer.invoke('webSocket.closeAll'), closeAll: () => ipcRenderer.invoke('webSocket.closeAll'),
readyState: { readyState: {

View File

@ -5,7 +5,7 @@ import styled from 'styled-components';
import { hotKeyRefs } from '../../../common/hotkeys'; import { hotKeyRefs } from '../../../common/hotkeys';
import { executeHotKey } from '../../../common/hotkeys-listener'; import { executeHotKey } from '../../../common/hotkeys-listener';
import { getRenderContext, render, RENDER_PURPOSE_SEND } from '../../../common/render'; import { getRenderContext, render, RENDER_PURPOSE_SEND } from '../../../common/render';
import { cookieJar } from '../../../models'; import * as models from '../../../models';
import { WebSocketRequest } from '../../../models/websocket-request'; import { WebSocketRequest } from '../../../models/websocket-request';
import { ReadyState } from '../../context/websocket-client/use-ws-ready-state'; import { ReadyState } from '../../context/websocket-client/use-ws-ready-state';
import { OneLineEditor } from '../codemirror/one-line-editor'; import { OneLineEditor } from '../codemirror/one-line-editor';
@ -81,22 +81,19 @@ export const WebSocketActionBar: FC<ActionBarProps> = ({ request, workspaceId, e
} }
try { try {
const renderContext = await getRenderContext({ request, environmentId, purpose: RENDER_PURPOSE_SEND }); const renderContext = await getRenderContext({ request, environmentId, purpose: RENDER_PURPOSE_SEND });
const { url: rawUrl, headers, authentication, parameters } = request;
// Render any nunjucks tags in the url/headers/authentication settings/cookies // Render any nunjucks tags in the url/headers/authentication settings/cookies
const workspaceCookieJar = await cookieJar.getOrCreateForParentId(workspaceId); const workspaceCookieJar = await models.cookieJar.getOrCreateForParentId(workspaceId);
const rendered = await render({ const rendered = await render({
url: rawUrl, url: request.url,
headers, headers: request.headers,
authentication, authentication: request.authentication,
parameters, parameters: request.parameters,
workspaceCookieJar, workspaceCookieJar,
}, renderContext); }, renderContext);
const queryString = buildQueryStringFromParams(rendered.parameters); window.main.webSocket.open({
const url = joinUrlAndQueryString(rendered.url, queryString);
window.main.webSocket.create({
requestId: request._id, requestId: request._id,
workspaceId, workspaceId,
url, url: joinUrlAndQueryString(rendered.url, buildQueryStringFromParams(rendered.parameters)),
headers: rendered.headers, headers: rendered.headers,
authentication: rendered.authentication, authentication: rendered.authentication,
cookieJar: rendered.workspaceCookieJar, cookieJar: rendered.workspaceCookieJar,

View File

@ -1,4 +1,5 @@
import React, { FC, FormEvent, useCallback, useEffect, useRef, useState } from 'react'; import { buildQueryStringFromParams, joinUrlAndQueryString } from 'insomnia-url';
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs'; import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
import styled from 'styled-components'; import styled from 'styled-components';
@ -10,7 +11,7 @@ import { Environment } from '../../../models/environment';
import { WebSocketRequest } from '../../../models/websocket-request'; import { WebSocketRequest } from '../../../models/websocket-request';
import { ReadyState, useWSReadyState } from '../../context/websocket-client/use-ws-ready-state'; import { ReadyState, useWSReadyState } from '../../context/websocket-client/use-ws-ready-state';
import { useActiveRequestSyncVCSVersion, useGitVCSVersion } from '../../hooks/use-vcs-version'; import { useActiveRequestSyncVCSVersion, useGitVCSVersion } from '../../hooks/use-vcs-version';
import { selectActiveRequestMeta, selectSettings } from '../../redux/selectors'; import { selectActiveRequestMeta, selectSettings } from '../../redux/selectors';
import { CodeEditor, UnconnectedCodeEditor } from '../codemirror/code-editor'; import { CodeEditor, UnconnectedCodeEditor } from '../codemirror/code-editor';
import { AuthDropdown } from '../dropdowns/auth-dropdown'; import { AuthDropdown } from '../dropdowns/auth-dropdown';
import { WebSocketPreviewModeDropdown } from '../dropdowns/websocket-preview-mode'; import { WebSocketPreviewModeDropdown } from '../dropdowns/websocket-preview-mode';
@ -34,20 +35,20 @@ const SendMessageForm = styled.form({
position: 'relative', position: 'relative',
boxSizing: 'border-box', boxSizing: 'border-box',
}); });
const SendButton = styled.button({ const SendButton = styled.button<{ isConnected: boolean }>(({ isConnected }) =>
padding: '0 var(--padding-md)', ({
marginLeft: 'var(--padding-xs)', padding: '0 var(--padding-md)',
height: '100%', marginLeft: 'var(--padding-xs)',
border: '1px solid var(--hl-lg)', height: '100%',
borderRadius: 'var(--radius-md)', border: '1px solid var(--hl-lg)',
':hover': { borderRadius: 'var(--radius-md)',
filter: 'brightness(0.8)', background: isConnected ? 'var(--color-surprise)' : 'inherit',
}, color: isConnected ? 'var(--color-font-surprise)' : 'inherit',
':enabled': { ':hover': {
background: 'var(--color-surprise)', filter: 'brightness(0.8)',
color: 'var(--color-font-surprise)', },
}, }));
});
const PaneSendButton = styled.div({ const PaneSendButton = styled.div({
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: 'row',
@ -65,6 +66,7 @@ interface FormProps {
request: WebSocketRequest; request: WebSocketRequest;
previewMode: string; previewMode: string;
environmentId: string; environmentId: string;
workspaceId: string;
} }
const PayloadTabPanel = styled(TabPanel)({ const PayloadTabPanel = styled(TabPanel)({
@ -78,29 +80,46 @@ const WebSocketRequestForm: FC<FormProps> = ({
request, request,
previewMode, previewMode,
environmentId, environmentId,
workspaceId,
}) => { }) => {
const editorRef = useRef<UnconnectedCodeEditor>(null); const editorRef = useRef<UnconnectedCodeEditor>(null);
useEffect(() => { useEffect(() => {
async function initMessageText(): Promise<void> { const init = async () => {
const payload = await models.webSocketPayload.getByParentId(request._id); const payload = await models.webSocketPayload.getByParentId(request._id);
const msg = payload?.value || ''; const msg = payload?.value || '';
editorRef.current?.codeMirror?.setValue(msg); editorRef.current?.codeMirror?.setValue(msg);
} };
initMessageText(); init();
}, [request._id]); }, [request._id]);
// NOTE: Nunjucks interpolation can throw errors
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => { const interpolateOpenAndSend = async (payload: string) => {
event.preventDefault();
const message = editorRef.current?.getValue() || '';
try { try {
// Render any nunjucks tag in the message
const renderContext = await getRenderContext({ request, environmentId, purpose: RENDER_PURPOSE_SEND }); const renderContext = await getRenderContext({ request, environmentId, purpose: RENDER_PURPOSE_SEND });
const renderedMessage = await render(message, renderContext); const renderedMessage = await render(payload, renderContext);
const readyState = await window.main.webSocket.readyState.getCurrent({ requestId: request._id });
window.main.webSocket.event.send({ requestId: request._id, message: renderedMessage }); if (readyState !== ReadyState.OPEN) {
const workspaceCookieJar = await models.cookieJar.getOrCreateForParentId(workspaceId);
const rendered = await render({
url: request.url,
headers: request.headers,
authentication: request.authentication,
parameters: request.parameters,
workspaceCookieJar,
}, renderContext);
window.main.webSocket.open({
requestId: request._id,
workspaceId,
url: joinUrlAndQueryString(rendered.url, buildQueryStringFromParams(rendered.parameters)),
headers: rendered.headers,
authentication: rendered.authentication,
cookieJar: rendered.workspaceCookieJar,
initialPayload: renderedMessage,
});
return;
}
window.main.webSocket.event.send({ requestId: request._id, payload: renderedMessage });
} catch (err) { } catch (err) {
if (err.type === 'render') { if (err.type === 'render') {
showModal(RequestRenderErrorModal, { showModal(RequestRenderErrorModal, {
@ -140,7 +159,13 @@ const WebSocketRequestForm: FC<FormProps> = ({
// To allow for disabling rendering of messages based on a per-request setting. // To allow for disabling rendering of messages based on a per-request setting.
// Same as with regular requests // Same as with regular requests
return ( return (
<SendMessageForm id="websocketMessageForm" onSubmit={handleSubmit}> <SendMessageForm
id="websocketMessageForm"
onSubmit={event => {
event.preventDefault();
interpolateOpenAndSend(editorRef.current?.getValue() || '');
}}
>
<CodeEditor <CodeEditor
manualPrettify manualPrettify
uniquenessKey={request._id} uniquenessKey={request._id}
@ -266,7 +291,7 @@ export const WebSocketRequestPane: FC<Props> = ({ request, workspaceId, environm
<SendButton <SendButton
type="submit" type="submit"
form="websocketMessageForm" form="websocketMessageForm"
disabled={readyState !== ReadyState.OPEN} isConnected={readyState === ReadyState.OPEN}
> >
Send Send
</SendButton> </SendButton>
@ -276,6 +301,7 @@ export const WebSocketRequestPane: FC<Props> = ({ request, workspaceId, environm
request={request} request={request}
previewMode={previewMode} previewMode={previewMode}
environmentId={environment?._id || ''} environmentId={environment?._id || ''}
workspaceId={workspaceId}
/> />
</PayloadTabPanel> </PayloadTabPanel>
<TabPanel className="react-tabs__tab-panel"> <TabPanel className="react-tabs__tab-panel">