Skip to content

Commit

Permalink
Add very basic Send UI (feature flagged) for initial testing
Browse files Browse the repository at this point in the history
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!
  • Loading branch information
pimterry committed Jul 21, 2023
1 parent 2c037c5 commit 8382e60
Show file tree
Hide file tree
Showing 13 changed files with 597 additions and 21 deletions.
32 changes: 31 additions & 1 deletion src/components/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,20 @@ import { appHistory } from '../routing';
import { useHotkeys, Ctrl } from '../util/ui';

import { AccountStore } from '../model/account/account-store';
import { serverVersion, versionSatisfies, MOCK_SERVER_RANGE } from '../services/service-versions';
import {
serverVersion,
versionSatisfies,
MOCK_SERVER_RANGE,
SERVER_SEND_API_SUPPORTED
} from '../services/service-versions';

import { Sidebar, SidebarItem, SIDEBAR_WIDTH } from './sidebar';
import { InterceptPage } from './intercept/intercept-page';
import { ViewPage } from './view/view-page';
import { MockPage } from './mock/mock-page';
import { SendPage } from './send/send-page';
import { SettingsPage } from './settings/settings-page';

import { PlanPicker } from './account/plan-picker';
import { ModalOverlay } from './account/modal-overlay';
import { CheckoutSpinner } from './account/checkout-spinner';
Expand Down Expand Up @@ -83,6 +90,16 @@ class App extends React.Component<{ accountStore: AccountStore }> {
return this.props.accountStore.isPaidUser || this.props.accountStore.isPastDueUser;
}

@computed
get canVisitSend() {
return this.props.accountStore.featureFlags.includes('send') && (
// Hide Send option if the server is too old for proper support.
// We show by default to avoid flicker in the most common case
serverVersion.state !== 'fulfilled' ||
versionSatisfies(serverVersion.value as string, SERVER_SEND_API_SUPPORTED)
);
}

