From 5bf6efb97d46df83e511479083e80e2804b1bde5 Mon Sep 17 00:00:00 2001 From: Jack Kavanagh Date: Wed, 18 Sep 2024 17:11:59 +0200 Subject: [PATCH] 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 --- packages/insomnia-inso/src/cli.test.ts | 12 ++ packages/insomnia-inso/src/cli.ts | 178 +++++++++++++----- .../src/db/models/environment.ts | 9 +- .../src/examples/three-requests.yml | 110 +++++++++++ packages/insomnia/src/common/send-request.ts | 34 +++- .../dropdowns/git-sync-dropdown.tsx | 2 +- .../components/dropdowns/method-dropdown.tsx | 1 - .../src/ui/components/mocks/mock-url-bar.tsx | 7 +- .../components/modals/cli-preview-modal.tsx | 79 ++++++++ .../src/ui/components/request-url-bar.tsx | 33 ++-- packages/insomnia/src/ui/css/main.css | 56 ------ packages/insomnia/src/ui/routes/runner.tsx | 85 ++++++--- 12 files changed, 441 insertions(+), 165 deletions(-) create mode 100644 packages/insomnia-inso/src/examples/three-requests.yml create mode 100644 packages/insomnia/src/ui/components/modals/cli-preview-modal.tsx diff --git a/packages/insomnia-inso/src/cli.test.ts b/packages/insomnia-inso/src/cli.test.ts index 286b983ff5..ce7d449bd5 100644 --- a/packages/insomnia-inso/src/cli.test.ts +++ b/packages/insomnia-inso/src/cli.test.ts @@ -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'); + }); }); }); diff --git a/packages/insomnia-inso/src/cli.ts b/packages/insomnia-inso/src/cli.ts index 5b1c6fbce3..d34c16a2d1 100644 --- a/packages/insomnia-inso/src/cli.ts +++ b/packages/insomnia-inso/src/cli.ts @@ -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[]> => { + 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, 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 ', 'environment to use', '') .option('-t, --testNamePattern ', 'run tests that match the regex', '') - .option('-r, --reporter ', `reporter to use, options are [${reporterTypes.join(', ')}] (default: ${defaultReporter})`, defaultReporter) + .option('-r, --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 ', 'run requests that match the regex', '') + .option('-i, --item ', 'request or folder id to run', collect, []) .option('-e, --env ', 'environment to use', '') - .option('-r, --reporter ', `reporter to use, options are [${reporterTypes.join(', ')}] (default: ${defaultReporter})`, defaultReporter) + .option('--delay-request ', 'milliseconds to delay between requests', '0') + .option('-n, --iteration-count ', 'number of times to repeat', '1') + .option('-d, --iteration-data ', 'file path or url (JSON or CSV)', '') + .option('-r, --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); diff --git a/packages/insomnia-inso/src/db/models/environment.ts b/packages/insomnia-inso/src/db/models/environment.ts index 9d331c4eba..d376a8c835 100644 --- a/packages/insomnia-inso/src/db/models/environment.ts +++ b/packages/insomnia-inso/src/db/models/environment.ts @@ -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 ( diff --git a/packages/insomnia-inso/src/examples/three-requests.yml b/packages/insomnia-inso/src/examples/three-requests.yml new file mode 100644 index 0000000000..fb16ed2d97 --- /dev/null +++ b/packages/insomnia-inso/src/examples/three-requests.yml @@ -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 diff --git a/packages/insomnia/src/common/send-request.ts b/packages/insomnia/src/common/send-request.ts index 8d98effae1..4af58c87be 100644 --- a/packages/insomnia/src/common/send-request.ts +++ b/packages/insomnia/src/common/send-request.ts @@ -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; - -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[], 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>( + 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 diff --git a/packages/insomnia/src/ui/components/dropdowns/git-sync-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/git-sync-dropdown.tsx index 7f2b180937..6cee61d9af 100644 --- a/packages/insomnia/src/ui/components/dropdowns/git-sync-dropdown.tsx +++ b/packages/insomnia/src/ui/components/dropdowns/git-sync-dropdown.tsx @@ -331,7 +331,7 @@ export const GitSyncDropdown: FC = ({ gitRepository, isInsomniaSyncEnable
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 ( + { + !isOpen && onClose(); + }} + className="w-full h-[--visual-viewport-height] fixed z-10 top-0 left-0 flex items-start justify-center bg-black/30" + > + { + !isOpen && onClose(); + }} + > + + {({ close }) => ( +
+
+ Run via CLI + +
+
+
Copy this command to run your collection in the terminal
+
+
+ {cliCommand} + + + + +
+
+
+
+ +
+
+ )} +
+
+
+ ); +}; diff --git a/packages/insomnia/src/ui/components/request-url-bar.tsx b/packages/insomnia/src/ui/components/request-url-bar.tsx index 3a85eee2dc..6e4e026ecd 100644 --- a/packages/insomnia/src/ui/components/request-url-bar.tsx +++ b/packages/insomnia/src/ui/components/request-url-bar.tsx @@ -214,17 +214,20 @@ export const RequestUrlBar = forwardRef(({ }); 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 ( -
- patchRequest(requestId, { method })} - method={method} - /> -
+
+
+ patchRequest(requestId, { method })} + method={method} + /> +
+
(({ })} onPaste={onPaste} /> -
+
{isCancellable ? ( ) : ( <> - + {isEventStreamRequest(activeRequest) ? null : ( .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; diff --git a/packages/insomnia/src/ui/routes/runner.tsx b/packages/insomnia/src/ui/routes/runner.tsx index 483c62292a..04fdb034f6 100644 --- a/packages/insomnia/src/ui/routes/runner.tsx +++ b/packages/insomnia/src/ui/routes/runner.tsx @@ -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 ( @@ -481,16 +483,45 @@ export const Runner: FC<{}> = () => {
-
- + + + + + } + > + + + + + + setShowCLIModal(true)} + /> + +
@@ -526,7 +557,6 @@ export const Runner: FC<{}> = () => { = () => {
+ {showCLIModal && ( + 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 && ( { @@ -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,