Skip to content

Commit a8ae365

Browse files
committed
Enable webhook & delay rules
1 parent 54e96f3 commit a8ae365

File tree

8 files changed

+177
-41
lines changed

8 files changed

+177
-41
lines changed

src/components/modify/rule-row.tsx

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -170,11 +170,21 @@ const Summary = styled.h1`
170170
margin: -5px;
171171
`;
172172

173-
const ArrowIcon = styled(Icon).attrs(() => ({
173+
const RightArrowIcon = styled(Icon).attrs(() => ({
174174
icon: ['fas', 'arrow-left']
175175
}))`
176176
transform: rotate(180deg);
177-
padding: 0 15px;
177+
padding: 0 10px;
178+
align-self: start;
179+
}
180+
`;
181+
182+
const NextStepArrowIcon = styled(Icon).attrs(() => ({
183+
icon: ['fas', 'arrow-left'],
184+
title: "Then..."
185+
}))`
186+
margin: 10px auto;
187+
transform: rotate(270deg);
178188
`;
179189

180190
const Details = styled.div`
@@ -481,7 +491,7 @@ export class RuleRow extends React.Component<{
481491

482492

483493
{ shouldShowSummary &&
484-
<ArrowIcon />
494+
<RightArrowIcon />
485495
}
486496

487497
<MatcherOrSteps>
@@ -495,7 +505,10 @@ export class RuleRow extends React.Component<{
495505
!collapsed && <Details>
496506
<DetailsHeader>Then:</DetailsHeader>
497507

498-
{ rule.steps.map((step, i) =>
508+
{ rule.steps.map((step, i) => <>
509+
{ i > 0 &&
510+
<NextStepArrowIcon key={`then-${i}`} />
511+
}
499512
<StepSection
500513
key={i}
501514

@@ -508,7 +521,7 @@ export class RuleRow extends React.Component<{
508521
availableSteps={availableSteps}
509522
updateStep={this.updateStep}
510523
/>
511-
)}
524+
</>)}
512525
</Details>
513526
}
514527
</MatcherOrSteps>

src/components/modify/step-config.tsx

Lines changed: 117 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
stringToBuffer,
2121
bufferToString
2222
} from '../../util/buffer';
23+
import { formatDuration } from '../../util/text';
2324
import {
2425
getHeaderValue,
2526
headersToRawHeaders,
@@ -50,10 +51,13 @@ import {
5051
RequestBreakpointStep,
5152
ResponseBreakpointStep,
5253
RequestAndResponseBreakpointStep,
54+
DelayStep,
5355
TimeoutStep,
5456
CloseConnectionStep,
5557
ResetConnectionStep,
56-
FromFileResponseStep
58+
FromFileResponseStep,
59+
WebhookStep,
60+
RequestWebhookEvents
5761
} from '../../model/rules/definitions/http-rule-definitions';
5862
import {
5963
WebSocketPassThroughStep,
@@ -177,12 +181,16 @@ export function StepConfiguration(props: {
177181
return <ResponseBreakpointStepConfig {...configProps} />;
178182
case 'request-and-response-breakpoint':
179183
return <RequestAndResponseBreakpointStepConfig {...configProps} />;
184+
case 'delay':
185+
return <WaitForDurationConfig {...configProps} />;
180186
case 'timeout':
181187
return <TimeoutStepConfig {...configProps} />;
182188
case 'close-connection':
183189
return <CloseConnectionStepConfig {...configProps} />;
184190
case 'reset-connection':
185191
return <ResetConnectionStepConfig {...configProps} />;
192+
case 'webhook':
193+
return <WebhookStepConfig {...configProps} />;
186194

187195
case 'ws-echo':
188196
return <WebSocketEchoStepConfig {...configProps} />;
@@ -226,7 +234,7 @@ export function StepConfiguration(props: {
226234
case 'wait-for-rtc-media':
227235
return <RTCWaitForMediaConfig {...configProps} />;
228236
case 'wait-for-duration':
229-
return <RTCWaitForDurationConfig {...configProps} />;
237+
return <WaitForDurationConfig {...configProps} />;
230238
case 'wait-for-rtc-data-channel':
231239
return <RTCWaitForChannelConfig {...configProps} />;
232240
case 'wait-for-rtc-message':
@@ -1725,6 +1733,83 @@ const validateRegexMatcher = (value: string): true | string => {
17251733
}
17261734
}
17271735

1736+
@observer
1737+
class WebhookStepConfig extends StepConfig<WebhookStep> {
1738+
1739+
@observable
1740+
private error: Error | undefined;
1741+
1742+
@observable
1743+
webhookUrl: string = this.props.step.url;
1744+
1745+
@observable
1746+
events: Array<RequestWebhookEvents> = this.props.step.events;
1747+
1748+
render() {
1749+
return <ConfigContainer>
1750+
<SectionLabel>Webhook target URL</SectionLabel>
1751+
<UrlInput
1752+
type="url"
1753+
value={this.webhookUrl}
1754+
invalid={!!this.error}
1755+
spellCheck={false}
1756+
onChange={this.onUrlChange}
1757+
/>
1758+
1759+
<SectionLabel>Webhook events</SectionLabel>
1760+
1761+
<label>
1762+
<input type="checkbox"
1763+
checked={this.events.includes('request')}
1764+
onChange={(e) => this.setEvent('request', e.target.checked)}
1765+
/>
1766+
Request received
1767+
</label>
1768+
<br />
1769+
<label>
1770+
<input
1771+
type="checkbox"
1772+
checked={this.events.includes('response')}
1773+
onChange={(e) => this.setEvent('response', e.target.checked)}
1774+
/>
1775+
Response sent
1776+
</label>
1777+
</ConfigContainer>;
1778+
}
1779+
1780+
@action.bound
1781+
onUrlChange(event: React.ChangeEvent<HTMLInputElement>) {
1782+
this.webhookUrl = event.target.value;
1783+
this.updateStep(event.target);
1784+
}
1785+
1786+
@action.bound
1787+
setEvent(event: RequestWebhookEvents, enabled: boolean) {
1788+
if (enabled && !this.events.includes(event)) {
1789+
this.events.push(event);
1790+
} else if (!enabled && this.events.includes(event)) {
1791+
this.events.splice(this.events.indexOf(event), 1);
1792+
}
1793+
this.updateStep();
1794+
}
1795+
1796+
updateStep(target?: HTMLInputElement) {
1797+
try {
1798+
if (!this.webhookUrl) throw new Error('A webhook URL is required');
1799+
1800+
this.props.onChange(new WebhookStep(this.webhookUrl, this.events));
1801+
1802+
this.error = undefined;
1803+
target?.setCustomValidity('');
1804+
} catch (e) {
1805+
this.error = asError(e);
1806+
target?.setCustomValidity(this.error.message);
1807+
if (this.props.onInvalidState) this.props.onInvalidState();
1808+
}
1809+
target?.reportValidity();
1810+
}
1811+
}
1812+
17281813
@observer
17291814
class PassThroughStepConfig extends StepConfig<
17301815
| PassThroughStep
@@ -1809,15 +1894,15 @@ class TimeoutStepConfig extends StepConfig<TimeoutStep> {
18091894
render() {
18101895
return <ConfigContainer>
18111896
<ConfigExplanation>
1812-
When a matching {
1897+
The {
18131898
isHttpCompatibleType(this.props.ruleType)
18141899
? 'request'
18151900
: this.props.ruleType === 'websocket'
18161901
? 'WebSocket'
18171902
: this.props.ruleType === 'webrtc'
18181903
? (() => { throw new Error('Not compatible with WebRTC rules') })()
18191904
: unreachableCheck(this.props.ruleType)
1820-
} is received, the server will keep the connection open but do nothing.
1905+
} will receive no response, keeping the connection open but doing nothing.
18211906
With no data or response, most clients will time out and abort the
18221907
request after sufficient time has passed.
18231908
</ConfigExplanation>
@@ -1830,15 +1915,15 @@ class CloseConnectionStepConfig extends StepConfig<CloseConnectionStep> {
18301915
render() {
18311916
return <ConfigContainer>
18321917
<ConfigExplanation>
1833-
As soon as a matching {
1918+
The {
18341919
isHttpCompatibleType(this.props.ruleType)
18351920
? 'request'
18361921
: this.props.ruleType === 'websocket'
18371922
? 'WebSocket'
18381923
: this.props.ruleType === 'webrtc'
18391924
? (() => { throw new Error('Not compatible with WebRTC rules') })()
18401925
: unreachableCheck(this.props.ruleType)
1841-
} is received, the connection will be cleanly closed, with no response.
1926+
}'s connection will be cleanly closed, with no response.
18421927
</ConfigExplanation>
18431928
</ConfigContainer>;
18441929
}
@@ -1849,16 +1934,16 @@ class ResetConnectionStepConfig extends StepConfig<ResetConnectionStep> {
18491934
render() {
18501935
return <ConfigContainer>
18511936
<ConfigExplanation>
1852-
As soon as a matching {
1937+
The {
18531938
isHttpCompatibleType(this.props.ruleType)
18541939
? 'request'
18551940
: this.props.ruleType === 'websocket'
18561941
? 'WebSocket'
18571942
: this.props.ruleType === 'webrtc'
18581943
? (() => { throw new Error('Not compatible with WebRTC rules') })()
18591944
: unreachableCheck(this.props.ruleType)
1860-
} is received, the connection will be killed with a TCP RST packet (or a
1861-
RST_STREAM frame, for HTTP/2 requests).
1945+
}'s connection will be abruptly killed with a TCP RST packet (or a
1946+
RST_STREAM frame, for HTTP/2).
18621947
</ConfigExplanation>
18631948
</ConfigContainer>;
18641949
}
@@ -2692,24 +2777,28 @@ class RTCWaitForMediaConfig extends StepConfig<WaitForMediaStep> {
26922777
}
26932778

26942779
@observer
2695-
class RTCWaitForDurationConfig extends StepConfig<WaitForDurationStep> {
2780+
class WaitForDurationConfig extends StepConfig<WaitForDurationStep | DelayStep> {
2781+
2782+
@computed
2783+
get stepDuration() {
2784+
return 'durationMs' in this.props.step
2785+
? this.props.step.durationMs
2786+
: this.props.step.delayMs;
2787+
}
26962788

26972789
@observable
2698-
duration: number | '' = this.props.step.durationMs;
2790+
inputDuration: number | '' = this.stepDuration;
26992791

27002792
componentDidMount() {
27012793
// If the step changes (or when its set initially), update our data fields
27022794
disposeOnUnmount(this, autorun(() => {
2703-
const { durationMs } = this.props.step;
2704-
runInAction(() => {
2705-
if (durationMs === 0 && this.duration === '') return; // Allows clearing the input, making it *implicitly* 0
2706-
this.duration = durationMs;
2707-
});
2795+
if (this.stepDuration === 0 && this.inputDuration === '') return; // Allows clearing the input, making it *implicitly* 0
2796+
this.inputDuration = this.stepDuration;
27082797
}));
27092798
}
27102799

27112800
render() {
2712-
const { duration } = this;
2801+
const { inputDuration: duration } = this;
27132802

27142803
return <ConfigContainer>
27152804
Wait for <TextInput
@@ -2718,7 +2807,10 @@ class RTCWaitForDurationConfig extends StepConfig<WaitForDurationStep> {
27182807
placeholder='Duration (ms)'
27192808
value={duration}
27202809
onChange={this.onChange}
2721-
/> milliseconds.
2810+
/> milliseconds{
2811+
duration !== '' && !formatDuration(duration).endsWith('ms') &&
2812+
` (${ formatDuration(duration) })`
2813+
}.
27222814
</ConfigContainer>
27232815
}
27242816

@@ -2728,12 +2820,16 @@ class RTCWaitForDurationConfig extends StepConfig<WaitForDurationStep> {
27282820

27292821
const newValue = inputValue === ''
27302822
? ''
2731-
: parseInt(inputValue, 10);
2823+
: Math.max(parseInt(inputValue, 10) || 0, 0);
27322824

27332825
if (_.isNaN(newValue)) return; // I.e. reject the edit
27342826

2735-
this.duration = newValue;
2736-
this.props.onChange(new WaitForDurationStep(newValue || 0));
2827+
this.inputDuration = newValue;
2828+
2829+
const step = this.props.ruleType === 'webrtc'
2830+
? new WaitForDurationStep(newValue || 0)
2831+
: new DelayStep(newValue || 0);
2832+
this.props.onChange(step);
27372833
}
27382834

27392835
}

src/components/modify/step-selection.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ import {
2727
TimeoutStep,
2828
CloseConnectionStep,
2929
ResetConnectionStep,
30-
FromFileResponseStep
30+
FromFileResponseStep,
31+
WebhookStep,
32+
DelayStep
3133
} from '../../model/rules/definitions/http-rule-definitions';
3234
import {
3335
WebSocketPassThroughStep,
@@ -109,12 +111,16 @@ const instantiateStep = (
109111
return new ResponseBreakpointStep(rulesStore);
110112
case 'request-and-response-breakpoint':
111113
return new RequestAndResponseBreakpointStep(rulesStore);
114+
case 'delay':
115+
return new DelayStep(0);
112116
case 'timeout':
113117
return new TimeoutStep();
114118
case 'close-connection':
115119
return new CloseConnectionStep();
116120
case 'reset-connection':
117121
return new ResetConnectionStep();
122+
case 'webhook':
123+
return new WebhookStep('http://', ['request', 'response']);
118124

119125
case 'ws-passthrough':
120126
return new WebSocketPassThroughStep(rulesStore);

src/model/rules/definitions/http-rule-definitions.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,16 @@ serializr.createModelSchema(RequestAndResponseBreakpointStep, {
336336
type: serializr.primitive()
337337
}, (context) => new RequestAndResponseBreakpointStep(context.args.rulesStore));
338338

339+
export type RequestWebhookEvents = httpSteps.RequestWebhookEvents;
340+
341+
export class WebhookStep extends httpSteps.WebhookStep {
342+
explain() {
343+
return "enable webhooks";
344+
}
345+
}
346+
347+
export type DelayStep = httpSteps.DelayStep;
348+
export const DelayStep = httpSteps.DelayStep;
339349
export type TimeoutStep = httpSteps.TimeoutStep;
340350
export const TimeoutStep = httpSteps.TimeoutStep;
341351
export type CloseConnectionStep = httpSteps.CloseConnectionStep;
@@ -368,7 +378,8 @@ export const HttpStepLookup = {
368378
'req-res-transformer': TransformingStep,
369379
'request-breakpoint': RequestBreakpointStep,
370380
'response-breakpoint': ResponseBreakpointStep,
371-
'request-and-response-breakpoint': RequestAndResponseBreakpointStep
381+
'request-and-response-breakpoint': RequestAndResponseBreakpointStep,
382+
'webhook': WebhookStep
372383
};
373384

374385
type HttpMatcherClass = typeof HttpMatcherLookup[keyof typeof HttpMatcherLookup];

0 commit comments

Comments
 (0)