add cli preview command modal (#7952)

* add pane

* add env id and copy button

* layout

* global env id support

* Add github action

* git only

* remove urlbar css

* fix css

* fix console error

* complete dropdown action

* move tab to modal

* first pass at request and folder selection

* add ui

* add test

* get item list parent workspace

* support selectable requests

* fighting the linter

* add iteration and delay to ui

* add delay to CLI

* wire up UI for iteration data

* support iteration count

* make iteration data work

* tweaks and feedback

* typo
This commit is contained in:
Jack Kavanagh
2024-09-18 17:11:59 +02:00
committed by GitHub
parent f89019701d
commit 5bf6efb97d
12 changed files with 441 additions and 165 deletions

View File

@@ -41,6 +41,8 @@ const shouldReturnSuccessCode = [
'$PWD/packages/insomnia-inso/bin/inso run collection -w packages/insomnia-smoke-test/fixtures/simple.yaml -e env_2eecf85b7f --requestNamePattern "example http" wrk_0702a5',
// after-response script and test
'$PWD/packages/insomnia-inso/bin/inso run collection -w packages/insomnia-inso/src/examples/after-response.yml wrk_616795 --verbose',
// select request by id
'$PWD/packages/insomnia-inso/bin/inso run collection -w packages/insomnia-inso/src/examples/three-requests.yml -i req_3fd28aabbb18447abab1f45e6ee4bdc1 -i req_6063adcdab5b409e9b4f00f47322df4a',
];
const shouldReturnErrorCode = [
@@ -110,6 +112,16 @@ describe('inso dev bundle', () => {
}
expect(result.stdout).toContain('log: "we did it: 200"');
});
it('iterationData and iterationCount args work', async () => {
const input = '$PWD/packages/insomnia-inso/bin/inso run collection -d packages/insomnia-smoke-test/fixtures/files/runner-data.json -w packages/insomnia-inso/src/examples/three-requests.yml -n 2 -i req_3fd28aabbb18447abab1f45e6ee4bdc1 -e env_86e135 --verbose';
const result = await runCliFromRoot(input);
if (result.code !== 0) {
console.log(result);
}
expect(result.stdout).toContain('expecting to see:file_value0');
expect(result.stdout).toContain('expecting to see:file_value1');
});
});
});

View File

