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

* 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>
This commit is contained in:
Hexxa 2024-06-18 14:58:09 +08:00 committed by GitHub
parent fd8f12e408
commit b8089ced6e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 238 additions and 66 deletions

View File

@ -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;

View File

@ -14,6 +14,7 @@ export type HandleChannels =
| 'curl.readyState'
| 'curlRequest'
| 'database.caCertificate.create'
| 'getExecution'
| 'grpc.loadMethods'
| 'grpc.loadMethodsFromReflection'
| 'installPlugin'
@ -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'

View File

@ -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';
@ -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);
});

View File

@ -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) });
}
}

View File

@ -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();

View File

@ -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),

View File

@ -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;';
@ -336,6 +337,11 @@ const miscMenu: SnippetMenuItem = {
'name': 'Require a module',
'snippet': requireAModule,
},
{
'id': 'delay',
'name': 'Delay',
'snippet': delay,
},
],
};

View File

@ -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';
@ -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 () => {
@ -69,6 +71,8 @@ export const MockResponsePane = () => {
<PlaceholderResponsePane>
{<ResponseTimer
handleCancel={() => activeResponse && cancelRequestById(activeResponse.parentId)}
activeRequestId={mockRoute._id}
steps={steps}
/>}
</PlaceholderResponsePane>
);
@ -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>

View File

@ -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;
@ -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>

View File

@ -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';
@ -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 || [];
@ -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');
@ -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 (
@ -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
@ -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>

View File

@ -42,7 +42,6 @@ interface Props {
handleAutocompleteUrls: () => Promise<string[]>;
nunjucksPowerUserMode: boolean;
uniquenessKey: string;
setLoading: (l: boolean) => void;
onPaste: (text: string) => void;
}
@ -53,7 +52,6 @@ export interface RequestUrlBarHandle {
export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
handleAutocompleteUrls,
uniquenessKey,
setLoading,
onPaste,
}, ref) => {
const [searchParams, setSearchParams] = useSearchParams();
@ -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),

View File

@ -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) {
@ -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"

View File

@ -1,6 +1,7 @@
import classnames from 'classnames';
import React, { FC, memo } from 'react';
import { TimingStep } from '../../../main/network/request-timing';
import { Tooltip } from '../tooltip';
interface Props {
@ -8,9 +9,9 @@ interface Props {
small?: boolean;
className?: string;
tooltipDelay?: number;
steps?: TimingStep[];
}
export const TimeTag: FC<Props> = memo(({ milliseconds, small, className, tooltipDelay }) => {
const getTimeAndUnit = (milliseconds: number) => {
let unit = 'ms';
let number = milliseconds;
@ -31,7 +32,15 @@ export const TimeTag: FC<Props> = memo(({ milliseconds, small, className, toolti
number = Math.round(number * 100) / 100;
}
const description = `${milliseconds.toFixed(3)} milliseconds`;
return { number, unit };
};
export const TimeTag: FC<Props> = memo(({ milliseconds, small, className, tooltipDelay, steps }) => {
const totalMs = steps?.reduce((acc, step) => acc + (step.duration || 0), 0) || milliseconds;
const { number, unit } = getTimeAndUnit(totalMs);
const timesandunits = steps?.map(step => {
const { number, unit } = getTimeAndUnit(step.duration || 0);
return { stepName: step.stepName, number, unit };
});
return (
<div
className={classnames(
@ -42,7 +51,21 @@ export const TimeTag: FC<Props> = memo(({ milliseconds, small, className, toolti
className,
)}
>
<Tooltip message={description} position="bottom" delay={tooltipDelay}>
<Tooltip
message={(
<div>
{timesandunits?.map(step =>
(<div key={step.stepName} className='flex justify-between'>
<div className='mr-5'>{step.stepName} </div><div>{step.number} {step.unit}</div>
</div>)
)}
<div key="total" className='flex justify-between'>
<div className='mr-5'>Total </div><div>{number} {unit}</div>
</div>
</div>)}
position="bottom"
delay={tooltipDelay}
>
{number}&nbsp;{unit}
</Tooltip>
</div>

View File

@ -128,7 +128,7 @@ const RealtimeActiveResponsePane: FC<{ response: WebSocketResponse | Response }>
<PaneHeader className="row-spaced">
<div className="no-wrap scrollable scrollable--no-bars pad-left">
<StatusTag statusCode={response.statusCode} statusMessage={response.statusMessage} />
<TimeTag milliseconds={response.elapsedTime} />
<TimeTag milliseconds={response.elapsedTime} steps={[]} />
<SizeTag bytesRead={0} bytesContent={0} />
</div>
<ResponseHistoryDropdown

View File

@ -2911,6 +2911,10 @@ input.editable {
.response-pane .overlay i.fa {
font-size: 4rem;
}
.response-pane .overlay .timer-list i.fa {
font-size: 1.2rem;
line-height: 2rem;
}
.response-pane .response-pane__notify {
display: flex;
flex-direction: column;

View File

@ -0,0 +1,48 @@
import { useEffect, useState } from 'react';
import { TimingStep } from '../../main/network/request-timing';
export function useExecutionState({ requestId }: { requestId?: string }) {
const [steps, setSteps] = useState<TimingStep[]>([]);
useEffect(() => {
let isMounted = true;
const fn = async () => {
if (!requestId) {
return;
}
const targetSteps = await window.main.getExecution({ requestId });
if (targetSteps) {
isMounted && setSteps(targetSteps);
}
};
fn();
return () => {
isMounted = false;
};
}, [requestId]);
useEffect(() => {
let isMounted = true;
// @ts-expect-error -- we use a dynamic channel here
const unsubscribe = window.main.on(`syncTimers.${requestId}`,
(_, { executions }: { executions: TimingStep[] }) => {
isMounted && setSteps(executions);
});
return () => {
isMounted = false;
unsubscribe();
};
}, [requestId]);
const isExecuting = () => {
const hasSteps = steps && steps.length > 0;
if (!hasSteps) {
return false;
}
const latest = steps[steps.length - 1];
return latest.duration === undefined;
};
return { steps, isExecuting: isExecuting() };
}

View File

@ -91,6 +91,7 @@ import { getMethodShortHand } from '../components/tags/method-tag';
import { ConnectionCircle } from '../components/websockets/action-bar';
import { RealtimeResponsePane } from '../components/websockets/realtime-response-pane';
import { WebSocketRequestPane } from '../components/websockets/websocket-request-pane';
import { useExecutionState } from '../hooks/use-execution-state';
import { useReadyState } from '../hooks/use-ready-state';
import {
CreateRequestType,
@ -164,6 +165,11 @@ const getRequestNameOrFallback = (doc: Request | RequestGroup | GrpcRequest | We
return !isRequestGroup(doc) ? doc.name || doc.url || 'Untitled request' : doc.name || 'Untitled folder';
};
const RequestTiming = ({ requestId }: { requestId: string }) => {
const { isExecuting } = useExecutionState({ requestId });
return isExecuting ? <ConnectionCircle className='flex-shrink-0' data-testid="WebSocketSpinner__Connected" /> : null;
};
export const Debug: FC = () => {
const {
activeWorkspace,
@ -227,18 +233,6 @@ export const Debug: FC = () => {
}, []);
const { settings } = useRootLoaderData();
const [runningRequests, setRunningRequests] = useState<
Record<string, boolean>
>({});
const setLoading = (isLoading: boolean) => {
invariant(requestId, 'No active request');
if (Boolean(runningRequests?.[requestId]) !== isLoading) {
setRunningRequests({
...runningRequests,
[requestId]: isLoading ? true : false,
});
}
};
const grpcState = grpcStates.find(s => s.requestId === requestId);
const setGrpcState = (newState: GrpcRequestState) =>
@ -1252,6 +1246,7 @@ export const Debug: FC = () => {
}}
/>
{isWebSocketRequest(item.doc) && <WebSocketSpinner requestId={item.doc._id} />}
{isRequest(item.doc) && <RequestTiming requestId={item.doc._id} />}
{isEventStreamRequest(item.doc) && <EventStreamSpinner requestId={item.doc._id} />}
{item.pinned && (
<Icon className='text-[--font-size-sm]' icon="thumb-tack" />
@ -1348,7 +1343,6 @@ export const Debug: FC = () => {
<RequestPane
environmentId={activeEnvironment ? activeEnvironment._id : ''}
settings={settings}
setLoading={setLoading}
onPaste={text => {
setPastedCurl(text);
setPasteCurlModalOpen(true);
@ -1376,7 +1370,7 @@ export const Debug: FC = () => {
<RealtimeResponsePane requestId={activeRequest._id} />
)}
{activeRequest && isRequest(activeRequest) && !isRealtimeRequest && (
<ResponsePane runningRequests={runningRequests} />
<ResponsePane activeRequestId={activeRequest._id} />
)}
</ErrorBoundary>
</Panel>

View File

@ -365,8 +365,11 @@ export const sendAction: ActionFunction = async ({ request, params }) => {
invariant(workspaceId, 'Workspace ID is required');
const { shouldPromptForPathAfterResponse, ignoreUndefinedEnvVariable } = await request.json() as SendActionParams;
try {
window.main.startExecution({ requestId });
const requestData = await fetchRequestData(requestId);
window.main.addExecutionStep({ requestId, stepName: 'Executing pre-request script' });
const mutatedContext = await getPreRequestScriptOutput(requestData, workspaceId);
window.main.completeExecutionStep({ requestId });
if (mutatedContext === null) {
return null;
}
@ -374,6 +377,7 @@ export const sendAction: ActionFunction = async ({ request, params }) => {
const afterResponseScript = `${mutatedContext.request.afterResponseScript}`;
mutatedContext.request.afterResponseScript = '';
window.main.addExecutionStep({ requestId, stepName: 'Rendering request' });
const renderedResult = await tryToInterpolateRequest(
mutatedContext.request,
mutatedContext.environment,
@ -383,6 +387,7 @@ export const sendAction: ActionFunction = async ({ request, params }) => {
ignoreUndefinedEnvVariable,
);
const renderedRequest = await tryToTransformRequestWithPlugins(renderedResult);
window.main.completeExecutionStep({ requestId });
// TODO: remove this temporary hack to support GraphQL variables in the request body properly
if (renderedRequest && renderedRequest.body?.text && renderedRequest.body?.mimeType === 'application/graphql') {
@ -397,6 +402,7 @@ export const sendAction: ActionFunction = async ({ request, params }) => {
}
}
window.main.addExecutionStep({ requestId, stepName: 'Sending request' });
const response = await sendCurlAndWriteTimeline(
renderedRequest,
mutatedContext.clientCertificates,
@ -405,6 +411,7 @@ export const sendAction: ActionFunction = async ({ request, params }) => {
requestData.timelinePath,
requestData.responseId
);
window.main.completeExecutionStep({ requestId });
const requestMeta = await models.requestMeta.getByParentId(requestId);
invariant(requestMeta, 'RequestMeta not found');
@ -416,7 +423,7 @@ export const sendAction: ActionFunction = async ({ request, params }) => {
if (requestData.request.afterResponseScript) {
const baseEnvironment = await models.environment.getOrCreateForParentId(workspaceId);
const cookieJar = await models.cookieJar.getOrCreateForParentId(workspaceId);
window.main.addExecutionStep({ requestId, stepName: 'Executing after-response script' });
const postMutatedContext = await tryToExecuteAfterResponseScript({
...requestData,
...mutatedContext,
@ -424,6 +431,7 @@ export const sendAction: ActionFunction = async ({ request, params }) => {
cookieJar,
response,
});
window.main.completeExecutionStep({ requestId });
if (!postMutatedContext?.request) {
// exiy early if there was a problem with the pre-request script
// TODO: improve error message?
@ -434,7 +442,6 @@ export const sendAction: ActionFunction = async ({ request, params }) => {
if (!shouldWriteToFile) {
const response = await models.response.create(responsePatch, requestData.settings.maxHistoryResponses);
await models.requestMeta.update(requestMeta, { activeResponseId: response._id });
// setLoading(false);
return null;
}
@ -453,7 +460,6 @@ export const sendAction: ActionFunction = async ({ request, params }) => {
...(defaultPath ? { defaultPath } : {}),
});
if (!filePath) {
// setLoading(false);
return null;
}
window.localStorage.setItem('insomnia.sendAndDownloadLocation', filePath);
@ -461,6 +467,7 @@ export const sendAction: ActionFunction = async ({ request, params }) => {
}
} catch (e) {
console.log('Failed to send request', e);
window.main.completeExecutionStep({ requestId });
const url = new URL(request.url);
url.searchParams.set('error', e);
if (e?.extraInfo && e?.extraInfo?.subType === RenderErrorSubType.EnvironmentVariable) {
@ -493,9 +500,22 @@ export const createAndSendToMockbinAction: ActionFunction = async ({ request })
timelinePath,
responseId,
} = await fetchRequestData(req._id);
window.main.startExecution({ requestId: req._id });
window.main.addExecutionStep({
requestId: req._id,
stepName: 'Rendering request',
}
);
const renderResult = await tryToInterpolateRequest(req, environment._id, RENDER_PURPOSE_SEND);
const renderedRequest = await tryToTransformRequestWithPlugins(renderResult);
window.main.completeExecutionStep({ requestId: req._id });
window.main.addExecutionStep({
requestId: req._id,
stepName: 'Sending request',
});
const res = await sendCurlAndWriteTimeline(
renderedRequest,
clientCertificates,
@ -504,8 +524,10 @@ export const createAndSendToMockbinAction: ActionFunction = async ({ request })
timelinePath,
responseId,
);
const response = await responseTransform(res, activeEnvironmentId, renderedRequest, renderResult.context);
await models.response.create(response);
window.main.completeExecutionStep({ requestId: req._id });
return null;
};
export const deleteAllResponsesAction: ActionFunction = async ({ params }) => {