Skip to content

Commit 920a5cc

Browse files
committed
Fix UX around pending requests and show live counting timers
1 parent 3af2202 commit 920a5cc

File tree

8 files changed

+118
-40
lines changed

8 files changed

+118
-40
lines changed

src/components/common/duration-pill.tsx

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import * as React from 'react';
22
import { observer } from 'mobx-react';
3+
34
import { TimingEvents } from '../../types';
5+
import { observableClock } from '../../util/observable';
46

57
import { Pill } from './pill';
68

@@ -11,20 +13,30 @@ function sigFig(num: number, figs: number): number {
1113
type DurationPillProps = { className?: string } & (
1214
| { durationMs: number }
1315
| { timingEvents: Partial<TimingEvents> }
14-
)
16+
);
17+
18+
const calculateDuration = (timingEvents: Partial<TimingEvents>) => {
19+
const doneTimestamp = timingEvents.responseSentTimestamp ?? timingEvents.abortedTimestamp;
20+
21+
if (timingEvents.startTimestamp !== undefined && doneTimestamp !== undefined) {
22+
return doneTimestamp - timingEvents.startTimestamp;
23+
}
24+
25+
if (timingEvents.startTime !== undefined) {
26+
// This may not be perfect - note that startTime comes from the server so we might be
27+
// mildly out of sync (ehhhh, in theory) but this is only for pending requests where
28+
// that's unlikely to be an issue - the final time will be correct regardless.
29+
return observableClock.getTime() - timingEvents.startTime;
30+
}
31+
}
1532