@@ -7,14 +7,14 @@ import consola, { BasicReporter, FancyReporter, LogLevel, logType } from 'consol
import { cosmiconfig } from 'cosmiconfig';
import fs from 'fs';
import { getSendRequestCallbackMemDb } from 'insomnia/src/common/send-request';
import type { RequestTestResult } from 'insomnia-sdk';
import { type RequestTestResult } from 'insomnia-sdk';
import { generate, runTestsCli } from 'insomnia-testing';
import { parseArgsStringToArgv } from 'string-argv';
import packageJson from '../package.json';
import { exportSpecification, writeFileWithCliOptions } from './commands/export-specification';
import { getRuleSetFileFromFolderByFilename, lintSpecification } from './commands/lint-specification';
import { loadDb } from './db';
import { Database, loadDb } from './db';
import { loadApiSpec, promptApiSpec } from './db/models/api-spec';
import { loadEnvironment, promptEnvironment } from './db/models/environment';
import { loadTestSuites, promptTestSuites } from './db/models/unit-test-suite';
@@ -162,7 +162,38 @@ const resolveSpecInDatabase = async (identifier: string, options: GlobalOptions)
}
return specFromDb.contents;
};
const getWorkspaceOrFallback = async (db: Database, identifier: string, ci: boolean) => {
if (identifier) {
return loadWorkspace(db, identifier);
}
if (ci && db.Workspace.length > 0) {
return db.Workspace[0];
}
return await promptWorkspace(db, !!ci);
};
const getRequestsToRunFromListOrWorkspace = (db: Database, workspaceId: string, item: string[]) => {
const getRequestGroupIdsRecursively = (from: string[]): string[] => {
const parentIds = db.RequestGroup.filter(rg => from.includes(rg.parentId)).map(rg => rg._id);
return [...parentIds, ...(parentIds.length > 0 ? getRequestGroupIdsRecursively(parentIds) : [])];
};
const hasItems = item.length > 0;
if (hasItems) {
const folderIds = item.filter(id => db.RequestGroup.find(rg => rg._id === id));
const allRequestGroupIds = getRequestGroupIdsRecursively(folderIds);
const folderRequests = db.Request.filter(req => allRequestGroupIds.includes(req.parentId));
const reqItems = db.Request.filter(req => item.includes(req._id));
return [...reqItems, ...folderRequests];
}
const allRequestGroupIds = getRequestGroupIdsRecursively([workspaceId]);
return db.Request.filter(req => [workspaceId, ...allRequestGroupIds].includes(req.parentId));
};
// adds support for repeating args in commander.js eg. -i 1 -i 2 -i 3
const collect = (val: string, memo: string[]) => {
memo.push(val);
return memo;
};
const localAppDir = getAppDataDir(getDefaultProductName());
const logTestResult = (reporter: TestReporter, testResults?: RequestTestResult[]) => {
if (!testResults || testResults.length === 0) {
@@ -198,6 +229,44 @@ function convertToTAP(testCases: RequestTestResult[]): string {
});
return tapOutput;
}
const readFileFromPathOrUrl = async (pathOrUrl: string) => {
if (pathOrUrl.startsWith('http')) {
const response = await fetch(pathOrUrl);
return response.text();
}
return readFile(pathOrUrl, 'utf8');
};
const getIterationDataFromFileOrUrl = async (pathOrUrl: string): Promise<Record<string, string>[]> => {
const fileType = pathOrUrl.split('.').pop()?.toLowerCase();
const content = await readFileFromPathOrUrl(pathOrUrl);
if (fileType === 'json') {
try {
const jsonDataContent = JSON.parse(content);
if (Array.isArray(jsonDataContent)) {
return jsonDataContent.filter(data => data && typeof data === 'object' && !Array.isArray(data) && data !== null);
}
throw new Error('Invalid JSON file uploaded, JSON file must be array of key-value pairs.');
} catch (error) {
throw new Error('Upload JSON file can not be parsed');
}
} else if (fileType === 'csv') {
// Replace CRLF (Windows line break) and CR (Mac link break) with \n, then split into csv arrays
const csvRows = content.replace(/\r\n|\r/g, '\n').split('\n').map(row => row.split(','));
// at least 2 rows required for csv
if (csvRows.length > 1) {
const csvHeaders = csvRows[0];
const csvContentRows = csvRows.slice(1, csvRows.length);
return csvContentRows.map(contentRow => csvHeaders.reduce((acc: Record<string, any>, cur, idx) => {
acc[cur] = contentRow[idx] ?? '';
return acc;
}, {}));
}
throw new Error('CSV file must contain at least two rows with first row as variable names');
}
throw new Error(`Uploaded file is unsupported ${fileType}`);
};
export const go = (args?: string[]) => {
const program = new commander.Command();
@@ -246,11 +315,11 @@ export const go = (args?: string[]) => {
.description('Run Insomnia unit test suites, identifier can be a test suite id or a API Spec id')
.option('-e, --env <identifier>', 'environment to use', '')
.option('-t, --testNamePattern <regex>', 'run tests that match the regex', '')
.option('-r, --reporter <reporter>', `reporter to use, options are [${reporterTypes.join(', ')}] (default: ${defaultReporter})`, defaultReporter)
.option('-r, --reporter <reporter>', `reporter to use, options are [${reporterTypes.join(', ')}]`, defaultReporter)
.option('-b, --bail', 'abort ("bail") after first test failure', false)
.option('--keepFile', 'do not delete the generated test file', false)
.option('--disableCertValidation', 'disable certificate validation for requests with SSL', false)
.action(async (identifier, cmd: { env: string; testNamePattern: string; reporter: TestReporter; bail: true; keepFile: true; disableCertValidation: true }) => {
.option('-k, --disableCertValidation', 'disable certificate validation for requests with SSL', false)
.action(async (identifier, cmd: { env: string; testNamePattern: string; reporter: TestReporter; bail: boolean; keepFile: boolean; disableCertValidation: boolean; ci: boolean }) => {
const globals: GlobalOptions = program.optsWithGlobals();
const commandOptions = { ...globals, ...cmd };
const __configFile = await tryToReadInsoConfigFile(commandOptions.config, commandOptions.workingDir);
@@ -290,6 +359,7 @@ export const go = (args?: string[]) => {
// Find environment
const workspaceId = suites[0].parentId;
const environment = options.env ? loadEnvironment(db, workspaceId, options.env) : await promptEnvironment(db, !!options.ci, workspaceId);
if (!environment) {
@@ -328,11 +398,15 @@ export const go = (args?: string[]) => {
run.command('collection [identifier]')
.description('Run Insomnia request collection, identifier can be a workspace id')
.option('-t, --requestNamePattern <regex>', 'run requests that match the regex', '')
.option('-i, --item <requestid>', 'request or folder id to run', collect, [])
.option('-e, --env <identifier>', 'environment to use', '')
.option('-r, --reporter <reporter>', `reporter to use, options are [${reporterTypes.join(', ')}] (default: ${defaultReporter})`, defaultReporter)
.option('--delay-request <duration>', 'milliseconds to delay between requests', '0')
.option('-n, --iteration-count <count>', 'number of times to repeat', '1')
.option('-d, --iteration-data <path/url>', 'file path or url (JSON or CSV)', '')
.option('-r, --reporter <reporter>', `reporter to use, options are [${reporterTypes.join(', ')}]`, defaultReporter)
.option('-b, --bail', 'abort ("bail") after first non-200 response', false)
.option('--disableCertValidation', 'disable certificate validation for requests with SSL', false)
.action(async (identifier, cmd: { env: string; disableCertValidation: true; requestNamePattern: string; bail: boolean }) => {
.action(async (identifier, cmd: { env: string; disableCertValidation: boolean; requestNamePattern: string; bail: boolean; item: string[]; delayRequest: string; iterationCount: string; iterationData: string }) => {
const globals: { config: string; workingDir: string; exportFile: string; ci: boolean; printOptions: boolean; verbose: boolean } = program.optsWithGlobals();
const commandOptions = { ...globals, ...cmd };
@@ -359,11 +433,21 @@ export const go = (args?: string[]) => {
pathToSearch,
filterTypes: [],
});
const workspace = identifier ? loadWorkspace(db, identifier) : await promptWorkspace(db, !!options.ci);
if (identifier && options.item.length) {
logger.fatal('Providing both workspace and item list is not supported');
return process.exit(1);
}
if (options.item.length) {
const matches = [
...db.Request.filter(req => options.item.includes(req._id)),
...db.RequestGroup.filter(rg => options.item.includes(rg._id)),
];
// overwrite identifier if found in request list parents
identifier = matches.find(req => req.parentId.startsWith('wrk_'))?.parentId;
}
const workspace = await getWorkspaceOrFallback(db, identifier, options.ci);
if (!workspace) {
logger.fatal('No workspace found; cannot run requests.', identifier);
logger.fatal('No workspace found in the provided data store or fallbacks.');
return process.exit(1);
}
@@ -375,53 +459,51 @@ export const go = (args?: string[]) => {
logger.fatal('No environment identified; cannot run requests without a valid environment.');
return process.exit(1);
}
const getRequestGroupIdsRecursively = (from: string[]): string[] => {
const parentIds = db.RequestGroup.filter(rg => from.includes(rg.parentId)).map(rg => rg._id);
return [...parentIds, ...(parentIds.length > 0 ? getRequestGroupIdsRecursively(parentIds) : [])];
};
const allRequestGroupIds = getRequestGroupIdsRecursively([workspaceId]);
let requests = db.Request.filter(req => [workspaceId, ...allRequestGroupIds].includes(req.parentId));
let requestsToRun = getRequestsToRunFromListOrWorkspace(db, workspaceId, options.item);
if (options.requestNamePattern) {
requests = requests.filter(req => req.name.match(new RegExp(options.requestNamePattern)));
requestsToRun = requestsToRun.filter(req => req.name.match(new RegExp(options.requestNamePattern)));
}
if (!requests.length) {
if (!requestsToRun.length) {
logger.fatal('No requests identified; nothing to run.');
return process.exit(1);
}
try {
const sendRequest = await getSendRequestCallbackMemDb(environment._id, db, { validateSSL: !options.disableCertValidation });
const iterationCount = parseInt(options.iterationCount, 10);
const iterationData = options.iterationData ? await getIterationDataFromFileOrUrl(options.iterationData) : undefined;
const sendRequest = await getSendRequestCallbackMemDb(environment._id, db, { validateSSL: !options.disableCertValidation }, iterationData, iterationCount);
let success = true;
for (const req of requests) {
if (options.bail && !success) {
return;
}
logger.log(`Running request: ${req.name} ${req._id}`);
const res = await sendRequest(req._id);
if (!res) {
logger.error('Timed out while running script');
success = false;
continue;
}
logger.trace(res);
const timelineString = await readFile(res.timelinePath, 'utf8');
const timeline = timelineString.split('\n').filter(e => e?.trim()).map(e => JSON.parse(e).value).join(' ');
logger.trace(timeline);
if (res.testResults?.length) {
console.log(`
Test results:`);
console.log(logTestResult(options.reporter, res.testResults));
const hasFailedTests = res.testResults.some(t => t.status === 'failed');
if (hasFailedTests) {
success = false;
for (let i = 0; i < iterationCount; i++) {
for (const req of requestsToRun) {
if (options.bail && !success) {
return;
}
logger.log(`Running request: ${req.name} ${req._id}`);
const res = await sendRequest(req._id, i);
if (!res) {
logger.error('Timed out while running script');
success = false;
continue;
}
logger.trace(res);
const timelineString = await readFile(res.timelinePath, 'utf8');
const timeline = timelineString.split('\n').filter(e => e?.trim()).map(e => JSON.parse(e).value).join(' ');
logger.trace(timeline);
if (res.testResults?.length) {
console.log(`
Test results:`);
console.log(logTestResult(options.reporter, res.testResults));
const hasFailedTests = res.testResults.some(t => t.status === 'failed');
if (hasFailedTests) {
success = false;
}
}
}
if (res.status !== 200) {
success = false;
logger.error(`Request failed with status ${res.status}`);
if (res.status !== 200) {
success = false;
logger.error(`Request failed with status ${res.status}`);
}
await new Promise(r => setTimeout(r, parseInt(options.delayRequest, 10)));
}
}
return process.exit(success ? 0 : 1);

View File

@@ -27,9 +27,6 @@ export const loadEnvironment = (
// Get the sub environments
const baseWorkspaceEnv = loadBaseEnvironmentForWorkspace(db, workspaceId);
const subEnvs = db.Environment.filter(
env => env.parentId === baseWorkspaceEnv._id,
);
// If no identifier, return base environment
if (!identifier) {
@@ -41,11 +38,7 @@ export const loadEnvironment = (
'Load sub environment with identifier `%s` from data store',
identifier,
);
const items = subEnvs.filter(
env => matchIdIsh(env, identifier) || env.name === identifier,
);
logger.trace('Found %d', items.length);
return ensureSingle(items, 'sub environment');
return db.Environment.find(env => matchIdIsh(env, identifier) || env.name === identifier);
};
export const promptEnvironment = async (

View File

@@ -0,0 +1,110 @@
_type: export
__export_format: 4
__export_date: 2024-09-18T13:31:59.297Z
__export_source: insomnia.desktop.app:v10.0.0
resources:
- _id: req_3fd28aabbb18447abab1f45e6ee4bdc1
parentId: wrk_c992d40ce76f4a3cb44c5fdb8435cbeb
modified: 1726666263873
created: 1726658232232
url: localhost:4010/echo
name: "1"
description: ""
method: GET
body:
mimeType: text/plain
text: expecting to see:{{value}}
parameters: []
headers:
- name: Content-Type
value: text/plain
- name: User-Agent
value: insomnia/10.0.0
authentication: {}
metaSortKey: -1726658232232
isPrivate: false
pathParameters: []
settingStoreCookies: true
settingSendCookies: true
settingDisableRenderRequestBody: false
settingEncodeUrl: true
settingRebuildPath: true
settingFollowRedirects: global
_type: request
- _id: wrk_c992d40ce76f4a3cb44c5fdb8435cbeb
parentId: null
modified: 1726658198059
created: 1726658198059
name: Collection with 3 requests
description: ""
scope: collection
_type: workspace
- _id: req_6063adcdab5b409e9b4f00f47322df4a
parentId: wrk_c992d40ce76f4a3cb44c5fdb8435cbeb
modified: 1726658253319
created: 1726658253319
url: localhost:4010/echo
name: "2"
description: ""
method: GET
body: {}
parameters: []
headers:
- name: User-Agent
value: insomnia/10.0.0
authentication: {}
metaSortKey: -1726273359624.5
isPrivate: false
pathParameters: []
settingStoreCookies: true
settingSendCookies: true
settingDisableRenderRequestBody: false
settingEncodeUrl: true
settingRebuildPath: true
settingFollowRedirects: global
_type: request
- _id: req_766390ffa47a4fbba7a0e3f94a4582d9
parentId: wrk_c992d40ce76f4a3cb44c5fdb8435cbeb
modified: 1726658259060
created: 1726658259060
url: localhost:4010/echo
name: "3"
description: ""
method: GET
body: {}
parameters: []
headers:
- name: User-Agent
value: insomnia/10.0.0
authentication: {}
metaSortKey: -1726080923320.75
isPrivate: false
pathParameters: []
settingStoreCookies: true
settingSendCookies: true
settingDisableRenderRequestBody: false
settingEncodeUrl: true
settingRebuildPath: true
settingFollowRedirects: global
_type: request
- _id: env_86e1354fb9909cdb109ccadf83c3353f3bb9bd09
parentId: wrk_c992d40ce76f4a3cb44c5fdb8435cbeb
modified: 1726666211581
created: 1726658198060
name: Base Environment
data:
value: 123
dataPropertyOrder:
"&":
- value
color: null
isPrivate: false
metaSortKey: 1726658198060
_type: environment
- _id: jar_86e1354fb9909cdb109ccadf83c3353f3bb9bd09
parentId: wrk_c992d40ce76f4a3cb44c5fdb8435cbeb
modified: 1726658198060
created: 1726658198060
name: Default Jar
cookies: []
_type: cookie_jar

View File

@@ -1,7 +1,9 @@
import orderedJSON from 'json-order';
import path from 'path';
import { type BaseModel, types as modelTypes } from '../models';
import * as models from '../models';
import type { UserUploadEnvironment } from '../models/environment';
import type { Request } from '../models/request';
import type { RequestGroup } from '../models/request-group';
import { getBodyBuffer } from '../models/response';
@@ -17,14 +19,23 @@ import {
tryToInterpolateRequest,
} from '../network/network';
import { invariant } from '../utils/invariant';
import { JSON_ORDER_PREFIX, JSON_ORDER_SEPARATOR } from './constants';
import { database } from './database';
import { generateId } from './misc';
// The network layer uses settings from the settings model
// We want to give consumers the ability to override certain settings
type SettingsOverride = Pick<Settings, 'validateSSL'>;
export async function getSendRequestCallbackMemDb(environmentId: string, memDB: any, settingsOverrides?: SettingsOverride) {
const wrapAroundIterationOverIterationData = (list?: UserUploadEnvironment[], currentIteration?: number): UserUploadEnvironment | undefined => {
if (currentIteration === undefined || !Array.isArray(list) || list.length === 0) {
return undefined;
}
if (list.length >= currentIteration + 1) {
return list[currentIteration];
};
return list[(currentIteration + 1) % list.length];
};
export async function getSendRequestCallbackMemDb(environmentId: string, memDB: any, settingsOverrides?: SettingsOverride, iterationData?: Record<string, any>[], iterationCount?: number) {
// Initialize the DB in-memory and fill it with data if we're given one
await database.init(
modelTypes(),
@@ -84,10 +95,23 @@ export async function getSendRequestCallbackMemDb(environmentId: string, memDB:
return { request, settings, clientCertificates, caCert, environment, activeEnvironmentId, workspace, timelinePath, responseId, ancestors };
};
const userUploadEnvs = iterationData?.map(data => {
const orderedJson = orderedJSON.parse<Record<string, any>>(
JSON.stringify(data),
JSON_ORDER_PREFIX,
JSON_ORDER_SEPARATOR,
);
return {
name: 'User Upload',
data: orderedJson.object,
dataPropertyOrder: orderedJson.map || null,
};
});
// Return callback helper to send requests
return async function sendRequest(requestId: string) {
return async function sendRequest(requestId: string, iteration?: number) {
const requestData = await fetchInsoRequestData(requestId, environmentId);
const mutatedContext = await tryToExecutePreRequestScript(requestData, requestData.workspace._id);
const mutatedContext = await tryToExecutePreRequestScript(requestData, requestData.workspace._id, wrapAroundIterationOverIterationData(userUploadEnvs, iteration), iteration, iterationCount);
if (mutatedContext === null) {
console.error('Time out while executing pre-request script');
return null;
@@ -101,7 +125,7 @@ export async function getSendRequestCallbackMemDb(environmentId: string, memDB:
purpose: 'send',
extraInfo: undefined,
baseEnvironment: mutatedContext.baseEnvironment,
userUploadEnv: undefined,
userUploadEnv: mutatedContext.userUploadEnv,
ignoreUndefinedEnvVariable,
});
// skip plugins

View File

@@ -331,7 +331,7 @@ export const GitSyncDropdown: FC<Props> = ({ gitRepository, isInsomniaSyncEnable
<MenuTrigger>
<div className="flex items-center h-[--line-height-sm] w-full aria-pressed:bg-[--hl-sm] text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all text-sm">
<Button
data-testId="git-dropdown"
data-testid="git-dropdown"
aria-label="Git Sync"
className="flex-1 flex h-full items-center gap-2 truncate px-[--padding-md]"
>

View File

@@ -68,7 +68,6 @@ export const MethodDropdown = forwardRef<DropdownHandle, Props>(({
return (
<Dropdown
ref={ref}
className="method-dropdown"
triggerButton={
<Button className='pl-2'>
<span className={`http-method-${method}`}>{method}</span>{' '}

View File

@@ -50,10 +50,9 @@ export const MockUrlBar = ({ onPathUpdate, onSend }: { onPathUpdate: (path: stri
},
});
const isCancellable = currentInterval || currentTimeout;
return (<div className='w-full flex justify-between urlbar'>
return (<div className='w-full flex justify-between self-stretch'>
<Dropdown
ref={methodDropdownRef}
className="method-dropdown"
triggerButton={
<Button className="pad-right pad-left vertically-center hover:bg-[--color-surprise] focus:bg-[--color-surprise]">
<span className={`http-method-${mockRoute.method}`}>{mockRoute.method}</span>{' '}
@@ -114,13 +113,13 @@ export const MockUrlBar = ({ onPathUpdate, onSend }: { onPathUpdate: (path: stri
</Button>
<Dropdown
key="dropdown"
className="tall"
className="flex"
ref={dropdownRef}
aria-label="Request Options"
closeOnSelect={false}
triggerButton={
<Button
className="urlbar__send-context rounded-r-sm enabled:hover:!bg-[--color-surprise] enabled:focus:!bg-[--color-surprise]"
className="px-1 bg-[--color-surprise] text-[--color-font-surprise] rounded-r-sm"
style={{
borderTopRightRadius: '0.125rem',
borderBottomRightRadius: '0.125rem',

View File

@@ -0,0 +1,79 @@
import React from 'react';
import { Button, Dialog, Heading, Modal, ModalOverlay } from 'react-aria-components';
import { useParams, useRouteLoaderData } from 'react-router-dom';
import type { WorkspaceLoaderData } from '../../routes/workspace';
import { CopyButton } from '../base/copy-button';
import { Icon } from '../icon';
export const CLIPreviewModal = ({ onClose, requestIds, allSelected, iterations, delay, filePath }: { onClose: () => void; requestIds: string[]; allSelected: boolean; iterations: number; delay: number; filePath: string }) => {
const { workspaceId } = useParams() as { workspaceId: string };
const { activeEnvironment } = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
const workspaceIdOrRequestIds = allSelected ? workspaceId.slice(0, 10) : '-i ' + requestIds.join(' -i ');
const iterationsArgument = iterations > 1 ? ` -n ${iterations}` : '';
const delayArgument = delay > 0 ? ` --delay-request ${delay}` : '';
const iterationFilePath = filePath ? ` -d "${filePath}"` : '';
const cliCommand = `inso run collection ${workspaceIdOrRequestIds} -e ${activeEnvironment._id.slice(0, 10)}${iterationsArgument}${delayArgument}${iterationFilePath}`;
return (
<ModalOverlay
isOpen
isDismissable
onOpenChange={isOpen => {
!isOpen && onClose();
}}
className="w-full h-[--visual-viewport-height] fixed z-10 top-0 left-0 flex items-start justify-center bg-black/30"
>
<Modal
className="max-h-[75%] overflow-auto flex flex-col w-full rounded-md border border-solid border-[--hl-sm] p-[--padding-lg] bg-[--color-bg] text-[--color-font] m-24"
onOpenChange={isOpen => {
!isOpen && onClose();
}}
>
<Dialog
className="outline-none flex-1 h-full flex flex-col overflow-hidden"
>
{({ close }) => (
<div className='flex-1 flex flex-col gap-4 overflow-hidden'>
<div className='flex gap-2 items-center justify-between'>
<Heading slot="title" className='text-2xl'>Run via CLI</Heading>
<Button
className="flex flex-shrink-0 items-center justify-center aspect-square h-6 aria-pressed:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all text-sm"
onPress={close}
>
<Icon icon="x" />
</Button>
</div>
<div className="h-full w-full text-md flex-row p-2">
<div className="pb-4">Copy this command to run your collection in the terminal</div>
<div className="max-h-32 flex flex-col overflow-y-auto min-h-[2em] bg-[--hl-xs] px-2 py-1 border border-solid border-[--hl-sm]">
<div className="flex justify-between overflow-auto relative h-full gap-[var(--padding-sm)] w-full font-mono">
<span>{cliCommand}</span>
<CopyButton
size="small"
content={cliCommand}
title="Copy Command"
confirmMessage=""
className='self-start sticky top-0'
>
<i className="fa fa-copy" />
</CopyButton>
</div>
</div>
</div>
<div className="flex justify-end mt-2">
<Button
className="hover:no-underline flex items-center gap-2 hover:bg-opacity-90 border border-solid border-[--hl-md] py-2 px-3 text-[--hl] transition-colors rounded-sm"
onPress={close}
>
Close
</Button>
</div>
</div>
)}
</Dialog>
</Modal>
</ModalOverlay>
);
};

View File

@@ -214,17 +214,20 @@ export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
});
const buttonText = isEventStreamRequest(activeRequest) ? 'Connect' : (downloadPath ? 'Download' : 'Send');
const borderRadius = isEventStreamRequest(activeRequest) ? 'rounded-sm' : 'rounded-l-sm';
const { url, method } = activeRequest;
const isEventStreamOpen = useReadyState({ requestId: activeRequest._id, protocol: 'curl' });
const isCancellable = currentInterval || currentTimeout || isEventStreamOpen;
return (
<div className="urlbar">
<MethodDropdown
ref={methodDropdownRef}
onChange={method => patchRequest(requestId, { method })}
method={method}
/>
<div className="urlbar__flex__right">
<div className="w-full flex justify-between self-stretch items-stretch">
<div className="flex items-center">
<MethodDropdown
ref={methodDropdownRef}
onChange={method => patchRequest(requestId, { method })}
method={method}
/>
</div>
<div className="flex flex-1 p-1 items-center">
<OneLineEditor
id="request-url-bar"
key={uniquenessKey}
@@ -239,11 +242,11 @@ export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
})}
onPaste={onPaste}
/>
<div className='flex p-1'>
<div className='flex self-stretch'>
{isCancellable ? (
<button
type="button"
className="urlbar__send-btn rounded-sm"
className="px-[--padding-md] bg-[--color-surprise] text-[--color-font-surprise] rounded-sm"
onClick={() => {
if (isEventStreamRequest(activeRequest)) {
window.main.curl.close({ requestId: activeRequest._id });
@@ -257,23 +260,17 @@ export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
</button>
) : (
<>
<button
onClick={() => sendOrConnect()}
className={`urlbar__send-btn ${isEventStreamRequest(activeRequest) ? 'rounded-sm' : 'rounded-l-sm'}`}
type="button"
>
{buttonText}
</button>
<button onClick={() => sendOrConnect()} className={`px-[--padding-md] bg-[--color-surprise] text-[--color-font-surprise] ${borderRadius}`} type="button">{buttonText}</button>
{isEventStreamRequest(activeRequest) ? null : (
<Dropdown
key="dropdown"
className="tall"
className="flex"
ref={dropdownRef}
aria-label="Request Options"
closeOnSelect={false}
triggerButton={
<Button
className="urlbar__send-context rounded-r-sm enabled:hover:!bg-[--color-surprise] enabled:focus:!bg-[--color-surprise]"
className="px-1 bg-[--color-surprise] text-[--color-font-surprise] rounded-r-sm"
style={{
borderTopRightRadius: '0.125rem',
borderBottomRightRadius: '0.125rem',

View File

@@ -3394,62 +3394,6 @@ input.editable {
bottom: var(--padding-md);
right: var(--padding-md);
}
.urlbar {
width: 100%;
display: flex;
flex-direction: row;
align-items: stretch;
align-self: stretch;
}
.urlbar > .dropdown > button {
height: 100%;
min-width: 4.5em;
display: flex;
justify-content: space-between;
}
.urlbar > .dropdown > button > *:first-child {
padding-left: 0.5em;
}
.urlbar > .dropdown > button,
.urlbar > .dropdown > button > i.fa {
line-height: var(--line-height-sm);
}
.urlbar .urlbar__send-btn {
padding-right: var(--padding-md);
padding-left: var(--padding-md);
margin-left: 0.75em;
text-align: center;
background: var(--color-surprise);
color: var(--color-font-surprise);
}
.urlbar .urlbar__flex__right {
width: 100%;
display: flex;
flex-direction: row;
margin-left: var(--padding-xxs);
}
.urlbar .urlbar__flex__right * {
font-size: var(--font-size-md);
}
.urlbar .urlbar__send-context {
padding-right: var(--padding-xs);
padding-left: var(--padding-xs);
text-align: center;
background: var(--color-surprise);
color: var(--color-font-surprise);
}
.urlbar .urlbar__send-context,
.urlbar > .dropdown {
height: 100%;
}
.urlbar .urlbar__send-context,
.urlbar .surprise {
border-left: 1px solid var(--hl-md);
}
.urlbar button:focus,
.urlbar button:hover {
filter: brightness(0.8);
}
.workspace-dropdown h1 {
white-space: nowrap;
overflow: hidden;

View File

@@ -19,10 +19,12 @@ import type { ResponseInfo, RunnerResultPerRequest, RunnerTestResult } from '../
import { cancelRequestById } from '../../network/cancellation';
import { invariant } from '../../utils/invariant';
import { SegmentEvent } from '../analytics';
import { Dropdown, DropdownItem, ItemContent } from '../components/base/dropdown';
import { ErrorBoundary } from '../components/error-boundary';
import { HelpTooltip } from '../components/help-tooltip';
import { Icon } from '../components/icon';
import { showAlert } from '../components/modals';
import { CLIPreviewModal } from '../components/modals/cli-preview-modal';
import { UploadDataModal, type UploadDataType } from '../components/modals/upload-runner-data-modal';
import { Pane, PaneBody, PaneHeader } from '../components/panes/pane';
import { RunnerResultHistoryPane } from '../components/panes/runner-result-history-pane';
@@ -145,7 +147,7 @@ export const Runner: FC<{}> = () => {
const { settings } = useRootLoaderData();
const { collection } = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
const [showUploadModal, setShowUploadModal] = useState(false);
const [showCLIModal, setShowCLIModal] = useState(false);
const [direction, setDirection] = useState<'horizontal' | 'vertical'>(settings.forceVerticalLayout ? 'vertical' : 'horizontal');
useEffect(() => {
if (settings.forceVerticalLayout) {
@@ -410,7 +412,7 @@ export const Runner: FC<{}> = () => {
const disabledKeys = useMemo(() => {
return isRunning ? allKeys : [];
}, [isRunning, allKeys]);
const isDisabled = isRunning || Array.from(reqList.selectedKeys).length === 0;
return (
<PanelGroup autoSaveId="insomnia-sidebar" id="wrapper" className='new-sidebar w-full h-full text-[--color-font]' direction='horizontal'>
<Panel>
@@ -481,16 +483,45 @@ export const Runner: FC<{}> = () => {
</Button>
</div>
</div>
<div className="w-[100px]">
<button
type="button"
className="rounded-sm text-center mr-1 bg-[--color-surprise] text-[--color-font-surprise]"
onClick={onRun}
style={{ width: '92px', height: '30px' }} // try to make its width same as "Send button"
disabled={Array.from(reqList.selectedKeys).length === 0 || isRunning}
<div className='flex p-1 self-stretch'>
<Button
isDisabled={isDisabled}
className="px-5 ml-1 text-[--color-font-surprise] bg-[--color-surprise] hover:bg-opacity-90 focus:bg-opacity-90 rounded-l-sm"
onPress={onRun}
>
Run
</button>
</Button>
<Dropdown
key="dropdown"
className="flex"
isDisabled={isDisabled}
aria-label="Run Options"
closeOnSelect={false}
triggerButton={
<Button
isDisabled={isDisabled}
className="px-1 bg-[--color-surprise] text-[--color-font-surprise] rounded-r-sm"
style={{
borderTopRightRadius: '0.125rem',
borderBottomRightRadius: '0.125rem',
}}
>
<i className="fa fa-caret-down" />
</Button>
}
>
<DropdownItem aria-label="send-now">
<ItemContent icon="arrow-circle-o-right" label="Run" onClick={onRun} />
</DropdownItem>
<DropdownItem aria-label='Run via CLI'>
<ItemContent
icon="code"
label="Run via CLI"
onClick={() => setShowCLIModal(true)}
/>
</DropdownItem>
</Dropdown>
</div>
</Heading>
</PaneHeader>
@@ -526,7 +557,6 @@ export const Runner: FC<{}> = () => {
<PaneBody placeholder className='p-0'>
<GridList
id="runner-request-list"
// style={{ height: virtualizer.getTotalSize() }}
items={reqList.items}
selectionMode="multiple"
selectedKeys={reqList.selectedKeys}
@@ -647,6 +677,16 @@ export const Runner: FC<{}> = () => {
</div>
</TabPanel>
</Tabs>
{showCLIModal && (
<CLIPreviewModal
onClose={() => setShowCLIModal(false)}
requestIds={Array.from(reqList.selectedKeys) as string[]}
allSelected={Array.from(reqList.selectedKeys).length === Array.from(reqList.items).length}
iterations={iterations}
delay={delay}
filePath={file?.path || ''}
/>
)}
{showUploadModal && (
<UploadDataModal
onUploadFile={(file, uploadData) => {
@@ -802,7 +842,15 @@ function cancelExecution(workspaceId: string) {
stopExecution(workspaceId);
}
}
const wrapAroundIterationOverIterationData = (list?: UserUploadEnvironment[], currentIteration?: number): UserUploadEnvironment | undefined => {
if (currentIteration === undefined || !Array.isArray(list) || list.length === 0) {
return undefined;
}
if (list.length >= currentIteration + 1) {
return list[currentIteration];
};
return list[(currentIteration + 1) % list.length];
};
export interface runCollectionActionParams {
requests: { id: string; name: string }[];
}
@@ -868,17 +916,6 @@ export const runCollectionAction: ActionFunction = async ({ request, params }) =
}
updateExecution(workspaceId, targetRequest.id);
const getCurIterationUserUploadData = (curIteration: number): UserUploadEnvironment | undefined => {
if (Array.isArray(userUploadEnvs) && userUploadEnvs.length > 0) {
const uploadDataLength = userUploadEnvs.length;
if (uploadDataLength >= curIteration + 1) {
return userUploadEnvs[curIteration];
};
return userUploadEnvs[(curIteration + 1) % uploadDataLength];
}
return undefined;
};
window.main.updateLatestStepName({ requestId: workspaceId, stepName: `Iteration ${i + 1} - Executing ${j + 1} of ${requests.length} requests - "${targetRequest.name}"` });
const activeRequestMeta = await models.requestMeta.updateOrCreateByParentId(
@@ -903,7 +940,7 @@ export const runCollectionAction: ActionFunction = async ({ request, params }) =
workspaceId,
iteration: i + 1,
iterationCount: iterations,
userUploadEnv: getCurIterationUserUploadData(i),
userUploadEnv: wrapAroundIterationOverIterationData(userUploadEnvs, i),
shouldPromptForPathAfterResponse: false,
ignoreUndefinedEnvVariable: true,
testResultCollector: resultCollector,