mirror of
https://github.com/Kong/insomnia.git
synced 2026-04-29 10:33:04 -04:00
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:
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 (
|
||||
|
||||
110
packages/insomnia-inso/src/examples/three-requests.yml
Normal file
110
packages/insomnia-inso/src/examples/three-requests.yml
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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]"
|
||||
>
|
||||
|
||||
@@ -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>{' '}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user