Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: display status of sending request steps - INS-3635 #7382

Merged
merged 26 commits into from
Jun 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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