@computed
get menuItems() {
return [
Expand Down Expand Up @@ -121,6 +138,18 @@ class App extends React.Component<{ accountStore: AccountStore }> {
: []
),

...(this.canVisitSend
? [{
name: 'Send',
title: `Send HTTP requests directly (${Ctrl}+4)`,
icon: ['far', 'paper-plane'],
position: 'top',
type: 'router',
url: '/send'
}]
: []
),

(this.canVisitSettings
? {
name: 'Settings',
Expand Down Expand Up @@ -191,6 +220,7 @@ class App extends React.Component<{ accountStore: AccountStore }> {
<Route path={'/view/:eventId'} pageComponent={ViewPage} />
<Route path={'/mock'} pageComponent={MockPage} />
<Route path={'/mock/:initialRuleId'} pageComponent={MockPage} />
<Route path={'/send'} pageComponent={SendPage} />
<Route path={'/settings'} pageComponent={SettingsPage} />
</Router>
</AppContainer>
Expand Down
173 changes: 173 additions & 0 deletions src/components/send/request-pane.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import * as _ from 'lodash';
import * as React from 'react';
import { action, computed } from 'mobx';
import { inject, observer } from 'mobx-react';
import {
MOCKTTP_PARAM_REF,
ProxySetting,
ProxySettingSource,
RuleParameterReference
} from 'mockttp';

import { RawHeaders } from '../../types';
import { styled } from '../../styles';
import { bufferToString, isProbablyUtf8, stringToBuffer } from '../../util';

import { sendRequest } from '../../services/server-api';
import { RulesStore } from '../../model/rules/rules-store';
import { SendStore } from '../../model/send/send-store';
import { ClientProxyConfig, RULE_PARAM_REF_KEY } from '../../model/send/send-data-model';

import { EditableRawHeaders } from '../common/editable-headers';
import { ThemedSelfSizedEditor } from '../editor/base-editor';
import { Button, TextInput } from '../common/inputs';

const RequestPaneContainer = styled.section`
display: flex;
flex-direction: column;
`;

const UrlInput = styled(TextInput)`
`;

export const getEffectivePort = (url: { protocol: string | null, port: string | null }) => {
if (url.port) {
return parseInt(url.port, 10);
} else if (url.protocol === 'https:' || url.protocol === 'wss:') {
return 443;
} else {
return 80;
}
}

@inject('rulesStore')
@inject('sendStore')
@observer
export class RequestPane extends React.Component<{
rulesStore?: RulesStore,
sendStore?: SendStore
}> {

@computed
get method() {
return this.props.sendStore!.requestInput.method;
}

@computed
get url() {
return this.props.sendStore!.requestInput.url;
}


@computed
get headers() {
return this.props.sendStore!.requestInput.headers;
}

@computed
get body() {
return this.props.sendStore!.requestInput.rawBody;
}

@computed
private get bodyTextEncoding() {
// If we're handling text data, we want to show & edit it as UTF8.
// If it's binary, that's a lossy operation, so we use binary (latin1) instead.
return isProbablyUtf8(this.body)
? 'utf8'
: 'binary';
}

render() {
const bodyString = bufferToString(this.body, this.bodyTextEncoding);

return <RequestPaneContainer>
<UrlInput
placeholder='https://example.com/hello?name=world'
value={this.url}
onChange={this.updateUrl}
/>
<EditableRawHeaders
headers={this.headers}
onChange={this.updateHeaders}
/>
<ThemedSelfSizedEditor
contentId='request'
language={'text'}
value={bodyString}
onChange={this.updateBody}
/>
<Button
onClick={this.sendRequest}
/>
</RequestPaneContainer>;
}

@action.bound
updateUrl(changeEvent: React.ChangeEvent<HTMLInputElement>) {
const { requestInput } = this.props.sendStore!;
requestInput.url = changeEvent.target.value;
}

@action.bound
updateHeaders(headers: RawHeaders) {
const { requestInput } = this.props.sendStore!;
requestInput.headers = headers;
}

@action.bound
updateBody(input: string) {
const { requestInput } = this.props.sendStore!;
requestInput.rawBody = stringToBuffer(input, this.bodyTextEncoding);
}

@action.bound
async sendRequest() {
const rulesStore = this.props.rulesStore!;
const passthroughOptions = rulesStore.activePassthroughOptions;

const url = new URL(this.url);
const effectivePort = getEffectivePort(url);
const hostWithPort = `${url.hostname}:${effectivePort}`;
const clientCertificate = passthroughOptions.clientCertificateHostMap?.[hostWithPort] ||
passthroughOptions.clientCertificateHostMap?.[url.hostname!] ||
undefined;

const responseStream = await sendRequest({
url: this.url,
method: this.method,
headers: this.headers,
rawBody: this.body
}, {
ignoreHostHttpsErrors: passthroughOptions.ignoreHostHttpsErrors,
trustAdditionalCAs: rulesStore.additionalCaCertificates.map((cert) => ({ cert: cert.rawPEM })),
clientCertificate,
proxyConfig: getProxyConfig(rulesStore.proxyConfig),
lookupOptions: passthroughOptions.lookupOptions
});

const reader = responseStream.getReader();
while(true) {
const { done, value } = await reader.read();
if (done) return;
else console.log(value);
}
}

}

function getProxyConfig(proxyConfig: RulesStore['proxyConfig']): ClientProxyConfig {
if (!proxyConfig) return undefined;

if (_.isArray(proxyConfig)) {
return proxyConfig.map((config) => getProxyConfig(config)) as ClientProxyConfig;
}

if (MOCKTTP_PARAM_REF in proxyConfig) {
return {
[RULE_PARAM_REF_KEY]: (proxyConfig as RuleParameterReference<ProxySettingSource>)[MOCKTTP_PARAM_REF]
};
}

return proxyConfig as ProxySetting;
}
18 changes: 18 additions & 0 deletions src/components/send/response-pane.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as React from "react";
import { observer } from "mobx-react";

import { styled } from '../../styles';

const ResponsePaneContainer = styled.section`
`;

@observer
export class ResponsePane extends React.Component<{}> {

render() {
return <ResponsePaneContainer>

</ResponsePaneContainer>;
}

}
33 changes: 33 additions & 0 deletions src/components/send/send-page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import * as React from 'react';
import { observer } from 'mobx-react';

import { styled } from '../../styles';

import { SplitPane } from '../split-pane';
import { RequestPane } from './request-pane';
import { ResponsePane } from './response-pane';

const SendPageContainer = styled.div`
height: 100vh;
position: relative;
`;

@observer
export class SendPage extends React.Component<{}> {

render() {
return <SendPageContainer>
<SplitPane
split='vertical'
primary='second'
defaultSize='50%'
minSize={300}
maxSize={-300}
>
<RequestPane />
<ResponsePane />
</SplitPane>
</SendPageContainer>;
}

}
2 changes: 2 additions & 0 deletions src/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import { faAlignLeft } from '@fortawesome/free-solid-svg-icons/faAlignLeft';
import { faClone } from '@fortawesome/free-regular-svg-icons/faClone';
import { faCheck } from '@fortawesome/free-solid-svg-icons/faCheck';
import { faLevelDownAlt } from '@fortawesome/free-solid-svg-icons/faLevelDownAlt';
import { faPaperPlane } from '@fortawesome/free-regular-svg-icons/faPaperPlane';

import { faChrome } from '@fortawesome/free-brands-svg-icons/faChrome';
import { faFirefox } from '@fortawesome/free-brands-svg-icons/faFirefox';
Expand Down Expand Up @@ -139,6 +140,7 @@ library.add(
faClone,
faCheck,
faLevelDownAlt,
faPaperPlane,

faChrome,
faFirefox,
Expand Down
8 changes: 6 additions & 2 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@ import { EventsStore } from './model/events/events-store';
import { RulesStore } from './model/rules/rules-store';
import { InterceptorStore } from './model/interception/interceptor-store';
import { ApiStore } from './model/api/api-store';
import { SendStore } from './model/send/send-store';

import { triggerServerUpdate } from './services/server-api';
import { serverVersion, lastServerVersion, UI_VERSION } from './services/service-versions';

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

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

Expand Down Expand Up @@ -78,6 +80,7 @@ const apiStore = new ApiStore(accountStore);
const uiStore = new UiStore(accountStore);
const proxyStore = new ProxyStore(accountStore);
const interceptorStore = new InterceptorStore(proxyStore, accountStore);
const sendStore = new SendStore();

// Some non-trivial interactions between rules & events stores here. Rules need to use events to
// handle breakpoints (where rule logic reads from received event data), while events need to use
Expand Down Expand Up @@ -106,7 +109,8 @@ const stores = {
proxyStore,
eventsStore,
interceptorStore,
rulesStore
rulesStore,
sendStore
};

const appStartupPromise = Promise.all(
Expand Down
11 changes: 9 additions & 2 deletions src/model/rules/rules-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import {
requestHandlers,
MOCKTTP_PARAM_REF,
ProxyConfig,
ProxySetting
ProxySetting,
RuleParameterReference,
ProxySettingSource
} from 'mockttp';
import * as MockRTC from 'mockrtc';

Expand Down Expand Up @@ -357,7 +359,12 @@ export class RulesStore {
}

@computed.struct
get proxyConfig(): ProxyConfig {
get proxyConfig():
| ProxySetting
| RuleParameterReference<ProxySettingSource>
| Array<ProxySetting | RuleParameterReference<ProxySettingSource>>
| undefined
{
const { userProxyConfig } = this;
const { httpProxyPort } = this.proxyStore;

Expand Down
Loading

0 comments on commit 8382e60

Please sign in to comment.