Skip to content

Commit 8382e60

Browse files
committed
Add very basic Send UI (feature flagged) for initial testing
Can send requests, accepts all the core inputs we'll need and persists them, has the top-level UI structure & data model sort-of in place. Does not show responses (they're just logged to the console), does not do lots of very necessary UX details, and doesn't have any kind of UI polish at all. But exciting!
1 parent 2c037c5 commit 8382e60

File tree

13 files changed

+597
-21
lines changed

13 files changed

+597
-21
lines changed

src/components/app.tsx

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,20 @@ import { appHistory } from '../routing';
1616
import { useHotkeys, Ctrl } from '../util/ui';
1717

1818
import { AccountStore } from '../model/account/account-store';
19-
import { serverVersion, versionSatisfies, MOCK_SERVER_RANGE } from '../services/service-versions';
19+
import {
20+
serverVersion,
21+
versionSatisfies,
22+
MOCK_SERVER_RANGE,
23+
SERVER_SEND_API_SUPPORTED
24+
} from '../services/service-versions';
2025

2126
import { Sidebar, SidebarItem, SIDEBAR_WIDTH } from './sidebar';
2227
import { InterceptPage } from './intercept/intercept-page';
2328
import { ViewPage } from './view/view-page';
2429
import { MockPage } from './mock/mock-page';
30+
import { SendPage } from './send/send-page';
2531
import { SettingsPage } from './settings/settings-page';
32+
2633
import { PlanPicker } from './account/plan-picker';
2734
import { ModalOverlay } from './account/modal-overlay';
2835
import { CheckoutSpinner } from './account/checkout-spinner';
@@ -83,6 +90,16 @@ class App extends React.Component<{ accountStore: AccountStore }> {
8390
return this.props.accountStore.isPaidUser || this.props.accountStore.isPastDueUser;
8491
}
8592