1633
export const DurationPill = observer((p: DurationPillProps) => {
1734
let duration: number | undefined;
1835

1936
if ('durationMs' in p) {
2037
duration = p.durationMs;
21-
} else {
22-
const timingEvents = p.timingEvents;
23-
const doneTimestamp = timingEvents.responseSentTimestamp ?? timingEvents.abortedTimestamp;
24-
25-
duration = doneTimestamp !== undefined && timingEvents.startTimestamp !== undefined
26-
? doneTimestamp - timingEvents.startTimestamp
27-
: undefined;
38+
} else if (p.timingEvents) {
39+
duration = calculateDuration(p.timingEvents);
2840
}
2941

3042
if (duration === undefined) return null;

src/components/send/request-pane.tsx

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ export class RequestPane extends React.Component<{
5656
editorNode: portals.HtmlPortalNode<typeof ContainerSizedEditor>,
5757

5858
requestInput: RequestInput,
59-
sendRequest: () => void
59+
sendRequest: () => void,
60+
isSending: boolean
6061
}> {
6162

6263
get cardProps() {
@@ -81,22 +82,28 @@ export class RequestPane extends React.Component<{
8182
}
8283

8384
render() {
84-
const { requestInput, editorNode, uiStore } = this.props;
85+
const {
86+
requestInput,
87+
sendRequest,
88+
isSending,
89+
editorNode,
90+
uiStore
91+
} = this.props;
8592

8693
return <SendCardContainer
8794
hasExpandedChild={!!uiStore?.expandedSendRequestCard}
8895
>
8996
<RequestPaneKeyboardShortcuts
90-
sendRequest={this.sendRequest}
97+
sendRequest={sendRequest}
9198
/>
9299

93100
<SendRequestLine
94101
method={requestInput.method}
95102
updateMethod={this.updateMethod}
96103
url={requestInput.url}
97104
updateUrl={this.updateUrl}
98-
isSending={this.isSending}
99-
sendRequest={this.sendRequest}
105+
isSending={isSending}
106+
sendRequest={sendRequest}
100107
/>
101108
<SendRequestHeadersCard
102109
{...this.cardProps.requestHeaders}
@@ -143,21 +150,4 @@ export class RequestPane extends React.Component<{
143150
requestInput.rawBody.updateDecodedBody(input);
144151
}
145152

146-
@observable
147-
private isSending = false;
148-
149-
sendRequest = flow(function * (this: RequestPane) {
150-
if (this.isSending) return;
151-
152-
this.isSending = true;
153-
154-
try {
155-
yield this.props.sendRequest();
156-
} catch (e) {
157-
console.warn('Sending request failed', e);
158-
} finally {
159-
this.isSending = false;
160-
}
161-
}).bind(this);
162-
163153
}

src/components/send/response-pane.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,12 @@ export class ResponsePane extends React.Component<{
101101
}
102102

103103
renderInProgressResponse() {
104-
const { uiStore, editorNode, requestInput } = this.props;
104+
const { uiStore, editorNode, requestInput, exchange } = this.props;
105105

106106
return <>
107107
<PendingResponseStatusSection
108108
theme={uiStore!.theme}
109+
timingEvents={exchange?.timingEvents}
109110
/>
110111
<PendingResponseHeaderSection
111112
{...this.cardProps.responseHeaders}

src/components/send/send-page.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,15 @@ export class SendPage extends React.Component<{
5353
attributes: { 'style': 'height: 100%' }
5454
});
5555

56+
private sendRequest = () => {
57+
const {
58+
sendRequest,
59+
selectedRequest
60+
} = this.props.sendStore!;
61+
62+
sendRequest(selectedRequest);
63+
};
64+
5665
render() {
5766
const {
5867
sendRequests,
@@ -91,7 +100,10 @@ export class SendPage extends React.Component<{
91100
>
92101
<RequestPane
93102
requestInput={selectedRequest.request}
94-
sendRequest={() => sendRequest(selectedRequest)}
103+
sendRequest={this.sendRequest}
104+
isSending={
105+
selectedRequest.pendingSendPromise?.state === 'pending'
106+
}
95107
editorNode={this.requestEditorNode}
96108
/>
97109
<ResponsePane

src/components/send/sent-response-status.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import * as _ from 'lodash';
22
import * as React from 'react';
3+
import { observer } from 'mobx-react-lite';
34

5+
import { TimingEvents } from '../../types';
46
import { Theme, styled } from '../../styles';
57
import { getReadableSize } from '../../util/buffer';
68

@@ -54,8 +56,9 @@ export const ResponseStatusSection = (props: {
5456
</ResponseStatusSectionCard>;
5557
}
5658

57-
export const PendingResponseStatusSection = (props: {
58-
theme: Theme
59+
export const PendingResponseStatusSection = observer((props: {
60+
theme: Theme,
61+
timingEvents?: Partial<TimingEvents>
5962
}) => {
6063
return <ResponseStatusSectionCard
6164
className='ignores-expanded' // This always shows, even if something is expanded
@@ -69,9 +72,10 @@ export const PendingResponseStatusSection = (props: {
6972
>
7073
&nbsp;...&nbsp;
7174
</Pill>
75+
<DurationPill timingEvents={props.timingEvents ?? {}} />
7276
</header>
7377
</ResponseStatusSectionCard>;
74-
}
78+
});
7579

7680
export const FailedResponseStatusSection = (props: {
7781
exchange: CompletedExchange,

src/model/send/send-request-model.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,15 @@ import * as serializr from 'serializr';
33
import { observable } from 'mobx';
44

55
import { HttpExchange, RawHeaders } from "../../types";
6+
import { ObservablePromise } from '../../util/observable';
7+
68
import { EditableContentType, getEditableContentTypeFromViewable } from "../events/content-types";
79
import { EditableBody } from '../http/editable-body';
8-
import { syncBodyToContentLength, syncFormattingToContentType, syncUrlToHeaders } from '../http/editable-request-parts';
10+
import {
11+
syncBodyToContentLength,
12+
syncFormattingToContentType,
13+
syncUrlToHeaders
14+
} from '../http/editable-request-parts';
915

1016
// This is our model of a Request for sending. Smilar to the API model,
1117
// but not identical, as we add extra UI metadata etc.
@@ -70,6 +76,7 @@ export interface SendRequest {
7076
id: string;
7177
request: RequestInput;
7278
sentExchange: HttpExchange | undefined;
79+
pendingSendPromise?: ObservablePromise<void>;
7380
}
7481

7582
const requestInputSchema = serializr.createModelSchema(RequestInput, {
@@ -94,7 +101,8 @@ const requestInputSchema = serializr.createModelSchema(RequestInput, {
94101
export const sendRequestSchema = serializr.createSimpleSchema({
95102
id: serializr.primitive(),
96103
request: serializr.object(requestInputSchema),
97-
sentExchange: false // Never persisted here
104+
sentExchange: false, // Never persisted here (exportable as HAR etc though)
105+
pendingSendPromise: false // Never persisted at all
98106
});
99107

100108
export async function buildRequestInputFromExchange(exchange: HttpExchange): Promise<RequestInput> {

src/model/send/send-store.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
} from 'mockttp';
1111

1212
import { logError } from '../../errors';
13-
import { lazyObservablePromise } from '../../util/observable';
13+
import { getObservableDeferred, lazyObservablePromise } from '../../util/observable';
1414
import { persist, hydrate } from '../../util/mobx-persist/persist';
1515
import { ErrorLike, UnreachableCheck } from '../../util/error';
1616
import { rawHeadersToHeaders } from '../../util/headers';
@@ -131,10 +131,15 @@ export class SendStore {
131131
trackEvent({ category: 'Send', action: 'Sent request' });
132132

133133
const requestInput = sendRequest.request;
134+
const pendingRequestDeferred = getObservableDeferred();
134135
runInAction(() => {
135136
sendRequest.sentExchange = undefined;
137+
138+
sendRequest.pendingSendPromise = pendingRequestDeferred.promise;
139+
sendRequest.pendingSendPromise.then(() => { sendRequest.pendingSendPromise = undefined; });
136140
});
137141

142+
138143
const exchangeId = uuid();
139144

140145
const passthroughOptions = this.rulesStore.activePassthroughOptions;
@@ -194,11 +199,14 @@ export class SendStore {
194199
},
195200
tags: error.code ? [`passthrough-error:${error.code}`] : []
196201
});
197-
}));
202+
}))
203+
.then(() => pendingRequestDeferred.resolve());
198204

199205
runInAction(() => {
200206
sendRequest.sentExchange = exchange;
201207
});
208+
209+
return sendRequest.pendingSendPromise;
202210
}
203211

204212
}

src/util/observable.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,4 +165,47 @@ export function debounceComputed<T>(
165165

166166
return computed(debounced(fn, timeoutMs), computedOptions);
167167
}
168-
}
168+
}
169+
170+
// An observable clock, allowing for time-reactive logic & UI, but ticking only while
171+
// observed, so no intervals etc are required any time it isn't in active use. Closely
172+
// based on the example in https://mobx.js.org/custom-observables.html.
173+
class Clock {
174+
175+
private atom: IAtom = createAtom(
176+
"Clock",
177+
() => this.startTicking(),
178+
() => this.stopTicking()
179+
);
180+
181+
private intervalHandler: ReturnType<typeof setInterval> | null = null;
182+
private currentDateTime: number = Date.now();
183+
184+
getTime() {
185+
if (this.atom.reportObserved()) {
186+
return this.currentDateTime;
187+
} else {
188+
return Date.now();
189+
}
190+
}
191+
192+
tick() {
193+
this.currentDateTime = Date.now();
194+
this.atom.reportChanged();
195+
}
196+
197+
startTicking() {
198+
this.tick();
199+
this.intervalHandler = setInterval(() => this.tick(), 50);
200+
}
201+
202+
stopTicking() {
203+
if (this.intervalHandler == null) return;
204+
205+
clearInterval(this.intervalHandler);
206+
this.intervalHandler = null;
207+
}
208+
209+
}
210+
211+
export const observableClock = new Clock();

0 commit comments

Comments
 (0)