mirror of
https://github.com/Kong/insomnia
synced 2024-11-08 06:39:48 +00:00
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:
parent
fd8f12e408
commit
b8089ced6e
@ -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;
|
||||
|
@ -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'
|
||||
|
@ -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);
|
||||
});
|
||||
|
39
packages/insomnia/src/main/network/request-timing.ts
Normal file
39
packages/insomnia/src/main/network/request-timing.ts
Normal 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) });
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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),
|
||||
|
@ -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"
|
||||
|
@ -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} {unit}
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
48
packages/insomnia/src/ui/hooks/use-execution-state.ts
Normal file
48
packages/insomnia/src/ui/hooks/use-execution-state.ts
Normal 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() };
|
||||
}
|
@ -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>
|
||||
|
@ -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 }) => {
|
||||
|
Loading…
Reference in New Issue
Block a user