diff --git a/packages/insomnia-components/src/assets/icn-checkmark-circle.svg b/packages/insomnia-components/src/assets/icn-checkmark-circle.svg
new file mode 100644
index 0000000000..1fdfdf74e1
--- /dev/null
+++ b/packages/insomnia-components/src/assets/icn-checkmark-circle.svg
@@ -0,0 +1,3 @@
+
diff --git a/packages/insomnia-components/src/assets/icn-disconnected.svg b/packages/insomnia-components/src/assets/icn-disconnected.svg
new file mode 100644
index 0000000000..0cc8d7d8e1
--- /dev/null
+++ b/packages/insomnia-components/src/assets/icn-disconnected.svg
@@ -0,0 +1,3 @@
+
diff --git a/packages/insomnia-components/src/assets/icn-receive.svg b/packages/insomnia-components/src/assets/icn-receive.svg
new file mode 100644
index 0000000000..8dec06e7cb
--- /dev/null
+++ b/packages/insomnia-components/src/assets/icn-receive.svg
@@ -0,0 +1,5 @@
+
diff --git a/packages/insomnia-components/src/assets/icn-sent.svg b/packages/insomnia-components/src/assets/icn-sent.svg
new file mode 100644
index 0000000000..454ca330e1
--- /dev/null
+++ b/packages/insomnia-components/src/assets/icn-sent.svg
@@ -0,0 +1,5 @@
+
diff --git a/packages/insomnia-components/src/assets/icn-system-event.svg b/packages/insomnia-components/src/assets/icn-system-event.svg
new file mode 100644
index 0000000000..025cbfa7d1
--- /dev/null
+++ b/packages/insomnia-components/src/assets/icn-system-event.svg
@@ -0,0 +1,3 @@
+
diff --git a/packages/insomnia-components/src/svg-icon.tsx b/packages/insomnia-components/src/svg-icon.tsx
index 2c0ccbde30..275573329e 100644
--- a/packages/insomnia-components/src/svg-icon.tsx
+++ b/packages/insomnia-components/src/svg-icon.tsx
@@ -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 {
[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() {
diff --git a/packages/insomnia-smoke-test/fixtures/certificates/client.localhost-client-key.pem b/packages/insomnia-smoke-test/fixtures/certificates/client.localhost-client-key.pem
new file mode 100644
index 0000000000..65e60dfa66
--- /dev/null
+++ b/packages/insomnia-smoke-test/fixtures/certificates/client.localhost-client-key.pem
@@ -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-----
diff --git a/packages/insomnia-smoke-test/fixtures/certificates/client.localhost-client.pem b/packages/insomnia-smoke-test/fixtures/certificates/client.localhost-client.pem
new file mode 100644
index 0000000000..c594558490
--- /dev/null
+++ b/packages/insomnia-smoke-test/fixtures/certificates/client.localhost-client.pem
@@ -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-----
diff --git a/packages/insomnia-smoke-test/fixtures/certificates/localhost-key.pem b/packages/insomnia-smoke-test/fixtures/certificates/localhost-key.pem
new file mode 100644
index 0000000000..d5c1caea10
--- /dev/null
+++ b/packages/insomnia-smoke-test/fixtures/certificates/localhost-key.pem
@@ -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-----
diff --git a/packages/insomnia-smoke-test/fixtures/certificates/localhost.pem b/packages/insomnia-smoke-test/fixtures/certificates/localhost.pem
new file mode 100644
index 0000000000..bd5854826c
--- /dev/null
+++ b/packages/insomnia-smoke-test/fixtures/certificates/localhost.pem
@@ -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-----
diff --git a/packages/insomnia-smoke-test/fixtures/certificates/rootCA-key.pem b/packages/insomnia-smoke-test/fixtures/certificates/rootCA-key.pem
new file mode 100644
index 0000000000..21eb9ab557
--- /dev/null
+++ b/packages/insomnia-smoke-test/fixtures/certificates/rootCA-key.pem
@@ -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-----
diff --git a/packages/insomnia-smoke-test/fixtures/certificates/rootCA.pem b/packages/insomnia-smoke-test/fixtures/certificates/rootCA.pem
new file mode 100644
index 0000000000..43d0f148c1
--- /dev/null
+++ b/packages/insomnia-smoke-test/fixtures/certificates/rootCA.pem
@@ -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-----
diff --git a/packages/insomnia-smoke-test/fixtures/websockets.yaml b/packages/insomnia-smoke-test/fixtures/websockets.yaml
new file mode 100644
index 0000000000..d6f8b8f333
--- /dev/null
+++ b/packages/insomnia-smoke-test/fixtures/websockets.yaml
@@ -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
diff --git a/packages/insomnia-smoke-test/package-lock.json b/packages/insomnia-smoke-test/package-lock.json
index 144b94d8f1..1e0ea56767 100644
--- a/packages/insomnia-smoke-test/package-lock.json
+++ b/packages/insomnia-smoke-test/package-lock.json
@@ -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",
diff --git a/packages/insomnia-smoke-test/package.json b/packages/insomnia-smoke-test/package.json
index 70b73d13ee..e22ccaee25 100644
--- a/packages/insomnia-smoke-test/package.json
+++ b/packages/insomnia-smoke-test/package.json
@@ -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"
}
}
diff --git a/packages/insomnia-smoke-test/server/index.ts b/packages/insomnia-smoke-test/server/index.ts
index 4d6cadd29a..5b8acd0bbe 100644
--- a/packages/insomnia-smoke-test/server/index.ts
+++ b/packages/insomnia-smoke-test/server/index.ts
@@ -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);
});
diff --git a/packages/insomnia-smoke-test/server/websocket.ts b/packages/insomnia-smoke-test/server/websocket.ts
new file mode 100644
index 0000000000..de19084886
--- /dev/null
+++ b/packages/insomnia-smoke-test/server/websocket.ts
@@ -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
+
+
+
+
+
+
401 Unauthorized
+
+
+ `);
+ 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);
+ });
+};
diff --git a/packages/insomnia-smoke-test/tests/websocket.test.ts b/packages/insomnia-smoke-test/tests/websocket.test.ts
new file mode 100644
index 0000000000..590221ba04
--- /dev/null
+++ b/packages/insomnia-smoke-test/tests/websocket.test.ts
@@ -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');
+
+});
diff --git a/packages/insomnia/package-lock.json b/packages/insomnia/package-lock.json
index 1d9a1aa1d3..538b61d1ab 100644
--- a/packages/insomnia/package-lock.json
+++ b/packages/insomnia/package-lock.json
@@ -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": {}
},
diff --git a/packages/insomnia/package.json b/packages/insomnia/package.json
index c8250b2309..14476ed550 100644
--- a/packages/insomnia/package.json
+++ b/packages/insomnia/package.json
@@ -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
diff --git a/packages/insomnia/src/__jest__/mock-code-editor.tsx b/packages/insomnia/src/__jest__/mock-code-editor.tsx
index e71b1efffb..985b63b5a2 100644
--- a/packages/insomnia/src/__jest__/mock-code-editor.tsx
+++ b/packages/insomnia/src/__jest__/mock-code-editor.tsx
@@ -12,6 +12,7 @@ export class MockCodeEditor extends PureComponent {
render() {
const { id, onChange, placeholder, defaultValue } = this.props;
return
- {error.path.match(/^body/) && (
+ {error.path.match(/^body/) && isRequest(request) && (
)}
- {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',
diff --git a/packages/insomnia/src/ui/components/modals/response-debug-modal.tsx b/packages/insomnia/src/ui/components/modals/response-debug-modal.tsx
index 36e4a208f1..8ec826a0ba 100644
--- a/packages/insomnia/src/ui/components/modals/response-debug-modal.tsx
+++ b/packages/insomnia/src/ui/components/modals/response-debug-modal.tsx
@@ -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((_, ref) => {
+ const modalRef = useRef(null);
+ const [state, setState] = useState({
+ 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 (
+
+ {title || 'Response Timeline'}
+
+
+ {(responseId && timeline) ? (
+
+ ) : (
+
No response found
+ )}
+
+
+
+ );
+});
- _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 (
-
- {title || 'Response Timeline'}
-
-
- {response ? (
-
- ) : (
-
No response found
- )}
-
-
-
- );
- }
-}
+ResponseDebugModal.displayName = 'ResponseDebugModal';
diff --git a/packages/insomnia/src/ui/components/panes/request-pane.tsx b/packages/insomnia/src/ui/components/panes/request-pane.tsx
index 0642c345a2..b308eff68d 100644
--- a/packages/insomnia/src/ui/components/panes/request-pane.tsx
+++ b/packages/insomnia/src/ui/components/panes/request-pane.tsx
@@ -165,7 +165,7 @@ export const RequestPane: FC = ({
diff --git a/packages/insomnia/src/ui/components/panes/response-pane.tsx b/packages/insomnia/src/ui/components/panes/response-pane.tsx
index 40c86834ca..fef468a45c 100644
--- a/packages/insomnia/src/ui/components/panes/response-pane.tsx
+++ b/packages/insomnia/src/ui/components/panes/response-pane.tsx
@@ -41,7 +41,7 @@ export const ResponsePane: FC = ({
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 = ({
);
}
-
+ const timeline = models.response.getTimeline(response);
const cookieHeaders = getSetCookieHeaders(response.headers);
return (
@@ -154,7 +154,7 @@ export const ResponsePane: FC = ({
-
+ {isWebSocketRequest(request) ? (
+
+ ) : (
+
+ )}
{isPinned && (
diff --git a/packages/insomnia/src/ui/components/tags/websocket-tag.tsx b/packages/insomnia/src/ui/components/tags/websocket-tag.tsx
new file mode 100644
index 0000000000..a465cc9cf0
--- /dev/null
+++ b/packages/insomnia/src/ui/components/tags/websocket-tag.tsx
@@ -0,0 +1,7 @@
+import React from 'react';
+
+export const WebSocketTag = () => (
+
+ WS
+
+);
diff --git a/packages/insomnia/src/ui/components/viewers/response-timeline-viewer.tsx b/packages/insomnia/src/ui/components/viewers/response-timeline-viewer.tsx
index 551ecd91be..2cabbbd179 100644
--- a/packages/insomnia/src/ui/components/viewers/response-timeline-viewer.tsx
+++ b/packages/insomnia/src/ui/components/viewers/response-timeline-viewer.tsx
@@ -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
{
- state: State = {
- timeline: [],
- timelineKey: '',
- };
+export const ResponseTimelineViewer: FC = ({ timeline }) => {
+ const editorRef = useRef(null);
+ const rows = timeline
+ .map(({ name, value }, i, all) => {
+ const prefixLookup: Record = {
+ 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 = {
- 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 (
-
- );
- }
-}
+ return (
+
+ );
+};
diff --git a/packages/insomnia/src/ui/components/websockets/action-bar.tsx b/packages/insomnia/src/ui/components/websockets/action-bar.tsx
new file mode 100644
index 0000000000..1b94883528
--- /dev/null
+++ b/packages/insomnia/src/ui/components/websockets/action-bar.tsx
@@ -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 = ({ requestId, readyState }) => {
+
+ if (readyState === ReadyState.CONNECTING || readyState === ReadyState.CLOSED) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+};
+
+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 = ({ request, workspaceId, environmentId, defaultValue, onChange, readyState }) => {
+ const isOpen = readyState === ReadyState.OPEN;
+ const handleSubmit = async (event: FormEvent) => {
+ 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: (
+
+
The request failed due to an unhandled error:
+
+ {err.message}
+
+
+ ),
+ });
+ }
+ }
+ };
+
+ return (
+ <>
+ {!isOpen && WS}
+ {isOpen && (
+
+
+ CONNECTED
+
+ )}
+
+
+ >
+ );
+};
diff --git a/packages/insomnia/src/ui/components/websockets/event-log-view.tsx b/packages/insomnia/src/ui/components/websockets/event-log-view.tsx
new file mode 100644
index 0000000000..05f3f1915d
--- /dev/null
+++ b/packages/insomnia/src/ui/components/websockets/event-log-view.tsx
@@ -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 = ({ events, onSelect, selectionId }) => {
+ const parentRef = useRef(null);
+ const virtualizer = useVirtual({
+ parentRef,
+ size: events.length,
+ estimateSize: React.useCallback(() => 30, []),
+ overscan: 30,
+ keyExtractor: index => events[index]._id,
+ });
+
+ const [autoSizeRef, { height }] = useMeasure();
+
+ return (
+
+
+
+
+
+
+ Data
+
+ Time
+
+
+
+
+ {virtualizer.virtualItems.map(item => {
+ const event = events[item.index];
+
+ return (
+ onSelect(event)}
+ isActive={event._id === selectionId}
+ style={{
+ height: `${item.size}px`,
+ transform: `translateY(${item.start}px)`,
+ }}
+ >
+
+
+
+
+
+ {getMessage(event)}
+
+
+
+
+
+ );
+ })}
+
+
+
+
+ );
+};
diff --git a/packages/insomnia/src/ui/components/websockets/event-view.tsx b/packages/insomnia/src/ui/components/websockets/event-view.tsx
new file mode 100644
index 0000000000..da196cfaad
--- /dev/null
+++ b/packages/insomnia/src/ui/components/websockets/event-view.tsx
@@ -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 {
+ 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> = ({ 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 (
+
+
+
+
+
+ {previewMode === PREVIEW_MODE_FRIENDLY &&
+ }
+ {previewMode === PREVIEW_MODE_SOURCE &&
+ }
+ {previewMode === PREVIEW_MODE_RAW &&
+ }
+
+
+ );
+};
+
+export const EventView: FC> = ({ event, ...props }) => {
+ switch (event.type) {
+ case 'message':
+ return ;
+ default:
+ return null;
+ }
+};
diff --git a/packages/insomnia/src/ui/components/websockets/websocket-preview-dropdown.tsx b/packages/insomnia/src/ui/components/websockets/websocket-preview-dropdown.tsx
new file mode 100644
index 0000000000..7c058a6db5
--- /dev/null
+++ b/packages/insomnia/src/ui/components/websockets/websocket-preview-dropdown.tsx
@@ -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 = ({
+ download,
+ copyToClipboard,
+ previewMode,
+ setPreviewMode,
+}) => {
+ return
+
+ {getPreviewModeName(previewMode)}
+
+
+ Preview Mode
+ {PREVIEW_MODES.map(mode =>
+ {previewMode === mode ? : }
+ {getPreviewModeName(mode, true)}
+ )}
+ Actions
+
+
+ Copy raw response
+
+
+
+ Export raw response
+
+ ;
+};
diff --git a/packages/insomnia/src/ui/components/websockets/websocket-request-pane.tsx b/packages/insomnia/src/ui/components/websockets/websocket-request-pane.tsx
new file mode 100644
index 0000000000..88a37f1499
--- /dev/null
+++ b/packages/insomnia/src/ui/components/websockets/websocket-request-pane.tsx
@@ -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;
+}
+
+const WebSocketRequestForm: FC = ({
+ request,
+ previewMode,
+ initialValue,
+ createOrUpdatePayload,
+ environmentId,
+}) => {
+ const editorRef = useRef(null);
+ useEffect(() => {
+ let isMounted = true;
+ if (isMounted) {
+ editorRef.current?.codeMirror?.setValue(initialValue);
+ }
+ return () => {
+ isMounted = false;
+ };
+ }, [initialValue]);
+
+ const handleSubmit = async (event: FormEvent) => {
+ 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: (
+
+
The request failed due to an unhandled error:
+
+ {err.message}
+
+
+ ),
+ });
+ }
+ }
+ };
+
+ // 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 (
+
+
+ createOrUpdatePayload(value, previewMode)}
+ enableNunjucks
+ />
+
+
+ );
+};
+
+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 = ({ 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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Send
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/insomnia/src/ui/components/websockets/websocket-response-pane.tsx b/packages/insomnia/src/ui/components/websockets/websocket-response-pane.tsx
new file mode 100644
index 0000000000..000e15f168
--- /dev/null
+++ b/packages/insomnia/src/ui/components/websockets/websocket-response-pane.tsx
@@ -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 (
+
+
+ }
+ 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"
+ />
+
+ );
+ }
+ return ;
+ };
+
+const WebSocketActiveResponsePane: FC<{ requestId: string; response: WebSocketResponse; handleSetActiveResponse: (requestId: string, activeResponse: WebSocketResponse | null) => void }> = ({
+ requestId,
+ response,
+ handleSetActiveResponse,
+}) => {
+ const [selectedEvent, setSelectedEvent] = useState(null);
+ const [timeline, setTimeline] = useState([]);
+ 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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {response.error ?
+ : <>
+ {Boolean(events?.length) && (
+
+
+
+ )}
+ {selectedEvent && (
+
+
+
+ )}
+ >}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Pane>
+ );
+};
diff --git a/packages/insomnia/src/ui/components/wrapper-debug.tsx b/packages/insomnia/src/ui/components/wrapper-debug.tsx
index 4fa0619e5f..ecce333a4b 100644
--- a/packages/insomnia/src/ui/components/wrapper-debug.tsx
+++ b/packages/insomnia/src/ui/components/wrapper-debug.tsx
@@ -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) => Promise;
handleForceUpdateRequestHeaders: (r: Request, headers: RequestHeader[]) => Promise;
handleImport: Function;
@@ -60,20 +64,24 @@ export const WrapperDebug: FC = ({
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 (
= ({
: null}
renderPaneOne={activeWorkspace ?
- {activeRequest && isGrpcRequest(activeRequest) ?
-
- :
- }
+ {activeRequest && (
+ isGrpcRequest(activeRequest) ? (
+
+ ) : (
+ isWebSocketRequest(activeRequest) ? (
+
+ ) : (
+
+ )
+ )
+ )}
: null}
renderPaneTwo={
- {activeRequest && isGrpcRequest(activeRequest) ?
-
- :
- }
+ {activeRequest && (
+ isGrpcRequest(activeRequest) ? (
+
+ ) : (
+ isWebSocketRequest(activeRequest) ? (
+
+ ) : (
+
+ )
+ )
+ )}
}
/>
);
diff --git a/packages/insomnia/src/ui/components/wrapper.tsx b/packages/insomnia/src/ui/components/wrapper.tsx
index 05028766c6..10c55f983b 100644
--- a/packages/insomnia/src/ui/components/wrapper.tsx
+++ b/packages/insomnia/src/ui/components/wrapper.tsx
@@ -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 {
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 {
/>
registerModal(instance, 'SettingsModal')} />
-
+ registerModal(instance, 'ResponseDebugModal')} />
{
];
}
- _requestDuplicate(request?: Request | GrpcRequest) {
+ _requestDuplicate(request?: Request | GrpcRequest | WebSocketRequest) {
if (!request) {
return;
}
@@ -353,6 +354,11 @@ class App extends PureComponent {
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,
);
diff --git a/packages/insomnia/src/ui/context/websocket-client/use-ws-connection-events.ts b/packages/insomnia/src/ui/context/websocket-client/use-ws-connection-events.ts
new file mode 100644
index 0000000000..7620046725
--- /dev/null
+++ b/packages/insomnia/src/ui/context/websocket-client/use-ws-connection-events.ts
@@ -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([]);
+ 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;
+}
diff --git a/packages/insomnia/src/ui/context/websocket-client/use-ws-ready-state.ts b/packages/insomnia/src/ui/context/websocket-client/use-ws-ready-state.ts
new file mode 100644
index 0000000000..7ea8fcaf03
--- /dev/null
+++ b/packages/insomnia/src/ui/context/websocket-client/use-ws-ready-state.ts
@@ -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.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;
+}
diff --git a/packages/insomnia/src/ui/hooks/create-request.ts b/packages/insomnia/src/ui/hooks/create-request.ts
index d90659025e..9a9245811b 100644
--- a/packages/insomnia/src/ui/hooks/create-request.ts
+++ b/packages/insomnia/src/ui/hooks/create-request.ts
@@ -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,
diff --git a/packages/insomnia/src/ui/hooks/use-active-request.ts b/packages/insomnia/src/ui/hooks/use-active-request.ts
index 7115e63372..a8b71c8613 100644
--- a/packages/insomnia/src/ui/hooks/use-active-request.ts
+++ b/packages/insomnia/src/ui/hooks/use-active-request.ts
@@ -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) => {
- 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) => updateAuth({ ...authentication, ...patch }), [authentication, updateAuth]);
+ const patchAuth = useCallback((patch: Partial) => updateAuth({ ...authentication, ...patch }), [authentication, updateAuth]);
return {
activeRequest,
diff --git a/packages/insomnia/src/ui/redux/__tests__/sidebar-selectors.test.ts b/packages/insomnia/src/ui/redux/__tests__/sidebar-selectors.test.ts
index 0aa89e9ea9..ad54048a20 100644
--- a/packages/insomnia/src/ui/redux/__tests__/sidebar-selectors.test.ts
+++ b/packages/insomnia/src/ui/redux/__tests__/sidebar-selectors.test.ts
@@ -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 => {
diff --git a/packages/insomnia/src/ui/redux/modules/entities.ts b/packages/insomnia/src/ui/redux/modules/entities.ts
index 686ce6c3ec..293f1c00f3 100644
--- a/packages/insomnia/src/ui/redux/modules/entities.ts
+++ b/packages/insomnia/src/ui/redux/modules/entities.ts
@@ -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;
grpcRequests: EntityRecord;
grpcRequestMetas: EntityRecord;
+ webSocketPayloads: EntityRecord;
+ webSocketRequests: EntityRecord;
+ webSocketResponses: EntityRecord;
}
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()),
];
}
diff --git a/packages/insomnia/src/ui/redux/modules/global.tsx b/packages/insomnia/src/ui/redux/modules/global.tsx
index a2ffa6cc96..af2bf55f79 100644
--- a/packages/insomnia/src/ui/redux/modules/global.tsx
+++ b/packages/insomnia/src/ui/redux/modules/global.tsx
@@ -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 = {};
diff --git a/packages/insomnia/src/ui/redux/selectors.ts b/packages/insomnia/src/ui/redux/selectors.ts
index c5361546e8..b0b86594c7 100644
--- a/packages/insomnia/src/ui/redux/selectors.ts
+++ b/packages/insomnia/src/ui/redux/selectors.ts
@@ -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 = {};
- 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) {
diff --git a/packages/insomnia/src/ui/redux/sidebar-selectors.ts b/packages/insomnia/src/ui/redux/sidebar-selectors.ts
index ed281e6a3f..69f2e1c08e 100644
--- a/packages/insomnia/src/ui/redux/sidebar-selectors.ts
+++ b/packages/insomnia/src/ui/redux/sidebar-selectors.ts
@@ -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) {