Skip to content

Commit

Permalink
feat: display status of sending request steps - INS-3635 (#7382)
Browse files Browse the repository at this point in the history
* feat: display status of sending request steps

* fix: activeRequest id is necessary for different cases

* chore: rephrase step name and improve its typing

* feat: show sending status for mock routes

* fix: make text vertically aligned and reformat code

* fix: spinner line height

* feat: add after-response script step to the progress

* fix: add useEffect deps

* refactor: add a function for finish last timing record and compact logic a bit

* simplify

* add tooltip

* fix timer

* use duration

* improve time tag

* refactor: replace callback with hook

* fix: add the missing hook

* rename isExecuting

* delay snippet

* works

* rename bridge methods

* remove undefined initial state

* force stop execution before cancel

* fix time tag layout

* fix hang

* simplify api

* tidy up

---------

Co-authored-by: jackkav <jackkav@gmail.com>
  • Loading branch information
ihexxa and jackkav committed Jun 18, 2024
1 parent fd8f12e commit b8089ce
Show file tree
Hide file tree
Showing 18 changed files with 239 additions and 67 deletions.
13 changes: 1 addition & 12 deletions packages/insomnia/src/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,7 @@ export const getClientString = () => `${getAppEnvironment()}::${getAppPlatform()

// Global Stuff
export const DEBOUNCE_MILLIS = 100;
export const REQUEST_TIME_TO_SHOW_COUNTER = 1; // Seconds

/**
* A number in milliseconds representing the time required to setup and teardown a request.
*
* Should not be used for anything a user may rely on for performance metrics of any kind.
*
* While this isn't a perfect "magic-number" (it can be as low as 120ms and as high as 300) it serves as a rough average.
*
* For initial introduction, see https://github.com/Kong/insomnia/blob/8aa274d21b351c4710f0bb833cba7deea3d56c29/app/ui/components/ResponsePane.js#L100
*/
export const REQUEST_SETUP_TEARDOWN_COMPENSATION = 200;

export const STATUS_CODE_PLUGIN_ERROR = -222;
export const LARGE_RESPONSE_MB = 5;
export const HUGE_RESPONSE_MB = 100;
Expand Down
6 changes: 5 additions & 1 deletion packages/insomnia/src/main/ipc/electron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type HandleChannels =
| 'curl.readyState'
| 'curlRequest'
| 'database.caCertificate.create'
| 'getExecution'
| 'grpc.loadMethods'
| 'grpc.loadMethodsFromReflection'
| 'installPlugin'
Expand Down Expand Up @@ -63,7 +64,10 @@ export type MainOnChannels =
| 'trackSegmentEvent'
| 'webSocket.close'
| 'webSocket.closeAll'
| 'writeText';
| 'writeText'
| 'addExecutionStep'
| 'completeExecutionStep'
| 'startExecution';
export type RendererOnChannels =
'clear-all-models'
| 'clear-model'
Expand Down
17 changes: 17 additions & 0 deletions packages/insomnia/src/main/ipc/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { backup, restoreBackup } from '../backup';
import installPlugin from '../install-plugin';
import { CurlBridgeAPI } from '../network/curl';
import { cancelCurlRequest, curlRequest } from '../network/libcurl-promise';
import { addExecutionStep, completeExecutionStep, getExecution, startExecution, StepName, TimingStep } from '../network/request-timing';
import { WebSocketBridgeAPI } from '../network/websocket';
import { ipcMainHandle, ipcMainOn, type RendererOnChannels } from './electron';
import { gRPCBridgeAPI } from './grpc';
Expand Down Expand Up @@ -41,8 +42,24 @@ export interface RendererToMainBridgeAPI {
};
};
hiddenBrowserWindow: HiddenBrowserWindowBridgeAPI;
getExecution: (options: { requestId: string }) => Promise<TimingStep[]>;
addExecutionStep: (options: { requestId: string; stepName: StepName }) => void;
startExecution: (options: { requestId: string }) => void;
completeExecutionStep: (options: { requestId: string }) => void;
}
export function registerMainHandlers() {
ipcMainOn('addExecutionStep', (_, options: { requestId: string; stepName: StepName }) => {
addExecutionStep(options.requestId, options.stepName);
});
ipcMainOn('startExecution', (_, options: { requestId: string }) => {
return startExecution(options.requestId);
});
ipcMainOn('completeExecutionStep', (_, options: { requestId: string }) => {
return completeExecutionStep(options.requestId);
});
ipcMainHandle('getExecution', (_, options: { requestId: string }) => {
return getExecution(options.requestId);
});
ipcMainHandle('database.caCertificate.create', async (_, options: { parentId: string; path: string }) => {
return models.caCertificate.create(options);
});
Expand Down
39 changes: 39 additions & 0 deletions packages/insomnia/src/main/network/request-timing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { BrowserWindow } from 'electron';

export type StepName = 'Executing pre-request script'
| 'Rendering request'
| 'Sending request'
| 'Executing after-response script';

export interface TimingStep {
stepName: StepName;
startedAt: number;
duration?: number;
}
export const executions = new Map<string, TimingStep[]>();
export const getExecution = (requestId?: string) => requestId ? executions.get(requestId) : [];
export const startExecution = (requestId: string) => executions.set(requestId, []);
export function addExecutionStep(
requestId: string,
stepName: StepName,
) {
// append to new step to execution
const record: TimingStep = {
stepName,
startedAt: Date.now(),
};
const execution = [...(executions.get(requestId) || []), record];
executions.set(requestId, execution);
for (const window of BrowserWindow.getAllWindows()) {
window.webContents.send(`syncTimers.${requestId}`, { executions: executions.get(requestId) });
}
}
export function completeExecutionStep(requestId: string) {
const latest = executions.get(requestId)?.at(-1);
if (latest) {
latest.duration = (Date.now() - latest.startedAt);
}
for (const window of BrowserWindow.getAllWindows()) {
window.webContents.send(`syncTimers.${requestId}`, { executions: executions.get(requestId) });
}
}
1 change: 1 addition & 0 deletions packages/insomnia/src/network/cancellation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Request } from '../models/request';
const cancelRequestFunctionMap = new Map<string, () => void>();

export async function cancelRequestById(requestId: string) {
window.main.completeExecutionStep({ requestId });
const cancel = cancelRequestFunctionMap.get(requestId);
if (cancel) {
return cancel();
Expand Down
4 changes: 4 additions & 0 deletions packages/insomnia/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ const grpc: gRPCBridgeAPI = {
loadMethodsFromReflection: options => ipcRenderer.invoke('grpc.loadMethodsFromReflection', options),
};
const main: Window['main'] = {
startExecution: options => ipcRenderer.send('startExecution', options),
addExecutionStep: options => ipcRenderer.send('addExecutionStep', options),
completeExecutionStep: options => ipcRenderer.send('completeExecutionStep', options),
getExecution: options => ipcRenderer.invoke('getExecution', options),
loginStateChange: () => ipcRenderer.send('loginStateChange'),
restart: () => ipcRenderer.send('restart'),
openInBrowser: options => ipcRenderer.send('openInBrowser', options),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ const updateRequestAuth =
'bearer'
);`;
const requireAModule = "const atob = require('atob');";
const delay = 'new Promise((resolve)=>setTimeout(resolve, 1000));';

const getStatusCode = 'const statusCode = insomnia.response.code;';
const getStatusMsg = 'const status = insomnia.response.status;';
Expand Down Expand Up @@ -336,6 +337,11 @@ const miscMenu: SnippetMenuItem = {
'name': 'Require a module',
'snippet': requireAModule,
},
{
'id': 'delay',
'name': 'Delay',
'snippet': delay,
},
],
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { Response } from '../../../models/response';
import { cancelRequestById } from '../../../network/cancellation';
import { insomniaFetch } from '../../../ui/insomniaFetch';
import { jsonPrettify } from '../../../utils/prettify/json';
import { useExecutionState } from '../../hooks/use-execution-state';
import { MockRouteLoaderData } from '../../routes/mock-route';
import { useRootLoaderData } from '../../routes/root';
import { Dropdown, DropdownButton, DropdownItem, DropdownSection, ItemContent } from '../base/dropdown';
Expand Down Expand Up @@ -54,6 +55,7 @@ export const MockResponsePane = () => {
const [timeline, setTimeline] = useState<ResponseTimelineEntry[]>([]);
const [previewMode, setPreviewMode] = useState<PreviewMode>(PREVIEW_MODE_FRIENDLY);
const requestFetcher = useFetcher({ key: 'mock-request-fetcher' });
const { steps } = useExecutionState({ requestId: activeResponse?.parentId });

useEffect(() => {
const fn = async () => {
Expand All @@ -69,6 +71,8 @@ export const MockResponsePane = () => {
<PlaceholderResponsePane>
{<ResponseTimer
handleCancel={() => activeResponse && cancelRequestById(activeResponse.parentId)}
activeRequestId={mockRoute._id}
steps={steps}
/>}
</PlaceholderResponsePane>
);
Expand All @@ -79,7 +83,7 @@ export const MockResponsePane = () => {
<PaneHeader className="row-spaced">
<div aria-atomic="true" aria-live="polite" className="no-wrap scrollable scrollable--no-bars pad-left">
<StatusTag statusCode={activeResponse.statusCode} statusMessage={activeResponse.statusMessage} />
<TimeTag milliseconds={activeResponse.elapsedTime} />
<TimeTag milliseconds={activeResponse.elapsedTime} steps={[]} />
<SizeTag bytesRead={activeResponse.bytesRead} bytesContent={activeResponse.bytesContent} />
</div>
</PaneHeader>
Expand Down
3 changes: 0 additions & 3 deletions packages/insomnia/src/ui/components/panes/request-pane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,12 @@ import { PlaceholderRequestPane } from './placeholder-request-pane';
interface Props {
environmentId: string;
settings: Settings;
setLoading: (l: boolean) => void;
onPaste: (text: string) => void;
}

export const RequestPane: FC<Props> = ({
environmentId,
settings,
setLoading,
onPaste,
}) => {
const { activeRequest, activeRequestMeta } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
Expand Down Expand Up @@ -104,7 +102,6 @@ export const RequestPane: FC<Props> = ({
uniquenessKey={uniqueKey}
handleAutocompleteUrls={() => queryAllWorkspaceUrls(workspaceId, models.request.type, requestId)}
nunjucksPowerUserMode={settings.nunjucksPowerUserMode}
setLoading={setLoading}
onPaste={onPaste}
/>
</ErrorBoundary>
Expand Down
19 changes: 14 additions & 5 deletions packages/insomnia/src/ui/components/panes/response-pane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { getSetCookieHeaders } from '../../../common/misc';
import * as models from '../../../models';
import { cancelRequestById } from '../../../network/cancellation';
import { jsonPrettify } from '../../../utils/prettify/json';
import { useExecutionState } from '../../hooks/use-execution-state';
import { useRequestMetaPatcher } from '../../hooks/use-request';
import { RequestLoaderData } from '../../routes/request';
import { useRootLoaderData } from '../../routes/root';
Expand All @@ -30,10 +31,10 @@ import { Pane, PaneHeader } from './pane';
import { PlaceholderResponsePane } from './placeholder-response-pane';

interface Props {
runningRequests: Record<string, boolean>;
activeRequestId: string;
}
export const ResponsePane: FC<Props> = ({
runningRequests,
activeRequestId,
}) => {
const { activeRequest, activeRequestMeta, activeResponse } = useRouteLoaderData('request/:requestId') as RequestLoaderData;
const filterHistory = activeRequestMeta.responseFilterHistory || [];
Expand Down Expand Up @@ -73,6 +74,9 @@ export const ResponsePane: FC<Props> = ({
window.clipboard.writeText(bodyBuffer.toString('utf8'));
}
}, [handleGetResponseBody]);

const { isExecuting, steps } = useExecutionState({ requestId: activeRequest._id });

const handleDownloadResponseBody = useCallback(async (prettify: boolean) => {
if (!activeResponse || !activeRequest) {
console.warn('Nothing to download');
Expand Down Expand Up @@ -128,12 +132,15 @@ export const ResponsePane: FC<Props> = ({
if (!activeResponse) {
return (
<PlaceholderResponsePane>
{runningRequests[activeRequest._id] && <ResponseTimer
{isExecuting && <ResponseTimer
handleCancel={() => cancelRequestById(activeRequest._id)}
activeRequestId={activeRequestId}
steps={steps}
/>}
</PlaceholderResponsePane>
);
}

const timeline = models.response.getTimeline(activeResponse);
const cookieHeaders = getSetCookieHeaders(activeResponse.headers);
return (
Expand All @@ -142,7 +149,7 @@ export const ResponsePane: FC<Props> = ({
<PaneHeader className="row-spaced">
<div aria-atomic="true" aria-live="polite" className="no-wrap scrollable scrollable--no-bars pad-left">
<StatusTag statusCode={activeResponse.statusCode} statusMessage={activeResponse.statusMessage} />
<TimeTag milliseconds={activeResponse.elapsedTime} />
<TimeTag milliseconds={activeResponse.elapsedTime} steps={steps} />
<SizeTag bytesRead={activeResponse.bytesRead} bytesContent={activeResponse.bytesContent} />
</div>
<ResponseHistoryDropdown
Expand Down Expand Up @@ -229,8 +236,10 @@ export const ResponsePane: FC<Props> = ({
</TabItem>
</Tabs>
<ErrorBoundary errorClassName="font-error pad text-center">
{runningRequests[activeRequest._id] && <ResponseTimer
{isExecuting && <ResponseTimer
handleCancel={() => cancelRequestById(activeRequest._id)}
activeRequestId={activeRequestId}
steps={steps}
/>}
</ErrorBoundary>
</Pane>
Expand Down
11 changes: 1 addition & 10 deletions packages/insomnia/src/ui/components/request-url-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ interface Props {
handleAutocompleteUrls: () => Promise<string[]>;
nunjucksPowerUserMode: boolean;
uniquenessKey: string;
setLoading: (l: boolean) => void;
onPaste: (text: string) => void;
}

Expand All @@ -53,7 +52,6 @@ export interface RequestUrlBarHandle {
export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
handleAutocompleteUrls,
uniquenessKey,
setLoading,
onPaste,
}, ref) => {
const [searchParams, setSearchParams] = useSearchParams();
Expand Down Expand Up @@ -105,14 +103,7 @@ export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
const [currentInterval, setCurrentInterval] = useState<number | null>(null);
const [currentTimeout, setCurrentTimeout] = useState<number | undefined>(undefined);
const fetcher = useFetcher();
// TODO: unpick this loading hack. This could be simplified if submit provides a way to update state when it finishes. https://github.com/remix-run/remix/discussions/9020
useEffect(() => {
if (fetcher.state !== 'idle') {
setLoading(true);
} else {
setLoading(false);
}
}, [fetcher.state, setLoading]);

const { organizationId, projectId, workspaceId, requestId } = useParams() as { organizationId: string; projectId: string; workspaceId: string; requestId: string };
const connect = useCallback((connectParams: ConnectActionParams) => {
fetcher.submit(JSON.stringify(connectParams),
Expand Down
46 changes: 33 additions & 13 deletions packages/insomnia/src/ui/components/response-timer.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import React, { DOMAttributes, FunctionComponent, useEffect, useState } from 'react';

import { REQUEST_SETUP_TEARDOWN_COMPENSATION, REQUEST_TIME_TO_SHOW_COUNTER } from '../../common/constants';
import type { TimingStep } from '../../main/network/request-timing';

interface Props {
handleCancel: DOMAttributes<HTMLButtonElement>['onClick'];
activeRequestId: string;
steps: TimingStep[];
}

export const ResponseTimer: FunctionComponent<Props> = ({ handleCancel }) => {
// triggers a 100 ms render in order to show a incrementing counter
const MillisecondTimer = () => {
const [milliseconds, setMilliseconds] = useState(0);

useEffect(() => {
let interval: NodeJS.Timeout | null = null;
const loadStartTime = Date.now();

interval = setInterval(() => {
setMilliseconds(Date.now() - loadStartTime - REQUEST_SETUP_TEARDOWN_COMPENSATION);
const delta = Date.now() - loadStartTime;
setMilliseconds(delta);
}, 100);
return () => {
if (interval !== null) {
Expand All @@ -23,16 +24,35 @@ export const ResponseTimer: FunctionComponent<Props> = ({ handleCancel }) => {
}
};
}, []);

const seconds = milliseconds / 1000;
const ms = (milliseconds / 1000);
return ms > 0 ? `${ms.toFixed(1)} s` : '0 s';
};
export const ResponseTimer: FunctionComponent<Props> = ({ handleCancel, activeRequestId, steps }) => {
return (
<div className="overlay theme--transparent-overlay">
<h2 style={{ fontVariantNumeric: 'tabular-nums' }}>
{seconds >= REQUEST_TIME_TO_SHOW_COUNTER ? `${seconds.toFixed(1)} seconds` : 'Loading'}...
</h2>
<div className="pad">
<i className="fa fa-refresh fa-spin" />
<div className="timer-list w-full">
{steps.map((record: TimingStep) => (
<div
key={`${activeRequestId}-${record.stepName}`}
className='flex w-full leading-8'
>
<div className='w-3/4 text-left content-center leading-8'>
<span className="leading-8">
{
record.duration ?
(<i className="fa fa-circle-check fa-2x mr-2 text-green-500" />) :
(<i className="fa fa-spinner fa-spin fa-2x mr-2" />)
}
</span>
<span className="inline-block align-top">
{record.stepName}
</span>
</div>
{record.duration ? `${((record.duration) / 1000).toFixed(1)} s` : (<MillisecondTimer />)}
</div>
))}
</div>

<div className="pad">
<button
className="btn btn--clicky"
Expand Down
Loading

0 comments on commit b8089ce

Please sign in to comment.