93+
@computed
94+
get canVisitSend() {
95+
return this.props.accountStore.featureFlags.includes('send') && (
96+
// Hide Send option if the server is too old for proper support.
97+
// We show by default to avoid flicker in the most common case
98+
serverVersion.state !== 'fulfilled' ||
99+
versionSatisfies(serverVersion.value as string, SERVER_SEND_API_SUPPORTED)
100+
);
101+
}
102+
86103
@computed
87104
get menuItems() {
88105
return [
@@ -121,6 +138,18 @@ class App extends React.Component<{ accountStore: AccountStore }> {
121138
: []
122139
),
123140

141+
...(this.canVisitSend
142+
? [{
143+
name: 'Send',
144+
title: `Send HTTP requests directly (${Ctrl}+4)`,
145+
icon: ['far', 'paper-plane'],
146+
position: 'top',
147+
type: 'router',
148+
url: '/send'
149+
}]
150+
: []
151+
),
152+
124153
(this.canVisitSettings
125154
? {
126155
name: 'Settings',
@@ -191,6 +220,7 @@ class App extends React.Component<{ accountStore: AccountStore }> {
191220
<Route path={'/view/:eventId'} pageComponent={ViewPage} />
192221
<Route path={'/mock'} pageComponent={MockPage} />
193222
<Route path={'/mock/:initialRuleId'} pageComponent={MockPage} />
223+
<Route path={'/send'} pageComponent={SendPage} />
194224
<Route path={'/settings'} pageComponent={SettingsPage} />
195225
</Router>
196226
</AppContainer>
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import * as _ from 'lodash';
2+
import * as React from 'react';
3+
import { action, computed } from 'mobx';
4+
import { inject, observer } from 'mobx-react';
5+
import {
6+
MOCKTTP_PARAM_REF,
7+
ProxySetting,
8+
ProxySettingSource,
9+
RuleParameterReference
10+
} from 'mockttp';
11+
12+
import { RawHeaders } from '../../types';
13+
import { styled } from '../../styles';
14+
import { bufferToString, isProbablyUtf8, stringToBuffer } from '../../util';
15+
16+
import { sendRequest } from '../../services/server-api';
17+
import { RulesStore } from '../../model/rules/rules-store';
18+
import { SendStore } from '../../model/send/send-store';
19+
import { ClientProxyConfig, RULE_PARAM_REF_KEY } from '../../model/send/send-data-model';
20+
21+
import { EditableRawHeaders } from '../common/editable-headers';
22+
import { ThemedSelfSizedEditor } from '../editor/base-editor';
23+
import { Button, TextInput } from '../common/inputs';
24+
25+
const RequestPaneContainer = styled.section`
26+
display: flex;
27+
flex-direction: column;
28+
`;
29+
30+
const UrlInput = styled(TextInput)`
31+
`;
32+
33+
export const getEffectivePort = (url: { protocol: string | null, port: string | null }) => {
34+
if (url.port) {
35+
return parseInt(url.port, 10);
36+
} else if (url.protocol === 'https:' || url.protocol === 'wss:') {
37+
return 443;
38+
} else {
39+
return 80;
40+
}
41+
}
42+
43+
@inject('rulesStore')
44+
@inject('sendStore')
45+
@observer
46+
export class RequestPane extends React.Component<{
47+
rulesStore?: RulesStore,
48+
sendStore?: SendStore
49+
}> {
50+
51+
@computed
52+
get method() {
53+
return this.props.sendStore!.requestInput.method;
54+
}
55+
56+
@computed
57+
get url() {
58+
return this.props.sendStore!.requestInput.url;
59+
}
60+
61+
62+
@computed
63+
get headers() {
64+
return this.props.sendStore!.requestInput.headers;
65+
}
66+
67+
@computed
68+
get body() {
69+
return this.props.sendStore!.requestInput.rawBody;
70+
}
71+
72+
@computed
73+
private get bodyTextEncoding() {
74+
// If we're handling text data, we want to show & edit it as UTF8.
75+
// If it's binary, that's a lossy operation, so we use binary (latin1) instead.
76+
return isProbablyUtf8(this.body)
77+
? 'utf8'
78+
: 'binary';
79+
}
80+
81+
render() {
82+
const bodyString = bufferToString(this.body, this.bodyTextEncoding);
83+
84+
return <RequestPaneContainer>
85+
<UrlInput
86+
placeholder='https://example.com/hello?name=world'
87+
value={this.url}
88+
onChange={this.updateUrl}
89+
/>
90+
<EditableRawHeaders
91+
headers={this.headers}
92+
onChange={this.updateHeaders}
93+
/>
94+
<ThemedSelfSizedEditor
95+
contentId='request'
96+
language={'text'}
97+
value={bodyString}
98+
onChange={this.updateBody}
99+
/>
100+
<Button
101+
onClick={this.sendRequest}
102+
/>
103+
</RequestPaneContainer>;
104+
}
105+
106+
@action.bound
107+
updateUrl(changeEvent: React.ChangeEvent<HTMLInputElement>) {
108+
const { requestInput } = this.props.sendStore!;
109+
requestInput.url = changeEvent.target.value;
110+
}
111+
112+
@action.bound
113+
updateHeaders(headers: RawHeaders) {
114+
const { requestInput } = this.props.sendStore!;
115+
requestInput.headers = headers;
116+
}
117+
118+
@action.bound
119+
updateBody(input: string) {
120+
const { requestInput } = this.props.sendStore!;
121+
requestInput.rawBody = stringToBuffer(input, this.bodyTextEncoding);
122+
}
123+
124+
@action.bound
125+
async sendRequest() {
126+
const rulesStore = this.props.rulesStore!;
127+
const passthroughOptions = rulesStore.activePassthroughOptions;
128+
129+
const url = new URL(this.url);
130+
const effectivePort = getEffectivePort(url);
131+
const hostWithPort = `${url.hostname}:${effectivePort}`;
132+
const clientCertificate = passthroughOptions.clientCertificateHostMap?.[hostWithPort] ||
133+
passthroughOptions.clientCertificateHostMap?.[url.hostname!] ||
134+
undefined;
135+
136+
const responseStream = await sendRequest({
137+
url: this.url,
138+
method: this.method,
139+
headers: this.headers,
140+
rawBody: this.body
141+
}, {
142+
ignoreHostHttpsErrors: passthroughOptions.ignoreHostHttpsErrors,
143+
trustAdditionalCAs: rulesStore.additionalCaCertificates.map((cert) => ({ cert: cert.rawPEM })),
144+
clientCertificate,
145+
proxyConfig: getProxyConfig(rulesStore.proxyConfig),
146+
lookupOptions: passthroughOptions.lookupOptions
147+
});
148+
149+
const reader = responseStream.getReader();
150+
while(true) {
151+
const { done, value } = await reader.read();
152+
if (done) return;
153+
else console.log(value);
154+
}
155+
}
156+
157+
}
158+
159+
function getProxyConfig(proxyConfig: RulesStore['proxyConfig']): ClientProxyConfig {
160+
if (!proxyConfig) return undefined;
161+
162+
if (_.isArray(proxyConfig)) {
163+
return proxyConfig.map((config) => getProxyConfig(config)) as ClientProxyConfig;
164+
}
165+
166+
if (MOCKTTP_PARAM_REF in proxyConfig) {
167+
return {
168+
[RULE_PARAM_REF_KEY]: (proxyConfig as RuleParameterReference<ProxySettingSource>)[MOCKTTP_PARAM_REF]
169+
};
170+
}
171+
172+
return proxyConfig as ProxySetting;
173+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import * as React from "react";
2+
import { observer } from "mobx-react";
3+
4+
import { styled } from '../../styles';
5+
6+
const ResponsePaneContainer = styled.section`
7+
`;
8+
9+
@observer
10+
export class ResponsePane extends React.Component<{}> {
11+
12+
render() {
13+
return <ResponsePaneContainer>
14+
15+
</ResponsePaneContainer>;
16+
}
17+
18+
}

src/components/send/send-page.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import * as React from 'react';
2+
import { observer } from 'mobx-react';
3+
4+
import { styled } from '../../styles';
5+
6+
import { SplitPane } from '../split-pane';
7+
import { RequestPane } from './request-pane';
8+
import { ResponsePane } from './response-pane';
9+
10+
const SendPageContainer = styled.div`
11+
height: 100vh;
12+
position: relative;
13+
`;
14+
15+
@observer
16+
export class SendPage extends React.Component<{}> {
17+
18+
render() {
19+
return <SendPageContainer>
20+
<SplitPane
21+
split='vertical'
22+
primary='second'
23+
defaultSize='50%'
24+
minSize={300}
25+
maxSize={-300}
26+
>
27+
<RequestPane />
28+
<ResponsePane />
29+
</SplitPane>
30+
</SendPageContainer>;
31+
}
32+
33+
}

src/icons.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import { faAlignLeft } from '@fortawesome/free-solid-svg-icons/faAlignLeft';
6363
import { faClone } from '@fortawesome/free-regular-svg-icons/faClone';
6464
import { faCheck } from '@fortawesome/free-solid-svg-icons/faCheck';
6565
import { faLevelDownAlt } from '@fortawesome/free-solid-svg-icons/faLevelDownAlt';
66+
import { faPaperPlane } from '@fortawesome/free-regular-svg-icons/faPaperPlane';
6667

6768
import { faChrome } from '@fortawesome/free-brands-svg-icons/faChrome';
6869
import { faFirefox } from '@fortawesome/free-brands-svg-icons/faFirefox';
@@ -139,6 +140,7 @@ library.add(
139140
faClone,
140141
faCheck,
141142
faLevelDownAlt,
143+
faPaperPlane,
142144

143145
faChrome,
144146
faFirefox,

src/index.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,14 @@ import { EventsStore } from './model/events/events-store';
3232
import { RulesStore } from './model/rules/rules-store';
3333
import { InterceptorStore } from './model/interception/interceptor-store';
3434
import { ApiStore } from './model/api/api-store';
35+
import { SendStore } from './model/send/send-store';
36+
3537
import { triggerServerUpdate } from './services/server-api';
38+
import { serverVersion, lastServerVersion, UI_VERSION } from './services/service-versions';
3639

3740
import { App } from './components/app';
3841
import { StorePoweredThemeProvider } from './components/store-powered-theme-provider';
3942
import { ErrorBoundary } from './components/error-boundary';
40-
import { serverVersion, lastServerVersion, UI_VERSION } from './services/service-versions';
4143

4244
console.log(`Initialising UI (version ${UI_VERSION})`);
4345

@@ -78,6 +80,7 @@ const apiStore = new ApiStore(accountStore);
7880
const uiStore = new UiStore(accountStore);
7981
const proxyStore = new ProxyStore(accountStore);
8082
const interceptorStore = new InterceptorStore(proxyStore, accountStore);
83+
const sendStore = new SendStore();
8184

8285
// Some non-trivial interactions between rules & events stores here. Rules need to use events to
8386
// handle breakpoints (where rule logic reads from received event data), while events need to use
@@ -106,7 +109,8 @@ const stores = {
106109
proxyStore,
107110
eventsStore,
108111
interceptorStore,
109-
rulesStore
112+
rulesStore,
113+
sendStore
110114
};
111115

112116
const appStartupPromise = Promise.all(

src/model/rules/rules-store.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import {
44
requestHandlers,
55
MOCKTTP_PARAM_REF,
66
ProxyConfig,
7-
ProxySetting
7+
ProxySetting,
8+
RuleParameterReference,
9+
ProxySettingSource
810
} from 'mockttp';
911
import * as MockRTC from 'mockrtc';
1012

@@ -357,7 +359,12 @@ export class RulesStore {
357359
}
358360

359361
@computed.struct
360-
get proxyConfig(): ProxyConfig {
362+
get proxyConfig():
363+
| ProxySetting
364+
| RuleParameterReference<ProxySettingSource>
365+
| Array<ProxySetting | RuleParameterReference<ProxySettingSource>>
366+
| undefined
367+
{
361368
const { userProxyConfig } = this;
362369
const { httpProxyPort } = this.proxyStore;
363370

0 commit comments

Comments
 (0)