mirror of
https://github.com/Kong/insomnia.git
synced 2026-04-22 15:18:27 -04:00
Initial WebSocket support (#5043)
* [INS-1697] Create WebSocket Request (#5041)
* add fail safe
Co-authored-by: Mark Kim <mark.kim@konghq.com>
* Websockets IPC API (#5044)
* add url to ws-request model
* fix webSocketRequest typo and add url
* add websocket api and expose it through preload ipc
* add typings
Co-authored-by: Mark Kim <mark.kim@konghq.com>
* [INS-1701] Create/Close Websocket Connection (#5046)
* add websocket action bar and its components
* remove comments
* clean up
* reflect on the electron api
* remove unused files
* import name change
* add styling
* remove commet
* add suggested changes
* remove default value
* Add WebSocketRequest to sidebar and update types (#5048)
* [INS-1700] Delete Websocket Request (#5055)
* add websocket actions dropdown
* add actions to websocket request in sidebar
* [INS-1703] Display WebSocket messages - first pass (#5054)
* Update event types to improve inference
* Update websocket response pane to fetch/subscribe and display the events
* [INS-1693] Add WS echo server for smoke tests (#5050)
Co-authored-by: Dimitri Mitropoulos <dimitrimitropoulos@gmail.com>
* fix sidebar unit tests (#5064)
* [INS-1776] adds `ws` dependency explicitly (#5066)
* [INS-1702] WebSocket Send Message (#5052)
* add initial changes for websocket message
* add abstraction for db operation and websocket operation
* remove console
* add rename
* add basic testing
* add basic testing
* add unit tests
* add form event type
* add comments
* disallow exporting context directly
* add suggested changes
* refresh the query
* using useDeepCompareEffect
* rename variable
* add mock
* clean up
* clean up
* correct the file name
* add some changes
* removing nedb-context and its hooks
* remove database changes for event sending
* [INS-1778] Fix Global Module Typing Issue (#5065)
* fix typings
* add jest import
* [INS-1703] View WebSocket Messages (#5074)
* save changes
* add styles and move files around
* remove unused code
* clean up some components
* add clean up
* add timestamp component
* add unit tests
* add case
* add style changes
* [INS-1786] WebSocket headers tab (#5080)
* first ui pass
* extract and wire up bulk editor
* raise ready state, move send
* add upgrade header debug logs
* can pass header to websocket upgrade
* implement readOnly headers
* add upgrade event and sent headers
* clean up
* fill out http upgrade into the event
* change upgrade message
* read only headers while connected
* remove upgrade event
* revert bulk editor change
* fix header editable toggle
* add nunjucks todos
* improve readOnly implementation
* disable codemirror/nunjucks for websocket headers
* take calculated headers out of the data model
* move hardcoded default headers down the tree
* fix request url rendering issue
* removed spammy warning
* clarify prop name
* refine pair typing
* change placeholder
* remove readOnly header property
* fix readOnly header layout
* Update packages/insomnia/src/ui/components/dropdowns/websocket-request-actions-dropdown.tsx
* Show HTTP->WS upgrade (handshake) (#5091)
* first pass as event
* add handshake ui
* add timeline tab
* simplify ResponseTimelineViewer
* transform res debug modal to change timeline props
* decouple timeline fetching from timeline component
* timeline ui pass
* record headers in request and response models
* can view timeline history
* write timeline to file
* some timeline
* can persist event logs
* put interface beside usage
* add note
* add event log history
* remove table event row
* tidying up
* make ws colors match
* enable multiple open connections
* close open connections at app exit
* remove old test
* Update packages/insomnia/src/models/request-version.ts
* fix type
* default readystate
* fix preview css scroll
Co-authored-by: James Gatz <jamesgatzos@gmail.com>
* INS-1788: Add control flow to improve responsiveness. (#5094)
* INS-1788: Add control flow for responsiveness.
This prevents events from flooding the UI thread entirely.
* Add additional code comments.
* fixup: webSocketEvent -> webSocketEvents
* display response headers (#5102)
* Show errors in timeline (#5100)
* remove unused context provider
* show errors in event tab
* updates timeline with message and close
* clean up
* show errors in timeline
* fix WebSocket capitalisation
* make timeline reader specific to ws
* write outbound messages to the timeline
* fix type
* Make the head row sticky in the events table (#5103)
* Add client certificate support to websocket connections (#5112)
* [INS-1810] close connection on response change (#5104)
* close ws connection when response is changed
* add delete logic for the queue mapg
* set error response to active response
* useRef for CodeEditor
* extract closeRequest
* use requestId to eliminate inconsistencies
* refactor extract clean up methods
* timeline feedback
* change type annotation
* fix type
* Revert "use requestId to eliminate inconsistencies"
This reverts commit 98335a927e.
* [INS-1803] show cookie tab in response pane (#5105)
* hard code sending and storing to true
* make tab naming consistent
* hard code cookie settings to false
* fix headers isDisabled bug
* [INS-1805] Add Auth Header Tab (#5115)
* add minimal change to the auth flow
* add disable state
* adding dropdown disable
* simplify reducer
* fix lint
* [INS-1839] Rename tab Header everywhere (#5119)
* can import/export websocket requests (#5122)
* use responseId for timeline name (#5124)
* add react-virtual to virtualize the event log view (#5126)
* [INS-1833] Include Auth Header in Headers mapping for WebSocket Connection (#5120)
* add auth to the header
* remove console log
* remove unneeded async
* add success redirect logic to websocket server
* add unexpected-response handler
* remove digest auth (#5129)
* remove auth header and outbound message timeline (#5130)
* [INS-1840] Add Connected Status Label and Extras (#5131)
* add status related changes
* text label change
* WebSocket ipc typing proposal (#5125)
* make consistent with main bridge
* rename webSocket
* remove deviated mock
* use consistent arrow function defintions
* Update packages/insomnia/src/main/network/websocket.ts
* Set the environment for websocket responses (#5132)
* first pass (#5123)
* [INS-1843] Add more checks to WS smoke test (#5138)
* Fix failing websocket smoke test
* Add remaining websocket smoke server endopints to fixtures
* Add checks for basic-auth, bearer and redirect
* Rm assertion
* can select payload type (#5133)
* Implement nunjucks rendering for websocket urls, authentication, headers and urls (#5134)
* fix websocket->webSocket (#5142)
* bug fix (#5144)
* INS-1844: Implement basic event view functionality. (#5146)
* INS-1844: Implement basic event view functionality
* CSS fix to align Preview button with tab buttons.
* Reverse the order of WebSocket events (#5148)
* Reverse the event log order in the view and subscribe to messages after the latest
* WebSocket response model (#5147)
* init websocket-response model
* add websocket response model
* remove unused timeline getter
* remove unused functionality from the ws-response model
* can select and save payload message and preview mode (#5143)
* [INS-1838] Add Empty State (#5140)
* add empty state
* use the empty state pane
Co-authored-by: gatzjames <jamesgatzos@gmail.com>
* Bug/stabilise-ws-fetch-method (#5152)
* just poll
* remove cts and eventlog channel
* Fix/nitpicks (#5156)
* remove unused names
* fix copypasta icon
* ensure all subcompnent drop state on request change
* move empty state to response pane
* add websockets to quick switch (#5159)
* [INS-1800] Remove value validity check in the WebSocket headers (#5154)
* remove valid value check in the headers
* add header name filtering
Co-authored-by: Dimitri Mitropoulos <dimitrimitropoulos@gmail.com>
Co-authored-by: Mark Kim <mark.kim@konghq.com>
Co-authored-by: Filipe Freire <livrofubia@gmail.com>
Co-authored-by: Jack Kavanagh <jackkav@gmail.com>
Co-authored-by: John Chadwick <86682572+johnwchadwick@users.noreply.github.com>
Co-authored-by: David Marby <david@dmarby.se>
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14 7C14 3.13401 10.866 0 7 0C3.13401 0 0 3.13401 0 7C0 10.866 3.13401 14 7 14C10.866 14 14 10.866 14 7ZM1.5 7C1.5 3.96243 3.96243 1.5 7 1.5C10.0376 1.5 12.5 3.96243 12.5 7C12.5 10.0376 10.0376 12.5 7 12.5C3.96243 12.5 1.5 10.0376 1.5 7ZM10.5304 5.53039L9.46973 4.46973L6.00006 7.9394L4.53039 6.46973L3.46973 7.53039L6.00006 10.0607L10.5304 5.53039Z" fill="#12C76C"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 520 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7 0C10.866 0 14 3.13401 14 7C14 10.866 10.866 14 7 14C3.13401 14 0 10.866 0 7C0 3.13401 3.13401 0 7 0ZM7 1.5C3.96243 1.5 1.5 3.96243 1.5 7C1.5 10.0376 3.96243 12.5 7 12.5C10.0376 12.5 12.5 10.0376 12.5 7C12.5 3.96243 10.0376 1.5 7 1.5ZM5.93934 7L4.46973 5.53039L5.53039 4.46973L7 5.93934L8.46961 4.46973L9.53027 5.53039L8.06066 7L9.53039 8.46973L8.46973 9.53039L7 8.06066L5.53027 9.53039L4.46961 8.46973L5.93934 7Z" fill="#EFA63B"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 586 B |
5
packages/insomnia-components/src/assets/icn-receive.svg
Normal file
5
packages/insomnia-components/src/assets/icn-receive.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g opacity="0.8">
|
||||
<path d="M6.25006 2V9.12868L4.53039 7.40901L3.46973 8.46967L7.00006 12L10.5304 8.46967L9.46973 7.40901L7.75006 9.12868V2H6.25006Z" fill="#36CB84"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 274 B |
5
packages/insomnia-components/src/assets/icn-sent.svg
Normal file
5
packages/insomnia-components/src/assets/icn-sent.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g opacity="0.8">
|
||||
<path d="M7.82397 12L7.82397 4.87132L9.54104 6.59099L10.6001 5.53033L7.0751 2L3.5501 5.53033L4.60916 6.59099L6.32623 4.87132L6.32623 12L7.82397 12Z" fill="#2978E5"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 292 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14 7C14 3.134 10.866 0 7 0C3.13403 0 0 3.134 0 7C0 10.866 3.13403 14 7 14C10.866 14 14 10.866 14 7ZM1.5 7C1.5 3.96243 3.96246 1.5 7 1.5C10.0375 1.5 12.5 3.96243 12.5 7C12.5 10.0376 10.0375 12.5 7 12.5C3.96246 12.5 1.5 10.0376 1.5 7ZM7 5C7.55231 5 8 4.55228 8 4C8 3.44772 7.55231 3 7 3C6.44769 3 6 3.44772 6 4C6 4.55228 6.44769 5 7 5ZM8 11V7V6H5V7H6V11H8Z" fill="white" fill-opacity="0.6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 543 B |
@@ -8,10 +8,12 @@ import { SvgIcnBrackets } from './assets/svgr/IcnBrackets';
|
||||
import { SvgIcnBug } from './assets/svgr/IcnBug';
|
||||
import { SvgIcnBurgerMenu } from './assets/svgr/IcnBurgerMenu';
|
||||
import { SvgIcnCheckmark } from './assets/svgr/IcnCheckmark';
|
||||
import { SvgIcnCheckmarkCircle } from './assets/svgr/IcnCheckmarkCircle';
|
||||
import { SvgIcnChevronDown } from './assets/svgr/IcnChevronDown';
|
||||
import { SvgIcnChevronUp } from './assets/svgr/IcnChevronUp';
|
||||
import { SvgIcnClock } from './assets/svgr/IcnClock';
|
||||
import { SvgIcnCookie } from './assets/svgr/IcnCookie';
|
||||
import { SvgIcnDisconnected } from './assets/svgr/IcnDisconnected';
|
||||
import { SvgIcnDraftingCompass } from './assets/svgr/IcnDraftingCompass';
|
||||
import { SvgIcnDragGrip } from './assets/svgr/IcnDragGrip';
|
||||
import { SvgIcnElevator } from './assets/svgr/IcnElevator';
|
||||
@@ -43,10 +45,13 @@ import { SvgIcnPlus } from './assets/svgr/IcnPlus';
|
||||
import { SvgIcnProhibited } from './assets/svgr/IcnProhibited';
|
||||
import { SvgIcnQuestion } from './assets/svgr/IcnQuestion';
|
||||
import { SvgIcnQuestionFill } from './assets/svgr/IcnQuestionFill';
|
||||
import { SvgIcnReceive } from './assets/svgr/IcnReceive';
|
||||
import { SvgIcnSearch } from './assets/svgr/IcnSearch';
|
||||
import { SvgIcnSecCert } from './assets/svgr/IcnSecCert';
|
||||
import { SvgIcnSent } from './assets/svgr/IcnSent';
|
||||
import { SvgIcnSuccess } from './assets/svgr/IcnSuccess';
|
||||
import { SvgIcnSync } from './assets/svgr/IcnSync';
|
||||
import { SvgIcnSystemEvent } from './assets/svgr/IcnSystemEvent';
|
||||
import { SvgIcnTrashcan } from './assets/svgr/IcnTrashcan';
|
||||
import { SvgIcnTriangle } from './assets/svgr/IcnTriangle';
|
||||
import { SvgIcnUser } from './assets/svgr/IcnUser';
|
||||
@@ -122,7 +127,11 @@ export const IconEnum = {
|
||||
warning: 'warning',
|
||||
warningCircle: 'warning-circle',
|
||||
x: 'x',
|
||||
|
||||
disconnected: 'disconnected',
|
||||
checkmarkCircle: 'checkmark-circle',
|
||||
receive: 'receive',
|
||||
sent: 'sent',
|
||||
systemEvent: 'system-event',
|
||||
/** Blank icon */
|
||||
empty: 'empty',
|
||||
} as const;
|
||||
@@ -224,6 +233,11 @@ export class SvgIcon extends Component<SvgIconProps> {
|
||||
[IconEnum.warningCircle]: [ThemeEnum.default, SvgIcnWarningCircle],
|
||||
[IconEnum.warning]: [ThemeEnum.notice, SvgIcnWarning],
|
||||
[IconEnum.x]: [ThemeEnum.default, SvgIcnX],
|
||||
[IconEnum.disconnected]: [ThemeEnum.default, SvgIcnDisconnected],
|
||||
[IconEnum.receive]: [ThemeEnum.default, SvgIcnReceive],
|
||||
[IconEnum.sent]: [ThemeEnum.default, SvgIcnSent],
|
||||
[IconEnum.checkmarkCircle]: [ThemeEnum.default, SvgIcnCheckmarkCircle],
|
||||
[IconEnum.systemEvent]: [ThemeEnum.default, SvgIcnSystemEvent],
|
||||
};
|
||||
|
||||
render() {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDQaumV23vAEqn4
|
||||
KAJ+zQ7nKlLWVwhqMjOS7pz3HohLf4HER6SQ6bhXj2dlsW2TEDZQcOxw9jyTs4Wu
|
||||
ckpqWy//UQ4LawQ3FGpst2kFAtFY6D93odh0mjPCLzQlXpcGKs6LOzlWAXWxrV6k
|
||||
HRCsPT6ytnSOZNMlDNQKB+dZJ+vFz2c5LYyPXUkVenYsYH38E/ZFIz5GVRJlLhcH
|
||||
IKRK3cJoGSQfljlFFDDiwvHc5/hszKIA0zHjk4MmnSw4Dm1P7fiFsI8zZEkNACBG
|
||||
O/Lf6lcWX8r+1LfrGsmRfku4GrP5LXWD/ToLgmo2CAP3TBM9ftCggKhTbJfZxMWb
|
||||
Q3DAui1RAgMBAAECggEBAKKM4AW7Gzdg1yPuwJN5B0iQH++ADdYVtVfBtraeH5sS
|
||||
pXkaj2VehCH2fKQ5z8ZFfLcce6xWwERKXdcC2Ls+x56P7y5ElxMGX0LPgZ8g5Xo8
|
||||
GVQK7LF0my22dys2LP/oXxMEa+GCXfLnzsqcyKYtVjs4RovQY0WgTbhNFcjZc+/g
|
||||
PL3DclvJHJ5n9Bb+ufxJO3K7i4sxD8gcGfgXTVSghM+VCkslw0BJcqpNyGgEjI64
|
||||
FTf7Qu1E0rKRGT0EKm70RffoZpLNdpMR/9GMHE5CAMSGie27AK2X8OKCgmqCh3Kp
|
||||
wKqy3hRNnk9LNfhaan2LuprKlv11bJwAFAQIJSKtzOkCgYEA6n3NI5U4h//w0SeG
|
||||
eaoOqXDyWmjSj4w1MbPV4sPvs1hG0iXn+w7+dfocXGawoPyu5zB9MhGf3mRB/8dp
|
||||
6gb+xvYYSoVysh4VboBMJbWBlkWH4/q5KBnwuEVZDPy5fDpK449SlVmiqfpOH7hZ
|
||||
wzGV5Wdwzofxp4nVIhCT/ryFYP8CgYEA44jcXh9rOHeRySo4ZofBP0QqAdT48ffl
|
||||
D+VZ2q3/DNj67dYPfo8hhnEgsgVPz02ODHh8aMYVQp4OYrXpdh3/qqnWR7BB5ch1
|
||||
lVFSOjytp17TSEm//vXz+WzSyrtlInGjqGazUGMpz+9j672be4y+wdxeyNDq04lr
|
||||
UQXIAsP0Ia8CgYAvhA5th29NH6/MshWt0afm7dwuNc91BxRAXhCZQtrvnJO9QbEg
|
||||
TomBnozgrG5eMNXAQzMbUjby+Z3mFqJ/qas25edGMoRrU21EVvsXKRB5Qt2mdMfQ
|
||||
OHFu5Z6F4zAy3B0Qv5ocaW1sxCvQgaquwv183tkdAK8XI/bsUC+tDsZ4QQKBgQDL
|
||||
kYTnSODa0k9CVV3Ejaydd9TFcs+PXKQ5ho7PkWBhFDfcVeni5xetesUvwITZCaAP
|
||||
FDTqYF5hDZv9QJexL8Gv5OdrmAw9Ew3wG6Ofqu4KklIhmKoH5/DxtSUacHJZUKaF
|
||||
Ye0H/NBJ0vnozeivrwpz0z+SFyghPg8fnDaIEtz2zQKBgHfG8/eXi1ZOiv1nLEuZ
|
||||
SvJY8IU89aTqhgVsVk0g6h1jEBhZI4mTyvJe4OyC2JjxX4YD9gxu2AXSf2w7yU94
|
||||
jRZdqJpLoXtcbIy9nAdYa0AyyyPKzwcSUepO2pqE3QCOSwxlplEqrWVzD9FyHFGJ
|
||||
0RyshruFkeq15L6ud/6fY8Wz
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -0,0 +1,25 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIESDCCArCgAwIBAgIRAKkiT43PW7dM415hMVMjLlIwDQYJKoZIhvcNAQELBQAw
|
||||
ezEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMSgwJgYDVQQLDB9kYXZp
|
||||
ZEBETWFyYnktV29yayAoRGF2aWQgTWFyYnkpMS8wLQYDVQQDDCZta2NlcnQgZGF2
|
||||
aWRARE1hcmJ5LVdvcmsgKERhdmlkIE1hcmJ5KTAeFw0yMjA4MjUxMzQ1NTBaFw0y
|
||||
NDExMjUxNDQ1NTBaMFMxJzAlBgNVBAoTHm1rY2VydCBkZXZlbG9wbWVudCBjZXJ0
|
||||
aWZpY2F0ZTEoMCYGA1UECwwfZGF2aWRARE1hcmJ5LVdvcmsgKERhdmlkIE1hcmJ5
|
||||
KTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANBq6ZXbe8ASqfgoAn7N
|
||||
DucqUtZXCGoyM5LunPceiEt/gcRHpJDpuFePZ2WxbZMQNlBw7HD2PJOzha5ySmpb
|
||||
L/9RDgtrBDcUamy3aQUC0VjoP3eh2HSaM8IvNCVelwYqzos7OVYBdbGtXqQdEKw9
|
||||
PrK2dI5k0yUM1AoH51kn68XPZzktjI9dSRV6dixgffwT9kUjPkZVEmUuFwcgpErd
|
||||
wmgZJB+WOUUUMOLC8dzn+GzMogDTMeOTgyadLDgObU/t+IWwjzNkSQ0AIEY78t/q
|
||||
VxZfyv7Ut+sayZF+S7gas/ktdYP9OguCajYIA/dMEz1+0KCAqFNsl9nExZtDcMC6
|
||||
LVECAwEAAaNvMG0wDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMC
|
||||
BggrBgEFBQcDATAfBgNVHSMEGDAWgBQgZSNC3VSTkAucIY9/41pfQ64sozAbBgNV
|
||||
HREEFDASghBjbGllbnQubG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4IBgQB4JLAo
|
||||
j24DvacnTOxPu7gtpLFfB7ykgeAhzdWla7Maqp62lnasYUL9+nX8+aNOxYbd3uq/
|
||||
55uSBBy2jnxce2B0oqkRhB0uOPZNwJsJXIZEp+anhz9E9jrgZhdMCDuhrwn5amPc
|
||||
UmisabO5rPrFxmhKVhkoWHFPKVVOqqlnUt9UdFQsdIQoKYuX1BNro0QDOQ/p4Z7y
|
||||
tCd6DsyhgV3iTDN3GkBv4fbYxxjD5tyxYKjCAwmcebOwEtAbHXFYpyVg0bNqEq0S
|
||||
YofcZHkxFtBXV1Ayb1yhNYqNTu3VHTQkkE+XO2vJGBhbueKWQADLZ2fQ+8qkTqL8
|
||||
AQ3YX4UQKcWcqwRUWpmEmr+XNfqYq8UrbO/liuPh0uQh7WkxHGlLMPe76jeK1FJP
|
||||
P8T+Ivh7x7MCj2G/KlwIb7OgpjCuKeR28aSz3+WDInF4lydUwHDZveByVidBaD32
|
||||
NIZeX3K8nj7tGpchbDHLiCtdSwTEXFKBPm5EcvAO92alis7mozgGrG3dK1M=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDTqFvEgk21fa9Z
|
||||
e1OhPQ5BcUDIBLSY3F+1nNv5U0HKzUKEcoU+d0TER2/iNYPs2yLU2mK1UMgQsJfB
|
||||
j/2ER0kVBSwJ4uk02HqtXtv/E9/RkwYycUsHZFX6wk8HVEKqfIxSgApGBoT15//l
|
||||
DYFkJwtMvwib5szzmyVswHrYC15HY67o6PkNPyMfn/erOnu4akf0CA1lIg/2haeu
|
||||
EmTx3mPs677f9xUt6MFHRAAZS6HZKu6THcW6VtOUybj9D+slDwQBJ2aS5Lq79VJd
|
||||
cBXxCbq2UkXK/bIHkylQ2gtK2Gq8CvbnxmrGU9pXrIFRT7xLQTaUFxXeUMbGO98k
|
||||
5Wu62FP9AgMBAAECggEBAMc+PCyvMPHBP9jvNFmbPRkzwHTJoSwof1xaLeT1kACV
|
||||
2qENoQqdgbl3OgZqtCa6Xn6amcLvKXY0lpbash7ccBp+hOdFmJxrkIg1vMjQ76e8
|
||||
TGAdsDBkLl/gnD5c/mi41+stpv4mUvGdlJENdplN++AiELuZt6M2kDNgugM4KGbv
|
||||
45cwuu0DDldCSlp9ujFvn4QipEzf4wcNqvMyaNCzGw/7FyRQncZTe9J3E1RV+Tow
|
||||
UWqXSR3LhRUNpn65khtPBgkE+ffAFf6d2+wwm5Cex+VOTlMQnkgtk6IgAweBkXY4
|
||||
KBmzTupm1TqfQX16zUzDaq3zO2YUhwAMwxo2Dm2rIE0CgYEA29vOXaD8PXepMwDw
|
||||
2tJS/c3WKNgTeZ1B8vQxlduwfuJeMHVXF20y2kO2rhEsrPTIUX7mM8Bj59vP9rcX
|
||||
2P4GTBjvsvZj/yYrFV2EyXu+LjPbPMJCi8P/J8w8f0Pwy3xME/awdi477rm43cjn
|
||||
cBlZNAl0KK3npDd5L5Dfs7fAuu8CgYEA9nNvdQ2EqH1wychTeg3Mj2CsnFI/lPAm
|
||||
CmUL/Dmt47NXMkG+i1DfyaKTqF7M2+cWphzsk3ppwLfU2am2Uk9DgQl7BkFoLCXY
|
||||
7xD0gfIAQKrbLKo9wHvjdn3j8rbnDb/A4qCA3ea5kItWviQbUdgl3amnp9Ts2tE8
|
||||
3H09kLmZz9MCgYBbXonw10p8sRIcJDP0fJwI5lYuOz48uGID+f/xa49569GBCgLb
|
||||
tYIAu0tcI78RUdk+JSK+NyJN5UgUHBtJDqjHT0Wudj8wdkhJZMgeg9KRmPNv2LuX
|
||||
IikT/QjXSwDzUAC9+zNyqdw2ZfCyGyAzshUkTxl7Hmq6EGPIpMiTA7aQ+QKBgQDr
|
||||
2Kp4DRi/mVPfdnsUWbJCH5Tv//Hi2TK+Tdb7aENVnaG7cZkkf5+5uYCu5xIK+4n8
|
||||
K7/mnoYnrITgS/4zpLEIAoeeA+fqH8oLdmFXHb1KJXebtctkseqK0YzcEFbrHG89
|
||||
MbZBJPS+M+ouCiWu3DfYeev8u9Jy0Tv6EUxifIuKiwKBgQDJPmxCPM3HS3fvO+7w
|
||||
DlMtR52IKjohaKmMnt5EtNUR1Xs9vZCa51SvNWA6YJ+zqoGQVCXUoy77O43cewK3
|
||||
crv3Hz4wWENG/bVfJQlkpFS4t+RJzNqEzeXSYQxFVfQlnhoeEAj/cZWuOJpkhmC2
|
||||
2JkrwTxrofjxbbfD/m56XUusQg==
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -0,0 +1,25 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIENzCCAp+gAwIBAgIRAPKpJUU8eOZ/zg0WbmKsy8IwDQYJKoZIhvcNAQELBQAw
|
||||
ezEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMSgwJgYDVQQLDB9kYXZp
|
||||
ZEBETWFyYnktV29yayAoRGF2aWQgTWFyYnkpMS8wLQYDVQQDDCZta2NlcnQgZGF2
|
||||
aWRARE1hcmJ5LVdvcmsgKERhdmlkIE1hcmJ5KTAeFw0yMjA4MjUxMzQ1MzdaFw0y
|
||||
NDExMjUxNDQ1MzdaMFMxJzAlBgNVBAoTHm1rY2VydCBkZXZlbG9wbWVudCBjZXJ0
|
||||
aWZpY2F0ZTEoMCYGA1UECwwfZGF2aWRARE1hcmJ5LVdvcmsgKERhdmlkIE1hcmJ5
|
||||
KTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANOoW8SCTbV9r1l7U6E9
|
||||
DkFxQMgEtJjcX7Wc2/lTQcrNQoRyhT53RMRHb+I1g+zbItTaYrVQyBCwl8GP/YRH
|
||||
SRUFLAni6TTYeq1e2/8T39GTBjJxSwdkVfrCTwdUQqp8jFKACkYGhPXn/+UNgWQn
|
||||
C0y/CJvmzPObJWzAetgLXkdjrujo+Q0/Ix+f96s6e7hqR/QIDWUiD/aFp64SZPHe
|
||||
Y+zrvt/3FS3owUdEABlLodkq7pMdxbpW05TJuP0P6yUPBAEnZpLkurv1Ul1wFfEJ
|
||||
urZSRcr9sgeTKVDaC0rYarwK9ufGasZT2lesgVFPvEtBNpQXFd5QxsY73yTla7rY
|
||||
U/0CAwEAAaNeMFwwDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMB
|
||||
MB8GA1UdIwQYMBaAFCBlI0LdVJOQC5whj3/jWl9DriyjMBQGA1UdEQQNMAuCCWxv
|
||||
Y2FsaG9zdDANBgkqhkiG9w0BAQsFAAOCAYEAORoaX+VVxk2HyJZveHJgQGcKgOwm
|
||||
0jm3VQ2d4fFlfiAsLbpkZAeTApu1A4ylan8q6clmraP7GyNf7iZ3OMPkS4vVONMs
|
||||
sJmFkrKsES4mF3CHKK7+8/bRtB0Y0yTGDxWOiQpZ3Ok6S84WTPylFyTZi4GVRN8P
|
||||
+KhOaGleNLaf4m2ts0RbMh33va3UKn1b5VgxhdQKGUHcQYTwhmXVW3GDNv2W0Adk
|
||||
hCaHIDIDAKoCZVm6HYcf/GW4EhJaujnxSWPR8os25+DePeHhrgKvlHMyyevSLcqP
|
||||
U+ZN3Lk8nNIgCOuhPke7J71IJAX4lklljfeL0sQHDd/5mwLlEtWvaRNGfovnfBfL
|
||||
ut5Pq0QU2WkOMwdYPJ62fuCm9GX58tXb9MfezFst0EEpf2S0Tn/7TvIXq7DOfzZu
|
||||
htaGf6HrfkV159ULFuAEog+2HwvPGW3wLVQN5n6QgV/uv0W40Wm1SVpEGG4cK1r4
|
||||
emap+9mwik3/bLP0xeuTG8S6X7MZde3By1xo
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,40 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIG/AIBADANBgkqhkiG9w0BAQEFAASCBuYwggbiAgEAAoIBgQCix4kz1jyTIvAh
|
||||
tzLqg40XpbNwbZg0VDPXkwPtb6YfOA+k/pPuPRPrhYkVoybNqWtQLBtol77SUuvy
|
||||
ytJZtKY1gRTM4Hzw+ZoReEPV1ekJr5DBqMXkVQHIlpokFQsgX/dW/vzfQ0jjcfMt
|
||||
HE+FYLJKWZ3Xby+fVkjSl9V75uMHD3f5klJmXKPTMGlBKf+aCwTlBvXdn9oXAqf+
|
||||
jQQi1b8xvfGdXMBYEhOoW4HbdPhwMHuzMlF5y8s6Cd9WMQoEVrKiAqT7C4p79GgL
|
||||
wAT6lrbCmvw85t7NKc/2+o8JvF85AcM9XkouydhMPsWl00vfApnN6EEJYX8+P5Vm
|
||||
comCC5fVygxzZOj/ElpIfx4OXy39lnJLGov4etpYtV5RDKrmvSjid6wXimYHORC7
|
||||
sWkJU7PqnHxooylgzbOsqlCp7GVZLzAeuSMnwJfsmSJ7GVMNq5UkQv9KygxwFmMu
|
||||
OgR087Ui340y+RuqMZzJdBow+2aCYPw+zZg+nq7ZAECZbJW/OmkCAwEAAQKCAYBb
|
||||
VByUquS9oOKd6A13KmvlEqEEuVimM4AKuX+Anh3Ucj3E0tjo1/fvMrLhIvLIfP+q
|
||||
sbSHEGyN0Nx1EnrGveZrKosjD+jJwyFAH/vfY+8l8g0Dus+c9lzT0DuXdv8RIQbD
|
||||
FrmGAlhI1EwdyT6MlN4zfOhkUQulGGIvVeT/aWGDOpiTvBbjO6LnAMhtOUUhhoEu
|
||||
hqM3v1I7is/6r9/cM5TcMbf4FGwOfcXttbm8CXrCZ1FgDyFLdp4FaDiYQVdfa8xU
|
||||
ZoSv5qF/C5TU2hlkgb28SrlgkY6WnsQ9lUHTg/vdEr7xqnE48yCwsdhiNd5dn9Wv
|
||||
Lm6gB+CkSbXFOZDFFqEbFljb7OseRNQEwoAkuRgqqni+z3Fw29T+N/YM7OEMJOGR
|
||||
63haPProD2OcyYs7lCvCL446qtCT+IdWG75q0DFjaCGCett/D9LEnuR7BgFAMJ9Q
|
||||
ZQHISFt0HevR8neWbse6gZk9sRRU8d0WEpT0YsBS6cHG5upWigTOuvEUEF82CqEC
|
||||
gcEAwmk08kbMEzSdKm2kQxuNCrVJCBkzDYCP95c2f2NsvGFjdbcw0zwl9/i4lLD2
|
||||
4XNzxcdbNjuAGoflkeR6DCYKpd6Ed0gJfFj8+Bpt3dgt7apnxWvba3Af07RhWkSx
|
||||
EweusKBJ4olNDacD2ayLlnXwU8KMmbngsEiz7476t4pt3SEX6CsIKQsd3QnhsFPK
|
||||
PpmPUyPFl6FOH07pWca5cx4r1/392sdXI1MLpwFycyaIuBg5zJD7YCQmU51eZeKD
|
||||
oJFVAoHBANZZAHumSApHUcplUDK7YW37vqESr6Hhn7YipLADppPcrjohI+OCRGW3
|
||||
mNNgrnG+MKKTu8w5eOEdXMPH2L89m6gXZLZhhupV++3/+xECAI0coJ5FhFbBeS+Z
|
||||
UbOhmba0cuurSH2f6UbOnBuk3r/xIzERmoUQHEPmy3nbvUae+2ZvDsSjFvUbFZRK
|
||||
Dhuc/6NnhZ8v1gGRAtkeB8atWNd8y4Wa3heG/BzjtG7v16J8bXbj9MAgAOC7XqxK
|
||||
8uriu4HUxQKBwDAi4pQ2iWMb/Oo7eZeQI80J9ApISwbF1V/Flh3WnV7Lclf8Qt+a
|
||||
ngAXGoTeiFJsRrcq1F/KPb7T9Ti5bKrDZJGLVhs+v/KFCiXYTWnHlB+ruMP+H7cr
|
||||
bQX7PLugFIQUu+FJ3uFzg5ukxeRIv8tCan4ixrNtfb/IUJ05NsTpRqihAA1hUkTv
|
||||
VrabMsF5DbOQTBeW3N7ddr1zyX2MIfDqAIsbfZaFEwNRFaqFRjRSzzld7jnDkCpO
|
||||
6Rp89Zmei17ffQKBwFKNBHqal7Qds4pXaoOfVu6cvdYa9DlMQ85JmVOQlF7t5svM
|
||||
Z53/VYg3JUyDN6vmq6RxcSo+GCfavxdHqFo+x+v81nTHKsalRtlqdK8gLkYqeFF8
|
||||
RHOFH78NNUIRQrny1S+eT9TR+W2jtMuQu5kAraUAOpp0ke40vLi5wDOqlvfkXbOt
|
||||
e/H59F2gB77qwCmWfQfJzInd51LnFeeWa1jSXy+dbVtySTZ3G8594HZbpWzcbi5w
|
||||
JOZoQxXn55Y+rChcYQKBwBZDTwcLMCW3WuHhNDAOOf7xx+OnvChcTDMhbMtNeGNv
|
||||
47UtHZ2FQF1UpeyGmTlKwbiBPPk8fU90yR+vf9wKAQTFLV/gtFsDC8cnMr75t4Jq
|
||||
GJzKfO88oNqY8Tvxr3s94wW1YTLqFqGBPSdKVy7+vcZfsR6e+x9I2Tj7r8b6wzLE
|
||||
qwNBQNufwef4eoDzyE69dIaaDHHTV1q+Z1bOF4XGAcm/efK3jhNlPK98tSCme+jr
|
||||
TmvRvDdQuagS/w4gM0gSrA==
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -0,0 +1,28 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIExTCCAy2gAwIBAgIQb/tURt+KKadPV6mtgAlcPDANBgkqhkiG9w0BAQsFADB7
|
||||
MR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExKDAmBgNVBAsMH2Rhdmlk
|
||||
QERNYXJieS1Xb3JrIChEYXZpZCBNYXJieSkxLzAtBgNVBAMMJm1rY2VydCBkYXZp
|
||||
ZEBETWFyYnktV29yayAoRGF2aWQgTWFyYnkpMB4XDTIyMDgyNTEzNDM1N1oXDTMy
|
||||
MDgyNTEzNDM1N1owezEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMSgw
|
||||
JgYDVQQLDB9kYXZpZEBETWFyYnktV29yayAoRGF2aWQgTWFyYnkpMS8wLQYDVQQD
|
||||
DCZta2NlcnQgZGF2aWRARE1hcmJ5LVdvcmsgKERhdmlkIE1hcmJ5KTCCAaIwDQYJ
|
||||
KoZIhvcNAQEBBQADggGPADCCAYoCggGBAKLHiTPWPJMi8CG3MuqDjRels3BtmDRU
|
||||
M9eTA+1vph84D6T+k+49E+uFiRWjJs2pa1AsG2iXvtJS6/LK0lm0pjWBFMzgfPD5
|
||||
mhF4Q9XV6QmvkMGoxeRVAciWmiQVCyBf91b+/N9DSONx8y0cT4VgskpZnddvL59W
|
||||
SNKX1Xvm4wcPd/mSUmZco9MwaUEp/5oLBOUG9d2f2hcCp/6NBCLVvzG98Z1cwFgS
|
||||
E6hbgdt0+HAwe7MyUXnLyzoJ31YxCgRWsqICpPsLinv0aAvABPqWtsKa/Dzm3s0p
|
||||
z/b6jwm8XzkBwz1eSi7J2Ew+xaXTS98Cmc3oQQlhfz4/lWZyiYILl9XKDHNk6P8S
|
||||
Wkh/Hg5fLf2Wcksai/h62li1XlEMqua9KOJ3rBeKZgc5ELuxaQlTs+qcfGijKWDN
|
||||
s6yqUKnsZVkvMB65IyfAl+yZInsZUw2rlSRC/0rKDHAWYy46BHTztSLfjTL5G6ox
|
||||
nMl0GjD7ZoJg/D7NmD6ertkAQJlslb86aQIDAQABo0UwQzAOBgNVHQ8BAf8EBAMC
|
||||
AgQwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUIGUjQt1Uk5ALnCGPf+Na
|
||||
X0OuLKMwDQYJKoZIhvcNAQELBQADggGBACVC0yCFLKD5BqBbvHOTPh4pDGl+dmxR
|
||||
Ta/GjZvXisbxKA/YlwZ+deENLW8veYoDgr7QEOb8QvXiqdd/no+rAU18KnTlwOsD
|
||||
ITITqF8tStTJKPBubws4S9fhQIeDs+7MS7mW7lfbIz4ho77KcweSbndak9NW7cBl
|
||||
AwoCPcK/uCH0F2EHDNAQnxf1OVdRREPWD4/Di9EhYr6T5BaRVuD/pB4eYUb9shqD
|
||||
6vLKVYb/vpYIHc5vEDYT4cmaFW8rXJc+gHZzs+SkTpjvUKZrz9JRrdAji+djPQlD
|
||||
ZRry4t9De7nVdCZDGjbwzwuEtJxoveldDKcZ/BtRb7dVbU7SB6WqKjUzqlnCoLdE
|
||||
eZBU2d6+r/ojnPCxHgzVRzABYF4vakIYg4VDVFYEVqdnBo8n5PJLX2gAHbaasOQ4
|
||||
hPYthHs8RVk2O5+A/mBVeE09yCwhOAxvqi91ttS4uuN2ullmr07232Wzgc64GSJf
|
||||
g1WKYRpirTEbH49z3lXRIwGaq2J1xArHsA==
|
||||
-----END CERTIFICATE-----
|
||||
87
packages/insomnia-smoke-test/fixtures/websockets.yaml
Normal file
87
packages/insomnia-smoke-test/fixtures/websockets.yaml
Normal file
@@ -0,0 +1,87 @@
|
||||
_type: export
|
||||
__export_format: 4
|
||||
__export_date: 2022-08-31T10:40:21.266Z
|
||||
__export_source: insomnia.desktop.app:v2022.5.0
|
||||
resources:
|
||||
- _id: ws-req_0ba3ad1a7a81483a8f0c0caab4e8998c
|
||||
parentId: wrk_db59cf2b74764e6a80c0dcbcf3d67130
|
||||
modified: 1661942223227
|
||||
created: 1661942202873
|
||||
name: localhost:4010
|
||||
url: ws://localhost:4010
|
||||
metaSortKey: -1661942202873
|
||||
headers: []
|
||||
authentication: {}
|
||||
_type: websocket_request
|
||||
- _id: wrk_db59cf2b74764e6a80c0dcbcf3d67130
|
||||
parentId: null
|
||||
modified: 1661942194367
|
||||
created: 1661942194367
|
||||
name: WebSockets
|
||||
description: ""
|
||||
scope: collection
|
||||
_type: workspace
|
||||
- _id: ws-req_c522379d686b44179e9626fc6c8ed88a
|
||||
parentId: wrk_db59cf2b74764e6a80c0dcbcf3d67130
|
||||
modified: 1662451274029
|
||||
created: 1662451239058
|
||||
name: basic-auth
|
||||
url: ws://localhost:4010/basic-auth
|
||||
metaSortKey: -1661942202823
|
||||
headers: []
|
||||
authentication:
|
||||
type: basic
|
||||
useISO88591: false
|
||||
disabled: false
|
||||
username: user
|
||||
password: password
|
||||
_type: websocket_request
|
||||
- _id: ws-req_2157c597bcbb4614b64c7c99c6f7a982
|
||||
parentId: wrk_db59cf2b74764e6a80c0dcbcf3d67130
|
||||
modified: 1662451369688
|
||||
created: 1662451318293
|
||||
name: bearer
|
||||
url: ws://localhost:4010/bearer
|
||||
metaSortKey: -1661942202723
|
||||
headers: []
|
||||
authentication:
|
||||
type: bearer
|
||||
token: insomnia-cool-token-!!!1112113243111
|
||||
disabled: false
|
||||
_type: websocket_request
|
||||
- _id: ws-req_6b9c944a7f034fcb8b0a92e5442538d7
|
||||
parentId: wrk_db59cf2b74764e6a80c0dcbcf3d67130
|
||||
modified: 1662451456879
|
||||
created: 1662451430343
|
||||
name: redirect
|
||||
url: ws://localhost:4010/redirect
|
||||
metaSortKey: -1661942202623
|
||||
headers: []
|
||||
authentication: {}
|
||||
_type: websocket_request
|
||||
- _id: env_78d7375877d288dfb527a256e6d7e92dce4ff968
|
||||
parentId: wrk_db59cf2b74764e6a80c0dcbcf3d67130
|
||||
modified: 1661942194375
|
||||
created: 1661942194375
|
||||
name: Base Environment
|
||||
data: {}
|
||||
dataPropertyOrder: null
|
||||
color: null
|
||||
isPrivate: false
|
||||
metaSortKey: 1661942194375
|
||||
_type: environment
|
||||
- _id: jar_78d7375877d288dfb527a256e6d7e92dce4ff968
|
||||
parentId: wrk_db59cf2b74764e6a80c0dcbcf3d67130
|
||||
modified: 1661942194378
|
||||
created: 1661942194378
|
||||
name: Default Jar
|
||||
cookies: []
|
||||
_type: cookie_jar
|
||||
- _id: spc_2d11197686aa40ec8f5f072727af4a7a
|
||||
parentId: wrk_db59cf2b74764e6a80c0dcbcf3d67130
|
||||
modified: 1661942194369
|
||||
created: 1661942194369
|
||||
fileName: ws example
|
||||
contents: ""
|
||||
contentType: yaml
|
||||
_type: api_spec
|
||||
48
packages/insomnia-smoke-test/package-lock.json
generated
48
packages/insomnia-smoke-test/package-lock.json
generated
@@ -22,6 +22,7 @@
|
||||
"@types/oidc-provider": "^7.8.1",
|
||||
"@types/ramda": "^0.27.45",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@types/ws": "^8.5.3",
|
||||
"concurrently": "^7.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"execa": "^5.0.0",
|
||||
@@ -38,6 +39,7 @@
|
||||
"ramda-adjunct": "^2.34.0",
|
||||
"ts-node": "^9.1.1",
|
||||
"uuid": "^8.3.2",
|
||||
"ws": "^8.8.1",
|
||||
"xvfb-maybe": "^0.2.1"
|
||||
}
|
||||
},
|
||||
@@ -1453,6 +1455,15 @@
|
||||
"integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
|
||||
"integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/yargs": {
|
||||
"version": "17.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.10.tgz",
|
||||
@@ -5644,6 +5655,27 @@
|
||||
"node": "^12.13.0 || ^14.15.0 || >=16"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.8.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.8.1.tgz",
|
||||
"integrity": "sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xvfb-maybe": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/xvfb-maybe/-/xvfb-maybe-0.2.1.tgz",
|
||||
@@ -6957,6 +6989,15 @@
|
||||
"integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/ws": {
|
||||
"version": "8.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
|
||||
"integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/yargs": {
|
||||
"version": "17.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.10.tgz",
|
||||
@@ -10192,6 +10233,13 @@
|
||||
"signal-exit": "^3.0.7"
|
||||
}
|
||||
},
|
||||
"ws": {
|
||||
"version": "8.8.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.8.1.tgz",
|
||||
"integrity": "sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"xvfb-maybe": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/xvfb-maybe/-/xvfb-maybe-0.2.1.tgz",
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
"@types/oidc-provider": "^7.8.1",
|
||||
"@types/ramda": "^0.27.45",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@types/ws": "^8.5.3",
|
||||
"concurrently": "^7.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"execa": "^5.0.0",
|
||||
@@ -57,6 +58,7 @@
|
||||
"ramda-adjunct": "^2.34.0",
|
||||
"ts-node": "^9.1.1",
|
||||
"uuid": "^8.3.2",
|
||||
"ws": "^8.8.1",
|
||||
"xvfb-maybe": "^0.2.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import express from 'express';
|
||||
import { graphqlHTTP } from 'express-graphql';
|
||||
import { readFileSync } from 'fs';
|
||||
import { createServer } from 'https';
|
||||
import { join } from 'path';
|
||||
|
||||
import { basicAuthRouter } from './basic-auth';
|
||||
import githubApi from './github-api';
|
||||
@@ -7,9 +10,11 @@ import gitlabApi from './gitlab-api';
|
||||
import { root, schema } from './graphql';
|
||||
import { startGRPCServer } from './grpc';
|
||||
import { oauthRoutes } from './oauth';
|
||||
import { startWebSocketServer } from './websocket';
|
||||
|
||||
const app = express();
|
||||
const port = 4010;
|
||||
const httpsPort = 4011;
|
||||
const grpcPort = 50051;
|
||||
|
||||
app.get('/pets/:id', (req, res) => {
|
||||
@@ -55,7 +60,23 @@ app.use('/graphql', graphqlHTTP({
|
||||
}));
|
||||
|
||||
startGRPCServer(grpcPort).then(() => {
|
||||
app.listen(port, () => {
|
||||
const server = app.listen(port, () => {
|
||||
console.log(`Listening at http://localhost:${port}`);
|
||||
console.log(`Listening at ws://localhost:${port}`);
|
||||
});
|
||||
|
||||
const httpsServer = createServer({
|
||||
cert: readFileSync(join(__dirname, '../fixtures/certificates/localhost.pem')),
|
||||
ca: readFileSync(join(__dirname, '../fixtures/certificates/rootCA.pem')),
|
||||
key: readFileSync(join(__dirname, '../fixtures/certificates/localhost-key.pem')),
|
||||
// Only allow connections using valid client certificates
|
||||
requestCert: true,
|
||||
rejectUnauthorized: true,
|
||||
}, app);
|
||||
httpsServer.listen(httpsPort, () => {
|
||||
console.log(`Listening at https://localhost:${httpsPort}`);
|
||||
console.log(`Listening at wss://localhost:${httpsPort}`);
|
||||
});
|
||||
|
||||
startWebSocketServer(server, httpsServer);
|
||||
});
|
||||
|
||||
83
packages/insomnia-smoke-test/server/websocket.ts
Normal file
83
packages/insomnia-smoke-test/server/websocket.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { IncomingMessage, Server } from 'http';
|
||||
import { Socket } from 'net';
|
||||
import { WebSocket, WebSocketServer } from 'ws';
|
||||
|
||||
/**
|
||||
* Starts an echo WebSocket server that receives messages from a client and echoes them back.
|
||||
*/
|
||||
export function startWebSocketServer(server: Server, httpsServer: Server) {
|
||||
const wsServer = new WebSocketServer({ noServer: true });
|
||||
const wssServer = new WebSocketServer({ noServer: true });
|
||||
|
||||
server.on('upgrade', (request, socket, head) => {
|
||||
upgrade(wsServer, request, socket, head);
|
||||
});
|
||||
httpsServer.on('upgrade', (request, socket, head) => {
|
||||
upgrade(wssServer, request, socket, head);
|
||||
});
|
||||
wsServer.on('connection', handleConnection);
|
||||
wssServer.on('connection', handleConnection);
|
||||
}
|
||||
|
||||
const handleConnection = (ws: WebSocket, req: IncomingMessage) => {
|
||||
console.log('WebSocket connection was opened');
|
||||
console.log('Upgrade headers:', req.headers);
|
||||
|
||||
ws.on('message', (message, isBinary) => {
|
||||
if (isBinary) {
|
||||
ws.send(message);
|
||||
return;
|
||||
}
|
||||
if (message.toString() === 'close') {
|
||||
ws.close(1003, 'Invalid message type');
|
||||
}
|
||||
ws.send(message.toString());
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
console.log('WebSocket connection was closed');
|
||||
});
|
||||
};
|
||||
const redirectOnSuccess = (socket: Socket) => {
|
||||
socket.end(`HTTP/1.1 302 Found
|
||||
Location: ws://localhost:4010
|
||||
|
||||
`);
|
||||
return;
|
||||
};
|
||||
const return401withBody = (socket: Socket) => {
|
||||
socket.end(`HTTP/1.1 401 Unauthorized
|
||||
|
||||
<!doctype html>
|
||||
<html>
|
||||
<body>
|
||||
<div>
|
||||
<h1>401 Unauthorized</h1>
|
||||
</div>
|
||||
</body>
|
||||
</html>`);
|
||||
return;
|
||||
};
|
||||
const upgrade = (wss: WebSocketServer, request: IncomingMessage, socket: Socket, head: Buffer) => {
|
||||
if (request.url === '/redirect') {
|
||||
return redirectOnSuccess(socket);
|
||||
}
|
||||
if (request.url === '/bearer') {
|
||||
if (request.headers.authorization !== 'Bearer insomnia-cool-token-!!!1112113243111') {
|
||||
return401withBody(socket);
|
||||
return;
|
||||
}
|
||||
return redirectOnSuccess(socket);
|
||||
}
|
||||
if (request.url === '/basic-auth') {
|
||||
// login with user:password
|
||||
if (request.headers.authorization !== 'Basic dXNlcjpwYXNzd29yZA==') {
|
||||
return401withBody(socket);
|
||||
return;
|
||||
}
|
||||
return redirectOnSuccess(socket);
|
||||
}
|
||||
wss.handleUpgrade(request, socket, head, ws => {
|
||||
wss.emit('connection', ws, request);
|
||||
});
|
||||
};
|
||||
52
packages/insomnia-smoke-test/tests/websocket.test.ts
Normal file
52
packages/insomnia-smoke-test/tests/websocket.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { loadFixture } from '../playwright/paths';
|
||||
import { test } from '../playwright/test';
|
||||
|
||||
test('can make websocket connection', async ({ app, page }) => {
|
||||
test.slow(process.platform === 'darwin' || process.platform === 'win32', 'Slow app start on these platforms');
|
||||
const statusTag = page.locator('[data-testid="response-status-tag"]:visible');
|
||||
const responseBody = page.locator('[data-testid="response-pane"] >> [data-testid="CodeEditor"]:visible', {
|
||||
has: page.locator('.CodeMirror-activeline'),
|
||||
});
|
||||
|
||||
await page.click('[data-testid="project"]');
|
||||
await page.click('text=Create');
|
||||
|
||||
const text = await loadFixture('websockets.yaml');
|
||||
await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text);
|
||||
|
||||
await page.click('button:has-text("Clipboard")');
|
||||
await page.click('text=CollectionWebSocketsjust now');
|
||||
|
||||
await page.click('button:has-text("localhost:4010")');
|
||||
await page.click('text=Connect');
|
||||
await expect(statusTag).toContainText('101 Switching Protocols');
|
||||
await page.click('[data-testid="response-pane"] >> [role="tab"]:has-text("Timeline")');
|
||||
await expect(responseBody).toContainText('WebSocket connection established');
|
||||
|
||||
await page.click('text=Disconnect');
|
||||
await expect(responseBody).toContainText('Closing connection with code 1005');
|
||||
|
||||
// Can connect with Basic Auth
|
||||
await page.click('button:has-text("basic-auth")');
|
||||
await page.click('text=Connect');
|
||||
await expect(statusTag).toContainText('101 Switching Protocols');
|
||||
await page.click('[data-testid="response-pane"] >> [role="tab"]:has-text("Timeline")');
|
||||
await expect(responseBody).toContainText('> authorization: Basic dXNlcjpwYXNzd29yZA==');
|
||||
|
||||
// Can connect with Bearer Auth
|
||||
await page.click('button:has-text("bearer")');
|
||||
await page.click('text=Connect');
|
||||
await expect(statusTag).toContainText('101 Switching Protocols');
|
||||
await page.click('[data-testid="response-pane"] >> [role="tab"]:has-text("Timeline")');
|
||||
await expect(responseBody).toContainText('> authorization: Bearer insomnia-cool-token-!!!1112113243111');
|
||||
|
||||
// Can handle redirects
|
||||
await page.click('button:has-text("redirect")');
|
||||
await page.click('text=Connect');
|
||||
await expect(statusTag).toContainText('101 Switching Protocols');
|
||||
await page.click('[data-testid="response-pane"] >> [role="tab"]:has-text("Timeline")');
|
||||
await expect(responseBody).toContainText('WebSocket connection established');
|
||||
|
||||
});
|
||||
53
packages/insomnia/package-lock.json
generated
53
packages/insomnia/package-lock.json
generated
@@ -98,6 +98,7 @@
|
||||
"@types/tough-cookie": "^2.3.7",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@types/vkbeautify": "^0.99.2",
|
||||
"@types/ws": "^8.5.3",
|
||||
"@types/yaml": "^1.9.7",
|
||||
"@vitejs/plugin-react": "^1.2.0",
|
||||
"buffer": "^6.0.3",
|
||||
@@ -140,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",
|
||||
@@ -151,7 +153,8 @@
|
||||
"typescript": "^4.5.5",
|
||||
"vite": "^2.8.6",
|
||||
"vite-plugin-commonjs-externals": "^0.1.1",
|
||||
"vkbeautify": "^0.99.1"
|
||||
"vkbeautify": "^0.99.1",
|
||||
"ws": "^8.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
@@ -3162,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",
|
||||
@@ -17604,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",
|
||||
@@ -20379,9 +20403,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz",
|
||||
"integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==",
|
||||
"version": "8.8.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.8.1.tgz",
|
||||
"integrity": "sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
@@ -22979,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",
|
||||
@@ -34248,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",
|
||||
@@ -36425,9 +36464,9 @@
|
||||
}
|
||||
},
|
||||
"ws": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz",
|
||||
"integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==",
|
||||
"version": "8.8.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.8.1.tgz",
|
||||
"integrity": "sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"@types/tough-cookie": "^2.3.7",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@types/vkbeautify": "^0.99.2",
|
||||
"@types/ws": "^8.5.3",
|
||||
"@types/yaml": "^1.9.7",
|
||||
"@vitejs/plugin-react": "^1.2.0",
|
||||
"buffer": "^6.0.3",
|
||||
@@ -196,6 +197,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",
|
||||
@@ -207,7 +209,8 @@
|
||||
"typescript": "^4.5.5",
|
||||
"vite": "^2.8.6",
|
||||
"vite-plugin-commonjs-externals": "^0.1.1",
|
||||
"vkbeautify": "^0.99.1"
|
||||
"vkbeautify": "^0.99.1",
|
||||
"ws": "^8.8.1"
|
||||
},
|
||||
"dev": {
|
||||
"dev-server-port": 3334
|
||||
|
||||
@@ -12,6 +12,7 @@ export class MockCodeEditor extends PureComponent<any> {
|
||||
render() {
|
||||
const { id, onChange, placeholder, defaultValue } = this.props;
|
||||
return <textarea
|
||||
data-testid="CodeEditor"
|
||||
ref={this.ref}
|
||||
id={id}
|
||||
onChange={event => onChange(event.currentTarget.value)}
|
||||
|
||||
@@ -25,7 +25,20 @@ export const isDevelopment = () => getAppEnvironment() === 'development';
|
||||
export const getSegmentWriteKey = () => appConfig.segmentWriteKeys[(isDevelopment() || env.PLAYWRIGHT) ? 'development' : 'production'];
|
||||
export const getSentryDsn = () => appConfig.sentryDsn;
|
||||
export const getAppBuildDate = () => new Date(process.env.BUILD_DATE ?? '').toLocaleDateString();
|
||||
|
||||
export type AuthType =
|
||||
| 'none'
|
||||
| 'oauth2'
|
||||
| 'oauth1'
|
||||
| 'basic'
|
||||
| 'digest'
|
||||
| 'bearer'
|
||||
| 'ntlm'
|
||||
| 'hawk'
|
||||
| 'iam'
|
||||
| 'netrc'
|
||||
| 'asap'
|
||||
| 'sha256'
|
||||
| 'sha1';
|
||||
export const getBrowserUserAgent = () => encodeURIComponent(
|
||||
String(window.navigator.userAgent)
|
||||
.replace(new RegExp(`${getAppId()}\\/\\d+\\.\\d+\\.\\d+ `), '')
|
||||
@@ -548,6 +561,8 @@ export const WORKSPACE_ID_KEY = '__WORKSPACE_ID__';
|
||||
export const BASE_ENVIRONMENT_ID_KEY = '__BASE_ENVIRONMENT_ID__';
|
||||
export const EXPORT_TYPE_REQUEST = 'request';
|
||||
export const EXPORT_TYPE_GRPC_REQUEST = 'grpc_request';
|
||||
export const EXPORT_TYPE_WEBSOCKET_REQUEST = 'websocket_request';
|
||||
export const EXPORT_TYPE_WEBSOCKET_PAYLOAD = 'websocket_payload';
|
||||
export const EXPORT_TYPE_REQUEST_GROUP = 'request_group';
|
||||
export const EXPORT_TYPE_UNIT_TEST_SUITE = 'unit_test_suite';
|
||||
export const EXPORT_TYPE_UNIT_TEST = 'unit_test';
|
||||
|
||||
@@ -260,7 +260,6 @@ export const database = {
|
||||
console.log(`[db] Dropped ${changes.length} changes.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Notify local listeners too
|
||||
for (const fn of changeListeners) {
|
||||
await fn(changes);
|
||||
@@ -685,9 +684,9 @@ function getDBFilePath(modelType: string) {
|
||||
let bufferingChanges = false;
|
||||
let bufferChangesId = 1;
|
||||
|
||||
export type ChangeBufferEvent = [
|
||||
export type ChangeBufferEvent<T extends BaseModel = BaseModel> = [
|
||||
event: string,
|
||||
doc: BaseModel,
|
||||
doc: T,
|
||||
fromSync: boolean
|
||||
];
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ import { isRequest } from '../models/request';
|
||||
import { isRequestGroup } from '../models/request-group';
|
||||
import { isUnitTest } from '../models/unit-test';
|
||||
import { isUnitTestSuite } from '../models/unit-test-suite';
|
||||
import { isWebSocketPayload } from '../models/websocket-payload';
|
||||
import { isWebSocketRequest } from '../models/websocket-request';
|
||||
import { isWorkspace, Workspace } from '../models/workspace';
|
||||
import { resetKeys } from '../sync/ignore-keys';
|
||||
import { SegmentEvent, trackSegmentEvent } from './analytics';
|
||||
@@ -28,6 +30,8 @@ import {
|
||||
EXPORT_TYPE_REQUEST_GROUP,
|
||||
EXPORT_TYPE_UNIT_TEST,
|
||||
EXPORT_TYPE_UNIT_TEST_SUITE,
|
||||
EXPORT_TYPE_WEBSOCKET_PAYLOAD,
|
||||
EXPORT_TYPE_WEBSOCKET_REQUEST,
|
||||
EXPORT_TYPE_WORKSPACE,
|
||||
getAppVersion,
|
||||
} from './constants';
|
||||
@@ -174,7 +178,8 @@ export async function exportRequestsData(
|
||||
isUnitTestSuite(d) ||
|
||||
isUnitTest(d) ||
|
||||
isProtoFile(d) ||
|
||||
isProtoDirectory(d)
|
||||
isProtoDirectory(d) ||
|
||||
isWebSocketPayload(d)
|
||||
);
|
||||
});
|
||||
docs.push(...descendants);
|
||||
@@ -188,6 +193,8 @@ export async function exportRequestsData(
|
||||
isUnitTestSuite(d) ||
|
||||
isUnitTest(d) ||
|
||||
isRequest(d) ||
|
||||
isWebSocketPayload(d) ||
|
||||
isWebSocketRequest(d) ||
|
||||
isGrpcRequest(d) ||
|
||||
isRequestGroup(d) ||
|
||||
isProtoFile(d) ||
|
||||
@@ -231,6 +238,12 @@ export async function exportRequestsData(
|
||||
} else if (isGrpcRequest(d)) {
|
||||
// @ts-expect-error -- TSCONVERSION maybe this needs to be added to the upstream type?
|
||||
d._type = EXPORT_TYPE_GRPC_REQUEST;
|
||||
} else if (isWebSocketPayload(d)) {
|
||||
// @ts-expect-error -- TSCONVERSION maybe this needs to be added to the upstream type?
|
||||
d._type = EXPORT_TYPE_WEBSOCKET_PAYLOAD;
|
||||
} else if (isWebSocketRequest(d)) {
|
||||
// @ts-expect-error -- TSCONVERSION maybe this needs to be added to the upstream type?
|
||||
d._type = EXPORT_TYPE_WEBSOCKET_REQUEST;
|
||||
} else if (isProtoFile(d)) {
|
||||
// @ts-expect-error -- TSCONVERSION maybe this needs to be added to the upstream type?
|
||||
d._type = EXPORT_TYPE_PROTO_FILE;
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { GrpcRequest, GrpcRequestBody } from '../models/grpc-request';
|
||||
import { isProject, Project } from '../models/project';
|
||||
import type { Request } from '../models/request';
|
||||
import { isRequestGroup, RequestGroup } from '../models/request-group';
|
||||
import { WebSocketRequest } from '../models/websocket-request';
|
||||
import { isWorkspace, Workspace } from '../models/workspace';
|
||||
import * as templating from '../templating';
|
||||
import * as templatingUtils from '../templating/utils';
|
||||
@@ -289,7 +290,7 @@ export async function render<T>(
|
||||
return next<T>(newObj, name, true);
|
||||
}
|
||||
|
||||
interface RenderRequest<T extends Request | GrpcRequest> {
|
||||
interface RenderRequest<T extends Request | GrpcRequest | WebSocketRequest> {
|
||||
request: T;
|
||||
}
|
||||
|
||||
@@ -299,7 +300,7 @@ interface BaseRenderContextOptions {
|
||||
extraInfo?: ExtraRenderInfo;
|
||||
}
|
||||
|
||||
interface RenderContextOptions extends BaseRenderContextOptions, Partial<RenderRequest<Request | GrpcRequest>> {
|
||||
interface RenderContextOptions extends BaseRenderContextOptions, Partial<RenderRequest<Request | GrpcRequest | WebSocketRequest>> {
|
||||
ancestors?: RenderContextAncestor[];
|
||||
}
|
||||
export async function getRenderContext(
|
||||
@@ -557,11 +558,12 @@ function _getOrderedEnvironmentKeys(finalRenderContext: Record<string, any>): st
|
||||
});
|
||||
}
|
||||
|
||||
type RenderContextAncestor = Request | GrpcRequest | RequestGroup | Workspace | Project;
|
||||
export async function getRenderContextAncestors(base?: Request | GrpcRequest | Workspace): Promise<RenderContextAncestor[]> {
|
||||
type RenderContextAncestor = Request | GrpcRequest | WebSocketRequest | RequestGroup | Workspace | Project;
|
||||
export async function getRenderContextAncestors(base?: Request | GrpcRequest | WebSocketRequest | Workspace): Promise<RenderContextAncestor[]> {
|
||||
return await db.withAncestors<RenderContextAncestor>(base || null, [
|
||||
models.request.type,
|
||||
models.grpcRequest.type,
|
||||
models.webSocketRequest.type,
|
||||
models.requestGroup.type,
|
||||
models.workspace.type,
|
||||
models.project.type,
|
||||
|
||||
@@ -13,6 +13,7 @@ import { validateInsomniaConfig } from './common/validate-insomnia-config';
|
||||
import { registerElectronHandlers } from './main/ipc/electron';
|
||||
import { registergRPCHandlers } from './main/ipc/grpc';
|
||||
import { registerMainHandlers } from './main/ipc/main';
|
||||
import { registerWebSocketHandlers } from './main/network/websocket';
|
||||
import { initializeSentry, sentryWatchAnalyticsEnabled } from './main/sentry';
|
||||
import { checkIfRestartNeeded } from './main/squirrel-startup';
|
||||
import * as updates from './main/updates';
|
||||
@@ -70,6 +71,7 @@ app.on('ready', async () => {
|
||||
registerElectronHandlers();
|
||||
registerMainHandlers();
|
||||
registergRPCHandlers();
|
||||
registerWebSocketHandlers();
|
||||
|
||||
disableSpellcheckerDownload();
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { writeFile } from 'fs/promises';
|
||||
import { authorizeUserInWindow } from '../../network/o-auth-2/misc';
|
||||
import installPlugin from '../install-plugin';
|
||||
import { cancelCurlRequest, curlRequest } from '../network/libcurl-promise';
|
||||
import { WebSocketBridgeAPI } from '../network/websocket';
|
||||
|
||||
export interface MainBridgeAPI {
|
||||
restart: () => void;
|
||||
@@ -13,7 +14,8 @@ export interface MainBridgeAPI {
|
||||
writeFile: (options: { path: string; content: string }) => Promise<string>;
|
||||
cancelCurlRequest: typeof cancelCurlRequest;
|
||||
curlRequest: typeof curlRequest;
|
||||
on: (channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void) => Function;
|
||||
on: (channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void) => () => void;
|
||||
webSocket: WebSocketBridgeAPI;
|
||||
}
|
||||
export function registerMainHandlers() {
|
||||
ipcMain.handle('authorizeUserInWindow', (_, options: Parameters<typeof authorizeUserInWindow>[0]) => {
|
||||
|
||||
429
packages/insomnia/src/main/network/websocket.ts
Normal file
429
packages/insomnia/src/main/network/websocket.ts
Normal file
@@ -0,0 +1,429 @@
|
||||
import electron, { ipcMain } from 'electron';
|
||||
import fs from 'fs';
|
||||
import { IncomingMessage } from 'http';
|
||||
import { setDefaultProtocol } from 'insomnia-url';
|
||||
import mkdirp from 'mkdirp';
|
||||
import path from 'path';
|
||||
import { KeyObject, PxfObject } from 'tls';
|
||||
import { v4 as uuidV4 } from 'uuid';
|
||||
import {
|
||||
CloseEvent,
|
||||
ErrorEvent,
|
||||
Event,
|
||||
MessageEvent,
|
||||
WebSocket,
|
||||
} from 'ws';
|
||||
|
||||
import { AUTH_BASIC, AUTH_BEARER } from '../../common/constants';
|
||||
import { generateId } from '../../common/misc';
|
||||
import { webSocketRequest } from '../../models';
|
||||
import * as models from '../../models';
|
||||
import { Environment } from '../../models/environment';
|
||||
import { RequestAuthentication, RequestHeader } from '../../models/request';
|
||||
import { BaseWebSocketRequest } from '../../models/websocket-request';
|
||||
import type { WebSocketResponse } from '../../models/websocket-response';
|
||||
import { getBasicAuthHeader } from '../../network/basic-auth/get-header';
|
||||
import { getBearerAuthHeader } from '../../network/bearer-auth/get-header';
|
||||
import { urlMatchesCertHost } from '../../network/url-matches-cert-host';
|
||||
|
||||
export interface WebSocketConnection extends WebSocket {
|
||||
_id: string;
|
||||
requestId: string;
|
||||
}
|
||||
|
||||
export type WebSocketOpenEvent = Omit<Event, 'target'> & {
|
||||
_id: string;
|
||||
requestId: string;
|
||||
type: 'open';
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
export type WebSocketMessageEvent = Omit<MessageEvent, 'target'> & {
|
||||
_id: string;
|
||||
requestId: string;
|
||||
direction: 'OUTGOING' | 'INCOMING';
|
||||
type: 'message';
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
export type WebSocketErrorEvent = Omit<ErrorEvent, 'target'> & {
|
||||
_id: string;
|
||||
requestId: string;
|
||||
type: 'error';
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
export type WebSocketCloseEvent = Omit<CloseEvent, 'target'> & {
|
||||
_id: string;
|
||||
requestId: string;
|
||||
type: 'close';
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
export type WebSocketEvent =
|
||||
| WebSocketOpenEvent
|
||||
| WebSocketMessageEvent
|
||||
| WebSocketErrorEvent
|
||||
| WebSocketCloseEvent;
|
||||
|
||||
export type WebSocketEventLog = WebSocketEvent[];
|
||||
|
||||
const WebSocketConnections = new Map<string, WebSocket>();
|
||||
const eventLogFileStreams = new Map<string, fs.WriteStream>();
|
||||
const timelineFileStreams = new Map<string, fs.WriteStream>();
|
||||
|
||||
const parseResponseAndBuildTimeline = (url: string, incomingMessage: IncomingMessage, clientRequestHeaders: string) => {
|
||||
const statusMessage = incomingMessage.statusMessage || '';
|
||||
const statusCode = incomingMessage.statusCode || 0;
|
||||
const httpVersion = incomingMessage.httpVersion;
|
||||
const responseHeaders = Object.entries(incomingMessage.headers).map(([name, value]) => ({ name, value: value?.toString() || '' }));
|
||||
const headersIn = responseHeaders.map(({ name, value }) => `${name}: ${value}`).join('\n');
|
||||
const timeline = [
|
||||
{ value: `Preparing request to ${url}`, name: 'Text', timestamp: Date.now() },
|
||||
{ value: `Current time is ${new Date().toISOString()}`, name: 'Text', timestamp: Date.now() },
|
||||
{ value: 'Using HTTP 1.1', name: 'Text', timestamp: Date.now() },
|
||||
{ value: clientRequestHeaders, name: 'HeaderOut', timestamp: Date.now() },
|
||||
{ value: `HTTP/${httpVersion} ${statusCode} ${statusMessage}`, name: 'HeaderIn', timestamp: Date.now() },
|
||||
{ value: headersIn, name: 'HeaderIn', timestamp: Date.now() },
|
||||
];
|
||||
return { timeline, responseHeaders, statusCode, statusMessage, httpVersion };
|
||||
};
|
||||
|
||||
const createWebSocketConnection = async (
|
||||
event: Electron.IpcMainInvokeEvent,
|
||||
options: {
|
||||
requestId: string;
|
||||
workspaceId: string;
|
||||
url: string;
|
||||
headers: RequestHeader[];
|
||||
authentication: RequestAuthentication;
|
||||
}
|
||||
): Promise<void> => {
|
||||
const existingConnection = WebSocketConnections.get(options.requestId);
|
||||
|
||||
if (existingConnection) {
|
||||
console.warn('Connection still open to ' + existingConnection.url);
|
||||
return;
|
||||
}
|
||||
const request = await webSocketRequest.getById(options.requestId);
|
||||
const responseId = generateId('res');
|
||||
if (!request) {
|
||||
return;
|
||||
}
|
||||
|
||||
const responsesDir = path.join(process.env['INSOMNIA_DATA_PATH'] || electron.app.getPath('userData'), 'responses');
|
||||
mkdirp.sync(responsesDir);
|
||||
const responseBodyPath = path.join(responsesDir, uuidV4() + '.response');
|
||||
eventLogFileStreams.set(options.requestId, fs.createWriteStream(responseBodyPath));
|
||||
const timelinePath = path.join(responsesDir, responseId + '.timeline');
|
||||
timelineFileStreams.set(options.requestId, fs.createWriteStream(timelinePath));
|
||||
|
||||
const workspaceMeta = await models.workspaceMeta.getOrCreateByParentId(options.workspaceId);
|
||||
const environmentId: string = workspaceMeta.activeEnvironmentId || 'n/a';
|
||||
const environment: Environment | null = await models.environment.getById(environmentId || 'n/a');
|
||||
const responseEnvironmentId = environment ? environment._id : null;
|
||||
|
||||
try {
|
||||
const readyStateChannel = `webSocket.${request._id}.readyState`;
|
||||
|
||||
const reduceArrayToLowerCaseKeyedDictionary = (acc: { [key: string]: string }, { name, value }: BaseWebSocketRequest['headers'][0]) =>
|
||||
({ ...acc, [name.toLowerCase() || '']: value || '' });
|
||||
const headers = options.headers;
|
||||
if (!options.authentication.disabled) {
|
||||
if (options.authentication.type === AUTH_BASIC) {
|
||||
const { username, password, useISO88591 } = options.authentication;
|
||||
const encoding = useISO88591 ? 'latin1' : 'utf8';
|
||||
headers.push(getBasicAuthHeader(username, password, encoding));
|
||||
}
|
||||
if (options.authentication.type === AUTH_BEARER) {
|
||||
const { token, prefix } = options.authentication;
|
||||
headers.push(getBearerAuthHeader(token, prefix));
|
||||
}
|
||||
}
|
||||
|
||||
const lowerCasedEnabledHeaders = headers
|
||||
.filter(({ name, disabled }) => Boolean(name) && !disabled)
|
||||
.reduce(reduceArrayToLowerCaseKeyedDictionary, {});
|
||||
const settings = await models.settings.getOrCreate();
|
||||
const start = performance.now();
|
||||
|
||||
const clientCertificates = await models.clientCertificate.findByParentId(options.workspaceId);
|
||||
const filteredClientCertificates = clientCertificates.filter(c => !c.disabled && urlMatchesCertHost(setDefaultProtocol(c.host, 'wss:'), options.url));
|
||||
const pemCertificates: string[] = [];
|
||||
const pemCertificateKeys: KeyObject[] = [];
|
||||
const pfxCertificates: PxfObject[] = [];
|
||||
|
||||
filteredClientCertificates.forEach(clientCertificate => {
|
||||
const { passphrase, cert, key, pfx } = clientCertificate;
|
||||
|
||||
if (cert) {
|
||||
timelineFileStreams.get(options.requestId)?.write(JSON.stringify({ value: `Adding SSL PEM certificate: ${cert}`, name: 'Text', timestamp: Date.now() }) + '\n');
|
||||
pemCertificates.push(fs.readFileSync(cert, 'utf-8'));
|
||||
}
|
||||
|
||||
if (key) {
|
||||
timelineFileStreams.get(options.requestId)?.write(JSON.stringify({ value: `Adding SSL KEY certificate: ${key}`, name: 'Text', timestamp: Date.now() }) + '\n');
|
||||
pemCertificateKeys.push({ pem: fs.readFileSync(key, 'utf-8'), passphrase: passphrase ?? undefined });
|
||||
}
|
||||
|
||||
if (pfx) {
|
||||
timelineFileStreams.get(options.requestId)?.write(JSON.stringify({ value: `Adding SSL P12 certificate: ${pfx}`, name: 'Text', timestamp: Date.now() }) + '\n');
|
||||
pfxCertificates.push({ buf: fs.readFileSync(pfx, 'utf-8'), passphrase: passphrase ?? undefined });
|
||||
}
|
||||
});
|
||||
|
||||
const ws = new WebSocket(options.url, {
|
||||
headers: lowerCasedEnabledHeaders,
|
||||
cert: pemCertificates,
|
||||
key: pemCertificateKeys,
|
||||
pfx: pfxCertificates,
|
||||
rejectUnauthorized: settings.validateSSL,
|
||||
followRedirects: true,
|
||||
});
|
||||
WebSocketConnections.set(options.requestId, ws);
|
||||
|
||||
ws.on('upgrade', async incomingMessage => {
|
||||
// @ts-expect-error -- private property
|
||||
const internalRequestHeader = ws._req._header;
|
||||
const { timeline, responseHeaders, statusCode, statusMessage, httpVersion } = parseResponseAndBuildTimeline(options.url, incomingMessage, internalRequestHeader);
|
||||
timeline.map(t => timelineFileStreams.get(options.requestId)?.write(JSON.stringify(t) + '\n'));
|
||||
const responsePatch: Partial<WebSocketResponse> = {
|
||||
_id: responseId,
|
||||
parentId: request._id,
|
||||
environmentId: responseEnvironmentId,
|
||||
headers: responseHeaders,
|
||||
url: options.url,
|
||||
statusCode,
|
||||
statusMessage,
|
||||
httpVersion,
|
||||
elapsedTime: performance.now() - start,
|
||||
timelinePath,
|
||||
eventLogPath: responseBodyPath,
|
||||
};
|
||||
const settings = await models.settings.getOrCreate();
|
||||
models.webSocketResponse.create(responsePatch, settings.maxHistoryResponses);
|
||||
models.requestMeta.updateOrCreateByParentId(request._id, { activeResponseId: null });
|
||||
});
|
||||
ws.on('unexpected-response', async (clientRequest, incomingMessage) => {
|
||||
incomingMessage.on('data', chunk => {
|
||||
timelineFileStreams.get(options.requestId)?.write(JSON.stringify({ value: chunk.toString(), name: 'DataOut', timestamp: Date.now() }) + '\n');
|
||||
});
|
||||
// @ts-expect-error -- private property
|
||||
const internalRequestHeader = clientRequest._header;
|
||||
const { timeline, responseHeaders, statusCode, statusMessage, httpVersion } = parseResponseAndBuildTimeline(options.url, incomingMessage, internalRequestHeader);
|
||||
timeline.map(t => timelineFileStreams.get(options.requestId)?.write(JSON.stringify(t) + '\n'));
|
||||
const responsePatch: Partial<WebSocketResponse> = {
|
||||
_id: responseId,
|
||||
parentId: request._id,
|
||||
environmentId: responseEnvironmentId,
|
||||
headers: responseHeaders,
|
||||
url: options.url,
|
||||
statusCode,
|
||||
statusMessage,
|
||||
httpVersion,
|
||||
elapsedTime: performance.now() - start,
|
||||
timelinePath,
|
||||
eventLogPath: responseBodyPath,
|
||||
};
|
||||
const settings = await models.settings.getOrCreate();
|
||||
models.webSocketResponse.create(responsePatch, settings.maxHistoryResponses);
|
||||
models.requestMeta.updateOrCreateByParentId(request._id, { activeResponseId: null });
|
||||
deleteRequestMaps(request._id, `Unexpected response ${incomingMessage.statusCode}`);
|
||||
});
|
||||
|
||||
ws.addEventListener('open', () => {
|
||||
const openEvent: WebSocketOpenEvent = {
|
||||
_id: uuidV4(),
|
||||
requestId: options.requestId,
|
||||
type: 'open',
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
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');
|
||||
event.sender.send(readyStateChannel, ws.readyState);
|
||||
});
|
||||
|
||||
ws.addEventListener('message', ({ data }: MessageEvent) => {
|
||||
const messageEvent: WebSocketMessageEvent = {
|
||||
_id: uuidV4(),
|
||||
requestId: options.requestId,
|
||||
data,
|
||||
type: 'message',
|
||||
direction: 'INCOMING',
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
eventLogFileStreams.get(options.requestId)?.write(JSON.stringify(messageEvent) + '\n');
|
||||
});
|
||||
|
||||
ws.addEventListener('close', ({ code, reason, wasClean }) => {
|
||||
const closeEvent: WebSocketCloseEvent = {
|
||||
_id: uuidV4(),
|
||||
requestId: options.requestId,
|
||||
code,
|
||||
reason,
|
||||
type: 'close',
|
||||
wasClean,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
const message = `Closing connection with code ${code}`;
|
||||
deleteRequestMaps(request._id, message, closeEvent);
|
||||
event.sender.send(readyStateChannel, ws.readyState);
|
||||
});
|
||||
|
||||
ws.addEventListener('error', async ({ error, message }: ErrorEvent) => {
|
||||
console.error(error);
|
||||
|
||||
const errorEvent: WebSocketErrorEvent = {
|
||||
_id: uuidV4(),
|
||||
requestId: options.requestId,
|
||||
message,
|
||||
type: 'error',
|
||||
error,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
deleteRequestMaps(request._id, message, errorEvent);
|
||||
event.sender.send(readyStateChannel, ws.readyState);
|
||||
createErrorResponse(responseId, request._id, responseEnvironmentId, timelinePath, message || 'Something went wrong');
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('unhandled error:', e);
|
||||
|
||||
deleteRequestMaps(request._id, e.message || 'Something went wrong');
|
||||
createErrorResponse(responseId, request._id, responseEnvironmentId, timelinePath, e.message || 'Something went wrong');
|
||||
}
|
||||
};
|
||||
|
||||
const createErrorResponse = async (responseId: string, requestId: string, environmentId: string | null, timelinePath: string, message: string) => {
|
||||
const settings = await models.settings.getOrCreate();
|
||||
const responsePatch = {
|
||||
_id: responseId,
|
||||
parentId: requestId,
|
||||
environmentId: environmentId,
|
||||
timelinePath,
|
||||
statusMessage: 'Error',
|
||||
error: message,
|
||||
};
|
||||
models.webSocketResponse.create(responsePatch, settings.maxHistoryResponses);
|
||||
models.requestMeta.updateOrCreateByParentId(requestId, { activeResponseId: null });
|
||||
};
|
||||
|
||||
const deleteRequestMaps = async (requestId: string, message: string, event?: WebSocketCloseEvent | WebSocketErrorEvent) => {
|
||||
if (event) {
|
||||
eventLogFileStreams.get(requestId)?.write(JSON.stringify(event) + '\n');
|
||||
}
|
||||
eventLogFileStreams.get(requestId)?.end();
|
||||
eventLogFileStreams.delete(requestId);
|
||||
timelineFileStreams.get(requestId)?.write(JSON.stringify({ value: message, name: 'Text', timestamp: Date.now() }) + '\n');
|
||||
timelineFileStreams.get(requestId)?.end();
|
||||
timelineFileStreams.delete(requestId);
|
||||
WebSocketConnections.delete(requestId);
|
||||
};
|
||||
|
||||
const getWebSocketReadyState = async (
|
||||
options: { requestId: string }
|
||||
): Promise<WebSocketConnection['readyState']> => {
|
||||
return WebSocketConnections.get(options.requestId)?.readyState ?? 0;
|
||||
};
|
||||
|
||||
const sendWebSocketEvent = async (
|
||||
options: { message: string; requestId: string }
|
||||
): 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
|
||||
// and update it here based on the error. e.g. status = 'sending' | 'sent' | 'error'
|
||||
if (error) {
|
||||
console.error(error);
|
||||
} else {
|
||||
console.log('Message sent');
|
||||
}
|
||||
});
|
||||
|
||||
const lastMessage: WebSocketMessageEvent = {
|
||||
_id: uuidV4(),
|
||||
requestId: options.requestId,
|
||||
data: options.message,
|
||||
direction: 'OUTGOING',
|
||||
type: 'message',
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
eventLogFileStreams.get(options.requestId)?.write(JSON.stringify(lastMessage) + '\n');
|
||||
const response = await models.webSocketResponse.getLatestByParentId(options.requestId);
|
||||
if (!response) {
|
||||
console.error('something went wrong');
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const closeWebSocketConnection = async (
|
||||
options: { requestId: string }
|
||||
): Promise<void> => {
|
||||
const ws = WebSocketConnections.get(options.requestId);
|
||||
if (!ws) {
|
||||
return;
|
||||
}
|
||||
ws.close();
|
||||
};
|
||||
|
||||
const closeAllWebSocketConnections = (): void => {
|
||||
WebSocketConnections.forEach(ws => ws.close());
|
||||
};
|
||||
|
||||
const findMany = async (
|
||||
options: { responseId: string }
|
||||
): Promise<WebSocketEvent[]> => {
|
||||
const response = await models.webSocketResponse.getById(options.responseId);
|
||||
if (!response || !response.eventLogPath) {
|
||||
return [];
|
||||
}
|
||||
const body = await fs.promises.readFile(response.eventLogPath);
|
||||
return body.toString().split('\n').filter(e => e?.trim())
|
||||
// Parse the message
|
||||
.map(e => JSON.parse(e))
|
||||
// Reverse the list of messages so that we get the latest message first
|
||||
.reverse() || [];
|
||||
};
|
||||
|
||||
export interface WebSocketBridgeAPI {
|
||||
create: (options: {
|
||||
requestId: string;
|
||||
workspaceId: string;
|
||||
url: string;
|
||||
headers: RequestHeader[];
|
||||
authentication: RequestAuthentication;
|
||||
}) => void;
|
||||
close: typeof closeWebSocketConnection;
|
||||
closeAll: typeof closeAllWebSocketConnections;
|
||||
readyState: {
|
||||
getCurrent: typeof getWebSocketReadyState;
|
||||
};
|
||||
event: {
|
||||
findMany: typeof findMany;
|
||||
send: (options: { requestId: string; message: string }) => void;
|
||||
};
|
||||
}
|
||||
export const registerWebSocketHandlers = () => {
|
||||
ipcMain.handle('webSocket.create', createWebSocketConnection);
|
||||
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.closeAll', closeAllWebSocketConnections);
|
||||
ipcMain.handle('webSocket.readyState', (_, options: Parameters<typeof getWebSocketReadyState>[0]) => getWebSocketReadyState(options));
|
||||
ipcMain.handle('webSocket.event.findMany', (_, options: Parameters<typeof findMany>[0]) => findMany(options));
|
||||
};
|
||||
|
||||
electron.app.on('window-all-closed', () => {
|
||||
WebSocketConnections.forEach(ws => {
|
||||
ws.close();
|
||||
});
|
||||
});
|
||||
@@ -1,31 +1,54 @@
|
||||
import { GrpcRequest, isGrpcRequest, isGrpcRequestId } from '../grpc-request';
|
||||
import * as models from '../index';
|
||||
import { Request } from '../request';
|
||||
import { isWebSocketRequest, isWebSocketRequestId, WebSocketRequest } from '../websocket-request';
|
||||
|
||||
export function getById(requestId: string): Promise<Request | GrpcRequest | null> {
|
||||
return isGrpcRequestId(requestId)
|
||||
? models.grpcRequest.getById(requestId)
|
||||
: models.request.getById(requestId);
|
||||
export function getById(requestId: string): Promise<Request | GrpcRequest | WebSocketRequest | null> {
|
||||
if (isGrpcRequestId(requestId)) {
|
||||
return models.grpcRequest.getById(requestId);
|
||||
}
|
||||
if (isWebSocketRequestId(requestId)) {
|
||||
return models.webSocketRequest.getById(requestId);
|
||||
}
|
||||
return models.request.getById(requestId);
|
||||
}
|
||||
|
||||
export function remove(request: Request | GrpcRequest) {
|
||||
return isGrpcRequest(request)
|
||||
? models.grpcRequest.remove(request)
|
||||
: models.request.remove(request);
|
||||
export function remove(request: Request | GrpcRequest | WebSocketRequest) {
|
||||
if (isGrpcRequest(request)) {
|
||||
return models.grpcRequest.remove(request);
|
||||
}
|
||||
if (isWebSocketRequest(request)) {
|
||||
return models.webSocketRequest.remove(request);
|
||||
}
|
||||
return models.request.remove(request);
|
||||
}
|
||||
|
||||
export function update<T extends object>(request: T, patch: Partial<T> = {}): Promise<T> {
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
return isGrpcRequest(request)
|
||||
? models.grpcRequest.update(request, patch)
|
||||
if (isGrpcRequest(request)) {
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
return models.grpcRequest.update(request, patch);
|
||||
}
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
: models.request.update(request, patch);
|
||||
if (isWebSocketRequest(request)) {
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
return models.webSocketRequest.update(request, patch);
|
||||
}
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
return models.request.update(request, patch);
|
||||
}
|
||||
|
||||
export function duplicate<T extends object>(request: T, patch: Partial<T> = {}): Promise<T> {
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
return isGrpcRequest(request)
|
||||
? models.grpcRequest.duplicate(request, patch)
|
||||
if (isGrpcRequest(request)) {
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
return models.grpcRequest.duplicate(request, patch);
|
||||
}
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
: models.request.duplicate(request, patch);
|
||||
if (isWebSocketRequest(request)) {
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
return models.webSocketRequest.duplicate(request, patch);
|
||||
}
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
return models.request.duplicate(request, patch);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
EXPORT_TYPE_REQUEST_GROUP,
|
||||
EXPORT_TYPE_UNIT_TEST,
|
||||
EXPORT_TYPE_UNIT_TEST_SUITE,
|
||||
EXPORT_TYPE_WEBSOCKET_PAYLOAD,
|
||||
EXPORT_TYPE_WEBSOCKET_REQUEST,
|
||||
EXPORT_TYPE_WORKSPACE,
|
||||
} from '../common/constants';
|
||||
import { generateId, pluralize } from '../common/misc';
|
||||
@@ -35,6 +37,9 @@ import * as _stats from './stats';
|
||||
import * as _unitTest from './unit-test';
|
||||
import * as _unitTestResult from './unit-test-result';
|
||||
import * as _unitTestSuite from './unit-test-suite';
|
||||
import * as _webSocketPayload from './websocket-payload';
|
||||
import * as _webSocketRequest from './websocket-request';
|
||||
import * as _webSocketResponse from './websocket-response';
|
||||
import * as _workspace from './workspace';
|
||||
import * as _workspaceMeta from './workspace-meta';
|
||||
|
||||
@@ -75,6 +80,9 @@ export const protoFile = _protoFile;
|
||||
export const protoDirectory = _protoDirectory;
|
||||
export const grpcRequest = _grpcRequest;
|
||||
export const grpcRequestMeta = _grpcRequestMeta;
|
||||
export const webSocketPayload = _webSocketPayload;
|
||||
export const webSocketRequest = _webSocketRequest;
|
||||
export const webSocketResponse = _webSocketResponse;
|
||||
export const workspace = _workspace;
|
||||
export const workspaceMeta = _workspaceMeta;
|
||||
|
||||
@@ -108,6 +116,9 @@ export function all() {
|
||||
protoDirectory,
|
||||
grpcRequest,
|
||||
grpcRequestMeta,
|
||||
webSocketPayload,
|
||||
webSocketRequest,
|
||||
webSocketResponse,
|
||||
] as const;
|
||||
}
|
||||
|
||||
@@ -207,6 +218,8 @@ export async function initModel<T extends BaseModel>(type: string, ...sources: R
|
||||
|
||||
export const MODELS_BY_EXPORT_TYPE: Record<string, any> = {
|
||||
[EXPORT_TYPE_REQUEST]: request,
|
||||
[EXPORT_TYPE_WEBSOCKET_PAYLOAD]: webSocketPayload,
|
||||
[EXPORT_TYPE_WEBSOCKET_REQUEST]: webSocketRequest,
|
||||
[EXPORT_TYPE_GRPC_REQUEST]: grpcRequest,
|
||||
[EXPORT_TYPE_REQUEST_GROUP]: requestGroup,
|
||||
[EXPORT_TYPE_UNIT_TEST_SUITE]: unitTestSuite,
|
||||
|
||||
@@ -2,9 +2,11 @@ import deepEqual from 'deep-equal';
|
||||
|
||||
import { database as db } from '../common/database';
|
||||
import { compressObject, decompressObject } from '../common/misc';
|
||||
import * as requestOperations from '../models/helpers/request-operations';
|
||||
import { GrpcRequest } from './grpc-request';
|
||||
import type { BaseModel } from './index';
|
||||
import * as models from './index';
|
||||
import { isRequest, Request } from './request';
|
||||
import { isWebSocketRequest, WebSocketRequest } from './websocket-request';
|
||||
|
||||
export const name = 'Request Version';
|
||||
|
||||
@@ -51,9 +53,9 @@ export function getById(id: string) {
|
||||
return db.get<RequestVersion>(type, id);
|
||||
}
|
||||
|
||||
export async function create(request: Request) {
|
||||
if (!isRequest(request)) {
|
||||
throw new Error(`New ${type} was not given a valid ${models.request.type} instance`);
|
||||
export async function create(request: Request | WebSocketRequest | GrpcRequest) {
|
||||
if (!isRequest(request) && !isWebSocketRequest(request)) {
|
||||
throw new Error(`New ${type} was not given a valid ${request.type} instance`);
|
||||
}
|
||||
|
||||
const parentId = request._id;
|
||||
@@ -90,7 +92,7 @@ export async function restore(requestVersionId: string) {
|
||||
}
|
||||
|
||||
const requestPatch = decompressObject(requestVersion.compressedRequest);
|
||||
const originalRequest = await models.request.getById(requestPatch._id);
|
||||
const originalRequest = await requestOperations.getById(requestPatch._id);
|
||||
|
||||
if (!originalRequest) {
|
||||
return null;
|
||||
@@ -101,20 +103,18 @@ export async function restore(requestVersionId: string) {
|
||||
delete requestPatch[field];
|
||||
}
|
||||
|
||||
return models.request.update(originalRequest, requestPatch);
|
||||
return requestOperations.update(originalRequest, requestPatch);
|
||||
}
|
||||
|
||||
function _diffRequests(rOld: Request | null, rNew: Request) {
|
||||
function _diffRequests(rOld: Request | WebSocketRequest | null, rNew: Request | WebSocketRequest) {
|
||||
if (!rOld) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const key of Object.keys(rOld) as (keyof Request)[]) {
|
||||
for (const key of Object.keys(rOld) as (keyof typeof rOld)[]) {
|
||||
// Skip fields that aren't useful
|
||||
if (FIELDS_TO_IGNORE.includes(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!deepEqual(rOld[key], rNew[key])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import zlib from 'zlib';
|
||||
|
||||
import { database as db, Query } from '../common/database';
|
||||
import type { ResponseTimelineEntry } from '../main/network/libcurl-promise';
|
||||
import * as requestOperations from '../models/helpers/request-operations';
|
||||
import type { BaseModel } from './index';
|
||||
import * as models from './index';
|
||||
|
||||
@@ -61,19 +62,19 @@ export function init(): BaseResponse {
|
||||
contentType: '',
|
||||
url: '',
|
||||
bytesRead: 0,
|
||||
bytesContent: -1,
|
||||
// -1 means that it was legacy and this property didn't exist yet
|
||||
bytesContent: -1,
|
||||
elapsedTime: 0,
|
||||
headers: [],
|
||||
timelinePath: '',
|
||||
// Actual timelines are stored on the filesystem
|
||||
bodyPath: '',
|
||||
timelinePath: '',
|
||||
// Actual bodies are stored on the filesystem
|
||||
bodyCompression: '__NEEDS_MIGRATION__',
|
||||
bodyPath: '',
|
||||
// For legacy bodies
|
||||
bodyCompression: '__NEEDS_MIGRATION__',
|
||||
error: '',
|
||||
requestVersionId: null,
|
||||
// Things from the request
|
||||
requestVersionId: null,
|
||||
settingStoreCookies: null,
|
||||
settingSendCookies: null,
|
||||
// Responses sent before environment filtering will have a special value
|
||||
@@ -163,7 +164,7 @@ export async function create(patch: Record<string, any> = {}, maxResponses = 20)
|
||||
|
||||
const { parentId } = patch;
|
||||
// Create request version snapshot
|
||||
const request = await models.request.getById(parentId);
|
||||
const request = await requestOperations.getById(parentId);
|
||||
const requestVersion = request ? await models.requestVersion.create(request) : null;
|
||||
patch.requestVersionId = requestVersion ? requestVersion._id : null;
|
||||
// Filter responses by environment if setting is enabled
|
||||
@@ -224,7 +225,8 @@ export function getTimeline(response: Response, showBody?: boolean) {
|
||||
|
||||
try {
|
||||
const rawBuffer = fs.readFileSync(timelinePath);
|
||||
const timeline = JSON.parse(rawBuffer.toString()) as ResponseTimelineEntry[];
|
||||
const timelineString = rawBuffer.toString();
|
||||
const timeline = JSON.parse(timelineString) as ResponseTimelineEntry[];
|
||||
const body: ResponseTimelineEntry[] = showBody ? [
|
||||
{
|
||||
name: 'DataOut',
|
||||
|
||||
70
packages/insomnia/src/models/websocket-payload.ts
Normal file
70
packages/insomnia/src/models/websocket-payload.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { database } from '../common/database';
|
||||
import type { BaseModel } from '.';
|
||||
|
||||
export const name = 'WebSocket Payload';
|
||||
|
||||
export const type = 'WebSocketPayload';
|
||||
|
||||
export const prefix = 'ws-payload';
|
||||
|
||||
export const canDuplicate = true;
|
||||
|
||||
// @TODO: enable this at some point
|
||||
export const canSync = false;
|
||||
|
||||
export interface BaseWebSocketPayload {
|
||||
value: string;
|
||||
mode: string;
|
||||
}
|
||||
|
||||
export type WebSocketPayload = BaseModel & BaseWebSocketPayload & { type: typeof type };
|
||||
|
||||
export const isWebSocketPayload = (model: Pick<BaseModel, 'type'>): model is WebSocketPayload => (
|
||||
model.type === type
|
||||
);
|
||||
|
||||
export const isWebSocketPayloadId = (id: string | null) => (
|
||||
id?.startsWith(`${prefix}_`)
|
||||
);
|
||||
|
||||
export const init = (): BaseWebSocketPayload => ({
|
||||
value: '',
|
||||
mode: 'application/json',
|
||||
});
|
||||
|
||||
export const migrate = (doc: WebSocketPayload) => doc;
|
||||
|
||||
export const create = (patch: Partial<WebSocketPayload> = {}) => {
|
||||
if (!patch.parentId) {
|
||||
throw new Error(`New WebSocketPayload missing \`parentId\`: ${JSON.stringify(patch)}`);
|
||||
}
|
||||
|
||||
return database.docCreate<WebSocketPayload>(type, patch);
|
||||
};
|
||||
|
||||
export const remove = (obj: WebSocketPayload) => database.remove(obj);
|
||||
|
||||
export const update = (
|
||||
obj: WebSocketPayload,
|
||||
patch: Partial<WebSocketPayload> = {}
|
||||
) => database.docUpdate(obj, patch);
|
||||
|
||||
export async function duplicate(request: WebSocketPayload, patch: Partial<WebSocketPayload> = {}) {
|
||||
// Only set name and "(Copy)" if the patch does
|
||||
// not define it and the request itself has a name.
|
||||
// Otherwise leave it blank so the request URL can
|
||||
// fill it in automatically.
|
||||
if (!patch.name && request.name) {
|
||||
patch.name = `${request.name} (Copy)`;
|
||||
}
|
||||
|
||||
return database.duplicate<WebSocketPayload>(request, {
|
||||
name,
|
||||
...patch,
|
||||
});
|
||||
}
|
||||
|
||||
export const getById = (_id: string) => database.getWhere<WebSocketPayload>(type, { _id });
|
||||
export const getByParentId = (parentId: string) => database.getWhere<WebSocketPayload>(type, { parentId });
|
||||
|
||||
export const all = () => database.all<WebSocketPayload>(type);
|
||||
92
packages/insomnia/src/models/websocket-request.ts
Normal file
92
packages/insomnia/src/models/websocket-request.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { database } from '../common/database';
|
||||
import type { BaseModel } from '.';
|
||||
import { RequestAuthentication, RequestHeader } from './request';
|
||||
|
||||
export const name = 'WebSocket Request';
|
||||
|
||||
export const type = 'WebSocketRequest';
|
||||
|
||||
export const prefix = 'ws-req';
|
||||
|
||||
export const canDuplicate = true;
|
||||
|
||||
// @TODO: enable this at some point
|
||||
export const canSync = false;
|
||||
|
||||
export interface BaseWebSocketRequest {
|
||||
name: string;
|
||||
url: string;
|
||||
metaSortKey: number;
|
||||
headers: RequestHeader[];
|
||||
authentication: RequestAuthentication;
|
||||
}
|
||||
|
||||
export type WebSocketRequest = BaseModel & BaseWebSocketRequest & { type: typeof type };
|
||||
|
||||
export const isWebSocketRequest = (model: Pick<BaseModel, 'type'>): model is WebSocketRequest => (
|
||||
model.type === type
|
||||
);
|
||||
|
||||
export const isWebSocketRequestId = (id: string | null) => (
|
||||
id?.startsWith(`${prefix}_`)
|
||||
);
|
||||
|
||||
export const init = (): BaseWebSocketRequest => ({
|
||||
name: 'New WebSocket Request',
|
||||
url: '',
|
||||
metaSortKey: -1 * Date.now(),
|
||||
headers: [],
|
||||
authentication: {},
|
||||
});
|
||||
|
||||
export const migrate = (doc: WebSocketRequest) => doc;
|
||||
|
||||
export const create = (patch: Partial<WebSocketRequest> = {}) => {
|
||||
if (!patch.parentId) {
|
||||
throw new Error(`New WebSocketRequest missing \`parentId\`: ${JSON.stringify(patch)}`);
|
||||
}
|
||||
|
||||
return database.docCreate<WebSocketRequest>(type, patch);
|
||||
};
|
||||
|
||||
export const remove = (obj: WebSocketRequest) => database.remove(obj);
|
||||
|
||||
export const update = (
|
||||
obj: WebSocketRequest,
|
||||
patch: Partial<WebSocketRequest> = {}
|
||||
) => database.docUpdate(obj, patch);
|
||||
|
||||
// This is duplicated (lol) from models/request.js
|
||||
export async function duplicate(request: WebSocketRequest, patch: Partial<WebSocketRequest> = {}) {
|
||||
// Only set name and "(Copy)" if the patch does
|
||||
// not define it and the request itself has a name.
|
||||
// Otherwise leave it blank so the request URL can
|
||||
// fill it in automatically.
|
||||
if (!patch.name && request.name) {
|
||||
patch.name = `${request.name} (Copy)`;
|
||||
}
|
||||
|
||||
// Get sort key of next request
|
||||
const q = {
|
||||
metaSortKey: {
|
||||
$gt: request.metaSortKey,
|
||||
},
|
||||
};
|
||||
// @ts-expect-error -- Database TSCONVERSION
|
||||
const [nextRequest] = await db.find<WebSocketRequest>(type, q, {
|
||||
metaSortKey: 1,
|
||||
});
|
||||
const nextSortKey = nextRequest ? nextRequest.metaSortKey : request.metaSortKey + 100;
|
||||
// Calculate new sort key
|
||||
const sortKeyIncrement = (nextSortKey - request.metaSortKey) / 2;
|
||||
const metaSortKey = request.metaSortKey + sortKeyIncrement;
|
||||
return database.duplicate<WebSocketRequest>(request, {
|
||||
name,
|
||||
metaSortKey,
|
||||
...patch,
|
||||
});
|
||||
}
|
||||
|
||||
export const getById = (_id: string) => database.getWhere<WebSocketRequest>(type, { _id });
|
||||
|
||||
export const all = () => database.all<WebSocketRequest>(type);
|
||||
151
packages/insomnia/src/models/websocket-response.ts
Normal file
151
packages/insomnia/src/models/websocket-response.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import fs from 'fs';
|
||||
|
||||
import { database as db } from '../common/database';
|
||||
import * as requestOperations from './helpers/request-operations';
|
||||
import type { BaseModel } from './index';
|
||||
import * as models from './index';
|
||||
import { ResponseHeader } from './response';
|
||||
|
||||
export const name = 'WebSocket Response';
|
||||
|
||||
export const type = 'WebSocketResponse';
|
||||
|
||||
export const prefix = 'ws-res';
|
||||
|
||||
export const canDuplicate = false;
|
||||
|
||||
export const canSync = false;
|
||||
|
||||
export interface BaseWebSocketResponse {
|
||||
environmentId: string | null;
|
||||
statusCode: number;
|
||||
statusMessage: string;
|
||||
httpVersion: string;
|
||||
contentType: string;
|
||||
url: string;
|
||||
elapsedTime: number;
|
||||
headers: ResponseHeader[];
|
||||
// Event logs are stored on the filesystem
|
||||
eventLogPath: string;
|
||||
// Actual timelines are stored on the filesystem
|
||||
timelinePath: string;
|
||||
error: string;
|
||||
requestVersionId: string | null;
|
||||
settingStoreCookies: boolean | null;
|
||||
settingSendCookies: boolean | null;
|
||||
}
|
||||
|
||||
export type WebSocketResponse = BaseModel & BaseWebSocketResponse;
|
||||
|
||||
export const isWebSocketResponse = (model: Pick<BaseModel, 'type'>): model is WebSocketResponse => (
|
||||
model.type === type
|
||||
);
|
||||
|
||||
export function init(): BaseWebSocketResponse {
|
||||
return {
|
||||
statusCode: 0,
|
||||
statusMessage: '',
|
||||
httpVersion: '',
|
||||
contentType: '',
|
||||
url: '',
|
||||
elapsedTime: 0,
|
||||
headers: [],
|
||||
timelinePath: '',
|
||||
eventLogPath: '',
|
||||
error: '',
|
||||
requestVersionId: null,
|
||||
settingStoreCookies: null,
|
||||
settingSendCookies: null,
|
||||
environmentId: null,
|
||||
};
|
||||
}
|
||||
|
||||
export async function migrate(doc: Response) {
|
||||
return doc;
|
||||
}
|
||||
|
||||
export function hookDatabaseInit(consoleLog: typeof console.log = console.log) {
|
||||
consoleLog('[db] Init websocket-responses DB');
|
||||
}
|
||||
|
||||
export function hookRemove(doc: WebSocketResponse, consoleLog: typeof console.log = console.log) {
|
||||
fs.unlink(doc.eventLogPath, () => {
|
||||
consoleLog(`[response] Delete body ${doc.eventLogPath}`);
|
||||
});
|
||||
|
||||
fs.unlink(doc.timelinePath, () => {
|
||||
consoleLog(`[response] Delete timeline ${doc.timelinePath}`);
|
||||
});
|
||||
}
|
||||
|
||||
export function getById(id: string) {
|
||||
return db.get<WebSocketResponse>(type, id);
|
||||
}
|
||||
|
||||
export async function all() {
|
||||
return db.all<WebSocketResponse>(type);
|
||||
}
|
||||
|
||||
export async function removeForRequest(parentId: string, environmentId?: string | null) {
|
||||
const settings = await models.settings.getOrCreate();
|
||||
const query: Record<string, any> = {
|
||||
parentId,
|
||||
};
|
||||
|
||||
// Only add if not undefined. null is not the same as undefined
|
||||
// null: find responses sent from base environment
|
||||
// undefined: find all responses
|
||||
if (environmentId !== undefined && settings.filterResponsesByEnv) {
|
||||
query.environmentId = environmentId;
|
||||
}
|
||||
|
||||
// Also delete legacy responses here or else the user will be confused as to
|
||||
// why some responses are still showing in the UI.
|
||||
await db.removeWhere(type, query);
|
||||
}
|
||||
|
||||
export function remove(response: WebSocketResponse) {
|
||||
return db.remove(response);
|
||||
}
|
||||
|
||||
export async function create(patch: Partial<WebSocketResponse> = {}, maxResponses = 20) {
|
||||
if (!patch.parentId) {
|
||||
throw new Error('New Response missing `parentId`');
|
||||
}
|
||||
|
||||
const { parentId } = patch;
|
||||
// Create request version snapshot
|
||||
const request = await requestOperations.getById(parentId);
|
||||
const requestVersion = request ? await models.requestVersion.create(request) : null;
|
||||
patch.requestVersionId = requestVersion ? requestVersion._id : null;
|
||||
// Filter responses by environment if setting is enabled
|
||||
const query: Record<string, any> = {
|
||||
parentId,
|
||||
};
|
||||
|
||||
if (
|
||||
(await models.settings.getOrCreate()).filterResponsesByEnv &&
|
||||
patch.hasOwnProperty('environmentId')
|
||||
) {
|
||||
query.environmentId = patch.environmentId;
|
||||
}
|
||||
|
||||
// Delete all other responses before creating the new one
|
||||
const allResponses = await db.findMostRecentlyModified<WebSocketResponse>(type, query, Math.max(1, maxResponses));
|
||||
const recentIds = allResponses.map(r => r._id);
|
||||
// Remove all that were in the last query, except the first `maxResponses` IDs
|
||||
await db.removeWhere(type, {
|
||||
...query,
|
||||
_id: {
|
||||
$nin: recentIds,
|
||||
},
|
||||
});
|
||||
// Actually create the new response
|
||||
return db.docCreate(type, patch);
|
||||
}
|
||||
|
||||
export function getLatestByParentId(parentId: string) {
|
||||
return db.getMostRecentlyModified<WebSocketResponse>(type, {
|
||||
parentId,
|
||||
});
|
||||
}
|
||||
@@ -451,7 +451,7 @@ async function _applyResponsePluginHooks(
|
||||
|
||||
}
|
||||
|
||||
function storeTimeline(timeline: ResponseTimelineEntry[]) {
|
||||
export function storeTimeline(timeline: ResponseTimelineEntry[]): Promise<string> {
|
||||
const timelineStr = JSON.stringify(timeline, null, '\t');
|
||||
const timelineHash = uuidv4();
|
||||
const responsesDir = pathJoin(getDataDirectory(), 'responses');
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
import { contextBridge, ipcRenderer } from 'electron';
|
||||
|
||||
import type { WebSocketBridgeAPI } from './main/network/websocket';
|
||||
|
||||
const webSocket: WebSocketBridgeAPI = {
|
||||
create: options => ipcRenderer.invoke('webSocket.create', options),
|
||||
close: options => ipcRenderer.invoke('webSocket.close', options),
|
||||
closeAll: () => ipcRenderer.invoke('webSocket.closeAll'),
|
||||
readyState: {
|
||||
getCurrent: options => ipcRenderer.invoke('webSocket.readyState', options),
|
||||
},
|
||||
event: {
|
||||
findMany: options => ipcRenderer.invoke('webSocket.event.findMany', options),
|
||||
send: options => ipcRenderer.invoke('webSocket.event.send', options),
|
||||
},
|
||||
};
|
||||
|
||||
const main: Window['main'] = {
|
||||
restart: () => ipcRenderer.send('restart'),
|
||||
authorizeUserInWindow: options => ipcRenderer.invoke('authorizeUserInWindow', options),
|
||||
@@ -12,6 +27,7 @@ const main: Window['main'] = {
|
||||
ipcRenderer.on(channel, listener);
|
||||
return () => ipcRenderer.removeListener(channel, listener);
|
||||
},
|
||||
webSocket,
|
||||
};
|
||||
const dialog: Window['dialog'] = {
|
||||
showOpenDialog: options => ipcRenderer.invoke('showOpenDialog', options),
|
||||
|
||||
@@ -385,6 +385,7 @@ export class OneLineEditor extends PureComponent<Props, State> {
|
||||
getAutocompleteConstants={getAutocompleteConstants}
|
||||
className={classnames('editor--single-line', className)}
|
||||
defaultValue={defaultValue}
|
||||
readOnly={this.props.readOnly}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
@@ -402,6 +403,7 @@ export class OneLineEditor extends PureComponent<Props, State> {
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
defaultValue={defaultValue}
|
||||
disabled={this.props.readOnly}
|
||||
onBlur={this._handleInputBlur}
|
||||
onChange={this._handleInputChange}
|
||||
onMouseEnter={this._handleInputMouseEnter}
|
||||
|
||||
@@ -2,21 +2,14 @@ import React, { FC, useCallback } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import {
|
||||
AUTH_ASAP,
|
||||
AUTH_AWS_IAM,
|
||||
AUTH_BASIC,
|
||||
AUTH_BEARER,
|
||||
AUTH_DIGEST,
|
||||
AUTH_HAWK,
|
||||
AUTH_NETRC,
|
||||
AUTH_NONE,
|
||||
AUTH_NTLM,
|
||||
AUTH_OAUTH_1,
|
||||
AUTH_OAUTH_2,
|
||||
AuthType,
|
||||
getAuthTypeName,
|
||||
HAWK_ALGORITHM_SHA256,
|
||||
} from '../../../common/constants';
|
||||
import * as models from '../../../models';
|
||||
import { update } from '../../../models/helpers/request-operations';
|
||||
import { RequestAuthentication } from '../../../models/request';
|
||||
import { SIGNATURE_METHOD_HMAC_SHA1 } from '../../../network/o-auth-1/constants';
|
||||
import { GRANT_TYPE_AUTHORIZATION_CODE } from '../../../network/o-auth-2/constants';
|
||||
import { selectActiveRequest } from '../../redux/selectors';
|
||||
import { Dropdown } from '../base/dropdown/dropdown';
|
||||
import { DropdownButton } from '../base/dropdown/dropdown-button';
|
||||
@@ -25,11 +18,110 @@ import { DropdownItem } from '../base/dropdown/dropdown-item';
|
||||
import { showModal } from '../modals';
|
||||
import { AlertModal } from '../modals/alert-modal';
|
||||
|
||||
const defaultTypes: AuthType[] = [
|
||||
'basic',
|
||||
'digest',
|
||||
'oauth1',
|
||||
'oauth2',
|
||||
'ntlm',
|
||||
'iam',
|
||||
'bearer',
|
||||
'hawk',
|
||||
'asap',
|
||||
'netrc',
|
||||
];
|
||||
|
||||
function makeNewAuth(type: string, oldAuth: RequestAuthentication = {}): RequestAuthentication {
|
||||
switch (type) {
|
||||
// No Auth
|
||||
case 'none':
|
||||
return {};
|
||||
|
||||
// HTTP Basic Authentication
|
||||
case 'basic':
|
||||
return {
|
||||
type,
|
||||
useISO88591: oldAuth.useISO88591 || false,
|
||||
disabled: oldAuth.disabled || false,
|
||||
username: oldAuth.username || '',
|
||||
password: oldAuth.password || '',
|
||||
};
|
||||
|
||||
case 'digest':
|
||||
case 'ntlm':
|
||||
return {
|
||||
type,
|
||||
disabled: oldAuth.disabled || false,
|
||||
username: oldAuth.username || '',
|
||||
password: oldAuth.password || '',
|
||||
};
|
||||
|
||||
case 'oauth1':
|
||||
return {
|
||||
type,
|
||||
disabled: false,
|
||||
signatureMethod: SIGNATURE_METHOD_HMAC_SHA1,
|
||||
consumerKey: '',
|
||||
consumerSecret: '',
|
||||
tokenKey: '',
|
||||
tokenSecret: '',
|
||||
privateKey: '',
|
||||
version: '1.0',
|
||||
nonce: '',
|
||||
timestamp: '',
|
||||
callback: '',
|
||||
};
|
||||
|
||||
// OAuth 2.0
|
||||
case 'oauth2':
|
||||
return {
|
||||
type,
|
||||
grantType: GRANT_TYPE_AUTHORIZATION_CODE,
|
||||
};
|
||||
|
||||
// Aws IAM
|
||||
case 'iam':
|
||||
return {
|
||||
type,
|
||||
disabled: oldAuth.disabled || false,
|
||||
accessKeyId: oldAuth.accessKeyId || '',
|
||||
secretAccessKey: oldAuth.secretAccessKey || '',
|
||||
sessionToken: oldAuth.sessionToken || '',
|
||||
};
|
||||
|
||||
// Hawk
|
||||
case 'hawk':
|
||||
return {
|
||||
type,
|
||||
algorithm: HAWK_ALGORITHM_SHA256,
|
||||
};
|
||||
|
||||
// Atlassian ASAP
|
||||
case 'asap':
|
||||
return {
|
||||
type,
|
||||
issuer: '',
|
||||
subject: '',
|
||||
audience: '',
|
||||
additionalClaims: '',
|
||||
keyId: '',
|
||||
privateKey: '',
|
||||
};
|
||||
|
||||
// Types needing no defaults
|
||||
case 'netrc':
|
||||
default:
|
||||
return {
|
||||
type,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const AuthItem: FC<{
|
||||
type: string;
|
||||
type: AuthType;
|
||||
nameOverride?: string;
|
||||
isCurrent: (type: string) => boolean;
|
||||
onClick: (type: string) => void;
|
||||
isCurrent: (type: AuthType) => boolean;
|
||||
onClick: (type: AuthType) => void;
|
||||
}> = ({ type, nameOverride, isCurrent, onClick }) => (
|
||||
<DropdownItem onClick={onClick} value={type}>
|
||||
{<i className={`fa fa-${isCurrent(type) ? 'check' : 'empty'}`} />}{' '}
|
||||
@@ -38,16 +130,15 @@ const AuthItem: FC<{
|
||||
);
|
||||
AuthItem.displayName = DropdownItem.name;
|
||||
|
||||
export const AuthDropdown: FC = () => {
|
||||
interface Props {
|
||||
authTypes?: AuthType[];
|
||||
disabled?: boolean;
|
||||
}
|
||||
export const AuthDropdown: FC<Props> = ({ authTypes = defaultTypes, disabled = false }) => {
|
||||
const activeRequest = useSelector(selectActiveRequest);
|
||||
|
||||
const onClick = useCallback(async (type: string) => {
|
||||
if (!activeRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!('authentication' in activeRequest)) {
|
||||
// gRPC Requests don't have `authentication`
|
||||
const onClick = useCallback(async (type: AuthType) => {
|
||||
if (!activeRequest || !('authentication' in activeRequest)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -58,8 +149,8 @@ export const AuthDropdown: FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const newAuthentication = models.request.newAuth(type, authentication);
|
||||
const defaultAuthentication = models.request.newAuth(authentication.type);
|
||||
const newAuthentication = makeNewAuth(type, authentication);
|
||||
const defaultAuthentication = makeNewAuth(authentication.type);
|
||||
|
||||
// Prompt the user if fields will change between new and old
|
||||
for (const key of Object.keys(authentication)) {
|
||||
@@ -80,16 +171,13 @@ export const AuthDropdown: FC = () => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
update(activeRequest, { authentication:newAuthentication });
|
||||
update(activeRequest, { authentication: newAuthentication });
|
||||
}, [activeRequest]);
|
||||
const isCurrent = useCallback((type: string) => {
|
||||
if (!activeRequest) {
|
||||
const isCurrent = useCallback((type: AuthType) => {
|
||||
if (!activeRequest || !('authentication' in activeRequest)) {
|
||||
return false;
|
||||
}
|
||||
if (!('authentication' in activeRequest)) {
|
||||
return false;
|
||||
}
|
||||
return type === (activeRequest.authentication.type || AUTH_NONE);
|
||||
return type === (activeRequest.authentication.type || 'none');
|
||||
}, [activeRequest]);
|
||||
|
||||
if (!activeRequest) {
|
||||
@@ -101,22 +189,25 @@ export const AuthDropdown: FC = () => {
|
||||
return (
|
||||
<Dropdown beside>
|
||||
<DropdownDivider>Auth Types</DropdownDivider>
|
||||
<DropdownButton className="tall">
|
||||
<DropdownButton className="tall" disabled={disabled}>
|
||||
{'authentication' in activeRequest ? getAuthTypeName(activeRequest.authentication.type) || 'Auth' : 'Auth'}
|
||||
<i className="fa fa-caret-down space-left" />
|
||||
</DropdownButton>
|
||||
<AuthItem type={AUTH_BASIC} {...itemProps} />
|
||||
<AuthItem type={AUTH_DIGEST} {...itemProps} />
|
||||
<AuthItem type={AUTH_OAUTH_1} {...itemProps} />
|
||||
<AuthItem type={AUTH_OAUTH_2} {...itemProps} />
|
||||
<AuthItem type={AUTH_NTLM} {...itemProps} />
|
||||
<AuthItem type={AUTH_AWS_IAM} {...itemProps} />
|
||||
<AuthItem type={AUTH_BEARER} {...itemProps} />
|
||||
<AuthItem type={AUTH_HAWK} {...itemProps} />
|
||||
<AuthItem type={AUTH_ASAP} {...itemProps} />
|
||||
<AuthItem type={AUTH_NETRC} {...itemProps} />
|
||||
<DropdownDivider>Other</DropdownDivider>
|
||||
<AuthItem type={AUTH_NONE} nameOverride="No Authentication" {...itemProps} />
|
||||
{authTypes.map(authType =>
|
||||
<AuthItem
|
||||
key={authType}
|
||||
type={authType}
|
||||
{...itemProps}
|
||||
/>)}
|
||||
<DropdownDivider key="divider-other">
|
||||
Other
|
||||
</DropdownDivider>
|
||||
<AuthItem
|
||||
key="none"
|
||||
type="none"
|
||||
nameOverride="No Authentication"
|
||||
{...itemProps}
|
||||
/>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
CONTENT_TYPE_YAML,
|
||||
getContentTypeName,
|
||||
} from '../../../common/constants';
|
||||
import { isWebSocketRequest } from '../../../models/websocket-request';
|
||||
import { selectActiveRequest } from '../../redux/selectors';
|
||||
import { Dropdown } from '../base/dropdown/dropdown';
|
||||
import { DropdownButton } from '../base/dropdown/dropdown-button';
|
||||
@@ -38,7 +39,9 @@ const MimeTypeItem: FC<{
|
||||
mimeType,
|
||||
onChange,
|
||||
}) => {
|
||||
const activeRequest = useSelector(selectActiveRequest);
|
||||
const request = useSelector(selectActiveRequest);
|
||||
const activeRequest = request && !isWebSocketRequest(request) ? request : null;
|
||||
|
||||
const handleChangeMimeType = useCallback(async (mimeType: string | null) => {
|
||||
if (!activeRequest) {
|
||||
return;
|
||||
@@ -91,7 +94,8 @@ const MimeTypeItem: FC<{
|
||||
MimeTypeItem.displayName = DropdownItem.name;
|
||||
|
||||
export const ContentTypeDropdown: FC<Props> = ({ onChange }) => {
|
||||
const activeRequest = useSelector(selectActiveRequest);
|
||||
const request = useSelector(selectActiveRequest);
|
||||
const activeRequest = request && !isWebSocketRequest(request) ? request : null;
|
||||
|
||||
if (!activeRequest) {
|
||||
return null;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { getPreviewModeName, PREVIEW_MODES, PreviewMode } from '../../../common/
|
||||
import { exportHarCurrentRequest } from '../../../common/har';
|
||||
import * as models from '../../../models';
|
||||
import { isRequest } from '../../../models/request';
|
||||
import { isResponse } from '../../../models/response';
|
||||
import { selectActiveRequest, selectActiveResponse, selectResponsePreviewMode } from '../../redux/selectors';
|
||||
import { Dropdown } from '../base/dropdown/dropdown';
|
||||
import { DropdownButton } from '../base/dropdown/dropdown-button';
|
||||
@@ -36,7 +37,7 @@ export const PreviewModeDropdown: FC<Props> = ({
|
||||
const handleDownloadNormal = useCallback(() => download(false), [download]);
|
||||
|
||||
const exportAsHAR = useCallback(async () => {
|
||||
if (!response || !request || !isRequest(request)) {
|
||||
if (!response || !request || !isRequest(request) || !isResponse(response)) {
|
||||
console.warn('Nothing to download');
|
||||
return;
|
||||
}
|
||||
@@ -61,7 +62,7 @@ export const PreviewModeDropdown: FC<Props> = ({
|
||||
}, [request, response]);
|
||||
|
||||
const exportDebugFile = useCallback(async () => {
|
||||
if (!response || !request) {
|
||||
if (!response || !request || !isResponse(response)) {
|
||||
console.warn('Nothing to download');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -47,7 +47,10 @@ export const RequestGroupActionsDropdown = forwardRef<RequestGroupActionsDropdow
|
||||
|
||||
const create = useCallback((requestType: CreateRequestType) => {
|
||||
if (activeWorkspaceId) {
|
||||
createRequest({ parentId: requestGroup._id, requestType, workspaceId: activeWorkspaceId });
|
||||
createRequest({
|
||||
parentId: requestGroup._id,
|
||||
requestType, workspaceId: activeWorkspaceId,
|
||||
});
|
||||
}
|
||||
}, [activeWorkspaceId, requestGroup._id]);
|
||||
|
||||
@@ -143,6 +146,10 @@ export const RequestGroupActionsDropdown = forwardRef<RequestGroupActionsDropdow
|
||||
<i className="fa fa-plus-circle" />New gRPC Request
|
||||
</DropdownItem>
|
||||
|
||||
<DropdownItem value="WebSocket" onClick={create}>
|
||||
<i className="fa fa-plus-circle" />WebSocket Request
|
||||
</DropdownItem>
|
||||
|
||||
<DropdownItem onClick={createGroup}>
|
||||
<i className="fa fa-folder" /> New Folder
|
||||
<DropdownHint keyBindings={hotKeyRegistry[hotKeyRefs.REQUEST_SHOW_CREATE_FOLDER.id]} />
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { differenceInHours, differenceInMinutes, isThisWeek, isToday } from 'date-fns';
|
||||
import React, { FC, Fragment, useCallback, useRef } from 'react';
|
||||
import React, { Fragment, useCallback, useRef } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { hotKeyRefs } from '../../../common/hotkeys';
|
||||
import { executeHotKey } from '../../../common/hotkeys-listener';
|
||||
import { decompressObject } from '../../../common/misc';
|
||||
import * as models from '../../../models/index';
|
||||
import type { Response } from '../../../models/response';
|
||||
import { Response } from '../../../models/response';
|
||||
import { isWebSocketResponse, WebSocketResponse } from '../../../models/websocket-response';
|
||||
import { selectActiveEnvironment, selectActiveRequest, selectActiveRequestResponses, selectRequestVersions } from '../../redux/selectors';
|
||||
import { type DropdownHandle, Dropdown } from '../base/dropdown/dropdown';
|
||||
import { DropdownButton } from '../base/dropdown/dropdown-button';
|
||||
@@ -20,26 +21,26 @@ import { TimeTag } from '../tags/time-tag';
|
||||
import { URLTag } from '../tags/url-tag';
|
||||
import { TimeFromNow } from '../time-from-now';
|
||||
|
||||
interface Props {
|
||||
activeResponse: Response;
|
||||
handleSetActiveResponse: (requestId: string, activeResponse: Response | null) => void;
|
||||
interface Props<GenericResponse extends Response | WebSocketResponse> {
|
||||
activeResponse: GenericResponse;
|
||||
handleSetActiveResponse: (requestId: string, activeResponse: GenericResponse | null) => void;
|
||||
className?: string;
|
||||
requestId: string;
|
||||
}
|
||||
|
||||
export const ResponseHistoryDropdown: FC<Props> = ({
|
||||
export const ResponseHistoryDropdown = <GenericResponse extends Response | WebSocketResponse>({
|
||||
activeResponse,
|
||||
handleSetActiveResponse,
|
||||
className,
|
||||
requestId,
|
||||
}) => {
|
||||
}: Props<GenericResponse>) => {
|
||||
const dropdownRef = useRef<DropdownHandle>(null);
|
||||
const activeEnvironment = useSelector(selectActiveEnvironment);
|
||||
const responses = useSelector(selectActiveRequestResponses);
|
||||
const responses = useSelector(selectActiveRequestResponses) as GenericResponse[];
|
||||
const activeRequest = useSelector(selectActiveRequest);
|
||||
const requestVersions = useSelector(selectRequestVersions);
|
||||
const now = new Date();
|
||||
const categories: Record<string, Response[]> = {
|
||||
const categories: Record<string, GenericResponse[]> = {
|
||||
minutes: [],
|
||||
hours: [],
|
||||
today: [],
|
||||
@@ -49,16 +50,24 @@ export const ResponseHistoryDropdown: FC<Props> = ({
|
||||
|
||||
const handleDeleteResponses = useCallback(async () => {
|
||||
const environmentId = activeEnvironment ? activeEnvironment._id : null;
|
||||
await models.response.removeForRequest(requestId, environmentId);
|
||||
if (isWebSocketResponse(activeResponse)) {
|
||||
await models.webSocketResponse.removeForRequest(requestId, environmentId);
|
||||
} else {
|
||||
await models.response.removeForRequest(requestId, environmentId);
|
||||
}
|
||||
|
||||
if (activeRequest && activeRequest._id === requestId) {
|
||||
handleSetActiveResponse(requestId, null);
|
||||
}
|
||||
}, [activeEnvironment, activeRequest, handleSetActiveResponse, requestId]);
|
||||
}, [activeEnvironment, activeRequest, activeResponse, handleSetActiveResponse, requestId]);
|
||||
|
||||
const handleDeleteResponse = useCallback(async () => {
|
||||
if (activeResponse) {
|
||||
await models.response.remove(activeResponse);
|
||||
if (isWebSocketResponse(activeResponse)) {
|
||||
await models.webSocketResponse.remove(activeResponse);
|
||||
} else {
|
||||
await models.response.remove(activeResponse);
|
||||
}
|
||||
}
|
||||
handleSetActiveResponse(requestId, null);
|
||||
}, [activeResponse, handleSetActiveResponse, requestId]);
|
||||
@@ -89,7 +98,7 @@ export const ResponseHistoryDropdown: FC<Props> = ({
|
||||
categories.other.push(response);
|
||||
});
|
||||
|
||||
const renderResponseRow = (response: Response) => {
|
||||
const renderResponseRow = (response: GenericResponse) => {
|
||||
const activeResponseId = activeResponse ? activeResponse._id : 'n/a';
|
||||
const active = response._id === activeResponseId;
|
||||
const requestVersion = requestVersions.find(({ _id }) => _id === response.requestVersionId);
|
||||
@@ -114,12 +123,14 @@ export const ResponseHistoryDropdown: FC<Props> = ({
|
||||
tooltipDelay={1000}
|
||||
/>
|
||||
<TimeTag milliseconds={response.elapsedTime} small tooltipDelay={1000} />
|
||||
<SizeTag
|
||||
bytesRead={response.bytesRead}
|
||||
bytesContent={response.bytesContent}
|
||||
small
|
||||
tooltipDelay={1000}
|
||||
/>
|
||||
{!isWebSocketResponse(response) && (
|
||||
<SizeTag
|
||||
bytesRead={response.bytesRead}
|
||||
bytesContent={response.bytesContent}
|
||||
small
|
||||
tooltipDelay={1000}
|
||||
/>
|
||||
)}
|
||||
{!response.requestVersionId ?
|
||||
<i
|
||||
className="icon fa fa-info-circle"
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import { CONTENT_TYPE_JSON, CONTENT_TYPE_PLAINTEXT } from '../../../common/constants';
|
||||
import { Dropdown } from '../base/dropdown/dropdown';
|
||||
import { DropdownButton } from '../base/dropdown/dropdown-button';
|
||||
import { DropdownItem } from '../base/dropdown/dropdown-item';
|
||||
interface Props {
|
||||
previewMode: string;
|
||||
onClick: (previewMode: string) => void;
|
||||
}
|
||||
export const WebSocketPreviewModeDropdown: FC<Props> = ({ previewMode, onClick }) => {
|
||||
return (
|
||||
<Dropdown>
|
||||
<DropdownButton className="tall">
|
||||
{{
|
||||
[CONTENT_TYPE_JSON]: 'JSON',
|
||||
[CONTENT_TYPE_PLAINTEXT]: 'Raw',
|
||||
}[previewMode]}
|
||||
<i className="fa fa-caret-down space-left" />
|
||||
</DropdownButton>
|
||||
<DropdownItem onClick={onClick} value={CONTENT_TYPE_JSON}>
|
||||
JSON
|
||||
</DropdownItem>
|
||||
<DropdownItem onClick={onClick} value={CONTENT_TYPE_PLAINTEXT}>
|
||||
Raw
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
import React, { forwardRef, useCallback } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { hotKeyRefs } from '../../../common/hotkeys';
|
||||
import * as requestOperations from '../../../models/helpers/request-operations';
|
||||
import { incrementDeletedRequests } from '../../../models/stats';
|
||||
import { WebSocketRequest } from '../../../models/websocket-request';
|
||||
import { updateRequestMetaByParentId } from '../../hooks/create-request';
|
||||
import { selectHotKeyRegistry } from '../../redux/selectors';
|
||||
import { type DropdownHandle, type DropdownProps, Dropdown } from '../base/dropdown/dropdown';
|
||||
import { DropdownButton } from '../base/dropdown/dropdown-button';
|
||||
import { DropdownDivider } from '../base/dropdown/dropdown-divider';
|
||||
import { DropdownHint } from '../base/dropdown/dropdown-hint';
|
||||
import { DropdownItem } from '../base/dropdown/dropdown-item';
|
||||
import { PromptButton } from '../base/prompt-button';
|
||||
|
||||
interface Props extends Pick<DropdownProps, 'right'> {
|
||||
handleDuplicateRequest: Function;
|
||||
isPinned: Boolean;
|
||||
request: WebSocketRequest;
|
||||
}
|
||||
|
||||
export const WebSocketRequestActionsDropdown = forwardRef<DropdownHandle, Props>(({
|
||||
handleDuplicateRequest,
|
||||
isPinned,
|
||||
request,
|
||||
right,
|
||||
}, ref) => {
|
||||
const hotKeyRegistry = useSelector(selectHotKeyRegistry);
|
||||
|
||||
const duplicate = useCallback(() => {
|
||||
handleDuplicateRequest(request);
|
||||
}, [handleDuplicateRequest, request]);
|
||||
|
||||
const togglePin = useCallback(() => {
|
||||
updateRequestMetaByParentId(request._id, { pinned: !isPinned });
|
||||
}, [isPinned, request]);
|
||||
|
||||
const deleteRequest = useCallback(() => {
|
||||
incrementDeletedRequests();
|
||||
requestOperations.remove(request);
|
||||
}, [request]);
|
||||
|
||||
return (
|
||||
<Dropdown right={right} ref={ref}>
|
||||
<DropdownButton>
|
||||
<i className="fa fa-caret-down" />
|
||||
</DropdownButton>
|
||||
|
||||
<DropdownItem onClick={duplicate}>
|
||||
<i className="fa fa-copy" /> Duplicate
|
||||
<DropdownHint keyBindings={hotKeyRegistry[hotKeyRefs.REQUEST_SHOW_DUPLICATE.id]} />
|
||||
</DropdownItem>
|
||||
|
||||
<DropdownItem onClick={togglePin}>
|
||||
<i className="fa fa-thumb-tack" /> {isPinned ? 'Unpin' : 'Pin'}
|
||||
<DropdownHint keyBindings={hotKeyRegistry[hotKeyRefs.REQUEST_TOGGLE_PIN.id]} />
|
||||
</DropdownItem>
|
||||
|
||||
<DropdownItem
|
||||
buttonClass={PromptButton}
|
||||
onClick={deleteRequest}
|
||||
addIcon
|
||||
>
|
||||
<i className="fa fa-trash-o" /> Delete
|
||||
<DropdownHint keyBindings={hotKeyRegistry[hotKeyRefs.REQUEST_SHOW_DELETE.id]} />
|
||||
</DropdownItem>
|
||||
|
||||
<DropdownDivider />
|
||||
</Dropdown>
|
||||
);
|
||||
});
|
||||
|
||||
WebSocketRequestActionsDropdown.displayName = 'WebSocketRequestActionsDropdown';
|
||||
@@ -3,9 +3,11 @@ import React, { FC } from 'react';
|
||||
import { AuthInputRow } from './components/auth-input-row';
|
||||
import { AuthPrivateKeyRow } from './components/auth-private-key-row';
|
||||
import { AuthTableBody } from './components/auth-table-body';
|
||||
import { AuthToggleRow } from './components/auth-toggle-row';
|
||||
|
||||
export const AsapAuth: FC = () => (
|
||||
<AuthTableBody>
|
||||
<AuthToggleRow label="Enabled" property="disabled" invert />
|
||||
<AuthInputRow label='Issuer (iss)' property='issuer' />
|
||||
<AuthInputRow label='Subject (sub)' property='subject' />
|
||||
<AuthInputRow label='Audience (aud)' property='audience' />
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
AUTH_OAUTH_1,
|
||||
AUTH_OAUTH_2,
|
||||
} from '../../../../common/constants';
|
||||
import { isRequest } from '../../../../models/request';
|
||||
import { selectActiveRequest } from '../../../redux/selectors';
|
||||
import { AsapAuth } from './asap-auth';
|
||||
import { AWSAuth } from './aws-auth';
|
||||
@@ -26,10 +25,10 @@ import { NTLMAuth } from './ntlm-auth';
|
||||
import { OAuth1Auth } from './o-auth-1-auth';
|
||||
import { OAuth2Auth } from './o-auth-2-auth';
|
||||
|
||||
export const AuthWrapper: FC = () => {
|
||||
export const AuthWrapper: FC<{ disabled?: boolean }> = ({ disabled = false }) => {
|
||||
const request = useSelector(selectActiveRequest);
|
||||
|
||||
if (!request || !isRequest(request)) {
|
||||
if (!request || !('authentication' in request)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -38,7 +37,7 @@ export const AuthWrapper: FC = () => {
|
||||
let authBody: ReactNode = null;
|
||||
|
||||
if (type === AUTH_BASIC) {
|
||||
authBody = <BasicAuth />;
|
||||
authBody = <BasicAuth disabled={disabled} />;
|
||||
} else if (type === AUTH_OAUTH_2) {
|
||||
authBody = <OAuth2Auth />;
|
||||
} else if (type === AUTH_HAWK) {
|
||||
@@ -46,11 +45,11 @@ export const AuthWrapper: FC = () => {
|
||||
} else if (type === AUTH_OAUTH_1) {
|
||||
authBody = <OAuth1Auth />;
|
||||
} else if (type === AUTH_DIGEST) {
|
||||
authBody = <DigestAuth />;
|
||||
authBody = <DigestAuth disabled={disabled} />;
|
||||
} else if (type === AUTH_NTLM) {
|
||||
authBody = <NTLMAuth />;
|
||||
} else if (type === AUTH_BEARER) {
|
||||
authBody = <BearerAuth />;
|
||||
authBody = <BearerAuth disabled={disabled} />;
|
||||
} else if (type === AUTH_AWS_IAM) {
|
||||
authBody = <AWSAuth />;
|
||||
} else if (type === AUTH_NETRC) {
|
||||
|
||||
@@ -2,9 +2,11 @@ import React, { FC } from 'react';
|
||||
|
||||
import { AuthInputRow } from './components/auth-input-row';
|
||||
import { AuthTableBody } from './components/auth-table-body';
|
||||
import { AuthToggleRow } from './components/auth-toggle-row';
|
||||
|
||||
export const AWSAuth: FC = () => (
|
||||
<AuthTableBody>
|
||||
<AuthToggleRow label="Enabled" property="disabled" invert />
|
||||
<AuthInputRow
|
||||
label="Access Key ID"
|
||||
property="accessKeyId"
|
||||
|
||||
@@ -4,14 +4,16 @@ import { AuthInputRow } from './components/auth-input-row';
|
||||
import { AuthTableBody } from './components/auth-table-body';
|
||||
import { AuthToggleRow } from './components/auth-toggle-row';
|
||||
|
||||
export const BasicAuth: FC = () => (
|
||||
export const BasicAuth: FC<{ disabled?: boolean }> = ({ disabled = false }) => (
|
||||
<AuthTableBody>
|
||||
<AuthInputRow label="Username" property="username" />
|
||||
<AuthInputRow label="Password" property="password" mask />
|
||||
<AuthToggleRow label="Enabled" property="disabled" invert disabled={disabled} />
|
||||
<AuthInputRow label="Username" property="username" disabled={disabled} />
|
||||
<AuthInputRow label="Password" property="password" mask disabled={disabled} />
|
||||
<AuthToggleRow
|
||||
label="Use ISO 8859-1"
|
||||
help="Check this to use ISO-8859-1 encoding instead of default UTF-8"
|
||||
property='useISO88591'
|
||||
disabled={disabled}
|
||||
/>
|
||||
</AuthTableBody>
|
||||
);
|
||||
|
||||
@@ -2,10 +2,12 @@ import React, { FC } from 'react';
|
||||
|
||||
import { AuthInputRow } from './components/auth-input-row';
|
||||
import { AuthTableBody } from './components/auth-table-body';
|
||||
import { AuthToggleRow } from './components/auth-toggle-row';
|
||||
|
||||
export const BearerAuth: FC = () => (
|
||||
export const BearerAuth: FC<{ disabled?: boolean }> = ({ disabled = false }) => (
|
||||
<AuthTableBody>
|
||||
<AuthInputRow label='Token' property='token' />
|
||||
<AuthInputRow label='Prefix' property='prefix' />
|
||||
<AuthToggleRow label="Enabled" property="disabled" invert disabled={disabled} />
|
||||
<AuthInputRow label='Token' property='token' disabled={disabled} />
|
||||
<AuthInputRow label='Prefix' property='prefix' disabled={disabled} />
|
||||
</AuthTableBody>
|
||||
);
|
||||
|
||||
@@ -14,9 +14,10 @@ interface Props extends Pick<ComponentProps<typeof OneLineEditor>, 'getAutocompl
|
||||
property: string;
|
||||
help?: ReactNode;
|
||||
mask?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const AuthInputRow: FC<Props> = ({ label, getAutocompleteConstants, property, mask, mode, help }) => {
|
||||
export const AuthInputRow: FC<Props> = ({ label, getAutocompleteConstants, property, mask, mode, help, disabled = false }) => {
|
||||
const { showPasswords } = useSelector(selectSettings);
|
||||
const { activeRequest: { authentication }, patchAuth } = useActiveRequest();
|
||||
|
||||
@@ -32,13 +33,14 @@ export const AuthInputRow: FC<Props> = ({ label, getAutocompleteConstants, prope
|
||||
const id = toKebabCase(label);
|
||||
|
||||
return (
|
||||
<AuthRow labelFor={id} label={label} help={help}>
|
||||
<AuthRow labelFor={id} label={label} help={help} disabled={disabled}>
|
||||
<OneLineEditor
|
||||
id={id}
|
||||
type={isMasked ? 'password' : 'text'}
|
||||
mode={mode}
|
||||
onChange={onChange}
|
||||
disabled={authentication.disabled}
|
||||
readOnly={disabled}
|
||||
defaultValue={authentication[property] || ''}
|
||||
getAutocompleteConstants={getAutocompleteConstants}
|
||||
/>
|
||||
@@ -47,6 +49,7 @@ export const AuthInputRow: FC<Props> = ({ label, getAutocompleteConstants, prope
|
||||
className="btn btn--super-duper-compact pointer"
|
||||
onClick={onClick}
|
||||
value={isMasked}
|
||||
disabled={disabled}
|
||||
>
|
||||
{isMasked ? <i className="fa fa-eye" data-testid="reveal-password-icon" /> : <i className="fa fa-eye-slash" data-testid="mask-password-icon" />}
|
||||
</Button>
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import React, { FC, ReactNode } from 'react';
|
||||
|
||||
import { AuthEnabledRow } from './auth-enabled-row';
|
||||
|
||||
export const AuthTableBody: FC<{children: ReactNode}> = ({ children }) => (
|
||||
<div className="pad">
|
||||
<table>
|
||||
<tbody>
|
||||
<AuthEnabledRow />
|
||||
{children}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -12,6 +12,7 @@ interface Props {
|
||||
help?: ReactNode;
|
||||
onTitle?: string;
|
||||
offTitle?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const ToggleIcon: FC<{isOn: boolean}> = ({ isOn }) => isOn ? <i data-testid="toggle-is-on" className="fa fa-check-square-o" /> : <i data-testid="toggle-is-off" className="fa fa-square-o" />;
|
||||
@@ -23,6 +24,7 @@ export const AuthToggleRow: FC<Props> = ({
|
||||
invert,
|
||||
onTitle = 'Disable item',
|
||||
offTitle = 'Enable item',
|
||||
disabled = false,
|
||||
}) => {
|
||||
const { activeRequest: { authentication }, patchAuth } = useActiveRequest();
|
||||
|
||||
@@ -38,13 +40,14 @@ export const AuthToggleRow: FC<Props> = ({
|
||||
const title = isActuallyOn ? onTitle : offTitle;
|
||||
|
||||
return (
|
||||
<AuthRow labelFor={id} label={label} help={help}>
|
||||
<AuthRow labelFor={id} label={label} help={help} disabled={disabled}>
|
||||
<Button
|
||||
className="btn btn--super-duper-compact"
|
||||
id={id}
|
||||
onClick={toggle}
|
||||
value={!databaseValue}
|
||||
title={title}
|
||||
disabled={disabled}
|
||||
>
|
||||
<ToggleIcon isOn={isActuallyOn} />
|
||||
</Button>
|
||||
|
||||
@@ -2,10 +2,12 @@ import React, { FC } from 'react';
|
||||
|
||||
import { AuthInputRow } from './components/auth-input-row';
|
||||
import { AuthTableBody } from './components/auth-table-body';
|
||||
import { AuthToggleRow } from './components/auth-toggle-row';
|
||||
|
||||
export const DigestAuth: FC = () => (
|
||||
export const DigestAuth: FC<{ disabled?: boolean }> = ({ disabled = false }) => (
|
||||
<AuthTableBody>
|
||||
<AuthInputRow label='Username' property='username' />
|
||||
<AuthInputRow label='Password' property='password' mask />
|
||||
<AuthToggleRow label="Enabled" property="disabled" invert disabled={disabled} />
|
||||
<AuthInputRow label='Username' property='username' disabled={disabled} />
|
||||
<AuthInputRow label='Password' property='password' mask disabled={disabled} />
|
||||
</AuthTableBody>
|
||||
);
|
||||
|
||||
@@ -11,6 +11,7 @@ import { AuthToggleRow } from './components/auth-toggle-row';
|
||||
|
||||
export const HawkAuth: FC = () => (
|
||||
<AuthTableBody>
|
||||
<AuthToggleRow label="Enabled" property="disabled" invert />
|
||||
<AuthInputRow label='Auth Id' property='id' />
|
||||
<AuthInputRow label='Auth Key' property='key' />
|
||||
<AuthSelectRow
|
||||
|
||||
@@ -2,9 +2,11 @@ import React, { FC } from 'react';
|
||||
|
||||
import { AuthInputRow } from './components/auth-input-row';
|
||||
import { AuthTableBody } from './components/auth-table-body';
|
||||
import { AuthToggleRow } from './components/auth-toggle-row';
|
||||
|
||||
export const NTLMAuth: FC = () => (
|
||||
<AuthTableBody>
|
||||
<AuthToggleRow label="Enabled" property="disabled" invert />
|
||||
<AuthInputRow label="Username" property="username" />
|
||||
<AuthInputRow label="Password" property="password" mask />
|
||||
</AuthTableBody>
|
||||
|
||||
@@ -37,6 +37,7 @@ export const OAuth1Auth: FC = () => {
|
||||
|
||||
return (
|
||||
<AuthTableBody>
|
||||
<AuthToggleRow label="Enabled" property="disabled" invert />
|
||||
<AuthInputRow label='Consumer Key' property='consumerKey' />
|
||||
<AuthInputRow label='Consumer Secret' property='consumerSecret' />
|
||||
<AuthInputRow label='Token Key' property='tokenKey' />
|
||||
|
||||
@@ -255,6 +255,7 @@ export const OAuth2Auth: FC = () => {
|
||||
return (
|
||||
<>
|
||||
<AuthTableBody>
|
||||
<AuthToggleRow label="Enabled" property="disabled" invert />
|
||||
<AuthSelectRow
|
||||
label='Grant Type'
|
||||
property='grantType'
|
||||
|
||||
@@ -3,17 +3,20 @@ import React, { FC, useCallback } from 'react';
|
||||
import { getCommonHeaderNames, getCommonHeaderValues } from '../../../common/common-headers';
|
||||
import { update } from '../../../models/helpers/request-operations';
|
||||
import type { Request, RequestHeader } from '../../../models/request';
|
||||
import { isWebSocketRequest, WebSocketRequest } from '../../../models/websocket-request';
|
||||
import { CodeEditor } from '../codemirror/code-editor';
|
||||
import { KeyValueEditor } from '../key-value-editor/key-value-editor';
|
||||
|
||||
interface Props {
|
||||
bulk: boolean;
|
||||
request: Request;
|
||||
isDisabled?: boolean;
|
||||
request: Request | WebSocketRequest;
|
||||
}
|
||||
|
||||
export const RequestHeadersEditor: FC<Props> = ({
|
||||
request,
|
||||
bulk,
|
||||
isDisabled,
|
||||
}) => {
|
||||
const handleBulkUpdate = useCallback((headersString: string) => {
|
||||
const headers: {
|
||||
@@ -82,6 +85,8 @@ export const RequestHeadersEditor: FC<Props> = ({
|
||||
handleGetAutocompleteNameConstants={getCommonHeaderNames}
|
||||
handleGetAutocompleteValueConstants={getCommonHeaderValues}
|
||||
onChange={onChangeHeaders}
|
||||
isDisabled={isDisabled}
|
||||
isWebSocketRequest={isWebSocketRequest(request)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import React, { FC, SyntheticEvent, useCallback } from 'react';
|
||||
|
||||
import { GrpcRequest, isGrpcRequest } from '../../../models/grpc-request';
|
||||
import type { Request } from '../../../models/request';
|
||||
import { isRequest, Request } from '../../../models/request';
|
||||
import { isWebSocketRequest, WebSocketRequest } from '../../../models/websocket-request';
|
||||
import { GrpcTag } from '../tags/grpc-tag';
|
||||
import { MethodTag } from '../tags/method-tag';
|
||||
import { WebSocketTag } from '../tags/websocket-tag';
|
||||
|
||||
interface Props {
|
||||
handleSetItemSelected: (...args: any[]) => any;
|
||||
isSelected: boolean;
|
||||
request: Request | GrpcRequest;
|
||||
request: Request | WebSocketRequest | GrpcRequest;
|
||||
}
|
||||
|
||||
export const RequestRow: FC<Props> = ({
|
||||
@@ -27,7 +29,9 @@ export const RequestRow: FC<Props> = ({
|
||||
<input type="checkbox" checked={isSelected} onChange={onChange} />
|
||||
</div>
|
||||
<button className="wide">
|
||||
{isGrpcRequest(request) ? <GrpcTag /> : <MethodTag method={request.method} />}
|
||||
{isRequest(request) ? <MethodTag method={request.method} /> : null}
|
||||
{isGrpcRequest(request) ? <GrpcTag /> : null}
|
||||
{isWebSocketRequest(request) ? <WebSocketTag /> : null}
|
||||
<span className="inline-block">{request.name}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { FC } from 'react';
|
||||
|
||||
import { isGrpcRequest } from '../../../models/grpc-request';
|
||||
import { isRequest } from '../../../models/request';
|
||||
import { isWebSocketRequest } from '../../../models/websocket-request';
|
||||
import type { Node } from '../modals/export-requests-modal';
|
||||
import { RequestGroupRow } from './request-group-row';
|
||||
import { RequestRow } from './request-row';
|
||||
@@ -18,7 +19,7 @@ export const Tree: FC<Props> = ({ root, handleSetRequestGroupCollapsed, handleSe
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isRequest(node.doc) || isGrpcRequest(node.doc)) {
|
||||
if (isRequest(node.doc) || isWebSocketRequest(node.doc) || isGrpcRequest(node.doc)) {
|
||||
return (
|
||||
<RequestRow
|
||||
key={node.doc._id}
|
||||
|
||||
@@ -41,6 +41,8 @@ interface Props {
|
||||
onDelete?: Function;
|
||||
onCreate?: Function;
|
||||
className?: string;
|
||||
isDisabled?: boolean;
|
||||
isWebSocketRequest?: boolean;
|
||||
}
|
||||
|
||||
interface State {
|
||||
@@ -405,7 +407,7 @@ export class KeyValueEditor extends PureComponent<Props, State> {
|
||||
|
||||
_setFocusedPair(pair: Pair) {
|
||||
if (pair) {
|
||||
this._focusedPairId = pair.id;
|
||||
this._focusedPairId = pair.id || 'n/a';
|
||||
} else {
|
||||
this._focusedPairId = null;
|
||||
}
|
||||
@@ -435,11 +437,36 @@ export class KeyValueEditor extends PureComponent<Props, State> {
|
||||
allowMultiline,
|
||||
sortable,
|
||||
disableDelete,
|
||||
isDisabled,
|
||||
isWebSocketRequest,
|
||||
} = this.props;
|
||||
const { pairs } = this.state;
|
||||
|
||||
const classes = classnames('key-value-editor', 'wide', className);
|
||||
const hasMaxPairsAndNotExceeded = !maxPairs || pairs.length < maxPairs;
|
||||
const showNewHeaderInput = !isDisabled && hasMaxPairsAndNotExceeded;
|
||||
const readOnlyPairs = [
|
||||
{ name: 'Connection', value: 'Upgrade' },
|
||||
{ name: 'Upgrade', value: 'websocket' },
|
||||
{ name: 'Sec-WebSocket-Key', value: '<calculated at runtime>' },
|
||||
{ name: 'Sec-WebSocket-Version', value: '13' },
|
||||
{ name: 'Sec-WebSocket-Extensions', value: 'permessage-deflate; client_max_window_bits' },
|
||||
];
|
||||
return (
|
||||
<ul className={classes}>
|
||||
{isWebSocketRequest ? readOnlyPairs.map((pair, i) => (
|
||||
<Row
|
||||
key={i}
|
||||
index={i}
|
||||
sortable={true}
|
||||
displayDescription={this.state.displayDescription}
|
||||
descriptionPlaceholder={descriptionPlaceholder}
|
||||
readOnly
|
||||
hideButtons
|
||||
forceInput
|
||||
pair={pair}
|
||||
/>
|
||||
)) : null}
|
||||
{pairs.map((pair, i) => (
|
||||
<Row
|
||||
noDelete={disableDelete}
|
||||
@@ -466,17 +493,18 @@ export class KeyValueEditor extends PureComponent<Props, State> {
|
||||
handleGetAutocompleteValueConstants={handleGetAutocompleteValueConstants}
|
||||
allowMultiline={allowMultiline}
|
||||
allowFile={allowFile}
|
||||
readOnly={isDisabled}
|
||||
hideButtons={isDisabled}
|
||||
pair={pair}
|
||||
/>
|
||||
))}
|
||||
|
||||
{!maxPairs || pairs.length < maxPairs ? (
|
||||
{showNewHeaderInput ? (
|
||||
<Row
|
||||
key="empty-row"
|
||||
hideButtons
|
||||
sortable
|
||||
noDropZone
|
||||
readOnly
|
||||
forceInput
|
||||
index={-1}
|
||||
onChange={noop}
|
||||
@@ -502,11 +530,9 @@ export class KeyValueEditor extends PureComponent<Props, State> {
|
||||
onFocusDescription={this._handleAddFromDescription}
|
||||
allowMultiline={allowMultiline}
|
||||
allowFile={allowFile}
|
||||
// @ts-expect-error -- TSCONVERSION missing defaults
|
||||
pair={{
|
||||
name: '',
|
||||
value: '',
|
||||
description: '',
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -19,10 +19,10 @@ import { CodePromptModal } from '../modals/code-prompt-modal';
|
||||
import { showModal } from '../modals/index';
|
||||
|
||||
export interface Pair {
|
||||
id: string;
|
||||
id?: string;
|
||||
name: string;
|
||||
value: string;
|
||||
description: string;
|
||||
description?: string;
|
||||
fileName?: string;
|
||||
type?: string;
|
||||
disabled?: boolean;
|
||||
@@ -34,11 +34,11 @@ export type AutocompleteHandler = (pair: Pair) => string[] | PromiseLike<string[
|
||||
type DragDirection = 0 | 1 | -1;
|
||||
|
||||
interface Props {
|
||||
onChange: (pair: Pair) => void;
|
||||
onDelete: (pair: Pair) => void;
|
||||
onFocusName: (pair: Pair, event: FocusEvent | React.FocusEvent<Element, Element>) => void;
|
||||
onFocusValue: (pair: Pair, event: FocusEvent | React.FocusEvent<Element, Element>) => void;
|
||||
onFocusDescription: (pair: Pair, event: FocusEvent | React.FocusEvent<Element, Element>) => void;
|
||||
onChange?: (pair: Pair) => void;
|
||||
onDelete?: (pair: Pair) => void;
|
||||
onFocusName?: (pair: Pair, event: FocusEvent | React.FocusEvent<Element, Element>) => void;
|
||||
onFocusValue?: (pair: Pair, event: FocusEvent | React.FocusEvent<Element, Element>) => void;
|
||||
onFocusDescription?: (pair: Pair, event: FocusEvent | React.FocusEvent<Element, Element>) => void;
|
||||
displayDescription: boolean;
|
||||
index: number;
|
||||
pair: Pair;
|
||||
@@ -195,15 +195,15 @@ class KeyValueEditorRowInternal extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
_handleFocusName(event: FocusEvent | React.FocusEvent<Element, Element>) {
|
||||
this.props.onFocusName(this.props.pair, event);
|
||||
this.props.onFocusName?.(this.props.pair, event);
|
||||
}
|
||||
|
||||
_handleFocusValue(event: FocusEvent | React.FocusEvent<Element, Element>) {
|
||||
this.props.onFocusValue(this.props.pair, event);
|
||||
this.props.onFocusValue?.(this.props.pair, event);
|
||||
}
|
||||
|
||||
_handleFocusDescription(event: FocusEvent | React.FocusEvent<Element, Element>) {
|
||||
this.props.onFocusDescription(this.props.pair, event);
|
||||
this.props.onFocusDescription?.(this.props.pair, event);
|
||||
}
|
||||
|
||||
_handleBlurName(event: FocusEvent | React.FocusEvent<Element, Element>) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import * as models from '../../../models';
|
||||
import { GrpcRequest, isGrpcRequest } from '../../../models/grpc-request';
|
||||
import { isRequest, Request } from '../../../models/request';
|
||||
import { isRequestGroup, RequestGroup } from '../../../models/request-group';
|
||||
import { isWebSocketRequest, WebSocketRequest } from '../../../models/websocket-request';
|
||||
import { RootState } from '../../redux/modules';
|
||||
import { exportRequestsToFile } from '../../redux/modules/global';
|
||||
import { selectSidebarChildren } from '../../redux/sidebar-selectors';
|
||||
@@ -18,7 +19,7 @@ import { ModalHeader } from '../base/modal-header';
|
||||
import { Tree } from '../export-requests/tree';
|
||||
|
||||
export interface Node {
|
||||
doc: Request | GrpcRequest | RequestGroup;
|
||||
doc: Request | WebSocketRequest | GrpcRequest | RequestGroup;
|
||||
children: Node[];
|
||||
collapsed: boolean;
|
||||
totalRequests: number;
|
||||
@@ -67,7 +68,7 @@ export class ExportRequestsModalClass extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
getSelectedRequestIds(node: Node): string[] {
|
||||
const docIsRequest = isRequest(node.doc) || isGrpcRequest(node.doc);
|
||||
const docIsRequest = isRequest(node.doc) || isWebSocketRequest(node.doc) || isGrpcRequest(node.doc);
|
||||
|
||||
if (docIsRequest && node.selectedRequests === node.totalRequests) {
|
||||
return [node.doc._id];
|
||||
@@ -117,7 +118,7 @@ export class ExportRequestsModalClass extends PureComponent<Props, State> {
|
||||
let totalRequests = children
|
||||
.map(child => child.totalRequests)
|
||||
.reduce((acc, totalRequests) => acc + totalRequests, 0);
|
||||
const docIsRequest = isRequest(item.doc) || isGrpcRequest(item.doc);
|
||||
const docIsRequest = isRequest(item.doc) || isWebSocketRequest(item.doc) || isGrpcRequest(item.doc);
|
||||
|
||||
if (docIsRequest) {
|
||||
totalRequests++;
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, { PureComponent } from 'react';
|
||||
|
||||
import { AUTOBIND_CFG } from '../../../common/constants';
|
||||
import { docsTemplateTags } from '../../../common/documentation';
|
||||
import { isRequest } from '../../../models/request';
|
||||
import { Link } from '../base/link';
|
||||
import { Modal } from '../base/modal';
|
||||
import { ModalBody } from '../base/modal-body';
|
||||
@@ -58,7 +59,7 @@ export class RequestRenderErrorModal extends PureComponent<{}, State> {
|
||||
Failed to render <strong>{fullPath}</strong> prior to sending
|
||||
</p>
|
||||
<div className="pad-top-sm">
|
||||
{error.path.match(/^body/) && (
|
||||
{error.path.match(/^body/) && isRequest(request) && (
|
||||
<button
|
||||
className="btn btn--clicky margin-right-sm"
|
||||
onClick={this._handleShowRequestSettings}
|
||||
|
||||
@@ -14,6 +14,7 @@ import * as models from '../../../models';
|
||||
import { GrpcRequest, isGrpcRequest } from '../../../models/grpc-request';
|
||||
import { isRequest, Request } from '../../../models/request';
|
||||
import { isRequestGroup, RequestGroup } from '../../../models/request-group';
|
||||
import { isWebSocketRequest, WebSocketRequest } from '../../../models/websocket-request';
|
||||
import { Workspace } from '../../../models/workspace';
|
||||
import { updateRequestMetaByParentId } from '../../hooks/create-request';
|
||||
import { RootState } from '../../redux/modules';
|
||||
@@ -51,7 +52,7 @@ const mapDispatchToProps = (dispatch: Dispatch<AnyAction>) => {
|
||||
interface State {
|
||||
searchString: string;
|
||||
workspacesForActiveProject: Workspace[];
|
||||
matchedRequests: (Request | GrpcRequest)[];
|
||||
matchedRequests: (Request | WebSocketRequest | GrpcRequest)[];
|
||||
matchedWorkspaces: Workspace[];
|
||||
activeIndex: number;
|
||||
maxRequests: number;
|
||||
@@ -167,7 +168,7 @@ class RequestSwitcherModal extends PureComponent<ReduxProps, State> {
|
||||
this.modal?.hide();
|
||||
}
|
||||
|
||||
async _activateRequest(request?: Request | GrpcRequest) {
|
||||
async _activateRequest(request?: Request | WebSocketRequest | GrpcRequest) {
|
||||
if (!request) {
|
||||
return;
|
||||
}
|
||||
@@ -184,7 +185,7 @@ class RequestSwitcherModal extends PureComponent<ReduxProps, State> {
|
||||
}
|
||||
|
||||
/** Return array of path segments for given request or folder */
|
||||
_groupOf(requestOrRequestGroup: Request | GrpcRequest | RequestGroup): string[] {
|
||||
_groupOf(requestOrRequestGroup: Request | WebSocketRequest | GrpcRequest | RequestGroup): string[] {
|
||||
const { workspaceRequestsAndRequestGroups } = this.props;
|
||||
const requestGroups = workspaceRequestsAndRequestGroups.filter(isRequestGroup);
|
||||
const matchedGroups = requestGroups.filter(g => g._id === requestOrRequestGroup.parentId);
|
||||
@@ -204,7 +205,7 @@ class RequestSwitcherModal extends PureComponent<ReduxProps, State> {
|
||||
return this._groupOf(matchedGroups[0]);
|
||||
}
|
||||
|
||||
_isMatch(request: Request | GrpcRequest, searchStrings: string): number | null {
|
||||
_isMatch(request: Request | WebSocketRequest | GrpcRequest, searchStrings: string): number | null {
|
||||
let finalUrl = request.url;
|
||||
let method = '';
|
||||
|
||||
@@ -254,7 +255,7 @@ class RequestSwitcherModal extends PureComponent<ReduxProps, State> {
|
||||
|
||||
// OPTIMIZATION: This only filters if we have a filter
|
||||
let matchedRequests = (workspaceRequestsAndRequestGroups
|
||||
.filter(child => isRequest(child) || isGrpcRequest(child)) as (Request | GrpcRequest)[])
|
||||
.filter(child => isRequest(child) || isWebSocketRequest(child) || isGrpcRequest(child)) as (Request | WebSocketRequest | GrpcRequest)[])
|
||||
.sort((a, b) => {
|
||||
const aLA = lastActiveMap[a._id] || 0;
|
||||
const bLA = lastActiveMap[b._id] || 0;
|
||||
@@ -446,7 +447,7 @@ class RequestSwitcherModal extends PureComponent<ReduxProps, State> {
|
||||
</div>
|
||||
)}
|
||||
<ul>
|
||||
{matchedRequests.map((r: Request | GrpcRequest, i) => {
|
||||
{matchedRequests.map((r: Request | WebSocketRequest | GrpcRequest, i) => {
|
||||
const requestGroup = requestGroups.find(rg => rg._id === r.parentId);
|
||||
const buttonClasses = classnames(
|
||||
'btn btn--expandable-small wide text-left pad-bottom',
|
||||
|
||||
@@ -1,73 +1,80 @@
|
||||
import { autoBindMethodsForReact } from 'class-autobind-decorator';
|
||||
import React, { PureComponent } from 'react';
|
||||
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';
|
||||
|
||||
import { AUTOBIND_CFG } from '../../../common/constants';
|
||||
import { ResponseTimelineEntry } from '../../../main/network/libcurl-promise';
|
||||
import * as models from '../../../models/index';
|
||||
import type { Response } from '../../../models/response';
|
||||
import { ResponseTimelineViewer } from '../../components/viewers/response-timeline-viewer';
|
||||
import { Modal } from '../base/modal';
|
||||
import { Modal, ModalProps } from '../base/modal';
|
||||
import { ModalBody } from '../base/modal-body';
|
||||
import { ModalHeader } from '../base/modal-header';
|
||||
|
||||
interface State {
|
||||
response: Response | null;
|
||||
interface ResponseDebugModalOptions {
|
||||
responseId?: string;
|
||||
response?: Response | null;
|
||||
showBody?: boolean;
|
||||
title: string | null;
|
||||
title?: string | null;
|
||||
}
|
||||
|
||||
@autoBindMethodsForReact(AUTOBIND_CFG)
|
||||
export class ResponseDebugModal extends PureComponent<{}, State> {
|
||||
modal: Modal | null = null;
|
||||
|
||||
state: State = {
|
||||
response: null,
|
||||
showBody: false,
|
||||
interface State {
|
||||
responseId?: string;
|
||||
timeline?: ResponseTimelineEntry[];
|
||||
title?: string | null;
|
||||
}
|
||||
export interface ResponseDebugModalHandle {
|
||||
show: (options: ResponseDebugModalOptions) => void;
|
||||
hide: () => void;
|
||||
}
|
||||
export const ResponseDebugModal = forwardRef<ResponseDebugModalHandle, ModalProps>((_, ref) => {
|
||||
const modalRef = useRef<Modal>(null);
|
||||
const [state, setState] = useState<State>({
|
||||
responseId: '',
|
||||
timeline: [],
|
||||
title: '',
|
||||
};
|
||||
});
|
||||
useImperativeHandle(ref, () => ({
|
||||
hide: () => {
|
||||
modalRef.current?.hide();
|
||||
},
|
||||
show: async options => {
|
||||
let response = options.response;
|
||||
if (!response) {
|
||||
response = await models.response.getById(options.responseId || 'n/a');
|
||||
}
|
||||
if (!response) {
|
||||
console.error('No response found');
|
||||
return;
|
||||
}
|
||||
const timeline = await models.response.getTimeline(response, options.showBody);
|
||||
setState({
|
||||
responseId: response._id,
|
||||
timeline,
|
||||
title: options.title || null,
|
||||
});
|
||||
modalRef.current?.show();
|
||||
},
|
||||
}), []);
|
||||
const { responseId, timeline, title } = state;
|
||||
return (
|
||||
<Modal ref={modalRef} tall>
|
||||
<ModalHeader>{title || 'Response Timeline'}</ModalHeader>
|
||||
<ModalBody>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
}}
|
||||
className="tall"
|
||||
>
|
||||
{(responseId && timeline) ? (
|
||||
<ResponseTimelineViewer
|
||||
key={responseId}
|
||||
timeline={timeline}
|
||||
/>
|
||||
) : (
|
||||
<div>No response found</div>
|
||||
)}
|
||||
</div>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
|
||||
_setModalRef(modal: Modal) {
|
||||
this.modal = modal;
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.modal?.hide();
|
||||
}
|
||||
|
||||
async show(options: { responseId?: string; response?: Response; title?: string; showBody?: boolean }) {
|
||||
const response = options.response
|
||||
? options.response
|
||||
: await models.response.getById(options.responseId || 'n/a');
|
||||
this.setState({
|
||||
response,
|
||||
title: options.title || null,
|
||||
showBody: options.showBody,
|
||||
});
|
||||
this.modal?.show();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { response, title, showBody } = this.state;
|
||||
return (
|
||||
<Modal ref={this._setModalRef} tall>
|
||||
<ModalHeader>{title || 'Response Timeline'}</ModalHeader>
|
||||
<ModalBody>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
}}
|
||||
className="tall"
|
||||
>
|
||||
{response ? (
|
||||
<ResponseTimelineViewer
|
||||
response={response}
|
||||
showBody={showBody}
|
||||
/>
|
||||
) : (
|
||||
<div>No response found</div>
|
||||
)}
|
||||
</div>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
ResponseDebugModal.displayName = 'ResponseDebugModal';
|
||||
|
||||
@@ -165,7 +165,7 @@ export const RequestPane: FC<Props> = ({
|
||||
</Tab>
|
||||
<Tab tabIndex="-1">
|
||||
<button>
|
||||
Header
|
||||
Headers
|
||||
{numHeaders > 0 && <span className="bubble space-left">{numHeaders}</span>}
|
||||
</button>
|
||||
</Tab>
|
||||
|
||||
@@ -41,7 +41,7 @@ export const ResponsePane: FC<Props> = ({
|
||||
handleSetActiveResponse,
|
||||
request,
|
||||
}) => {
|
||||
const response = useSelector(selectActiveResponse);
|
||||
const response = useSelector(selectActiveResponse) as Response | null;
|
||||
const filterHistory = useSelector(selectResponseFilterHistory);
|
||||
const filter = useSelector(selectResponseFilter);
|
||||
const settings = useSelector(selectSettings);
|
||||
@@ -122,7 +122,7 @@ export const ResponsePane: FC<Props> = ({
|
||||
</PlaceholderResponsePane>
|
||||
);
|
||||
}
|
||||
|
||||
const timeline = models.response.getTimeline(response);
|
||||
const cookieHeaders = getSetCookieHeaders(response.headers);
|
||||
return (
|
||||
<Pane type="response">
|
||||
@@ -154,7 +154,7 @@ export const ResponsePane: FC<Props> = ({
|
||||
</Tab>
|
||||
<Tab tabIndex="-1">
|
||||
<Button>
|
||||
Header{' '}
|
||||
Headers{' '}
|
||||
{response.headers.length > 0 && (
|
||||
<span className="bubble">{response.headers.length}</span>
|
||||
)}
|
||||
@@ -162,7 +162,7 @@ export const ResponsePane: FC<Props> = ({
|
||||
</Tab>
|
||||
<Tab tabIndex="-1">
|
||||
<Button>
|
||||
Cookie{' '}
|
||||
Cookies{' '}
|
||||
{cookieHeaders.length ? (
|
||||
<span className="bubble">{cookieHeaders.length}</span>
|
||||
) : null}
|
||||
@@ -212,7 +212,8 @@ export const ResponsePane: FC<Props> = ({
|
||||
<TabPanel className="react-tabs__tab-panel">
|
||||
<ErrorBoundary key={response._id} errorClassName="font-error pad text-center">
|
||||
<ResponseTimelineViewer
|
||||
response={response}
|
||||
key={response._id}
|
||||
timeline={timeline}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</TabPanel>
|
||||
|
||||
@@ -7,13 +7,14 @@ import * as models from '../../../models';
|
||||
import { GrpcRequest } from '../../../models/grpc-request';
|
||||
import { Request } from '../../../models/request';
|
||||
import { RequestGroup } from '../../../models/request-group';
|
||||
import { WebSocketRequest } from '../../../models/websocket-request';
|
||||
|
||||
export type DnDDragProps = ReturnType<typeof sourceCollect>;
|
||||
export type DnDDropProps = ReturnType<typeof targetCollect>;
|
||||
export type DnDProps = DnDDragProps & DnDDropProps;
|
||||
|
||||
export interface DragObject {
|
||||
item?: GrpcRequest | Request | RequestGroup;
|
||||
item?: GrpcRequest | Request | WebSocketRequest | RequestGroup;
|
||||
}
|
||||
|
||||
export const sourceCollect = (connect: DragSourceConnector, monitor: DragSourceMonitor) => ({
|
||||
|
||||
@@ -2,10 +2,11 @@ import React, { FC, Fragment } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { GrpcRequest, isGrpcRequest } from '../../../models/grpc-request';
|
||||
import { GrpcRequest } from '../../../models/grpc-request';
|
||||
import * as models from '../../../models/index';
|
||||
import { isRequest, Request } from '../../../models/request';
|
||||
import type { RequestGroup } from '../../../models/request-group';
|
||||
import { Request } from '../../../models/request';
|
||||
import { isRequestGroup, RequestGroup } from '../../../models/request-group';
|
||||
import { WebSocketRequest } from '../../../models/websocket-request';
|
||||
import { updateRequestMetaByParentId } from '../../hooks/create-request';
|
||||
import { selectActiveRequest, selectActiveWorkspaceMeta } from '../../redux/selectors';
|
||||
import { selectSidebarChildren } from '../../redux/sidebar-selectors';
|
||||
@@ -14,7 +15,7 @@ import { SidebarRequestGroupRow } from './sidebar-request-group-row';
|
||||
import { SidebarRequestRow } from './sidebar-request-row';
|
||||
|
||||
export interface Child {
|
||||
doc: Request | GrpcRequest | RequestGroup;
|
||||
doc: Request | GrpcRequest | WebSocketRequest | RequestGroup;
|
||||
children: Child[];
|
||||
collapsed: boolean;
|
||||
hidden: boolean;
|
||||
@@ -50,16 +51,37 @@ export const SidebarChildren: FC<Props> = ({
|
||||
updateRequestMetaByParentId(requestId, { lastActive: Date.now() });
|
||||
};
|
||||
|
||||
const RecursiveSidebarRows: FC<RecursiveSidebarRowsProps> = ({ rows, isInPinnedList }) => {
|
||||
const RecursiveSidebarRows: FC<RecursiveSidebarRowsProps> = ({
|
||||
rows,
|
||||
isInPinnedList,
|
||||
}) => {
|
||||
const activeRequest = useSelector(selectActiveRequest);
|
||||
const activeRequestId = activeRequest ? activeRequest._id : 'n/a';
|
||||
|
||||
return (
|
||||
<>
|
||||
{rows.map(row => (!isInPinnedList && row.hidden)
|
||||
? null
|
||||
: (isRequest(row.doc) || isGrpcRequest(row.doc))
|
||||
? (
|
||||
{rows
|
||||
.filter(row => !(!isInPinnedList && row.hidden))
|
||||
.map(row => {
|
||||
if (isRequestGroup(row.doc)) {
|
||||
return (
|
||||
<SidebarRequestGroupRow
|
||||
key={row.doc._id}
|
||||
filter={filter || ''}
|
||||
isActive={hasActiveChild(row.children, activeRequestId)}
|
||||
isCollapsed={row.collapsed}
|
||||
requestGroup={row.doc}
|
||||
>
|
||||
{row.children.filter(Boolean).length > 0 ? (
|
||||
<RecursiveSidebarRows
|
||||
isInPinnedList={isInPinnedList}
|
||||
rows={row.children}
|
||||
/>
|
||||
) : null}
|
||||
</SidebarRequestGroupRow>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<SidebarRequestRow
|
||||
key={row.doc._id}
|
||||
filter={isInPinnedList ? '' : filter || ''}
|
||||
@@ -70,19 +92,12 @@ export const SidebarChildren: FC<Props> = ({
|
||||
disableDragAndDrop={isInPinnedList}
|
||||
request={row.doc}
|
||||
/>
|
||||
) : (
|
||||
<SidebarRequestGroupRow
|
||||
key={row.doc._id}
|
||||
filter={filter || ''}
|
||||
isActive={hasActiveChild(row.children, activeRequestId)}
|
||||
isCollapsed={row.collapsed}
|
||||
requestGroup={row.doc}
|
||||
>
|
||||
{row.children.filter(Boolean).length > 0 ? <RecursiveSidebarRows isInPinnedList={isInPinnedList} rows={row.children} /> : null}
|
||||
</SidebarRequestGroupRow>
|
||||
))}
|
||||
</>);
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const { all, pinned } = sidebarChildren;
|
||||
const showSeparator = sidebarChildren.pinned.length > 0;
|
||||
const contextMenuPortal = ReactDOM.createPortal(
|
||||
|
||||
@@ -56,6 +56,10 @@ export const SidebarCreateDropdown: FC<Props> = ({ right }) => {
|
||||
<i className="fa fa-plus-circle" />gRPC Request
|
||||
</DropdownItem>
|
||||
|
||||
<DropdownItem value="WebSocket" onClick={create}>
|
||||
<i className="fa fa-plus-circle" />WebSocket Request
|
||||
</DropdownItem>
|
||||
|
||||
<DropdownItem onClick={createGroup}>
|
||||
<i className="fa fa-folder" />New Folder
|
||||
<DropdownHint keyBindings={hotKeyRegistry[hotKeyRefs.REQUEST_SHOW_CREATE_FOLDER.id]} />
|
||||
|
||||
@@ -7,8 +7,9 @@ import { CONTENT_TYPE_GRAPHQL } from '../../../common/constants';
|
||||
import { getMethodOverrideHeader } from '../../../common/misc';
|
||||
import { GrpcRequest, isGrpcRequest } from '../../../models/grpc-request';
|
||||
import * as requestOperations from '../../../models/helpers/request-operations';
|
||||
import { Request } from '../../../models/request';
|
||||
import { isRequest, Request } from '../../../models/request';
|
||||
import { RequestGroup } from '../../../models/request-group';
|
||||
import { isWebSocketRequest, WebSocketRequest } from '../../../models/websocket-request';
|
||||
import { useNunjucks } from '../../context/nunjucks/use-nunjucks';
|
||||
import { createRequest } from '../../hooks/create-request';
|
||||
import { selectActiveEnvironment, selectActiveProject, selectActiveWorkspace } from '../../redux/selectors';
|
||||
@@ -16,11 +17,13 @@ import type { DropdownHandle } from '../base/dropdown/dropdown';
|
||||
import { Editable } from '../base/editable';
|
||||
import { Highlight } from '../base/highlight';
|
||||
import { RequestActionsDropdown } from '../dropdowns/request-actions-dropdown';
|
||||
import { WebSocketRequestActionsDropdown } from '../dropdowns/websocket-request-actions-dropdown';
|
||||
import { GrpcSpinner } from '../grpc-spinner';
|
||||
import { showModal } from '../modals/index';
|
||||
import { RequestSettingsModal } from '../modals/request-settings-modal';
|
||||
import { GrpcTag } from '../tags/grpc-tag';
|
||||
import { MethodTag } from '../tags/method-tag';
|
||||
import { WebSocketTag } from '../tags/websocket-tag';
|
||||
import { DnDProps, DragObject, dropHandleCreator, hoverHandleCreator, sourceCollect, targetCollect } from './dnd';
|
||||
|
||||
interface RawProps {
|
||||
@@ -30,7 +33,7 @@ interface RawProps {
|
||||
handleDuplicateRequest: Function;
|
||||
isActive: boolean;
|
||||
isPinned: boolean;
|
||||
request?: Request | GrpcRequest;
|
||||
request?: Request | GrpcRequest | WebSocketRequest;
|
||||
requestGroup?: RequestGroup;
|
||||
}
|
||||
|
||||
@@ -138,7 +141,7 @@ export const _SidebarRequestRow: FC<Props> = forwardRef(({
|
||||
return;
|
||||
}
|
||||
|
||||
if (isGrpcRequest(request)) {
|
||||
if (!isRequest(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -203,12 +206,17 @@ export const _SidebarRequestRow: FC<Props> = forwardRef(({
|
||||
</li>
|
||||
);
|
||||
} else {
|
||||
const methodTag =
|
||||
isGrpcRequest(request) ? (
|
||||
<GrpcTag />
|
||||
) : (
|
||||
<MethodTag method={request.method} override={methodOverrideValue} />
|
||||
);
|
||||
|
||||
let methodTag = null;
|
||||
|
||||
if (isGrpcRequest(request)) {
|
||||
methodTag = <GrpcTag />;
|
||||
} else if (isWebSocketRequest(request)) {
|
||||
methodTag = <WebSocketTag />;
|
||||
} else if (isRequest(request)) {
|
||||
methodTag = <MethodTag method={request.method} override={methodOverrideValue} />;
|
||||
}
|
||||
|
||||
node = (
|
||||
<li ref={nodeRef} className={classes}>
|
||||
<div
|
||||
@@ -243,17 +251,27 @@ export const _SidebarRequestRow: FC<Props> = forwardRef(({
|
||||
</div>
|
||||
</button>
|
||||
<div className="sidebar__actions">
|
||||
<RequestActionsDropdown
|
||||
right
|
||||
ref={requestActionsDropdown}
|
||||
handleDuplicateRequest={handleDuplicateRequest}
|
||||
handleShowSettings={handleShowRequestSettings}
|
||||
request={request}
|
||||
isPinned={isPinned}
|
||||
requestGroup={requestGroup}
|
||||
activeEnvironment={activeEnvironment}
|
||||
activeProject={activeProject}
|
||||
/>
|
||||
{isWebSocketRequest(request) ? (
|
||||
<WebSocketRequestActionsDropdown
|
||||
right
|
||||
ref={requestActionsDropdown}
|
||||
handleDuplicateRequest={handleDuplicateRequest}
|
||||
request={request}
|
||||
isPinned={isPinned}
|
||||
/>
|
||||
) : (
|
||||
<RequestActionsDropdown
|
||||
right
|
||||
ref={requestActionsDropdown}
|
||||
handleDuplicateRequest={handleDuplicateRequest}
|
||||
handleShowSettings={handleShowRequestSettings}
|
||||
request={request}
|
||||
isPinned={isPinned}
|
||||
requestGroup={requestGroup}
|
||||
activeEnvironment={activeEnvironment}
|
||||
activeProject={activeProject}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{isPinned && (
|
||||
<div className="sidebar__item__icon-pin">
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
export const WebSocketTag = () => (
|
||||
<div className="tag tag--no-bg tag--small">
|
||||
<span className="tag__inner" style={{ color: 'var(--color-notice)' }}>WS</span>
|
||||
</div>
|
||||
);
|
||||
@@ -1,94 +1,55 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import React, { FC, useEffect, useRef } from 'react';
|
||||
|
||||
import { clickLink } from '../../../common/electron-helpers';
|
||||
import type { ResponseTimelineEntry } from '../../../main/network/libcurl-promise';
|
||||
import * as models from '../../../models';
|
||||
import type { Response } from '../../../models/response';
|
||||
import { CodeEditor } from '../codemirror/code-editor';
|
||||
import { CodeEditor, UnconnectedCodeEditor } from '../codemirror/code-editor';
|
||||
|
||||
interface Props {
|
||||
showBody?: boolean;
|
||||
response: Response;
|
||||
}
|
||||
|
||||
interface State {
|
||||
timeline: ResponseTimelineEntry[];
|
||||
timelineKey: string;
|
||||
}
|
||||
|
||||
export class ResponseTimelineViewer extends PureComponent<Props, State> {
|
||||
state: State = {
|
||||
timeline: [],
|
||||
timelineKey: '',
|
||||
};
|
||||
export const ResponseTimelineViewer: FC<Props> = ({ timeline }) => {
|
||||
const editorRef = useRef<UnconnectedCodeEditor>(null);
|
||||
const rows = timeline
|
||||
.map(({ name, value }, i, all) => {
|
||||
const prefixLookup: Record<ResponseTimelineEntry['name'], string> = {
|
||||
HeaderIn: '< ',
|
||||
DataIn: '| ',
|
||||
SslDataIn: '<< ',
|
||||
HeaderOut: '> ',
|
||||
DataOut: '| ',
|
||||
SslDataOut: '>> ',
|
||||
Text: '* ',
|
||||
};
|
||||
const prefix: string = prefixLookup[name] || '* ';
|
||||
const lines = (value + '').replace(/\n$/, '').split('\n');
|
||||
const newLines = lines.filter(l => !l.match(/^\s*$/)).map(l => `${prefix}${l}`);
|
||||
// Prefix each section with a newline to separate them
|
||||
const previousName = i > 0 ? all[i - 1].name : '';
|
||||
|
||||
componentDidMount() {
|
||||
this.refreshTimeline();
|
||||
}
|
||||
const hasNameChanged = previousName !== name;
|
||||
// Join all lines together
|
||||
return (hasNameChanged ? '\n' : '') + newLines.join('\n');
|
||||
})
|
||||
.filter(r => r !== null)
|
||||
.join('\n')
|
||||
.trim();
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
const { response } = this.props;
|
||||
|
||||
if (response._id !== prevProps.response._id) {
|
||||
this.refreshTimeline();
|
||||
useEffect(() => {
|
||||
if (editorRef.current) {
|
||||
editorRef.current.codeMirror?.setValue(rows);
|
||||
}
|
||||
}
|
||||
}, [rows]);
|
||||
|
||||
async refreshTimeline() {
|
||||
const { response, showBody } = this.props;
|
||||
const timeline = models.response.getTimeline(response, showBody);
|
||||
|
||||
this.setState({
|
||||
timeline,
|
||||
timelineKey: response._id,
|
||||
});
|
||||
}
|
||||
|
||||
renderRow(row: ResponseTimelineEntry, i: number, all: ResponseTimelineEntry[]) {
|
||||
const { name, value } = row;
|
||||
const previousName = i > 0 ? all[i - 1].name : '';
|
||||
const prefixLookup: Record<ResponseTimelineEntry['name'], string> = {
|
||||
HeaderIn: '< ',
|
||||
DataIn: '| ',
|
||||
SslDataIn: '<< ',
|
||||
HeaderOut: '> ',
|
||||
DataOut: '| ',
|
||||
SslDataOut: '>> ',
|
||||
Text: '* ',
|
||||
};
|
||||
const prefix: string = prefixLookup[name] || '* ';
|
||||
|
||||
const lines = (value + '').replace(/\n$/, '').split('\n');
|
||||
const newLines = lines.filter(l => !l.match(/^\s*$/)).map(l => `${prefix}${l}`);
|
||||
let leadingSpace = '';
|
||||
|
||||
// Prefix each section with a newline to separate them
|
||||
if (previousName !== name) {
|
||||
leadingSpace = '\n';
|
||||
}
|
||||
|
||||
// Join all lines together
|
||||
return leadingSpace + newLines.join('\n');
|
||||
}
|
||||
|
||||
render() {
|
||||
const { timeline, timelineKey } = this.state;
|
||||
const rows = timeline
|
||||
.map(this.renderRow)
|
||||
.filter(r => r !== null)
|
||||
.join('\n')
|
||||
.trim();
|
||||
|
||||
return (
|
||||
<CodeEditor
|
||||
key={timelineKey}
|
||||
hideLineNumbers
|
||||
readOnly
|
||||
onClickLink={clickLink}
|
||||
defaultValue={rows}
|
||||
className="pad-left"
|
||||
mode="curl"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<CodeEditor
|
||||
ref={editorRef}
|
||||
hideLineNumbers
|
||||
readOnly
|
||||
onClickLink={clickLink}
|
||||
defaultValue={rows}
|
||||
className="pad-left"
|
||||
mode="curl"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
167
packages/insomnia/src/ui/components/websockets/action-bar.tsx
Normal file
167
packages/insomnia/src/ui/components/websockets/action-bar.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
import React, { FC, FormEvent } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { getRenderContext, render, RENDER_PURPOSE_SEND } from '../../../common/render';
|
||||
import { WebSocketRequest } from '../../../models/websocket-request';
|
||||
import { ReadyState } from '../../context/websocket-client/use-ws-ready-state';
|
||||
import { OneLineEditor } from '../codemirror/one-line-editor';
|
||||
import { showAlert, showModal } from '../modals';
|
||||
import { RequestRenderErrorModal } from '../modals/request-render-error-modal';
|
||||
|
||||
const Button = styled.button<{ warning?: boolean }>(({ warning }) => ({
|
||||
paddingRight: 'var(--padding-md)',
|
||||
paddingLeft: 'var(--padding-md)',
|
||||
textAlign: 'center',
|
||||
background: warning ? 'var(--color-danger)' : 'var(--color-surprise)',
|
||||
color: 'var(--color-font-surprise)',
|
||||
flex: '0 0 100px',
|
||||
':hover': {
|
||||
filter: 'brightness(0.8)',
|
||||
},
|
||||
}));
|
||||
|
||||
interface ActionButtonProps {
|
||||
requestId: string;
|
||||
readyState: ReadyState;
|
||||
}
|
||||
const ActionButton: FC<ActionButtonProps> = ({ requestId, readyState }) => {
|
||||
|
||||
if (readyState === ReadyState.CONNECTING || readyState === ReadyState.CLOSED) {
|
||||
return (
|
||||
<Button
|
||||
type="submit"
|
||||
form="websocketUrlForm"
|
||||
>
|
||||
Connect
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
className="urlbar__send-btn"
|
||||
type="button"
|
||||
warning
|
||||
onClick={() => {
|
||||
window.main.webSocket.close({ requestId });
|
||||
}}
|
||||
>
|
||||
Disconnect
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
interface ActionBarProps {
|
||||
request: WebSocketRequest;
|
||||
workspaceId: string;
|
||||
environmentId: string;
|
||||
defaultValue: string;
|
||||
readyState: ReadyState;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
const Form = styled.form({
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
});
|
||||
|
||||
const StyledUrlBar = styled.div({
|
||||
boxSizing: 'border-box',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
paddingRight: 'var(--padding-md)',
|
||||
paddingLeft: 'var(--padding-md)',
|
||||
});
|
||||
|
||||
const WebSocketIcon = styled.span({
|
||||
color: 'var(--color-notice)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
paddingLeft: 'var(--padding-md)',
|
||||
});
|
||||
|
||||
const ConnectionStatus = styled.span({
|
||||
color: 'var(--color-success)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
paddingLeft: 'var(--padding-md)',
|
||||
});
|
||||
const ConnectionCircle = styled.span({
|
||||
backgroundColor: 'var(--color-success)',
|
||||
marginRight: 'var(--padding-sm)',
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: '50%',
|
||||
});
|
||||
|
||||
export const WebSocketActionBar: FC<ActionBarProps> = ({ request, workspaceId, environmentId, defaultValue, onChange, readyState }) => {
|
||||
const isOpen = readyState === ReadyState.OPEN;
|
||||
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
try {
|
||||
const renderContext = await getRenderContext({ request, environmentId, purpose: RENDER_PURPOSE_SEND });
|
||||
|
||||
const { url, headers, authentication } = request;
|
||||
|
||||
// Render any nunjucks tags in the url/headers/authentication settings
|
||||
const rendered = await render({
|
||||
url,
|
||||
headers,
|
||||
authentication,
|
||||
}, renderContext);
|
||||
|
||||
window.main.webSocket.create({
|
||||
requestId: request._id,
|
||||
workspaceId,
|
||||
url: rendered.url,
|
||||
headers: rendered.headers,
|
||||
authentication: rendered.authentication,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.type === 'render') {
|
||||
showModal(RequestRenderErrorModal, {
|
||||
request,
|
||||
error: err,
|
||||
});
|
||||
} else {
|
||||
showAlert({
|
||||
title: 'Unexpected Request Failure',
|
||||
message: (
|
||||
<div>
|
||||
<p>The request failed due to an unhandled error:</p>
|
||||
<code className="wide selectable">
|
||||
<pre>{err.message}</pre>
|
||||
</code>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isOpen && <WebSocketIcon>WS</WebSocketIcon>}
|
||||
{isOpen && (
|
||||
<ConnectionStatus>
|
||||
<ConnectionCircle />
|
||||
CONNECTED
|
||||
</ConnectionStatus>
|
||||
)}
|
||||
<Form aria-disabled={isOpen} id="websocketUrlForm" onSubmit={handleSubmit}>
|
||||
<StyledUrlBar>
|
||||
<OneLineEditor
|
||||
disabled={readyState === ReadyState.OPEN}
|
||||
placeholder="wss://example.com/chat"
|
||||
defaultValue={defaultValue}
|
||||
onChange={onChange}
|
||||
type="text"
|
||||
forceEditor
|
||||
/>
|
||||
</StyledUrlBar>
|
||||
</Form>
|
||||
<ActionButton requestId={request._id} readyState={readyState} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,200 @@
|
||||
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';
|
||||
|
||||
const Timestamp: FC<{ time: Date | number }> = ({ time }) => {
|
||||
const date = format(time, 'HH:mm:ss');
|
||||
return <>{date}</>;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
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 'checkmark-circle';
|
||||
}
|
||||
case 'close': {
|
||||
return 'disconnected';
|
||||
}
|
||||
case 'error': {
|
||||
return 'error';
|
||||
}
|
||||
default: {
|
||||
return 'bug';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<Props> = ({ events, onSelect, selectionId }) => {
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
const virtualizer = useVirtual({
|
||||
parentRef,
|
||||
size: events.length,
|
||||
estimateSize: React.useCallback(() => 30, []),
|
||||
overscan: 30,
|
||||
keyExtractor: index => events[index]._id,
|
||||
});
|
||||
|
||||
const [autoSizeRef, { height }] = useMeasure<HTMLDivElement>();
|
||||
|
||||
return (
|
||||
<EventLog>
|
||||
<HeadingRow>
|
||||
<EventIconCell>
|
||||
<div style={{ width: '13px' }} />
|
||||
</EventIconCell>
|
||||
<Divider />
|
||||
<EventMessageCell>Data</EventMessageCell>
|
||||
<Divider />
|
||||
<EventTimestampCell>Time</EventTimestampCell>
|
||||
</HeadingRow>
|
||||
<AutoSize ref={autoSizeRef}>
|
||||
<Scrollable style={{ height }} ref={parentRef}>
|
||||
<List
|
||||
style={{
|
||||
height: `${virtualizer.totalSize}px`,
|
||||
}}
|
||||
>
|
||||
{virtualizer.virtualItems.map(item => {
|
||||
const event = events[item.index];
|
||||
|
||||
return (
|
||||
<Row
|
||||
key={item.key}
|
||||
onClick={() => onSelect(event)}
|
||||
isActive={event._id === selectionId}
|
||||
style={{
|
||||
height: `${item.size}px`,
|
||||
transform: `translateY(${item.start}px)`,
|
||||
}}
|
||||
>
|
||||
<EventIconCell>
|
||||
<SvgIcon icon={getIcon(event)} />
|
||||
</EventIconCell>
|
||||
<Divider />
|
||||
|
||||
<EventMessageCell>{getMessage(event)}</EventMessageCell>
|
||||
<Divider />
|
||||
<EventTimestampCell>
|
||||
<Timestamp time={event.timestamp} />
|
||||
</EventTimestampCell>
|
||||
</Row>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</Scrollable>
|
||||
</AutoSize>
|
||||
</EventLog>
|
||||
);
|
||||
};
|
||||
136
packages/insomnia/src/ui/components/websockets/event-view.tsx
Normal file
136
packages/insomnia/src/ui/components/websockets/event-view.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
import { clipboard } from 'electron';
|
||||
import fs from 'fs';
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { PREVIEW_MODE_FRIENDLY, PREVIEW_MODE_RAW, PREVIEW_MODE_SOURCE, PreviewMode } from '../../../common/constants';
|
||||
import { WebSocketEvent, WebSocketMessageEvent } from '../../../main/network/websocket';
|
||||
import { requestMeta } from '../../../models';
|
||||
import { selectResponsePreviewMode } from '../../redux/selectors';
|
||||
import { CodeEditor } from '../codemirror/code-editor';
|
||||
import { showError } from '../modals';
|
||||
import { WebSocketPreviewModeDropdown } from './websocket-preview-dropdown';
|
||||
|
||||
interface Props<T extends WebSocketEvent> {
|
||||
event: T;
|
||||
requestId: string;
|
||||
}
|
||||
|
||||
const PreviewPane = styled.div({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
});
|
||||
|
||||
const PreviewPaneButtons = styled.div({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
boxSizing: 'border-box',
|
||||
height: 'var(--line-height-sm)',
|
||||
borderBottom: '1px solid var(--hl-lg)',
|
||||
padding: 'var(--padding-sm) var(--padding-md)',
|
||||
});
|
||||
|
||||
const PreviewPaneContents = styled.div({
|
||||
padding: 'var(--padding-sm)',
|
||||
flexGrow: 1,
|
||||
});
|
||||
|
||||
export const MessageEventView: FC<Props<WebSocketMessageEvent>> = ({ event, requestId }) => {
|
||||
// TODO: Handle non-string data.
|
||||
const raw = event.data.toString('utf-8');
|
||||
|
||||
const handleDownloadResponseBody = useCallback(async () => {
|
||||
const { canceled, filePath: outputPath } = await window.dialog.showSaveDialog({
|
||||
title: 'Save Response Body',
|
||||
buttonLabel: 'Save',
|
||||
});
|
||||
|
||||
if (canceled || !outputPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const to = fs.createWriteStream(outputPath);
|
||||
|
||||
to.on('error', err => {
|
||||
showError({
|
||||
title: 'Save Failed',
|
||||
message: 'Failed to save response body',
|
||||
error: err,
|
||||
});
|
||||
});
|
||||
|
||||
to.write(raw);
|
||||
|
||||
to.end();
|
||||
}, [raw]);
|
||||
|
||||
const handleCopyResponseToClipboard = useCallback(() => {
|
||||
clipboard.writeText(raw);
|
||||
}, [raw]);
|
||||
|
||||
const previewMode = useSelector(selectResponsePreviewMode);
|
||||
|
||||
const setPreviewMode = async (previewMode: PreviewMode) => {
|
||||
return requestMeta.updateOrCreateByParentId(requestId, { previewMode });
|
||||
};
|
||||
|
||||
// TODO(johnwchadwick): Maybe shouldn't try if it's too large.
|
||||
// TODO(johnwchadwick): Should allow selecting a type instead of assuming JSON.
|
||||
let pretty = raw;
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
pretty = JSON.stringify(parsed, null, '\t');
|
||||
} catch {
|
||||
// Can't parse as JSON.
|
||||
}
|
||||
|
||||
return (
|
||||
<PreviewPane>
|
||||
<PreviewPaneButtons>
|
||||
<WebSocketPreviewModeDropdown
|
||||
download={handleDownloadResponseBody}
|
||||
copyToClipboard={handleCopyResponseToClipboard}
|
||||
previewMode={previewMode}
|
||||
setPreviewMode={setPreviewMode}
|
||||
/>
|
||||
</PreviewPaneButtons>
|
||||
<PreviewPaneContents>
|
||||
{previewMode === PREVIEW_MODE_FRIENDLY &&
|
||||
<CodeEditor
|
||||
hideLineNumbers
|
||||
mode={'text/json'}
|
||||
defaultValue={pretty}
|
||||
uniquenessKey={event._id}
|
||||
readOnly
|
||||
/>}
|
||||
{previewMode === PREVIEW_MODE_SOURCE &&
|
||||
<CodeEditor
|
||||
hideLineNumbers
|
||||
mode={'text/json'}
|
||||
defaultValue={raw}
|
||||
uniquenessKey={event._id}
|
||||
readOnly
|
||||
/>}
|
||||
{previewMode === PREVIEW_MODE_RAW &&
|
||||
<CodeEditor
|
||||
hideLineNumbers
|
||||
mode={'text/plain'}
|
||||
defaultValue={raw}
|
||||
uniquenessKey={event._id}
|
||||
readOnly
|
||||
/>}
|
||||
</PreviewPaneContents>
|
||||
</PreviewPane>
|
||||
);
|
||||
};
|
||||
|
||||
export const EventView: FC<Props<WebSocketEvent>> = ({ event, ...props }) => {
|
||||
switch (event.type) {
|
||||
case 'message':
|
||||
return <MessageEventView event={event} {...props}/>;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import { getPreviewModeName, PREVIEW_MODES, PreviewMode } from '../../../common/constants';
|
||||
import { Dropdown } from '../base/dropdown/dropdown';
|
||||
import { DropdownButton } from '../base/dropdown/dropdown-button';
|
||||
import { DropdownDivider } from '../base/dropdown/dropdown-divider';
|
||||
import { DropdownItem } from '../base/dropdown/dropdown-item';
|
||||
|
||||
interface Props {
|
||||
download: () => void;
|
||||
copyToClipboard: () => void;
|
||||
previewMode: PreviewMode;
|
||||
setPreviewMode: (mode: PreviewMode) => void;
|
||||
}
|
||||
|
||||
export const WebSocketPreviewModeDropdown: FC<Props> = ({
|
||||
download,
|
||||
copyToClipboard,
|
||||
previewMode,
|
||||
setPreviewMode,
|
||||
}) => {
|
||||
return <Dropdown beside>
|
||||
<DropdownButton className="tall">
|
||||
{getPreviewModeName(previewMode)}
|
||||
<i className="fa fa-caret-down space-left" />
|
||||
</DropdownButton>
|
||||
<DropdownDivider>Preview Mode</DropdownDivider>
|
||||
{PREVIEW_MODES.map(mode => <DropdownItem key={mode} onClick={setPreviewMode} value={mode}>
|
||||
{previewMode === mode ? <i className="fa fa-check" /> : <i className="fa fa-empty" />}
|
||||
{getPreviewModeName(mode, true)}
|
||||
</DropdownItem>)}
|
||||
<DropdownDivider>Actions</DropdownDivider>
|
||||
<DropdownItem onClick={copyToClipboard}>
|
||||
<i className="fa fa-copy" />
|
||||
Copy raw response
|
||||
</DropdownItem>
|
||||
<DropdownItem onClick={download}>
|
||||
<i className="fa fa-save" />
|
||||
Export raw response
|
||||
</DropdownItem>
|
||||
</Dropdown>;
|
||||
};
|
||||
@@ -0,0 +1,254 @@
|
||||
import React, { FC, FormEvent, useEffect, useRef, useState } from 'react';
|
||||
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { AuthType, CONTENT_TYPE_JSON } from '../../../common/constants';
|
||||
import { getRenderContext, render, RENDER_PURPOSE_SEND } from '../../../common/render';
|
||||
import * as models from '../../../models';
|
||||
import { WebSocketRequest } from '../../../models/websocket-request';
|
||||
import { ReadyState, useWSReadyState } from '../../context/websocket-client/use-ws-ready-state';
|
||||
import { CodeEditor, UnconnectedCodeEditor } from '../codemirror/code-editor';
|
||||
import { AuthDropdown } from '../dropdowns/auth-dropdown';
|
||||
import { WebSocketPreviewModeDropdown } from '../dropdowns/websocket-preview-mode';
|
||||
import { AuthWrapper } from '../editors/auth/auth-wrapper';
|
||||
import { RequestHeadersEditor } from '../editors/request-headers-editor';
|
||||
import { showAlert, showModal } from '../modals';
|
||||
import { RequestRenderErrorModal } from '../modals/request-render-error-modal';
|
||||
import { Pane, PaneHeader as OriginalPaneHeader } from '../panes/pane';
|
||||
import { WebSocketActionBar } from './action-bar';
|
||||
|
||||
const supportedAuthTypes: AuthType[] = ['basic', 'bearer'];
|
||||
|
||||
const EditorWrapper = styled.div({
|
||||
height: '100%',
|
||||
});
|
||||
const SendMessageForm = styled.form({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'relative',
|
||||
boxSizing: 'border-box',
|
||||
});
|
||||
const SendButton = styled.button({
|
||||
padding: '0 var(--padding-md)',
|
||||
marginLeft: 'var(--padding-xs)',
|
||||
height: '100%',
|
||||
border: '1px solid var(--hl-lg)',
|
||||
borderRadius: 'var(--radius-md)',
|
||||
':hover': {
|
||||
filter: 'brightness(0.8)',
|
||||
},
|
||||
});
|
||||
const PaneSendButton = styled.div({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
boxSizing: 'border-box',
|
||||
height: 'var(--line-height-sm)',
|
||||
borderBottom: '1px solid var(--hl-lg)',
|
||||
padding: 3,
|
||||
});
|
||||
const PaneHeader = styled(OriginalPaneHeader)({
|
||||
'&&': { alignItems: 'stretch' },
|
||||
});
|
||||
|
||||
interface FormProps {
|
||||
request: WebSocketRequest;
|
||||
previewMode: string;
|
||||
initialValue: string;
|
||||
environmentId: string;
|
||||
createOrUpdatePayload: (payload: string, mode: string) => Promise<void>;
|
||||
}
|
||||
|
||||
const WebSocketRequestForm: FC<FormProps> = ({
|
||||
request,
|
||||
previewMode,
|
||||
initialValue,
|
||||
createOrUpdatePayload,
|
||||
environmentId,
|
||||
}) => {
|
||||
const editorRef = useRef<UnconnectedCodeEditor>(null);
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
if (isMounted) {
|
||||
editorRef.current?.codeMirror?.setValue(initialValue);
|
||||
}
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [initialValue]);
|
||||
|
||||
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
const message = editorRef.current?.getValue() || '';
|
||||
|
||||
try {
|
||||
// Render any nunjucks tag in the message
|
||||
const renderContext = await getRenderContext({ request, environmentId, purpose: RENDER_PURPOSE_SEND });
|
||||
const renderedMessage = await render(message, renderContext);
|
||||
|
||||
window.main.webSocket.event.send({ requestId: request._id, message: renderedMessage });
|
||||
} catch (err) {
|
||||
if (err.type === 'render') {
|
||||
showModal(RequestRenderErrorModal, {
|
||||
request,
|
||||
error: err,
|
||||
});
|
||||
} else {
|
||||
showAlert({
|
||||
title: 'Unexpected Request Failure',
|
||||
message: (
|
||||
<div>
|
||||
<p>The request failed due to an unhandled error:</p>
|
||||
<code className="wide selectable">
|
||||
<pre>{err.message}</pre>
|
||||
</code>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// TODO(@dmarby): Wrap the CodeEditor in a NunjucksEnabledProvider here?
|
||||
// To allow for disabling rendering of messages based on a per-request setting.
|
||||
// Same as with regular requests
|
||||
return (
|
||||
<SendMessageForm id="websocketMessageForm" onSubmit={handleSubmit}>
|
||||
<EditorWrapper>
|
||||
<CodeEditor
|
||||
uniquenessKey={request._id}
|
||||
mode={previewMode}
|
||||
ref={editorRef}
|
||||
defaultValue=''
|
||||
onChange={value => createOrUpdatePayload(value, previewMode)}
|
||||
enableNunjucks
|
||||
/>
|
||||
</EditorWrapper>
|
||||
</SendMessageForm>
|
||||
);
|
||||
};
|
||||
|
||||
interface Props {
|
||||
request: WebSocketRequest;
|
||||
workspaceId: string;
|
||||
environmentId: string;
|
||||
forceRefreshKey: number;
|
||||
}
|
||||
|
||||
// requestId is something we can read from the router params in the future.
|
||||
// essentially we can lift up the states and merge request pane and response pane into a single page and divide the UI there.
|
||||
// currently this is blocked by the way page layout divide the panes with dragging functionality
|
||||
// TODO: @gatzjames discuss above assertion in light of request and settings drills
|
||||
export const WebSocketRequestPane: FC<Props> = ({ request, workspaceId, environmentId, forceRefreshKey }) => {
|
||||
const readyState = useWSReadyState(request._id);
|
||||
|
||||
const disabled = readyState === ReadyState.OPEN || readyState === ReadyState.CLOSING;
|
||||
const handleOnChange = (url: string) => {
|
||||
if (url !== request.url) {
|
||||
models.webSocketRequest.update(request, { url });
|
||||
}
|
||||
};
|
||||
const [previewMode, setPreviewMode] = useState(CONTENT_TYPE_JSON);
|
||||
const [initialValue, setInitialValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
const fn = async () => {
|
||||
const payload = await models.webSocketPayload.getByParentId(request._id);
|
||||
if (isMounted && payload) {
|
||||
setInitialValue(payload.value);
|
||||
setPreviewMode(payload.mode);
|
||||
}
|
||||
};
|
||||
fn();
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [request._id]);
|
||||
|
||||
const changeMode = (mode: string) => {
|
||||
setPreviewMode(mode);
|
||||
createOrUpdatePayload(initialValue, mode);
|
||||
};
|
||||
|
||||
const createOrUpdatePayload = async (value: string, mode: string) => {
|
||||
// @TODO: multiple payloads
|
||||
const payload = await models.webSocketPayload.getByParentId(request._id);
|
||||
if (payload) {
|
||||
await models.webSocketPayload.update(payload, { value, mode });
|
||||
return;
|
||||
}
|
||||
await models.webSocketPayload.create({
|
||||
parentId: request._id,
|
||||
value,
|
||||
mode,
|
||||
});
|
||||
};
|
||||
|
||||
const uniqueKey = `${forceRefreshKey}::${request._id}`;
|
||||
|
||||
return (
|
||||
<Pane type="request">
|
||||
<PaneHeader>
|
||||
<WebSocketActionBar
|
||||
key={uniqueKey}
|
||||
request={request}
|
||||
workspaceId={workspaceId}
|
||||
environmentId={environmentId}
|
||||
defaultValue={request.url}
|
||||
readyState={readyState}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
</PaneHeader>
|
||||
<Tabs className="pane__body theme--pane__body react-tabs">
|
||||
<TabList>
|
||||
<Tab tabIndex="-1" >
|
||||
<WebSocketPreviewModeDropdown previewMode={previewMode} onClick={changeMode} />
|
||||
</Tab>
|
||||
<Tab tabIndex="-1">
|
||||
<AuthDropdown authTypes={supportedAuthTypes} disabled={disabled} />
|
||||
</Tab>
|
||||
<Tab tabIndex="-1">
|
||||
<button>Headers</button>
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanel className="react-tabs__tab-panel">
|
||||
<PaneSendButton>
|
||||
<SendButton
|
||||
type="submit"
|
||||
form="websocketMessageForm"
|
||||
disabled={readyState !== ReadyState.OPEN}
|
||||
>
|
||||
Send
|
||||
</SendButton>
|
||||
</PaneSendButton>
|
||||
<WebSocketRequestForm
|
||||
key={uniqueKey}
|
||||
request={request}
|
||||
previewMode={previewMode}
|
||||
initialValue={initialValue}
|
||||
createOrUpdatePayload={createOrUpdatePayload}
|
||||
environmentId={environmentId}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel className="react-tabs__tab-panel">
|
||||
<AuthWrapper
|
||||
key={`${uniqueKey}-${request.authentication.type}-auth-header`}
|
||||
disabled={
|
||||
readyState === ReadyState.OPEN ||
|
||||
readyState === ReadyState.CLOSING
|
||||
}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel className="react-tabs__tab-panel header-editor">
|
||||
<RequestHeadersEditor
|
||||
key={`${uniqueKey}-${readyState}-header-editor`}
|
||||
request={request}
|
||||
bulk={false}
|
||||
isDisabled={readyState === ReadyState.OPEN}
|
||||
/>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</Pane>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,209 @@
|
||||
import fs from 'fs';
|
||||
import React, { FC, useEffect, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { getSetCookieHeaders } from '../../../common/misc';
|
||||
import { ResponseTimelineEntry } from '../../../main/network/libcurl-promise';
|
||||
import { WebSocketEvent } from '../../../main/network/websocket';
|
||||
import { WebSocketResponse } from '../../../models/websocket-response';
|
||||
import { useWebSocketConnectionEvents } from '../../context/websocket-client/use-ws-connection-events';
|
||||
import { selectActiveResponse } from '../../redux/selectors';
|
||||
import { ResponseHistoryDropdown } from '../dropdowns/response-history-dropdown';
|
||||
import { ErrorBoundary } from '../error-boundary';
|
||||
import { EmptyStatePane } from '../panes/empty-state-pane';
|
||||
import { Pane, PaneHeader as OriginalPaneHeader } from '../panes/pane';
|
||||
import { SizeTag } from '../tags/size-tag';
|
||||
import { StatusTag } from '../tags/status-tag';
|
||||
import { TimeTag } from '../tags/time-tag';
|
||||
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 { EventLogView } from './event-log-view';
|
||||
import { EventView } from './event-view';
|
||||
|
||||
const PaneHeader = styled(OriginalPaneHeader)({
|
||||
'&&': { justifyContent: 'unset' },
|
||||
});
|
||||
|
||||
const EventLogTableWrapper = styled.div({
|
||||
width: '100%',
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
padding: 'var(--padding-sm)',
|
||||
boxSizing: 'border-box',
|
||||
});
|
||||
|
||||
const EventViewWrapper = styled.div({
|
||||
flex: 1,
|
||||
borderTop: '1px solid var(--hl-md)',
|
||||
height: '100%',
|
||||
});
|
||||
|
||||
const PaneBodyContent = styled.div({
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'grid',
|
||||
gridTemplateRows: 'repeat(auto-fit, minmax(0, 1fr))',
|
||||
});
|
||||
|
||||
export const WebSocketResponsePane: FC<{ requestId: string; handleSetActiveResponse: (requestId: string, activeResponse: WebSocketResponse | null) => void }> =
|
||||
({
|
||||
requestId,
|
||||
handleSetActiveResponse,
|
||||
}) => {
|
||||
const response = useSelector(selectActiveResponse) as WebSocketResponse | null;
|
||||
if (!response) {
|
||||
return (
|
||||
<Pane type="response">
|
||||
<PaneHeader />
|
||||
<EmptyStatePane
|
||||
icon={<i className="fa fa-paper-plane" />}
|
||||
documentationLinks={[
|
||||
{
|
||||
title: 'Introduction to Insomnia',
|
||||
url: 'https://docs.insomnia.rest/insomnia/get-started',
|
||||
},
|
||||
]}
|
||||
title="Enter a URL and connect to a WebSocket server to start sending data"
|
||||
secondaryAction="Select a payload type from above to send data to the connection"
|
||||
/>
|
||||
</Pane>
|
||||
);
|
||||
}
|
||||
return <WebSocketActiveResponsePane requestId={requestId} response={response} handleSetActiveResponse={handleSetActiveResponse} />;
|
||||
};
|
||||
|
||||
const WebSocketActiveResponsePane: FC<{ requestId: string; response: WebSocketResponse; handleSetActiveResponse: (requestId: string, activeResponse: WebSocketResponse | null) => void }> = ({
|
||||
requestId,
|
||||
response,
|
||||
handleSetActiveResponse,
|
||||
}) => {
|
||||
const [selectedEvent, setSelectedEvent] = useState<WebSocketEvent | null>(null);
|
||||
const [timeline, setTimeline] = useState<ResponseTimelineEntry[]>([]);
|
||||
const events = useWebSocketConnectionEvents({ responseId: response._id });
|
||||
const handleSelection = (event: WebSocketEvent) => {
|
||||
setSelectedEvent((selected: WebSocketEvent | null) => selected?._id === event._id ? null : event);
|
||||
};
|
||||
|
||||
const setActiveResponseAndDisconnect = (requestId: string, response: WebSocketResponse | null) => {
|
||||
handleSetActiveResponse(requestId, response);
|
||||
window.main.webSocket.close({ requestId });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedEvent(null);
|
||||
}, [response._id]);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
const fn = async () => {
|
||||
// @TODO: this needs to fs.watch or tail the file, instead of reading the whole thing on every event.
|
||||
// or alternatively a throttle to keep it from reading too frequently
|
||||
const rawBuffer = await fs.promises.readFile(response.timelinePath);
|
||||
const timelineString = rawBuffer.toString();
|
||||
const timelineParsed = timelineString.split('\n').filter(e => e?.trim()).map(e => JSON.parse(e));
|
||||
isMounted && setTimeline(timelineParsed);
|
||||
};
|
||||
fn();
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [response.timelinePath, events.length]);
|
||||
|
||||
const cookieHeaders = getSetCookieHeaders(response.headers);
|
||||
return (
|
||||
<Pane type="response">
|
||||
<PaneHeader className="row-spaced">
|
||||
<div className="no-wrap scrollable scrollable--no-bars pad-left">
|
||||
<StatusTag statusCode={response.statusCode} statusMessage={response.statusMessage} />
|
||||
<TimeTag milliseconds={response.elapsedTime} />
|
||||
<SizeTag bytesRead={0} bytesContent={0} />
|
||||
</div>
|
||||
<ResponseHistoryDropdown
|
||||
activeResponse={response}
|
||||
handleSetActiveResponse={setActiveResponseAndDisconnect}
|
||||
requestId={requestId}
|
||||
className="tall pane__header__right"
|
||||
/>
|
||||
</PaneHeader>
|
||||
<Tabs className="pane__body theme--pane__body react-tabs">
|
||||
<TabList>
|
||||
<Tab tabIndex="-1" >
|
||||
<button>Events</button>
|
||||
</Tab>
|
||||
<Tab tabIndex="-1">
|
||||
<button>
|
||||
Headers{' '}
|
||||
{response?.headers.length > 0 && (
|
||||
<span className="bubble">{response.headers.length}</span>
|
||||
)}
|
||||
</button>
|
||||
</Tab>
|
||||
<Tab tabIndex="-1">
|
||||
<button>
|
||||
Cookies{' '}
|
||||
{cookieHeaders.length ? (
|
||||
<span className="bubble">{cookieHeaders.length}</span>
|
||||
) : null}
|
||||
</button>
|
||||
</Tab>
|
||||
<Tab tabIndex="-1" >
|
||||
<button>Timeline</button>
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanel className="react-tabs__tab-panel">
|
||||
<PaneBodyContent>
|
||||
{response.error ? <ResponseErrorViewer url={response.url} error={response.error} />
|
||||
: <>
|
||||
{Boolean(events?.length) && (
|
||||
<EventLogTableWrapper>
|
||||
<EventLogView
|
||||
events={events}
|
||||
onSelect={handleSelection}
|
||||
selectionId={selectedEvent?._id}
|
||||
/>
|
||||
</EventLogTableWrapper>
|
||||
)}
|
||||
{selectedEvent && (
|
||||
<EventViewWrapper>
|
||||
<EventView
|
||||
requestId={requestId}
|
||||
event={selectedEvent}
|
||||
/>
|
||||
</EventViewWrapper>
|
||||
)}
|
||||
</>}
|
||||
</PaneBodyContent>
|
||||
</TabPanel>
|
||||
<TabPanel className="react-tabs__tab-panel scrollable-container">
|
||||
<div className="scrollable pad">
|
||||
<ErrorBoundary key={response._id} errorClassName="font-error pad text-center">
|
||||
<ResponseHeadersViewer headers={response.headers} />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel className="react-tabs__tab-panel scrollable-container">
|
||||
<div className="scrollable pad">
|
||||
<ErrorBoundary key={response._id} errorClassName="font-error pad text-center">
|
||||
<ResponseCookiesViewer
|
||||
// @TODO: Implement cookie storing and sending
|
||||
cookiesSent={false}
|
||||
cookiesStored={false}
|
||||
headers={cookieHeaders}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel className="react-tabs__tab-panel">
|
||||
<ResponseTimelineViewer
|
||||
key={response._id}
|
||||
timeline={timeline}
|
||||
/>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</ Pane>
|
||||
);
|
||||
};
|
||||
@@ -1,10 +1,12 @@
|
||||
import React, { FC, Fragment, ReactNode } from 'react';
|
||||
import React, { FC, Fragment, ReactNode, useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { isGrpcRequest } from '../../models/grpc-request';
|
||||
import { isRemoteProject } from '../../models/project';
|
||||
import { Request, RequestHeader } from '../../models/request';
|
||||
import type { Response } from '../../models/response';
|
||||
import { isWebSocketRequest } from '../../models/websocket-request';
|
||||
import { WebSocketResponse } from '../../models/websocket-response';
|
||||
import { isCollection, isDesign } from '../../models/workspace';
|
||||
import { VCS } from '../../sync/vcs/vcs';
|
||||
import {
|
||||
@@ -27,6 +29,8 @@ import { RequestPane } from './panes/request-pane';
|
||||
import { ResponsePane } from './panes/response-pane';
|
||||
import { SidebarChildren } from './sidebar/sidebar-children';
|
||||
import { SidebarFilter } from './sidebar/sidebar-filter';
|
||||
import { WebSocketRequestPane } from './websockets/websocket-request-pane';
|
||||
import { WebSocketResponsePane } from './websockets/websocket-response-pane';
|
||||
import { WorkspacePageHeader } from './workspace-page-header';
|
||||
import type { HandleActivityChange } from './wrapper';
|
||||
|
||||
@@ -35,7 +39,7 @@ interface Props {
|
||||
gitSyncDropdown: ReactNode;
|
||||
handleActivityChange: HandleActivityChange;
|
||||
handleSetActiveEnvironment: (id: string | null) => void;
|
||||
handleSetActiveResponse: (requestId: string, activeResponse: Response | null) => void;
|
||||
handleSetActiveResponse: (requestId: string, activeResponse: Response | WebSocketResponse | null) => void;
|
||||
handleForceUpdateRequest: (r: Request, patch: Partial<Request>) => Promise<Request>;
|
||||
handleForceUpdateRequestHeaders: (r: Request, headers: RequestHeader[]) => Promise<Request>;
|
||||
handleImport: Function;
|
||||
@@ -60,20 +64,24 @@ export const WrapperDebug: FC<Props> = ({
|
||||
headerEditorKey,
|
||||
vcs,
|
||||
}) => {
|
||||
|
||||
const activeProject = useSelector(selectActiveProject);
|
||||
const isLoggedIn = useSelector(selectIsLoggedIn);
|
||||
|
||||
const activeEnvironment = useSelector(selectActiveEnvironment);
|
||||
const activeRequest = useSelector(selectActiveRequest);
|
||||
|
||||
const activeWorkspace = useSelector(selectActiveWorkspace);
|
||||
|
||||
const settings = useSelector(selectSettings);
|
||||
const sidebarFilter = useSelector(selectSidebarFilter);
|
||||
|
||||
const isTeamSync = isLoggedIn && activeWorkspace && isCollection(activeWorkspace) && isRemoteProject(activeProject) && vcs;
|
||||
|
||||
// Close all websocket connections when the active environment changes
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
window.main.webSocket.closeAll();
|
||||
};
|
||||
}, [activeEnvironment?._id]);
|
||||
|
||||
return (
|
||||
<PageLayout
|
||||
renderPageHeader={activeWorkspace ?
|
||||
@@ -114,42 +122,65 @@ export const WrapperDebug: FC<Props> = ({
|
||||
: null}
|
||||
renderPaneOne={activeWorkspace ?
|
||||
<ErrorBoundary showAlert>
|
||||
{activeRequest && isGrpcRequest(activeRequest) ?
|
||||
<GrpcRequestPane
|
||||
activeRequest={activeRequest}
|
||||
environmentId={activeEnvironment ? activeEnvironment._id : ''}
|
||||
workspaceId={activeWorkspace._id}
|
||||
forceRefreshKey={forceRefreshKey}
|
||||
settings={settings}
|
||||
/>
|
||||
:
|
||||
<RequestPane
|
||||
environmentId={activeEnvironment ? activeEnvironment._id : ''}
|
||||
forceRefreshCounter={forceRefreshKey}
|
||||
forceUpdateRequest={handleForceUpdateRequest}
|
||||
forceUpdateRequestHeaders={handleForceUpdateRequestHeaders}
|
||||
handleImport={handleImport}
|
||||
headerEditorKey={headerEditorKey}
|
||||
request={activeRequest}
|
||||
settings={settings}
|
||||
updateRequestMimeType={handleUpdateRequestMimeType}
|
||||
workspace={activeWorkspace}
|
||||
/>}
|
||||
{activeRequest && (
|
||||
isGrpcRequest(activeRequest) ? (
|
||||
<GrpcRequestPane
|
||||
activeRequest={activeRequest}
|
||||
environmentId={activeEnvironment ? activeEnvironment._id : ''}
|
||||
workspaceId={activeWorkspace._id}
|
||||
forceRefreshKey={forceRefreshKey}
|
||||
settings={settings}
|
||||
/>
|
||||
) : (
|
||||
isWebSocketRequest(activeRequest) ? (
|
||||
<WebSocketRequestPane
|
||||
key={activeRequest._id}
|
||||
request={activeRequest}
|
||||
workspaceId={activeWorkspace._id}
|
||||
environmentId={activeEnvironment ? activeEnvironment._id : ''}
|
||||
forceRefreshKey={forceRefreshKey}
|
||||
/>
|
||||
) : (
|
||||
<RequestPane
|
||||
environmentId={activeEnvironment ? activeEnvironment._id : ''}
|
||||
forceRefreshCounter={forceRefreshKey}
|
||||
forceUpdateRequest={handleForceUpdateRequest}
|
||||
forceUpdateRequestHeaders={handleForceUpdateRequestHeaders}
|
||||
handleImport={handleImport}
|
||||
headerEditorKey={headerEditorKey}
|
||||
request={activeRequest}
|
||||
settings={settings}
|
||||
updateRequestMimeType={handleUpdateRequestMimeType}
|
||||
workspace={activeWorkspace}
|
||||
/>
|
||||
)
|
||||
)
|
||||
)}
|
||||
</ErrorBoundary>
|
||||
: null}
|
||||
renderPaneTwo={
|
||||
<ErrorBoundary showAlert>
|
||||
{activeRequest && isGrpcRequest(activeRequest) ?
|
||||
<GrpcResponsePane
|
||||
activeRequest={activeRequest}
|
||||
forceRefreshKey={forceRefreshKey}
|
||||
/>
|
||||
:
|
||||
<ResponsePane
|
||||
handleSetFilter={handleSetResponseFilter}
|
||||
request={activeRequest}
|
||||
handleSetActiveResponse={handleSetActiveResponse}
|
||||
/>}
|
||||
{activeRequest && (
|
||||
isGrpcRequest(activeRequest) ? (
|
||||
<GrpcResponsePane
|
||||
activeRequest={activeRequest}
|
||||
forceRefreshKey={forceRefreshKey}
|
||||
/>
|
||||
) : (
|
||||
isWebSocketRequest(activeRequest) ? (
|
||||
<WebSocketResponsePane
|
||||
requestId={activeRequest._id}
|
||||
handleSetActiveResponse={handleSetActiveResponse}
|
||||
/>
|
||||
) : (
|
||||
<ResponsePane
|
||||
handleSetFilter={handleSetResponseFilter}
|
||||
request={activeRequest}
|
||||
handleSetActiveResponse={handleSetActiveResponse}
|
||||
/>
|
||||
)
|
||||
)
|
||||
)}
|
||||
</ErrorBoundary>}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
RequestHeader,
|
||||
} from '../../models/request';
|
||||
import { Response } from '../../models/response';
|
||||
import { WebSocketResponse } from '../../models/websocket-response';
|
||||
import { GitVCS } from '../../sync/git/git-vcs';
|
||||
import { VCS } from '../../sync/vcs/vcs';
|
||||
import { CookieModifyModal } from '../components/modals/cookie-modify-modal';
|
||||
@@ -185,7 +186,8 @@ export class WrapperClass extends PureComponent<Props, State> {
|
||||
|
||||
return null;
|
||||
}
|
||||
async handleSetActiveResponse(requestId: string, activeResponse: Response | null = null) {
|
||||
|
||||
async handleSetActiveResponse(requestId: string, activeResponse: Response | WebSocketResponse | null = null) {
|
||||
const { activeEnvironment } = this.props;
|
||||
const activeResponseId = activeResponse ? activeResponse._id : null;
|
||||
await updateRequestMetaByParentId(requestId, {
|
||||
@@ -388,7 +390,7 @@ export class WrapperClass extends PureComponent<Props, State> {
|
||||
/>
|
||||
|
||||
<SettingsModal ref={instance => registerModal(instance, 'SettingsModal')} />
|
||||
<ResponseDebugModal ref={registerModal} />
|
||||
<ResponseDebugModal ref={instance => registerModal(instance, 'ResponseDebugModal')} />
|
||||
|
||||
<RequestSwitcherModal
|
||||
ref={registerModal}
|
||||
|
||||
@@ -29,6 +29,7 @@ import { isNotDefaultProject } from '../../models/project';
|
||||
import { Request, updateMimeType } from '../../models/request';
|
||||
import { type RequestGroupMeta } from '../../models/request-group-meta';
|
||||
import { getByParentId as getRequestMetaByParentId } from '../../models/request-meta';
|
||||
import { isWebSocketRequest, WebSocketRequest } from '../../models/websocket-request';
|
||||
import { isWorkspace } from '../../models/workspace';
|
||||
import * as plugins from '../../plugins';
|
||||
import * as themes from '../../plugins/misc';
|
||||
@@ -266,7 +267,7 @@ class App extends PureComponent<AppProps, State> {
|
||||
];
|
||||
}
|
||||
|
||||
_requestDuplicate(request?: Request | GrpcRequest) {
|
||||
_requestDuplicate(request?: Request | GrpcRequest | WebSocketRequest) {
|
||||
if (!request) {
|
||||
return;
|
||||
}
|
||||
@@ -353,6 +354,11 @@ class App extends PureComponent<AppProps, State> {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isWebSocketRequest(this.props.activeRequest)) {
|
||||
console.warn('Tried to update request mime-type on WebSocket request');
|
||||
return null;
|
||||
}
|
||||
|
||||
const requestMeta = await models.requestMeta.getOrCreateByParentId(
|
||||
this.props.activeRequest._id,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { useState } from 'react';
|
||||
import { useInterval } from 'react-use';
|
||||
|
||||
import { WebSocketEvent } from '../../../main/network/websocket';
|
||||
|
||||
export function useWebSocketConnectionEvents({ responseId }: { responseId: string }) {
|
||||
const [events, setEvents] = useState<WebSocketEvent[]>([]);
|
||||
useInterval(
|
||||
() => {
|
||||
let isMounted = true;
|
||||
const fn = async () => {
|
||||
const allEvents = await window.main.webSocket.event.findMany({ responseId });
|
||||
if (isMounted) {
|
||||
setEvents(allEvents);
|
||||
}
|
||||
};
|
||||
fn();
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
},
|
||||
500
|
||||
);
|
||||
return events;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export enum ReadyState {
|
||||
CONNECTING = 0,
|
||||
OPEN = 1,
|
||||
CLOSING = 2,
|
||||
CLOSED = 3,
|
||||
}
|
||||
export function useWSReadyState(requestId: string): ReadyState {
|
||||
const [readyState, setReadyState] = useState<ReadyState>(ReadyState.CLOSED);
|
||||
|
||||
useEffect(() => {
|
||||
window.main.webSocket.readyState.getCurrent({ requestId })
|
||||
.then((currentReadyState: ReadyState) => {
|
||||
setReadyState(currentReadyState);
|
||||
});
|
||||
}, [requestId]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = window.main.on(`webSocket.${requestId}.readyState`,
|
||||
(_, incomingReadyState: ReadyState) => {
|
||||
setReadyState(incomingReadyState);
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, [requestId]);
|
||||
|
||||
return readyState;
|
||||
}
|
||||
@@ -50,7 +50,7 @@ export const setActiveRequest = async (
|
||||
});
|
||||
};
|
||||
|
||||
export type CreateRequestType = 'HTTP' | 'gRPC' | 'GraphQL';
|
||||
export type CreateRequestType = 'HTTP' | 'gRPC' | 'GraphQL' | 'WebSocket';
|
||||
type RequestCreator = (input: {
|
||||
parentId: string;
|
||||
requestType: CreateRequestType;
|
||||
@@ -110,6 +110,16 @@ export const createRequest: RequestCreator = async ({
|
||||
break;
|
||||
}
|
||||
|
||||
case 'WebSocket': {
|
||||
const request = await models.webSocketRequest.create({
|
||||
parentId,
|
||||
name: 'New WebSocket Request',
|
||||
});
|
||||
models.stats.incrementCreatedRequests();
|
||||
setActiveRequest(request._id, workspaceId);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
unreachableCase(
|
||||
requestType,
|
||||
|
||||
@@ -1,30 +1,24 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import * as models from '../../models';
|
||||
import { isGrpcRequest } from '../../models/grpc-request';
|
||||
import { Request } from '../../models/request';
|
||||
import * as requestOperations from '../../models/helpers/request-operations';
|
||||
import { Request, RequestAuthentication } from '../../models/request';
|
||||
import { WebSocketRequest } from '../../models/websocket-request';
|
||||
import { selectActiveRequest } from '../redux/selectors';
|
||||
|
||||
export const useActiveRequest = () => {
|
||||
const activeRequest = useSelector(selectActiveRequest);
|
||||
|
||||
if (!activeRequest) {
|
||||
throw new Error('Tried to load null request');
|
||||
if (!activeRequest || !('authentication' in activeRequest)) {
|
||||
throw new Error('Tried to load invalid request type');
|
||||
}
|
||||
|
||||
if (isGrpcRequest(activeRequest)) {
|
||||
throw new Error('Loaded GrpcRequest, expected to load Request');
|
||||
}
|
||||
|
||||
const patchRequest = useCallback(async (patch: Partial<Request>) => {
|
||||
await models.request.update(activeRequest, patch);
|
||||
const updateAuth = useCallback((authentication: RequestAuthentication) => {
|
||||
requestOperations.update(activeRequest, { authentication });
|
||||
}, [activeRequest]);
|
||||
|
||||
const updateAuth = useCallback((authentication: Request['authentication']) => patchRequest({ authentication }), [patchRequest]);
|
||||
|
||||
const { authentication } = activeRequest;
|
||||
const patchAuth = useCallback((patch: Partial<Request['authentication']>) => updateAuth({ ...authentication, ...patch }), [authentication, updateAuth]);
|
||||
const patchAuth = useCallback((patch: Partial<Request['authentication'] | WebSocketRequest['authentication']>) => updateAuth({ ...authentication, ...patch }), [authentication, updateAuth]);
|
||||
|
||||
return {
|
||||
activeRequest,
|
||||
|
||||
@@ -17,7 +17,7 @@ const grpcRequestModelBuilder = createBuilder(grpcRequestModelSchema);
|
||||
|
||||
describe('shouldShowInSidebar', () => {
|
||||
const allTypes = models.types();
|
||||
const supported = [models.request.type, models.requestGroup.type, models.grpcRequest.type];
|
||||
const supported = [models.request.type, models.requestGroup.type, models.grpcRequest.type, models.webSocketRequest.type];
|
||||
const unsupported = difference(allTypes, supported);
|
||||
|
||||
it.each(supported)('should show %s in sidebar', type => {
|
||||
|
||||
@@ -27,6 +27,9 @@ import { Stats } from '../../../models/stats';
|
||||
import { UnitTest } from '../../../models/unit-test';
|
||||
import { UnitTestResult } from '../../../models/unit-test-result';
|
||||
import { UnitTestSuite } from '../../../models/unit-test-suite';
|
||||
import { WebSocketPayload } from '../../../models/websocket-payload';
|
||||
import { WebSocketRequest } from '../../../models/websocket-request';
|
||||
import { WebSocketResponse } from '../../../models/websocket-response';
|
||||
import { Workspace } from '../../../models/workspace';
|
||||
import { WorkspaceMeta } from '../../../models/workspace-meta';
|
||||
|
||||
@@ -70,6 +73,9 @@ export interface EntitiesState {
|
||||
protoDirectories: EntityRecord<ProtoDirectory>;
|
||||
grpcRequests: EntityRecord<GrpcRequest>;
|
||||
grpcRequestMetas: EntityRecord<GrpcRequestMeta>;
|
||||
webSocketPayloads: EntityRecord<WebSocketPayload>;
|
||||
webSocketRequests: EntityRecord<WebSocketRequest>;
|
||||
webSocketResponses: EntityRecord<WebSocketResponse>;
|
||||
}
|
||||
|
||||
export const initialEntitiesState: EntitiesState = {
|
||||
@@ -98,6 +104,9 @@ export const initialEntitiesState: EntitiesState = {
|
||||
protoDirectories: {},
|
||||
grpcRequests: {},
|
||||
grpcRequestMetas: {},
|
||||
webSocketPayloads: {},
|
||||
webSocketRequests: {},
|
||||
webSocketResponses: {},
|
||||
};
|
||||
|
||||
export function reducer(state = initialEntitiesState, action: any) {
|
||||
@@ -108,6 +117,9 @@ export function reducer(state = initialEntitiesState, action: any) {
|
||||
|
||||
for (const doc of docs) {
|
||||
const referenceName = getReducerName(doc.type);
|
||||
if (!(freshState as any)[referenceName]) {
|
||||
(freshState as any)[referenceName] = {};
|
||||
}
|
||||
(freshState as any)[referenceName][doc._id] = doc;
|
||||
}
|
||||
|
||||
@@ -194,5 +206,8 @@ export async function allDocs() {
|
||||
...(await models.protoDirectory.all()),
|
||||
...(await models.grpcRequest.all()),
|
||||
...(await models.grpcRequestMeta.all()),
|
||||
...(await models.webSocketPayload.all()),
|
||||
...(await models.webSocketRequest.all()),
|
||||
...(await models.webSocketResponse.all()),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import { GrpcRequest } from '../../../models/grpc-request';
|
||||
import * as requestOperations from '../../../models/helpers/request-operations';
|
||||
import { DEFAULT_PROJECT_ID } from '../../../models/project';
|
||||
import { Request } from '../../../models/request';
|
||||
import { WebSocketRequest } from '../../../models/websocket-request';
|
||||
import { isWorkspace } from '../../../models/workspace';
|
||||
import { reloadPlugins } from '../../../plugins';
|
||||
import { createPlugin } from '../../../plugins/create';
|
||||
@@ -555,7 +556,7 @@ export const exportRequestsToFile = (requestIds: string[]) => async (dispatch: D
|
||||
dispatch(loadStop());
|
||||
},
|
||||
onDone: async selectedFormat => {
|
||||
const requests: (GrpcRequest | Request)[] = [];
|
||||
const requests: (GrpcRequest | Request | WebSocketRequest)[] = [];
|
||||
const privateEnvironments: Environment[] = [];
|
||||
const workspaceLookup: any = {};
|
||||
|
||||
|
||||
@@ -10,7 +10,10 @@ import { sortProjects } from '../../models/helpers/project';
|
||||
import { DEFAULT_PROJECT_ID, isRemoteProject } from '../../models/project';
|
||||
import { isRequest, Request } from '../../models/request';
|
||||
import { isRequestGroup, RequestGroup } from '../../models/request-group';
|
||||
import { type Response } from '../../models/response';
|
||||
import { UnitTestResult } from '../../models/unit-test-result';
|
||||
import { isWebSocketRequest, WebSocketRequest } from '../../models/websocket-request';
|
||||
import { type WebSocketResponse } from '../../models/websocket-response';
|
||||
import { isCollection } from '../../models/workspace';
|
||||
import { RootState } from './modules';
|
||||
|
||||
@@ -311,7 +314,7 @@ export const selectActiveWorkspaceEntities = createSelector(
|
||||
|
||||
export const selectPinnedRequests = createSelector(selectEntitiesLists, entities => {
|
||||
const pinned: Record<string, boolean> = {};
|
||||
const requests = [...entities.requests, ...entities.grpcRequests];
|
||||
const requests = [...entities.requests, ...entities.grpcRequests, ...entities.webSocketRequests];
|
||||
const requestMetas = [...entities.requestMetas, ...entities.grpcRequestMetas];
|
||||
|
||||
// Default all to unpinned
|
||||
@@ -331,8 +334,8 @@ export const selectWorkspaceRequestsAndRequestGroups = createSelector(
|
||||
selectActiveWorkspaceEntities,
|
||||
entities => {
|
||||
return entities.filter(
|
||||
entity => isRequest(entity) || isGrpcRequest(entity) || isRequestGroup(entity),
|
||||
) as (Request | GrpcRequest | RequestGroup)[];
|
||||
entity => isRequest(entity) || isWebSocketRequest(entity) || isGrpcRequest(entity) || isRequestGroup(entity),
|
||||
) as (Request | WebSocketRequest | GrpcRequest | RequestGroup)[];
|
||||
},
|
||||
);
|
||||
|
||||
@@ -341,13 +344,20 @@ export const selectActiveRequest = createSelector(
|
||||
selectActiveWorkspaceMeta,
|
||||
(entities, workspaceMeta) => {
|
||||
const id = workspaceMeta?.activeRequestId || 'n/a';
|
||||
|
||||
if (id in entities.requests) {
|
||||
return entities.requests[id];
|
||||
} else if (id in entities.grpcRequests) {
|
||||
return entities.grpcRequests[id];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (id in entities.grpcRequests) {
|
||||
return entities.grpcRequests[id];
|
||||
}
|
||||
|
||||
if (id in entities.webSocketRequests) {
|
||||
return entities.webSocketRequests[id];
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -431,19 +441,21 @@ export const selectActiveRequestResponses = createSelector(
|
||||
selectSettings,
|
||||
(activeRequest, entities, activeEnvironment, settings) => {
|
||||
const requestId = activeRequest ? activeRequest._id : 'n/a';
|
||||
// Filter responses down if the setting is enabled
|
||||
return entities.responses
|
||||
.filter(response => {
|
||||
const requestMatches = requestId === response.parentId;
|
||||
|
||||
if (settings.filterResponsesByEnv) {
|
||||
const activeEnvironmentId = activeEnvironment ? activeEnvironment._id : null;
|
||||
const environmentMatches = response.environmentId === activeEnvironmentId;
|
||||
return requestMatches && environmentMatches;
|
||||
} else {
|
||||
return requestMatches;
|
||||
}
|
||||
})
|
||||
const responses: (Response | WebSocketResponse)[] = (activeRequest && isWebSocketRequest(activeRequest)) ? entities.webSocketResponses : entities.responses;
|
||||
|
||||
// Filter responses down if the setting is enabled
|
||||
return responses.filter(response => {
|
||||
const requestMatches = requestId === response.parentId;
|
||||
|
||||
if (settings.filterResponsesByEnv) {
|
||||
const activeEnvironmentId = activeEnvironment ? activeEnvironment._id : null;
|
||||
const environmentMatches = response.environmentId === activeEnvironmentId;
|
||||
return requestMatches && environmentMatches;
|
||||
} else {
|
||||
return requestMatches;
|
||||
}
|
||||
})
|
||||
.sort((a, b) => (a.created > b.created ? -1 : 1));
|
||||
},
|
||||
);
|
||||
@@ -453,6 +465,7 @@ export const selectActiveResponse = createSelector(
|
||||
selectActiveRequestResponses,
|
||||
(activeRequestMeta, responses) => {
|
||||
const activeResponseId = activeRequestMeta ? activeRequestMeta.activeResponseId : 'n/a';
|
||||
|
||||
const activeResponse = responses.find(response => response._id === activeResponseId);
|
||||
|
||||
if (activeResponse) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { BaseModel } from '../../models';
|
||||
import { GrpcRequest, isGrpcRequest } from '../../models/grpc-request';
|
||||
import { isRequest, Request } from '../../models/request';
|
||||
import { isRequestGroup, RequestGroup } from '../../models/request-group';
|
||||
import { isWebSocketRequest } from '../../models/websocket-request';
|
||||
import {
|
||||
selectActiveWorkspace,
|
||||
selectActiveWorkspaceMeta,
|
||||
@@ -17,10 +18,10 @@ import {
|
||||
type SidebarModel = Request | GrpcRequest | RequestGroup;
|
||||
|
||||
export const shouldShowInSidebar = (model: BaseModel): boolean =>
|
||||
isRequest(model) || isGrpcRequest(model) || isRequestGroup(model);
|
||||
isRequest(model) || isWebSocketRequest(model) || isGrpcRequest(model) || isRequestGroup(model);
|
||||
|
||||
export const shouldIgnoreChildrenOf = (model: SidebarModel): boolean =>
|
||||
isRequest(model) || isGrpcRequest(model);
|
||||
isRequest(model) || isWebSocketRequest(model) || isGrpcRequest(model);
|
||||
|
||||
export const sortByMetaKeyOrId = (a: SidebarModel, b: SidebarModel): number => {
|
||||
if (a.metaSortKey === b.metaSortKey) {
|
||||
|
||||
Reference in New Issue
Block a user