mirror of
https://github.com/Kong/insomnia
synced 2024-11-07 14:19:58 +00:00
fix: http request loading indicator (#6265)
* flatten url bar * flattening continued * more flat * more more flat * fix lint * flatten network send into one call * unpack send everywhere * remove send with settgins * fix types * fix bug in download * contain interpolation and modal * abstract render try catch * send action * extract to file * remove plugin ignore code * remove unused * unpack misc functions * less misc functions * readd inso tests * split test runs * fix test * fix test * fix test * use workspace pathing * remove check-engine * add tech debt list
This commit is contained in:
parent
94d035b6b1
commit
f9bd4ff82a
4
.github/workflows/release-recurring.yml
vendored
4
.github/workflows/release-recurring.yml
vendored
@ -52,8 +52,8 @@ jobs:
|
||||
shell: bash
|
||||
run: NODE_OPTIONS='--max_old_space_size=6144' BUILD_TARGETS='${{ matrix.build-targets }}' npm run app-package
|
||||
|
||||
- name: Run critical test on packaged app
|
||||
run: npm run test:package --prefix packages/insomnia-smoke-test -- --project=Critical
|
||||
- name: Test critical path on packaged electron app
|
||||
run: npm run test:package -w packages/insomnia-smoke-test -- --project=Critical
|
||||
|
||||
- name: Upload smoke test traces
|
||||
uses: actions/upload-artifact@v3
|
||||
|
26
.github/workflows/test.yml
vendored
26
.github/workflows/test.yml
vendored
@ -43,21 +43,30 @@ jobs:
|
||||
- name: Type checks
|
||||
run: npm run type-check
|
||||
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
- name: Test Insomnia
|
||||
run: npm test -w packages/insomnia
|
||||
|
||||
- name: Test Inso
|
||||
run: npm test -w packages/insomnia-inso
|
||||
|
||||
- name: Test Insomnia Testing
|
||||
run: npm test -w packages/insomnia-testing
|
||||
|
||||
- name: Test O2K
|
||||
run: npm test -w packages/openapi-2-kong
|
||||
|
||||
- name: Build app for smoke tests
|
||||
run: npm run app-build
|
||||
|
||||
- name: Run Smoke tests
|
||||
- name: Smoke test electron app
|
||||
# Partial Smoke test run, for regular CI triggers
|
||||
if: ${{ !startsWith(github.head_ref, 'release/') }}
|
||||
run: npm run test:build --prefix packages/insomnia-smoke-test -- --project=Smoke
|
||||
run: npm run test:build -w packages/insomnia-smoke-test -- --project=Smoke
|
||||
|
||||
- name: Run Prerelease tests
|
||||
- name: Prerelease test electron app
|
||||
# Full Smoke test run, for Release PRs
|
||||
if: ${{ startsWith(github.head_ref, 'release/') }}
|
||||
run: npm run test:build --prefix packages/insomnia-smoke-test -- --project=Default
|
||||
run: npm run test:build -w packages/insomnia-smoke-test -- --project=Default
|
||||
|
||||
- name: Set Inso CLI variables
|
||||
id: inso-variables
|
||||
@ -65,7 +74,6 @@ jobs:
|
||||
run: |
|
||||
INSO_VERSION="$(jq .version packages/insomnia-inso/package.json -rj)-run.${{ github.run_number }}"
|
||||
PKG_NAME="inso-ubuntu-latest-$INSO_VERSION"
|
||||
|
||||
echo "pkg-name=$PKG_NAME" >> $GITHUB_OUTPUT
|
||||
echo "inso-version=$INSO_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
@ -74,7 +82,7 @@ jobs:
|
||||
|
||||
- name: Package Inso CLI binary
|
||||
run: |
|
||||
echo "Replacing electron binary with node binary"
|
||||
echo "Replacing node-libcurl electron binary with node binary"
|
||||
node_modules/.bin/node-pre-gyp install --update-binary --directory node_modules/@getinsomnia/node-libcurl
|
||||
npm run inso-package
|
||||
env:
|
||||
@ -92,7 +100,7 @@ jobs:
|
||||
name: ${{ steps.inso-variables.outputs.pkg-name }}
|
||||
path: packages/insomnia-inso/artifacts
|
||||
|
||||
- name: Run Inso CLI smoke tests
|
||||
- name: Smoke test Inso CLI
|
||||
run: npm run test:smoke:cli
|
||||
|
||||
- name: Upload smoke test traces
|
||||
|
1
.npmrc
1
.npmrc
@ -2,3 +2,4 @@ runtime = electron
|
||||
target = 25.2.0
|
||||
disturl = https://electronjs.org/headers
|
||||
playwright_skip_browser_download=true
|
||||
engine-strict=true
|
||||
|
@ -69,7 +69,26 @@ This is just a brief summary of Insomnia's current technical debt.
|
||||
- Loading large responses (~20 MB) can crash the app on weaker hardware.
|
||||
- Bundling `libcurl` (native module) has caused many weeks of headaches trying to get builds working across Windows, Mac, and Linux. More expertise here is definitely needed.
|
||||
- All input fields that support features like templating or code completion are actually [CodeMirror](https://codemirror.net/6/) instances. This isn't really debt, but may affect things going forward.
|
||||
- Use of `libcurl` means Insomnia can't run in a web browser and can't support bidirectional socket communication.
|
||||
|
||||
- [x] upgrade spectral e2e testing
|
||||
- [x] upgrading electron
|
||||
- [x] preload electron main functions
|
||||
- [x] update react classes to function components
|
||||
- [x] remove excess packages
|
||||
- [x] migrate redux to remix
|
||||
- [x] migrate lerna to npm workspaces
|
||||
- [x] CI slow ~30m (now 10m)
|
||||
- [x] styling vision (react-aria + tailwind)
|
||||
- [ ] de-polymorph database
|
||||
- [ ] codemirror is unmaintained
|
||||
- [ ] nedb is unmaintained
|
||||
- [ ] grpc state state should be in main rather than renderer
|
||||
- [ ] drag and drop is flakey
|
||||
- [ ] sync code is spaghetti
|
||||
- [ ] template rendering is spaghetti and has poor discoverability
|
||||
- [ ] inso abstraction limits networking improvements
|
||||
- [ ] testing feature doesn't scale with investment
|
||||
- [ ] unify curl.ts and libcurl-promise implementations
|
||||
|
||||
## Electron upgrade
|
||||
|
||||
|
1
package-lock.json
generated
1
package-lock.json
generated
@ -7,7 +7,6 @@
|
||||
"": {
|
||||
"name": "insomnia",
|
||||
"version": "1.0.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"packages/openapi-2-kong",
|
||||
|
@ -28,7 +28,6 @@
|
||||
"type-check": "npm run type-check --workspaces --if-present",
|
||||
"test": "npm run test --workspaces --if-present",
|
||||
"lint:markdown": "npx markdownlint-cli2 \"**/*.md\" \"#**/node_modules\"",
|
||||
"preinstall": "npx -y check-engine",
|
||||
"clean": "git clean -dfX",
|
||||
"inso-start": "npm start --workspace=packages/insomnia-inso",
|
||||
"inso-package": "npm run build:sr --workspace=packages/insomnia && npm run package --workspace=packages/insomnia-inso",
|
||||
|
@ -21,7 +21,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint . --ext .js,.ts,.tsx --cache",
|
||||
"test": "echo 'fix these jest --runInBand'",
|
||||
"test": "esr esbuild.ts jest",
|
||||
"test:watch": "npm run test -- --watch",
|
||||
"test:snapshots": "npm run build && npm run test -- -u",
|
||||
"test:bundled-inso": "cross-env ./bin/inso run test \"Another suite\" -e \"OpenAPI env\" --src src/db/fixtures/nedb",
|
||||
|
@ -33,13 +33,13 @@ From project root, in separate terminals:
|
||||
|
||||
```sh
|
||||
# start smoke test api
|
||||
npm run serve --prefix packages/insomnia-smoke-test
|
||||
npm run serve -w packages/insomnia-smoke-test
|
||||
|
||||
# build send-request
|
||||
npm run build:sr --prefix packages/insomnia
|
||||
npm run build:sr -w packages/insomnia
|
||||
|
||||
# watch inso
|
||||
npm run start --prefix packages/insomnia-inso
|
||||
npm run start -w packages/insomnia-inso
|
||||
|
||||
# run api test with dev bundle
|
||||
$PWD/packages/insomnia-inso/bin/inso run test "Echo Test Suite" --src $PWD/packages/insomnia-smoke-test/fixtures/inso-nedb --env Dev --verbose
|
||||
@ -49,7 +49,7 @@ $PWD/packages/insomnia-inso/bin/inso run test "Echo Test Suite" --src $PWD/packa
|
||||
|
||||
```sh
|
||||
# run modify package command and then a unit test
|
||||
npm run package --prefix packages/insomnia-inso && \
|
||||
npm run package -w packages/insomnia-inso && \
|
||||
$PWD/packages/insomnia-inso/binaries/inso run test "Echo Test Suite" --src $PWD/packages/insomnia-smoke-test/fixtures/inso-nedb --env Dev --verbose
|
||||
|
||||
```
|
||||
|
@ -126,5 +126,5 @@ Each of the above commands will automatically run the Express server, so you do
|
||||
Non recurring / non-CI tests, like pre-release ones, can be run using [Playwright VS Code extension](#playwright-vs-code-extension) or by running `test:dev` against the desired test file:
|
||||
|
||||
```shell
|
||||
npm run test:dev --prefix packages/insomnia-smoke-test -- preferences-interactions
|
||||
npm run test:dev -w packages/insomnia-smoke-test -- preferences-interactions
|
||||
```
|
||||
|
@ -1,12 +1,9 @@
|
||||
import { beforeEach, describe, expect, it, jest } from '@jest/globals';
|
||||
|
||||
import { globalBeforeEach } from '../../__jest__/before-each';
|
||||
import { chunkArray } from '../../sync/vcs/vcs';
|
||||
import {
|
||||
capitalize,
|
||||
chunkArray,
|
||||
convertEpochToMilliseconds,
|
||||
debounce,
|
||||
diffPatchObj,
|
||||
filterHeaders,
|
||||
fuzzyMatch,
|
||||
fuzzyMatchAll,
|
||||
@ -14,11 +11,7 @@ import {
|
||||
hasAuthHeader,
|
||||
isNotNullOrUndefined,
|
||||
keyedDebounce,
|
||||
pluralize,
|
||||
snapNumberToLimits,
|
||||
toKebabCase,
|
||||
toTitleCase,
|
||||
xmlDecode,
|
||||
} from '../misc';
|
||||
|
||||
describe('hasAuthHeader()', () => {
|
||||
@ -257,7 +250,6 @@ describe('fuzzyMatchAll()', () => {
|
||||
expect(fuzzyMatchAll('wrong this ou', ['testing', 'this', 'out'])).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('chunkArray()', () => {
|
||||
it('works with exact divisor', () => {
|
||||
const chunks = chunkArray([1, 2, 3, 4, 5, 6], 3);
|
||||
@ -286,121 +278,6 @@ describe('chunkArray()', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('pluralize()', () => {
|
||||
it('should not change pluralization', () => {
|
||||
expect(pluralize('Requests')).toBe('Requests');
|
||||
});
|
||||
|
||||
it('should end with s', () => {
|
||||
expect(pluralize('Request')).toBe('Requests');
|
||||
});
|
||||
|
||||
it('should end with ies', () => {
|
||||
expect(pluralize('Directory')).toBe('Directories');
|
||||
});
|
||||
});
|
||||
|
||||
describe('diffPatchObj()', () => {
|
||||
const a = {
|
||||
x: 1,
|
||||
};
|
||||
const b = {
|
||||
x: 2,
|
||||
y: 3,
|
||||
};
|
||||
const c = {
|
||||
x: 4,
|
||||
y: {
|
||||
z: 5,
|
||||
},
|
||||
};
|
||||
|
||||
it('does a basic merge', () => {
|
||||
expect(diffPatchObj(a, b)).toEqual({
|
||||
x: 2,
|
||||
y: 3,
|
||||
});
|
||||
expect(diffPatchObj(b, a)).toEqual({
|
||||
x: 1,
|
||||
y: 3,
|
||||
});
|
||||
});
|
||||
|
||||
it.skip('does a basic merge, deep', () => {
|
||||
expect(diffPatchObj(a, c, true)).toEqual({
|
||||
x: 2,
|
||||
y: 3,
|
||||
});
|
||||
expect(diffPatchObj(c, a, true)).toEqual({
|
||||
x: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it.skip('does a basic nested merge', () => {
|
||||
expect(diffPatchObj(a, b)).toEqual({
|
||||
x: 2,
|
||||
y: 3,
|
||||
});
|
||||
expect(diffPatchObj(b, a)).toEqual({
|
||||
x: 1,
|
||||
y: {
|
||||
z: 5,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it.skip('does a basic nested merge, deep', () => {
|
||||
expect(diffPatchObj(a, c, true)).toEqual({
|
||||
x: 2,
|
||||
y: 3,
|
||||
});
|
||||
expect(diffPatchObj(c, a, true)).toEqual({
|
||||
x: 1,
|
||||
y: {
|
||||
z: 5,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertEpochToMilliseconds()', () => {
|
||||
it('should convert microseconds to milliseconds', () => {
|
||||
expect(convertEpochToMilliseconds(1617616858412123)).toBe(1617616858412);
|
||||
});
|
||||
|
||||
it('should convert seconds to milliseconds', () => {
|
||||
expect(convertEpochToMilliseconds(1617617010)).toBe(1617617010000);
|
||||
});
|
||||
|
||||
it('should output same if value already in milliseconds', () => {
|
||||
expect(convertEpochToMilliseconds(1617617141412)).toBe(1617617141412);
|
||||
});
|
||||
});
|
||||
|
||||
describe('snapNumberToLimits()', () => {
|
||||
it('should return value', () => {
|
||||
expect(snapNumberToLimits(2)).toBe(2);
|
||||
expect(snapNumberToLimits(2, 0)).toBe(2);
|
||||
expect(snapNumberToLimits(2, 0, 3)).toBe(2);
|
||||
expect(snapNumberToLimits(2, 2, 2)).toBe(2);
|
||||
expect(snapNumberToLimits(2, null, null)).toBe(2);
|
||||
expect(snapNumberToLimits(2, NaN, NaN)).toBe(2);
|
||||
});
|
||||
|
||||
it('should snap to min', () => {
|
||||
expect(snapNumberToLimits(2, 3)).toBe(3);
|
||||
expect(snapNumberToLimits(2, 3, 5)).toBe(3);
|
||||
expect(snapNumberToLimits(2, 3, null)).toBe(3);
|
||||
expect(snapNumberToLimits(2, 3, NaN)).toBe(3);
|
||||
});
|
||||
|
||||
it('should snap to max', () => {
|
||||
expect(snapNumberToLimits(5, 0, 3)).toBe(3);
|
||||
expect(snapNumberToLimits(5, null, 3)).toBe(3);
|
||||
expect(snapNumberToLimits(5, NaN, 3)).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isNotNullOrUndefined', () => {
|
||||
it('should return correctly', () => {
|
||||
expect(isNotNullOrUndefined(0)).toBe(true);
|
||||
@ -411,14 +288,6 @@ describe('isNotNullOrUndefined', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('xmlDecode()', () => {
|
||||
it('unescape characters', () => {
|
||||
const input = '<a href="http://example.com?query1=value1&query2=value2">a link</a>';
|
||||
const output = '<a href="http://example.com?query1=value1&query2=value2">a link</a>';
|
||||
expect(xmlDecode(input)).toEqual(output);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toKebabCase', () => {
|
||||
it('leaves strings without spaces alone', () => {
|
||||
expect(toKebabCase('')).toEqual('');
|
||||
@ -433,31 +302,3 @@ describe('toKebabCase', () => {
|
||||
expect(toKebabCase('a A b B c')).toEqual('a-A-b-B-c');
|
||||
});
|
||||
});
|
||||
|
||||
describe('capitalize', () => {
|
||||
it('capitalizes first letter', () => {
|
||||
expect(capitalize('')).toEqual('');
|
||||
expect(capitalize('a')).toEqual('A');
|
||||
expect(capitalize('A')).toEqual('A');
|
||||
expect(capitalize('abcd')).toEqual('Abcd');
|
||||
expect(capitalize('abcd efg')).toEqual('Abcd efg');
|
||||
});
|
||||
it('lowercases all other letters but the first', () => {
|
||||
expect(capitalize('aBcd efg')).toEqual('Abcd efg');
|
||||
expect(capitalize('aBcd Efg')).toEqual('Abcd efg');
|
||||
});
|
||||
});
|
||||
|
||||
describe('toTitleCase', () => {
|
||||
it('capitalizes first letter of each word', () => {
|
||||
expect(toTitleCase('')).toEqual('');
|
||||
expect(toTitleCase('a')).toEqual('A');
|
||||
expect(toTitleCase('A')).toEqual('A');
|
||||
expect(toTitleCase('abcd')).toEqual('Abcd');
|
||||
expect(toTitleCase('abcd efg')).toEqual('Abcd Efg');
|
||||
});
|
||||
it('lowercases all other letters but the first of each word', () => {
|
||||
expect(toTitleCase('aBcd efg')).toEqual('Abcd Efg');
|
||||
expect(toTitleCase('aBcd Efg')).toEqual('Abcd Efg');
|
||||
});
|
||||
});
|
||||
|
@ -1,9 +1,8 @@
|
||||
import fuzzysort from 'fuzzysort';
|
||||
import { join as pathJoin } from 'path';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import zlib from 'zlib';
|
||||
|
||||
import { DEBOUNCE_MILLIS, METHOD_DELETE, METHOD_OPTIONS } from './constants';
|
||||
import { DEBOUNCE_MILLIS } from './constants';
|
||||
|
||||
const ESCAPE_REGEX_MATCH = /[-[\]/{}()*+?.\\^$|]/g;
|
||||
|
||||
@ -12,23 +11,7 @@ interface Header {
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface Parameter {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export function filterParameters<T extends Parameter>(
|
||||
parameters: T[],
|
||||
name: string,
|
||||
): T[] {
|
||||
if (!Array.isArray(parameters) || !name) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return parameters.filter(h => (!h || !h.name ? false : h.name === name));
|
||||
}
|
||||
|
||||
export function filterHeaders<T extends Header>(headers: T[], name?: string): T[] {
|
||||
export function filterHeaders<T extends { name: string; value: string }>(headers: T[], name?: string): T[] {
|
||||
if (!Array.isArray(headers) || !name || typeof name !== 'string') {
|
||||
return [];
|
||||
}
|
||||
@ -111,22 +94,6 @@ export function delay(milliseconds: number = DEBOUNCE_MILLIS) {
|
||||
return new Promise<void>(resolve => setTimeout(resolve, milliseconds));
|
||||
}
|
||||
|
||||
export function removeVowels(str: string) {
|
||||
return str.replace(/[aeiouyAEIOUY]/g, '');
|
||||
}
|
||||
|
||||
export function formatMethodName(method: string) {
|
||||
let methodName = method || '';
|
||||
|
||||
if (method === METHOD_DELETE || method === METHOD_OPTIONS) {
|
||||
methodName = method.slice(0, 3);
|
||||
} else if (method.length > 4) {
|
||||
methodName = removeVowels(method).slice(0, 4);
|
||||
}
|
||||
|
||||
return methodName;
|
||||
}
|
||||
|
||||
export function keyedDebounce<T>(
|
||||
callback: (t: Record<string, T[]>) => void,
|
||||
millis: number = DEBOUNCE_MILLIS
|
||||
@ -186,19 +153,6 @@ export function describeByteSize(bytes: number, long = false) {
|
||||
return `${rounded} ${unit}`;
|
||||
}
|
||||
|
||||
export function xmlDecode(input: string) {
|
||||
const ESCAPED_CHARACTERS_MAP = {
|
||||
'&': '&',
|
||||
'"': '"',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
};
|
||||
|
||||
return input.replace(/("|<|>|&)/g, (_: string, item: keyof typeof ESCAPED_CHARACTERS_MAP) => (
|
||||
ESCAPED_CHARACTERS_MAP[item])
|
||||
);
|
||||
}
|
||||
|
||||
export function fnOrString(v: string | ((...args: any[]) => any), ...args: any[]) {
|
||||
if (typeof v === 'string') {
|
||||
return v;
|
||||
@ -221,28 +175,6 @@ export function decompressObject<ObjectType>(input: string | null): ObjectType |
|
||||
return JSON.parse(jsonBuffer.toString('utf8')) as ObjectType;
|
||||
}
|
||||
|
||||
export function resolveHomePath(p: string) {
|
||||
if (p.indexOf('~/') === 0) {
|
||||
return pathJoin(process.env['HOME'] || '/', p.slice(1));
|
||||
} else {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
|
||||
export function jsonParseOr(str: string, fallback: any): any {
|
||||
try {
|
||||
return JSON.parse(str);
|
||||
} catch (err) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
export function escapeHTML(unsafeText: string) {
|
||||
const div = document.createElement('div');
|
||||
div.innerText = unsafeText;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape a dynamic string for use inside of a regular expression
|
||||
* @param str - string to escape
|
||||
@ -327,101 +259,6 @@ export function fuzzyMatchAll(
|
||||
};
|
||||
}
|
||||
|
||||
export function chunkArray<T>(arr: T[], chunkSize: number) {
|
||||
const chunks: T[][] = [];
|
||||
|
||||
for (let i = 0, j = arr.length; i < j; i += chunkSize) {
|
||||
chunks.push(arr.slice(i, i + chunkSize));
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
export function pluralize(text: string) {
|
||||
let trailer = 's';
|
||||
let chop = 0;
|
||||
|
||||
// Things already ending with 's' stay that way
|
||||
if (text.match(/s$/)) {
|
||||
trailer = '';
|
||||
chop = 0;
|
||||
}
|
||||
|
||||
// Things ending in 'y' convert to ies
|
||||
if (text.match(/y$/)) {
|
||||
trailer = 'ies';
|
||||
chop = 1;
|
||||
}
|
||||
|
||||
// Add the trailer for pluralization
|
||||
return `${text.slice(0, text.length - chop)}${trailer}`;
|
||||
}
|
||||
|
||||
export function diffPatchObj(baseObj: any, patchObj: any, deep = false) {
|
||||
const clonedBaseObj = JSON.parse(JSON.stringify(baseObj));
|
||||
|
||||
for (const prop in baseObj) {
|
||||
if (!Object.prototype.hasOwnProperty.call(baseObj, prop)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(patchObj, prop)) {
|
||||
const left = baseObj[prop];
|
||||
const right = patchObj[prop];
|
||||
|
||||
if (right !== left) {
|
||||
if (deep && isObject(left) && isObject(right)) {
|
||||
clonedBaseObj[prop] = diffPatchObj(left, right, deep);
|
||||
} else if (isObject(left) && !isObject(right)) {
|
||||
// when right is empty but left isn't, prefer left to avoid a sparse array
|
||||
clonedBaseObj[prop] = left;
|
||||
} else {
|
||||
// otherwise prefer right when both elements aren't objects to ensure values don't get overwritten
|
||||
clonedBaseObj[prop] = right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const prop in patchObj) {
|
||||
if (!Object.prototype.hasOwnProperty.call(patchObj, prop)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!Object.prototype.hasOwnProperty.call(baseObj, prop)) {
|
||||
clonedBaseObj[prop] = patchObj[prop];
|
||||
}
|
||||
}
|
||||
|
||||
return clonedBaseObj;
|
||||
}
|
||||
|
||||
export function isObject(obj: unknown) {
|
||||
return obj !== null && typeof obj === 'object';
|
||||
}
|
||||
|
||||
/**
|
||||
Finds epoch's digit count and converts it to make it exactly 13 digits.
|
||||
Which is the epoch millisecond representation.
|
||||
*/
|
||||
export function convertEpochToMilliseconds(epoch: number) {
|
||||
const expDigitCount = epoch.toString().length;
|
||||
return parseInt(String(epoch * 10 ** (13 - expDigitCount)), 10);
|
||||
}
|
||||
|
||||
export function snapNumberToLimits(value: number, min?: number, max?: number) {
|
||||
const moreThanMax = max && !Number.isNaN(max) && value > max;
|
||||
const lessThanMin = min && !Number.isNaN(min) && value < min;
|
||||
|
||||
if (moreThanMax) {
|
||||
return max;
|
||||
} else if (lessThanMin) {
|
||||
return min;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export function isNotNullOrUndefined<ValueType>(
|
||||
value: ValueType | null | undefined
|
||||
): value is ValueType {
|
||||
@ -433,15 +270,3 @@ export function isNotNullOrUndefined<ValueType>(
|
||||
}
|
||||
|
||||
export const toKebabCase = (value: string) => value.replace(/ /g, '-');
|
||||
|
||||
export const capitalize = (value: string) => (
|
||||
`${value.slice(0, 1).toUpperCase()}${value.slice(1).toLowerCase()}`
|
||||
);
|
||||
|
||||
export const toTitleCase = (value: string) => (
|
||||
value
|
||||
.toLowerCase()
|
||||
.split(' ')
|
||||
.map(capitalize)
|
||||
.join(' ')
|
||||
);
|
||||
|
@ -9,7 +9,6 @@ import {
|
||||
tryToInterpolateRequest,
|
||||
tryToTransformRequestWithPlugins,
|
||||
} from '../network/network';
|
||||
import * as plugins from '../plugins';
|
||||
import { invariant } from '../utils/invariant';
|
||||
import { database } from './database';
|
||||
import { RENDER_PURPOSE_SEND } from './render';
|
||||
@ -65,30 +64,27 @@ export async function getSendRequestCallbackMemDb(environmentId: string, memDB:
|
||||
};
|
||||
// Return callback helper to send requests
|
||||
return async function sendRequest(requestId: string) {
|
||||
try {
|
||||
const {
|
||||
request,
|
||||
settings,
|
||||
clientCertificates,
|
||||
caCert,
|
||||
} = await fetchInsoRequestData(requestId);
|
||||
// NOTE: inso ignores active environment, using the one passed in
|
||||
const renderResult = await tryToInterpolateRequest(request, environmentId, RENDER_PURPOSE_SEND);
|
||||
const renderedRequest = await tryToTransformRequestWithPlugins(renderResult);
|
||||
const response = await sendCurlAndWriteTimeline(
|
||||
renderedRequest,
|
||||
clientCertificates,
|
||||
caCert,
|
||||
settings,
|
||||
);
|
||||
const res = await responseTransform(response, environmentId, renderedRequest, renderResult.context);
|
||||
const { statusCode: status, statusMessage, headers: headerArray, elapsedTime: responseTime } = res;
|
||||
const headers = headerArray?.reduce((acc, { name, value }) => ({ ...acc, [name.toLowerCase() || '']: value || '' }), []);
|
||||
const bodyBuffer = await getBodyBuffer(res) as Buffer;
|
||||
const data = bodyBuffer ? bodyBuffer.toString('utf8') : undefined;
|
||||
return { status, statusMessage, data, headers, responseTime };
|
||||
} finally {
|
||||
plugins.clearIgnores();
|
||||
}
|
||||
const {
|
||||
request,
|
||||
settings,
|
||||
clientCertificates,
|
||||
caCert,
|
||||
} = await fetchInsoRequestData(requestId);
|
||||
// NOTE: inso ignores active environment, using the one passed in
|
||||
const renderResult = await tryToInterpolateRequest(request, environmentId, RENDER_PURPOSE_SEND);
|
||||
const renderedRequest = await tryToTransformRequestWithPlugins(renderResult);
|
||||
const response = await sendCurlAndWriteTimeline(
|
||||
renderedRequest,
|
||||
clientCertificates,
|
||||
caCert,
|
||||
settings,
|
||||
);
|
||||
const res = await responseTransform(response, environmentId, renderedRequest, renderResult.context);
|
||||
const { statusCode: status, statusMessage, headers: headerArray, elapsedTime: responseTime } = res;
|
||||
const headers = headerArray?.reduce((acc, { name, value }) => ({ ...acc, [name.toLowerCase() || '']: value || '' }), []);
|
||||
const bodyBuffer = await getBodyBuffer(res) as Buffer;
|
||||
const data = bodyBuffer ? bodyBuffer.toString('utf8') : undefined;
|
||||
return { status, statusMessage, data, headers, responseTime };
|
||||
|
||||
};
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ import {
|
||||
EXPORT_TYPE_WEBSOCKET_REQUEST,
|
||||
EXPORT_TYPE_WORKSPACE,
|
||||
} from '../common/constants';
|
||||
import { generateId, pluralize } from '../common/misc';
|
||||
import { generateId } from '../common/misc';
|
||||
import * as _apiSpec from './api-spec';
|
||||
import * as _caCertificate from './ca-certificate';
|
||||
import * as _clientCertificate from './client-certificate';
|
||||
@ -163,18 +163,6 @@ export function canDuplicate(type: string) {
|
||||
return model ? model.canDuplicate : false;
|
||||
}
|
||||
|
||||
export function getModelName(type: string, count = 1) {
|
||||
const model = getModel(type);
|
||||
|
||||
if (!model) {
|
||||
return 'Unknown';
|
||||
} else if (count === 1) {
|
||||
return model.name;
|
||||
} else {
|
||||
return pluralize(model.name);
|
||||
}
|
||||
}
|
||||
|
||||
export async function initModel<T extends BaseModel>(type: string, ...sources: Record<string, any>[]): Promise<T> {
|
||||
const model = getModel(type);
|
||||
|
||||
|
@ -15,7 +15,6 @@ import type { ExtraRenderInfo, RenderedRequest, RenderPurpose, RequestAndContext
|
||||
import {
|
||||
getRenderedRequestAndContext,
|
||||
RENDER_PURPOSE_NO_RENDER,
|
||||
RENDER_PURPOSE_SEND,
|
||||
} from '../common/render';
|
||||
import type { HeaderResult, ResponsePatch, ResponseTimelineEntry } from '../main/network/libcurl-promise';
|
||||
import * as models from '../models';
|
||||
@ -38,69 +37,7 @@ import { getAuthHeader, getAuthQueryParams } from './authentication';
|
||||
import { cancellableCurlRequest } from './cancellation';
|
||||
import { urlMatchesCertHost } from './url-matches-cert-host';
|
||||
|
||||
// used for oauth grant types
|
||||
// creates a new request with the patch args
|
||||
// and uses env and settings from workspace
|
||||
// not cancellable but currently is
|
||||
// used indirectly by send and getAuthHeader to fetch tokens
|
||||
// @TODO unpack oauth into regular timeline and remove oauth timeine dialog
|
||||
export async function sendWithSettings(
|
||||
requestId: string,
|
||||
requestPatch: Record<string, any>,
|
||||
) {
|
||||
console.log(`[network] Sending with settings req=${requestId}`);
|
||||
const { request,
|
||||
environment,
|
||||
settings,
|
||||
clientCertificates,
|
||||
caCert,
|
||||
activeEnvironmentId } = await fetchRequestData(requestId);
|
||||
|
||||
const newRequest: Request = await models.initModel(models.request.type, requestPatch, {
|
||||
_id: request._id + '.other',
|
||||
parentId: request._id,
|
||||
});
|
||||
|
||||
const renderResult = await tryToInterpolateRequest(newRequest, environment._id);
|
||||
const renderedRequest = await tryToTransformRequestWithPlugins(renderResult);
|
||||
|
||||
const response = await sendCurlAndWriteTimeline(
|
||||
renderResult.request,
|
||||
clientCertificates,
|
||||
caCert,
|
||||
{ ...settings, validateSSL: settings.validateAuthSSL },
|
||||
);
|
||||
return responseTransform(response, activeEnvironmentId, renderedRequest, renderResult.context);
|
||||
}
|
||||
|
||||
// used by test feature, inso, and plugin api
|
||||
// not all need to be cancellable or to use curl
|
||||
export async function send(
|
||||
requestId: string,
|
||||
environmentId?: string,
|
||||
extraInfo?: ExtraRenderInfo,
|
||||
) {
|
||||
console.log(`[network] Sending req=${requestId} env=${environmentId || 'null'}`);
|
||||
|
||||
const { request,
|
||||
environment,
|
||||
settings,
|
||||
clientCertificates,
|
||||
caCert,
|
||||
activeEnvironmentId } = await fetchRequestData(requestId);
|
||||
|
||||
const renderResult = await tryToInterpolateRequest(request, environment._id, RENDER_PURPOSE_SEND, extraInfo);
|
||||
const renderedRequest = await tryToTransformRequestWithPlugins(renderResult);
|
||||
const response = await sendCurlAndWriteTimeline(
|
||||
renderedRequest,
|
||||
clientCertificates,
|
||||
caCert,
|
||||
settings,
|
||||
);
|
||||
return responseTransform(response, activeEnvironmentId, renderedRequest, renderResult.context);
|
||||
}
|
||||
|
||||
const fetchRequestData = async (requestId: string) => {
|
||||
export const fetchRequestData = async (requestId: string) => {
|
||||
const request = await models.request.getById(requestId);
|
||||
invariant(request, 'failed to find request');
|
||||
const ancestors = await db.withAncestors(request, [
|
||||
@ -224,8 +161,8 @@ export async function sendCurlAndWriteTimeline(
|
||||
...patch,
|
||||
};
|
||||
}
|
||||
export const responseTransform = (patch: ResponsePatch, environmentId: string | null, renderedRequest: RenderedRequest, context: Record<string, any>) => {
|
||||
const response = {
|
||||
export const responseTransform = async (patch: ResponsePatch, environmentId: string | null, renderedRequest: RenderedRequest, context: Record<string, any>) => {
|
||||
const response: ResponsePatch = {
|
||||
...patch,
|
||||
// important for filter by responses
|
||||
environmentId,
|
||||
@ -239,7 +176,7 @@ export const responseTransform = (patch: ResponsePatch, environmentId: string |
|
||||
return response;
|
||||
}
|
||||
console.log(`[network] Response succeeded req=${patch.parentId} status=${response.statusCode || '?'}`,);
|
||||
return _applyResponsePluginHooks(
|
||||
return await _applyResponsePluginHooks(
|
||||
response,
|
||||
renderedRequest,
|
||||
context,
|
||||
@ -325,7 +262,7 @@ async function _applyRequestPluginHooks(
|
||||
...pluginContexts.data.init(renderedContext.getProjectId()),
|
||||
...(pluginContexts.store.init(plugin) as Record<string, any>),
|
||||
...(pluginContexts.request.init(newRenderedRequest, renderedContext) as Record<string, any>),
|
||||
...(pluginContexts.network.init(renderedContext.getEnvironmentId?.()) as Record<string, any>),
|
||||
...(pluginContexts.network.init() as Record<string, any>),
|
||||
};
|
||||
|
||||
try {
|
||||
@ -355,7 +292,7 @@ async function _applyResponsePluginHooks(
|
||||
...(pluginContexts.store.init(plugin) as Record<string, any>),
|
||||
...(pluginContexts.response.init(newResponse) as Record<string, any>),
|
||||
...(pluginContexts.request.init(newRequest, renderedContext, true) as Record<string, any>),
|
||||
...(pluginContexts.network.init(renderedContext.getEnvironmentId?.()) as Record<string, any>),
|
||||
...(pluginContexts.network.init() as Record<string, any>),
|
||||
};
|
||||
|
||||
try {
|
||||
|
@ -6,11 +6,12 @@ import { escapeRegex } from '../../common/misc';
|
||||
import * as models from '../../models';
|
||||
import type { OAuth2Token } from '../../models/o-auth-2-token';
|
||||
import type { AuthTypeOAuth2, OAuth2ResponseType, RequestHeader, RequestParameter } from '../../models/request';
|
||||
import type { Request } from '../../models/request';
|
||||
import type { Response } from '../../models/response';
|
||||
import { invariant } from '../../utils/invariant';
|
||||
import { setDefaultProtocol } from '../../utils/url/protocol';
|
||||
import { getBasicAuthHeader } from '../basic-auth/get-header';
|
||||
import { sendWithSettings } from '../network';
|
||||
import { fetchRequestData, responseTransform, sendCurlAndWriteTimeline, tryToInterpolateRequest, tryToTransformRequestWithPlugins } from '../network';
|
||||
import {
|
||||
AuthKeys,
|
||||
GRANT_TYPE_AUTHORIZATION_CODE,
|
||||
@ -289,7 +290,16 @@ const transformNewAccessTokenToOauthModel = (accessToken: Partial<Record<AuthKey
|
||||
|
||||
const sendAccessTokenRequest = async (requestId: string, authentication: AuthTypeOAuth2, params: RequestParameter[], headers: RequestHeader[]) => {
|
||||
invariant(authentication.accessTokenUrl, 'Missing access token URL');
|
||||
const responsePatch = await sendWithSettings(requestId, {
|
||||
console.log(`[network] Sending with settings req=${requestId}`);
|
||||
// @TODO unpack oauth into regular timeline and remove oauth timeine dialog
|
||||
const { request,
|
||||
environment,
|
||||
settings,
|
||||
clientCertificates,
|
||||
caCert,
|
||||
activeEnvironmentId } = await fetchRequestData(requestId);
|
||||
|
||||
const newRequest: Request = await models.initModel(models.request.type, {
|
||||
headers: [
|
||||
{ name: 'Content-Type', value: 'application/x-www-form-urlencoded' },
|
||||
{ name: 'Accept', value: 'application/x-www-form-urlencoded, application/json' },
|
||||
@ -301,7 +311,22 @@ const sendAccessTokenRequest = async (requestId: string, authentication: AuthTyp
|
||||
mimeType: 'application/x-www-form-urlencoded',
|
||||
params,
|
||||
},
|
||||
}, {
|
||||
_id: request._id + '.other',
|
||||
parentId: request._id,
|
||||
});
|
||||
|
||||
const renderResult = await tryToInterpolateRequest(newRequest, environment._id);
|
||||
const renderedRequest = await tryToTransformRequestWithPlugins(renderResult);
|
||||
|
||||
const response = await sendCurlAndWriteTimeline(
|
||||
renderResult.request,
|
||||
clientCertificates,
|
||||
caCert,
|
||||
{ ...settings, validateSSL: settings.validateAuthSSL },
|
||||
);
|
||||
const responsePatch = await responseTransform(response, activeEnvironmentId, renderedRequest, renderResult.context);
|
||||
|
||||
return await models.response.create(responsePatch);
|
||||
};
|
||||
export const encodePKCE = (buffer: Buffer) => {
|
||||
|
@ -1,22 +1,33 @@
|
||||
import { RENDER_PURPOSE_SEND } from '../common/render';
|
||||
import { stats } from '../models';
|
||||
import { getBodyBuffer } from '../models/response';
|
||||
import * as plugins from '../plugins';
|
||||
import { send } from './network';
|
||||
import { fetchRequestData, responseTransform, sendCurlAndWriteTimeline, tryToInterpolateRequest, tryToTransformRequestWithPlugins } from './network';
|
||||
|
||||
export function getSendRequestCallback(environmentId?: string) {
|
||||
export function getSendRequestCallback() {
|
||||
return async function sendRequest(requestId: string) {
|
||||
stats.incrementExecutedRequests();
|
||||
try {
|
||||
// NOTE: unit tests will use the UI selected environment
|
||||
// TODO: unpack this and then unpack all other network.sends in order to match the realtime loading mechanism workaround
|
||||
const res = await send(requestId, environmentId);
|
||||
const { statusCode: status, statusMessage, headers: headerArray, elapsedTime: responseTime } = res;
|
||||
const headers = headerArray?.reduce((acc, { name, value }) => ({ ...acc, [name.toLowerCase() || '']: value || '' }), []);
|
||||
const bodyBuffer = await getBodyBuffer(res) as Buffer;
|
||||
const data = bodyBuffer ? bodyBuffer.toString('utf8') : undefined;
|
||||
return { status, statusMessage, data, headers, responseTime };
|
||||
} finally {
|
||||
plugins.clearIgnores();
|
||||
}
|
||||
// NOTE: unit tests will use the UI selected environment
|
||||
const { request,
|
||||
environment,
|
||||
settings,
|
||||
clientCertificates,
|
||||
caCert,
|
||||
activeEnvironmentId } = await fetchRequestData(requestId);
|
||||
|
||||
const renderResult = await tryToInterpolateRequest(request, environment._id, RENDER_PURPOSE_SEND);
|
||||
const renderedRequest = await tryToTransformRequestWithPlugins(renderResult);
|
||||
const response = await sendCurlAndWriteTimeline(
|
||||
renderedRequest,
|
||||
clientCertificates,
|
||||
caCert,
|
||||
settings,
|
||||
);
|
||||
const res = await responseTransform(response, activeEnvironmentId, renderedRequest, renderResult.context);
|
||||
const { statusCode: status, statusMessage, headers: headerArray, elapsedTime: responseTime } = res;
|
||||
const headers = headerArray?.reduce((acc, { name, value }) => ({ ...acc, [name.toLowerCase() || '']: value || '' }), []);
|
||||
const bodyBuffer = await getBodyBuffer(res) as Buffer;
|
||||
const data = bodyBuffer ? bodyBuffer.toString('utf8') : undefined;
|
||||
return { status, statusMessage, data, headers, responseTime };
|
||||
|
||||
};
|
||||
}
|
||||
|
@ -1,14 +1,28 @@
|
||||
import type { ExtraRenderInfo } from '../../common/render';
|
||||
import { ExtraRenderInfo, RENDER_PURPOSE_SEND } from '../../common/render';
|
||||
import * as models from '../../models';
|
||||
import type { Request } from '../../models/request';
|
||||
import { send } from '../../network/network';
|
||||
import { fetchRequestData, responseTransform, sendCurlAndWriteTimeline, tryToInterpolateRequest, tryToTransformRequestWithPlugins } from '../../network/network';
|
||||
|
||||
export function init(activeEnvironmentId: string | null) {
|
||||
export function init() {
|
||||
return {
|
||||
network: {
|
||||
async sendRequest(request: Request, extraInfo?: ExtraRenderInfo) {
|
||||
const responsePatch = await send(request._id, activeEnvironmentId || undefined, extraInfo);
|
||||
const settings = await models.settings.getOrCreate();
|
||||
async sendRequest(req: Request, extraInfo?: ExtraRenderInfo) {
|
||||
const { request,
|
||||
environment,
|
||||
settings,
|
||||
clientCertificates,
|
||||
caCert,
|
||||
activeEnvironmentId } = await fetchRequestData(req._id);
|
||||
|
||||
const renderResult = await tryToInterpolateRequest(request, environment._id, RENDER_PURPOSE_SEND, extraInfo);
|
||||
const renderedRequest = await tryToTransformRequestWithPlugins(renderResult);
|
||||
const response = await sendCurlAndWriteTimeline(
|
||||
renderedRequest,
|
||||
clientCertificates,
|
||||
caCert,
|
||||
settings,
|
||||
);
|
||||
const responsePatch = await responseTransform(response, activeEnvironmentId, renderedRequest, renderResult.context);
|
||||
return models.response.create(responsePatch, settings.maxHistoryResponses);
|
||||
},
|
||||
},
|
||||
|
@ -1,7 +1,16 @@
|
||||
import * as misc from '../../common/misc';
|
||||
import type { RenderedRequest } from '../../common/render';
|
||||
import type { RequestBody } from '../../models/request';
|
||||
export function filterParameters<T extends { name: string; value: string }>(
|
||||
parameters: T[],
|
||||
name: string,
|
||||
): T[] {
|
||||
if (!Array.isArray(parameters) || !name) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return parameters.filter(h => (!h || !h.name ? false : h.name === name));
|
||||
}
|
||||
export function init(
|
||||
renderedRequest: RenderedRequest | null,
|
||||
renderedContext: Record<string, any>,
|
||||
@ -129,7 +138,7 @@ export function init(
|
||||
},
|
||||
|
||||
getParameter(name: string) {
|
||||
const parameters = misc.filterParameters(renderedRequest.parameters, name);
|
||||
const parameters = filterParameters(renderedRequest.parameters, name);
|
||||
|
||||
if (parameters.length) {
|
||||
// Use the last parameter if there are multiple of the same
|
||||
@ -152,12 +161,12 @@ export function init(
|
||||
},
|
||||
|
||||
removeParameter(name: string) {
|
||||
const parameters = misc.filterParameters(renderedRequest.parameters, name);
|
||||
const parameters = filterParameters(renderedRequest.parameters, name);
|
||||
renderedRequest.parameters = renderedRequest.parameters.filter(p => !parameters.includes(p));
|
||||
},
|
||||
|
||||
setParameter(name: string, value: string) {
|
||||
const parameter = misc.filterParameters(renderedRequest.parameters, name)[0];
|
||||
const parameter = filterParameters(renderedRequest.parameters, name)[0];
|
||||
|
||||
if (parameter) {
|
||||
parameter.value = value;
|
||||
@ -167,7 +176,7 @@ export function init(
|
||||
},
|
||||
|
||||
addParameter(name: string, value: string) {
|
||||
const parameter = misc.filterParameters(renderedRequest.parameters, name)[0];
|
||||
const parameter = filterParameters(renderedRequest.parameters, name)[0];
|
||||
|
||||
if (!parameter) {
|
||||
renderedRequest.parameters.push({
|
||||
|
@ -3,7 +3,6 @@ import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { ParsedApiSpec } from '../common/api-specs';
|
||||
import { resolveHomePath } from '../common/misc';
|
||||
import type { PluginConfig, PluginConfigMap } from '../common/settings';
|
||||
import * as models from '../models';
|
||||
import { GrpcRequest } from '../models/grpc-request';
|
||||
@ -105,25 +104,12 @@ export type ColorScheme = 'default' | 'light' | 'dark';
|
||||
|
||||
let plugins: Plugin[] | null | undefined = null;
|
||||
|
||||
let ignorePlugins: string[] = [];
|
||||
|
||||
export async function init() {
|
||||
clearIgnores();
|
||||
await reloadPlugins();
|
||||
}
|
||||
|
||||
export function ignorePlugin(name: string) {
|
||||
if (!ignorePlugins.includes(name)) {
|
||||
ignorePlugins.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
export function clearIgnores() {
|
||||
ignorePlugins = [];
|
||||
}
|
||||
|
||||
async function _traversePluginPath(
|
||||
pluginMap: Record<string, any>,
|
||||
pluginMap: Record<string, Plugin>,
|
||||
allPaths: string[],
|
||||
allConfigs: PluginConfigMap,
|
||||
) {
|
||||
@ -169,8 +155,16 @@ async function _traversePluginPath(
|
||||
// Delete require cache entry and re-require
|
||||
const module = global.require(modulePath);
|
||||
|
||||
const pluginName = pluginJson.name;
|
||||
pluginMap[pluginName] = _initPlugin(pluginJson || {}, module, allConfigs, modulePath);
|
||||
pluginMap[pluginJson.name] = {
|
||||
name: pluginJson.name,
|
||||
description: pluginJson.description || pluginJson.insomnia.description || '',
|
||||
version: pluginJson.version || 'unknown',
|
||||
directory: modulePath || '',
|
||||
config: allConfigs.hasOwnProperty(pluginJson.name)
|
||||
? allConfigs[pluginJson.name]
|
||||
: { disabled: false },
|
||||
module: module,
|
||||
};
|
||||
console.log(`[plugin] Loaded ${modulePath}`);
|
||||
} catch (err) {
|
||||
showError({
|
||||
@ -194,7 +188,13 @@ export async function getPlugins(force = false): Promise<Plugin[]> {
|
||||
const extraPaths = settings.pluginPath
|
||||
.split(':')
|
||||
.filter(p => p)
|
||||
.map(resolveHomePath);
|
||||
.map(p => {
|
||||
if (p.indexOf('~/') === 0) {
|
||||
return path.join(process.env['HOME'] || '/', p.slice(1));
|
||||
} else {
|
||||
return p;
|
||||
}
|
||||
});
|
||||
// Make sure the default directories exist
|
||||
const pluginPath = path.join(process.env['INSOMNIA_DATA_PATH'] || (process.type === 'renderer' ? window : electron).app.getPath('userData'), 'plugins');
|
||||
fs.mkdirSync(pluginPath, { recursive: true });
|
||||
@ -400,29 +400,3 @@ export async function getThemes(): Promise<Theme[]> {
|
||||
|
||||
return extensions;
|
||||
}
|
||||
|
||||
const _defaultPluginConfig: PluginConfig = {
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
function _initPlugin(
|
||||
packageJSON: Record<string, any>,
|
||||
module: any,
|
||||
allConfigs: PluginConfigMap,
|
||||
path?: string | null,
|
||||
): Plugin {
|
||||
const meta = packageJSON.insomnia || {};
|
||||
const name = packageJSON.name || meta.name;
|
||||
// Find config
|
||||
const config: PluginConfig = allConfigs.hasOwnProperty(name)
|
||||
? allConfigs[name]
|
||||
: _defaultPluginConfig;
|
||||
return {
|
||||
name,
|
||||
description: packageJSON.description || meta.description || '',
|
||||
version: packageJSON.version || 'unknown',
|
||||
directory: path || '',
|
||||
config,
|
||||
module: module,
|
||||
};
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import path from 'path';
|
||||
|
||||
import * as crypt from '../../account/crypt';
|
||||
import * as session from '../../account/session';
|
||||
import { chunkArray, generateId } from '../../common/misc';
|
||||
import { generateId } from '../../common/misc';
|
||||
import { strings } from '../../common/strings';
|
||||
import { BaseModel } from '../../models';
|
||||
import Store from '../store';
|
||||
@ -43,6 +43,14 @@ const EMPTY_HASH = crypto.createHash('sha1').digest('hex').replace(/./g, '0');
|
||||
|
||||
type ConflictHandler = (conflicts: MergeConflict[]) => Promise<MergeConflict[]>;
|
||||
|
||||
// breaks one array into multiple arrays of size chunkSize
|
||||
export function chunkArray<T>(arr: T[], chunkSize: number) {
|
||||
const chunks: T[][] = [];
|
||||
for (let i = 0, j = arr.length; i < j; i += chunkSize) {
|
||||
chunks.push(arr.slice(i, i + chunkSize));
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
export class VCS {
|
||||
_store: Store;
|
||||
_driver: BaseDriver;
|
||||
|
@ -96,8 +96,6 @@ export default class BaseExtension {
|
||||
const renderMeta = renderContext.getMeta ? renderContext.getMeta() : {};
|
||||
// Pull out the purpose
|
||||
const renderPurpose = renderContext.getPurpose ? renderContext.getPurpose() : null;
|
||||
// Pull out the environment ID
|
||||
const environmentId = renderContext.getEnvironmentId ? renderContext.getEnvironmentId() : 'n/a';
|
||||
// Extract the rest of the args
|
||||
const args = runArgs
|
||||
.slice(0, runArgs.length - 1)
|
||||
@ -108,7 +106,7 @@ export default class BaseExtension {
|
||||
...pluginContexts.app.init(renderPurpose),
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
...pluginContexts.store.init(this._plugin),
|
||||
...pluginContexts.network.init(environmentId),
|
||||
...pluginContexts.network.init(),
|
||||
context: renderContext,
|
||||
meta: renderMeta,
|
||||
renderPurpose,
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { type Environment } from 'nunjucks';
|
||||
import nunjucks from 'nunjucks/browser/nunjucks';
|
||||
|
||||
import type { TemplateTag } from '../plugins/index';
|
||||
import * as plugins from '../plugins/index';
|
||||
import { localTemplateTags } from '../ui/components/templating/local-template-tags';
|
||||
import BaseExtension from './base-extension';
|
||||
@ -178,16 +177,10 @@ async function getNunjucks(renderMode: string): Promise<NunjucksEnvironment> {
|
||||
// Create Env with Extensions //
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~ //
|
||||
const nunjucksEnvironment = nunjucks.configure(config) as NunjucksEnvironment;
|
||||
let allTemplateTagPlugins: TemplateTag[];
|
||||
|
||||
try {
|
||||
const pluginTemplateTags = await plugins.getTemplateTags();
|
||||
allTemplateTagPlugins = [...pluginTemplateTags, ...localTemplateTags] as TemplateTag[];
|
||||
} finally {
|
||||
plugins.clearIgnores();
|
||||
}
|
||||
const pluginTemplateTags = await plugins.getTemplateTags();
|
||||
|
||||
const allExtensions = allTemplateTagPlugins;
|
||||
const allExtensions = [...pluginTemplateTags, ...localTemplateTags];
|
||||
|
||||
for (const extension of allExtensions) {
|
||||
const { templateTag, plugin } = extension;
|
||||
|
@ -3,7 +3,7 @@ import 'codemirror/addon/mode/overlay';
|
||||
import CodeMirror, { EnvironmentAutocompleteOptions, Hint, ShowHintOptions } from 'codemirror';
|
||||
|
||||
import { getPlatformKeyCombinations } from '../../../../common/hotkeys';
|
||||
import { escapeHTML, escapeRegex, fnOrString, isNotNullOrUndefined } from '../../../../common/misc';
|
||||
import { escapeRegex, fnOrString, isNotNullOrUndefined } from '../../../../common/misc';
|
||||
import { getDefaultFill, NunjucksParsedTag } from '../../../../templating/utils';
|
||||
import { isNunjucksMode } from '../modes/nunjucks';
|
||||
|
||||
@ -506,6 +506,11 @@ function replaceWithSurround(text: string, find: string, prefix: string, suffix:
|
||||
return text.replace(re, matched => prefix + matched + suffix);
|
||||
}
|
||||
|
||||
function escapeHTML(unsafeText: string) {
|
||||
const div = document.createElement('div');
|
||||
div.innerText = unsafeText;
|
||||
return div.innerHTML;
|
||||
}
|
||||
/**
|
||||
* Render the autocomplete list entry
|
||||
*/
|
||||
|
@ -60,12 +60,11 @@ export const RequestActionsDropdown = forwardRef<DropdownHandle, Props>(({
|
||||
setLoadingActions({ ...loadingActions, [label]: true });
|
||||
|
||||
try {
|
||||
const activeEnvironmentId = activeEnvironment ? activeEnvironment._id : null;
|
||||
const context = {
|
||||
...(pluginContexts.app.init(RENDER_PURPOSE_NO_RENDER)),
|
||||
...pluginContexts.data.init(activeProject._id),
|
||||
...(pluginContexts.store.init(plugin)),
|
||||
...(pluginContexts.network.init(activeEnvironmentId)),
|
||||
...(pluginContexts.network.init()),
|
||||
};
|
||||
await action(context, {
|
||||
request,
|
||||
@ -81,7 +80,7 @@ export const RequestActionsDropdown = forwardRef<DropdownHandle, Props>(({
|
||||
if (ref && 'current' in ref) { // this `in` operator statement type-narrows to `MutableRefObject`
|
||||
ref.current?.hide();
|
||||
}
|
||||
}, [request, activeEnvironment, requestGroup, loadingActions, activeProject._id, ref]);
|
||||
}, [request, requestGroup, loadingActions, activeProject._id, ref]);
|
||||
|
||||
const duplicate = useCallback(() => {
|
||||
handleDuplicateRequest(request);
|
||||
|
@ -29,7 +29,6 @@ export const RequestGroupActionsDropdown = forwardRef<RequestGroupActionsDropdow
|
||||
...other
|
||||
}, ref) => {
|
||||
const {
|
||||
activeEnvironment,
|
||||
activeProject,
|
||||
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
|
||||
const {
|
||||
@ -125,7 +124,7 @@ export const RequestGroupActionsDropdown = forwardRef<RequestGroupActionsDropdow
|
||||
...(pluginContexts.app.init(RENDER_PURPOSE_NO_RENDER) as Record<string, any>),
|
||||
...pluginContexts.data.init(activeProject._id),
|
||||
...(pluginContexts.store.init(plugin) as Record<string, any>),
|
||||
...(pluginContexts.network.init(activeEnvironment._id) as Record<string, any>),
|
||||
...(pluginContexts.network.init() as Record<string, any>),
|
||||
};
|
||||
const requests = await models.request.findByParentId(requestGroup._id);
|
||||
requests.sort((a, b) => a.metaSortKey - b.metaSortKey);
|
||||
@ -147,7 +146,7 @@ export const RequestGroupActionsDropdown = forwardRef<RequestGroupActionsDropdow
|
||||
|
||||
dropdownRef.current?.hide();
|
||||
|
||||
}, [dropdownRef, loadingActions, activeEnvironment, requestGroup, activeProject]);
|
||||
}, [dropdownRef, loadingActions, requestGroup, activeProject]);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
|
@ -30,7 +30,6 @@ export const WorkspaceDropdown: FC = () => {
|
||||
const {
|
||||
activeWorkspace,
|
||||
activeWorkspaceMeta,
|
||||
activeEnvironment,
|
||||
activeProject,
|
||||
activeApiSpec,
|
||||
clientCertificates,
|
||||
@ -63,7 +62,7 @@ export const WorkspaceDropdown: FC = () => {
|
||||
...(pluginContexts.app.init(RENDER_PURPOSE_NO_RENDER) as Record<string, any>),
|
||||
...pluginContexts.data.init(activeProject._id),
|
||||
...(pluginContexts.store.init(plugin) as Record<string, any>),
|
||||
...(pluginContexts.network.init(activeEnvironment._id) as Record<string, any>),
|
||||
...(pluginContexts.network.init() as Record<string, any>),
|
||||
};
|
||||
|
||||
const docs = await db.withDescendants(workspace);
|
||||
@ -86,7 +85,7 @@ export const WorkspaceDropdown: FC = () => {
|
||||
}
|
||||
setLoadingActions({ ...loadingActions, [label]: false });
|
||||
dropdownRef.current?.hide();
|
||||
}, [activeEnvironment, activeProject._id, loadingActions]);
|
||||
}, [activeProject._id, loadingActions]);
|
||||
|
||||
const handleDropdownOpen = useCallback(async () => {
|
||||
const actionPlugins = await getWorkspaceActions();
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { ChangeEvent, FC, ReactNode, useEffect, useMemo, useState } from 'react';
|
||||
import { useRouteLoaderData } from 'react-router-dom';
|
||||
|
||||
import { convertEpochToMilliseconds, toKebabCase } from '../../../../common/misc';
|
||||
import { toKebabCase } from '../../../../common/misc';
|
||||
import accessTokenUrls from '../../../../datasets/access-token-urls';
|
||||
import authorizationUrls from '../../../../datasets/authorization-urls';
|
||||
import * as models from '../../../../models';
|
||||
@ -280,7 +280,14 @@ export const OAuth2Auth: FC = () => {
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
Finds epoch's digit count and converts it to make it exactly 13 digits.
|
||||
Which is the epoch millisecond representation. (trims last 2 digits)
|
||||
*/
|
||||
export function convertEpochToMilliseconds(epoch: number) {
|
||||
const expDigitCount = epoch.toString().length;
|
||||
return parseInt(String(epoch * 10 ** (13 - expDigitCount)), 10);
|
||||
}
|
||||
const renderIdentityTokenExpiry = (token?: Pick<OAuth2Token, 'identityToken'>) => {
|
||||
if (!token || !token.identityToken) {
|
||||
return;
|
||||
|
@ -14,11 +14,11 @@ import { useLocalStorage } from 'react-use';
|
||||
import { CONTENT_TYPE_JSON } from '../../../../common/constants';
|
||||
import { database as db } from '../../../../common/database';
|
||||
import { markdownToHTML } from '../../../../common/markdown-to-html';
|
||||
import { jsonParseOr } from '../../../../common/misc';
|
||||
import { RENDER_PURPOSE_SEND } from '../../../../common/render';
|
||||
import type { ResponsePatch } from '../../../../main/network/libcurl-promise';
|
||||
import * as models from '../../../../models';
|
||||
import type { Request } from '../../../../models/request';
|
||||
import * as network from '../../../../network/network';
|
||||
import { fetchRequestData, responseTransform, sendCurlAndWriteTimeline, tryToInterpolateRequest, tryToTransformRequestWithPlugins } from '../../../../network/network';
|
||||
import { invariant } from '../../../../utils/invariant';
|
||||
import { jsonPrettify } from '../../../../utils/prettify/json';
|
||||
import { RootLoaderData } from '../../../routes/root';
|
||||
@ -63,7 +63,6 @@ const isOperationDefinition = (def: DefinitionNode): def is OperationDefinitionN
|
||||
|
||||
const fetchGraphQLSchemaForRequest = async ({
|
||||
requestId,
|
||||
environmentId,
|
||||
url,
|
||||
}: {
|
||||
requestId: string;
|
||||
@ -74,9 +73,9 @@ const fetchGraphQLSchemaForRequest = async ({
|
||||
return;
|
||||
}
|
||||
|
||||
const request = await models.request.getById(requestId);
|
||||
const req = await models.request.getById(requestId);
|
||||
|
||||
if (!request) {
|
||||
if (!req) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -87,10 +86,10 @@ const fetchGraphQLSchemaForRequest = async ({
|
||||
operationName: 'IntrospectionQuery',
|
||||
});
|
||||
const introspectionRequest = await db.upsert(
|
||||
Object.assign({}, request, {
|
||||
_id: request._id + '.graphql',
|
||||
Object.assign({}, req, {
|
||||
_id: req._id + '.graphql',
|
||||
settingMaxTimelineDataSize: 5000,
|
||||
parentId: request._id,
|
||||
parentId: req._id,
|
||||
isPrivate: true,
|
||||
// So it doesn't get synced or exported
|
||||
body: {
|
||||
@ -99,7 +98,22 @@ const fetchGraphQLSchemaForRequest = async ({
|
||||
},
|
||||
}),
|
||||
);
|
||||
const response = await network.send(introspectionRequest._id, environmentId);
|
||||
const { request,
|
||||
environment,
|
||||
settings,
|
||||
clientCertificates,
|
||||
caCert,
|
||||
activeEnvironmentId } = await fetchRequestData(introspectionRequest._id);
|
||||
|
||||
const renderResult = await tryToInterpolateRequest(request, environment._id, RENDER_PURPOSE_SEND);
|
||||
const renderedRequest = await tryToTransformRequestWithPlugins(renderResult);
|
||||
const res = await sendCurlAndWriteTimeline(
|
||||
renderedRequest,
|
||||
clientCertificates,
|
||||
caCert,
|
||||
settings,
|
||||
);
|
||||
const response = await responseTransform(res, activeEnvironmentId, renderedRequest, renderResult.context);
|
||||
const statusCode = response.statusCode || 0;
|
||||
if (!response) {
|
||||
return {
|
||||
@ -160,6 +174,7 @@ interface State {
|
||||
documentAST: null | DocumentNode;
|
||||
disabledOperationMarkers: (TextMarker | undefined)[];
|
||||
}
|
||||
|
||||
export const GraphQLEditor: FC<Props> = ({
|
||||
request,
|
||||
environmentId,
|
||||
@ -175,7 +190,11 @@ export const GraphQLEditor: FC<Props> = ({
|
||||
requestBody = { query: '' };
|
||||
}
|
||||
if (typeof requestBody.variables === 'string') {
|
||||
requestBody.variables = jsonParseOr(requestBody.variables, '');
|
||||
try {
|
||||
requestBody.variables = JSON.parse(requestBody.variables);
|
||||
} catch (err) {
|
||||
requestBody.variables = '';
|
||||
}
|
||||
}
|
||||
let documentAST;
|
||||
try {
|
||||
|
@ -68,7 +68,7 @@ export const SyncStagingModal = ({ vcs, branch, onSnapshot, handlePush, onHide }
|
||||
lookupMap[key] = {
|
||||
changes: hasDocAndLastSnapshot ? describeChanges(document, lastSnapshot) : null,
|
||||
entry: entry,
|
||||
type: models.getModelName(docOrLastSnapshot.type),
|
||||
type: models.getModel(docOrLastSnapshot.type)?.name || 'Unknown',
|
||||
checked: !!status.stage[key],
|
||||
};
|
||||
}
|
||||
|
@ -6,11 +6,12 @@ import styled from 'styled-components';
|
||||
import { getCommonHeaderNames, getCommonHeaderValues } from '../../../common/common-headers';
|
||||
import { documentationLinks } from '../../../common/documentation';
|
||||
import { generateId } from '../../../common/misc';
|
||||
import { getRenderContext, getRenderedGrpcRequest, getRenderedGrpcRequestMessage, render, RENDER_PURPOSE_SEND } from '../../../common/render';
|
||||
import { getRenderedGrpcRequest, getRenderedGrpcRequestMessage, RENDER_PURPOSE_SEND } from '../../../common/render';
|
||||
import { GrpcMethodType } from '../../../main/ipc/grpc';
|
||||
import * as models from '../../../models';
|
||||
import type { GrpcRequestHeader } from '../../../models/grpc-request';
|
||||
import { queryAllWorkspaceUrls } from '../../../models/helpers/query-all-workspace-urls';
|
||||
import { tryToInterpolateRequestOrShowRenderErrorModal } from '../../../utils/try-interpolate';
|
||||
import { useRequestPatcher } from '../../hooks/use-request';
|
||||
import { useActiveRequestSyncVCSVersion, useGitVCSVersion } from '../../hooks/use-vcs-version';
|
||||
import { GrpcRequestState } from '../../routes/debug';
|
||||
@ -195,8 +196,7 @@ export const GrpcRequestPane: FunctionComponent<Props> = ({
|
||||
disabled={!activeRequest.url}
|
||||
onClick={async () => {
|
||||
try {
|
||||
const renderContext = await getRenderContext({ request: activeRequest, environmentId, purpose: RENDER_PURPOSE_SEND });
|
||||
const rendered = await render({ url: activeRequest.url, metadata: activeRequest.metadata }, renderContext);
|
||||
const rendered = await tryToInterpolateRequestOrShowRenderErrorModal({ request: activeRequest, environmentId, payload: { url: activeRequest.url, metadata: activeRequest.metadata } });
|
||||
const methods = await window.main.grpc.loadMethodsFromReflection(rendered);
|
||||
setGrpcState({ ...grpcState, methods });
|
||||
patchRequest(requestId, { protoFileId: '', protoMethodName: '' });
|
||||
|
@ -1,27 +1,23 @@
|
||||
import * as contentDisposition from 'content-disposition';
|
||||
import fs from 'fs';
|
||||
import { extension as mimeExtension } from 'mime-types';
|
||||
import path from 'path';
|
||||
import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react';
|
||||
|
||||
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||
import { useFetcher, useParams, useRouteLoaderData } from 'react-router-dom';
|
||||
import { useInterval } from 'react-use';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { database } from '../../common/database';
|
||||
import { getContentDispositionHeader } from '../../common/misc';
|
||||
import { getRenderContext, render, RENDER_PURPOSE_SEND } from '../../common/render';
|
||||
import { RENDER_PURPOSE_SEND } from '../../common/render';
|
||||
import * as models from '../../models';
|
||||
import { isEventStreamRequest, isRequest } from '../../models/request';
|
||||
import * as network from '../../network/network';
|
||||
import { fetchRequestData, tryToInterpolateRequest, tryToTransformRequestWithPlugins } from '../../network/network';
|
||||
import { convert } from '../../utils/importers/convert';
|
||||
import { invariant } from '../../utils/invariant';
|
||||
import { tryToInterpolateRequestOrShowRenderErrorModal } from '../../utils/try-interpolate';
|
||||
import { buildQueryStringFromParams, joinUrlAndQueryString } from '../../utils/url/querystring';
|
||||
import { SegmentEvent } from '../analytics';
|
||||
import { useReadyState } from '../hooks/use-ready-state';
|
||||
import { useRequestPatcher } from '../hooks/use-request';
|
||||
import { useRequestMetaPatcher } from '../hooks/use-request';
|
||||
import { useTimeoutWhen } from '../hooks/useTimeoutWhen';
|
||||
import { ConnectActionParams, RequestLoaderData } from '../routes/request';
|
||||
import { ConnectActionParams, RequestLoaderData, SendActionParams } from '../routes/request';
|
||||
import { RootLoaderData } from '../routes/root';
|
||||
import { WorkspaceLoaderData } from '../routes/workspace';
|
||||
import { Dropdown, DropdownButton, type DropdownHandle, DropdownItem, DropdownSection, ItemContent } from './base/dropdown';
|
||||
@ -30,7 +26,6 @@ import { MethodDropdown } from './dropdowns/method-dropdown';
|
||||
import { createKeybindingsHandler, useDocBodyKeyboardShortcuts } from './keydown-binder';
|
||||
import { GenerateCodeModal } from './modals/generate-code-modal';
|
||||
import { showAlert, showModal, showPrompt } from './modals/index';
|
||||
import { RequestRenderErrorModal } from './modals/request-render-error-modal';
|
||||
|
||||
const StyledDropdownButton = styled(DropdownButton)({
|
||||
'&:hover:not(:disabled)': {
|
||||
@ -75,9 +70,6 @@ export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
|
||||
const inputRef = useRef<OneLineEditorHandle>(null);
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const handleGenerateCode = () => {
|
||||
showModal(GenerateCodeModal, { request: activeRequest });
|
||||
};
|
||||
const focusInput = useCallback(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focusEnd();
|
||||
@ -88,9 +80,34 @@ export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
|
||||
|
||||
const [currentInterval, setCurrentInterval] = useState<number | null>(null);
|
||||
const [currentTimeout, setCurrentTimeout] = useState<number | undefined>(undefined);
|
||||
const fetcher = useFetcher();
|
||||
// TODO: unpick this loading hack
|
||||
useEffect(() => {
|
||||
if (fetcher.state !== 'idle') {
|
||||
setLoading(true);
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [fetcher.state, setLoading]);
|
||||
const { organizationId, projectId, workspaceId, requestId } = useParams() as { organizationId: string; projectId: string; workspaceId: string; requestId: string };
|
||||
const connect = (connectParams: ConnectActionParams) => {
|
||||
fetcher.submit(JSON.stringify(connectParams),
|
||||
{
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request/${requestId}/connect`,
|
||||
method: 'post',
|
||||
encType: 'application/json',
|
||||
});
|
||||
};
|
||||
const send = (sendParams: SendActionParams) => {
|
||||
fetcher.submit(JSON.stringify(sendParams),
|
||||
{
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request/${requestId}/send`,
|
||||
method: 'post',
|
||||
encType: 'application/json',
|
||||
});
|
||||
};
|
||||
|
||||
const sendThenSetFilePath = useCallback(async () => {
|
||||
// Update request stats
|
||||
const sendOrConnect = async (shouldPromptForPathAfterResponse?: boolean) => {
|
||||
models.stats.incrementExecutedRequests();
|
||||
window.main.trackSegmentEvent({
|
||||
event: SegmentEvent.requestExecute,
|
||||
@ -100,61 +117,47 @@ export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
|
||||
mimeType: activeRequest.body.mimeType,
|
||||
},
|
||||
});
|
||||
setLoading(true);
|
||||
try {
|
||||
const responsePatch = await network.send(activeRequest._id, activeEnvironment._id);
|
||||
const is2XXWithBodyPath = responsePatch.statusCode && responsePatch.statusCode >= 200 && responsePatch.statusCode < 300 && responsePatch.bodyPath;
|
||||
if (!is2XXWithBodyPath) {
|
||||
// Save the bad responses so failures are shown still
|
||||
await models.response.create(responsePatch, settings.maxHistoryResponses);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
let downloadPathAndName = '';
|
||||
if (downloadPath) {
|
||||
const sanitizedExtension = responsePatch.contentType && mimeExtension(responsePatch.contentType);
|
||||
const extension = sanitizedExtension || 'unknown';
|
||||
const headers = responsePatch.headers || [];
|
||||
const header = getContentDispositionHeader(headers);
|
||||
const nameFromHeader = header ? contentDisposition.parse(header.value).parameters.filename : null;
|
||||
const name = nameFromHeader || `${activeRequest.name.replace(/\s/g, '-').toLowerCase()}.${extension}`;
|
||||
downloadPathAndName = path.join(downloadPath, name);
|
||||
}
|
||||
if (!downloadPath) {
|
||||
const defaultPath = window.localStorage.getItem('insomnia.sendAndDownloadLocation');
|
||||
const { filePath } = await window.dialog.showSaveDialog({
|
||||
title: 'Select Download Location',
|
||||
buttonLabel: 'Save',
|
||||
// NOTE: An error will be thrown if defaultPath is supplied but not a String
|
||||
...(defaultPath ? { defaultPath } : {}),
|
||||
});
|
||||
if (!filePath) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
window.localStorage.setItem('insomnia.sendAndDownloadLocation', filePath);
|
||||
downloadPathAndName = filePath;
|
||||
}
|
||||
invariant(downloadPathAndName, 'filename should be set by now');
|
||||
// reset timeout
|
||||
setCurrentTimeout(undefined);
|
||||
|
||||
const to = fs.createWriteStream(downloadPathAndName);
|
||||
const readStream = models.response.getBodyStream(responsePatch);
|
||||
if (!readStream || typeof readStream === 'string') {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
readStream.pipe(to);
|
||||
readStream.on('end', async () => {
|
||||
responsePatch.error = `Saved to ${downloadPathAndName}`;
|
||||
await models.response.create(responsePatch, settings.maxHistoryResponses);
|
||||
setLoading(false);
|
||||
return;
|
||||
});
|
||||
readStream.on('error', async err => {
|
||||
console.warn('Failed to download request after sending', responsePatch.bodyPath, err);
|
||||
await models.response.create(responsePatch, settings.maxHistoryResponses);
|
||||
setLoading(false);
|
||||
return;
|
||||
if (isEventStreamRequest(activeRequest)) {
|
||||
const startListening = async () => {
|
||||
const environmentId = activeEnvironment._id;
|
||||
const workspaceId = activeWorkspace._id;
|
||||
// Render any nunjucks tags in the url/headers/authentication settings/cookies
|
||||
const workspaceCookieJar = await models.cookieJar.getOrCreateForParentId(workspaceId);
|
||||
const rendered = await tryToInterpolateRequestOrShowRenderErrorModal({
|
||||
request: activeRequest,
|
||||
environmentId,
|
||||
payload: {
|
||||
url: activeRequest.url,
|
||||
headers: activeRequest.headers,
|
||||
authentication: activeRequest.authentication,
|
||||
parameters: activeRequest.parameters.filter(p => !p.disabled),
|
||||
workspaceCookieJar,
|
||||
},
|
||||
});
|
||||
rendered && connect({
|
||||
url: joinUrlAndQueryString(rendered.url, buildQueryStringFromParams(rendered.parameters)),
|
||||
headers: rendered.headers,
|
||||
authentication: rendered.authentication,
|
||||
cookieJar: rendered.workspaceCookieJar,
|
||||
});
|
||||
};
|
||||
startListening();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { request,
|
||||
environment } = await fetchRequestData(requestId);
|
||||
|
||||
const renderResult = await tryToInterpolateRequest(request, environment._id, RENDER_PURPOSE_SEND);
|
||||
const renderedRequest = await tryToTransformRequestWithPlugins(renderResult);
|
||||
renderedRequest && send({
|
||||
renderedRequest,
|
||||
shouldPromptForPathAfterResponse,
|
||||
context: renderResult.context,
|
||||
});
|
||||
} catch (err) {
|
||||
showAlert({
|
||||
@ -168,143 +171,12 @@ export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
|
||||
</div>
|
||||
),
|
||||
});
|
||||
setLoading(false);
|
||||
}
|
||||
}, [activeEnvironment._id, activeRequest._id, activeRequest.authentication?.type, activeRequest.body.mimeType, activeRequest.name, downloadPath, setLoading, settings.maxHistoryResponses, settings.preferredHttpVersion]);
|
||||
|
||||
const handleSend = useCallback(async () => {
|
||||
if (!activeRequest) {
|
||||
return;
|
||||
}
|
||||
// Update request stats
|
||||
models.stats.incrementExecutedRequests();
|
||||
window.main.trackSegmentEvent({
|
||||
event: SegmentEvent.requestExecute,
|
||||
properties: {
|
||||
preferredHttpVersion: settings.preferredHttpVersion,
|
||||
authenticationType: activeRequest.authentication?.type,
|
||||
mimeType: activeRequest.body.mimeType,
|
||||
},
|
||||
});
|
||||
setLoading(true);
|
||||
try {
|
||||
const responsePatch = await network.send(activeRequest._id, activeEnvironment._id);
|
||||
const response = await models.response.create(responsePatch, settings.maxHistoryResponses);
|
||||
await patchRequestMeta(activeRequest._id, { activeResponseId: response._id });
|
||||
} catch (err) {
|
||||
if (err.type === 'render') {
|
||||
showModal(RequestRenderErrorModal, {
|
||||
request: activeRequest,
|
||||
error: err,
|
||||
});
|
||||
} else {
|
||||
showAlert({
|
||||
title: 'Unexpected Request Failure',
|
||||
message: (
|
||||
<div>
|
||||
<p>The request failed due to an unhandled error:</p>
|
||||
<code className="wide selectable">
|
||||
<pre>{err.message}</pre>
|
||||
</code>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
setLoading(false);
|
||||
}, [activeEnvironment._id, activeRequest, setLoading, settings.maxHistoryResponses, settings.preferredHttpVersion, patchRequestMeta]);
|
||||
const fetcher = useFetcher();
|
||||
const { organizationId, projectId, workspaceId, requestId } = useParams() as { organizationId: string; projectId: string; workspaceId: string; requestId: string };
|
||||
const connect = (connectParams: ConnectActionParams) => {
|
||||
fetcher.submit(JSON.stringify(connectParams),
|
||||
{
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request/${requestId}/connect`,
|
||||
method: 'post',
|
||||
encType: 'application/json',
|
||||
});
|
||||
};
|
||||
const send = () => {
|
||||
setCurrentTimeout(undefined);
|
||||
if (downloadPath) {
|
||||
sendThenSetFilePath();
|
||||
return;
|
||||
}
|
||||
if (isEventStreamRequest(activeRequest)) {
|
||||
const startListening = async () => {
|
||||
const environmentId = activeEnvironment._id;
|
||||
const workspaceId = activeWorkspace._id;
|
||||
const renderContext = await getRenderContext({ request: activeRequest, environmentId, purpose: RENDER_PURPOSE_SEND });
|
||||
// Render any nunjucks tags in the url/headers/authentication settings/cookies
|
||||
const workspaceCookieJar = await models.cookieJar.getOrCreateForParentId(workspaceId);
|
||||
const rendered = await render({
|
||||
url: activeRequest.url,
|
||||
headers: activeRequest.headers,
|
||||
authentication: activeRequest.authentication,
|
||||
parameters: activeRequest.parameters.filter(p => !p.disabled),
|
||||
workspaceCookieJar,
|
||||
}, renderContext);
|
||||
connect({
|
||||
url: joinUrlAndQueryString(rendered.url, buildQueryStringFromParams(rendered.parameters)),
|
||||
headers: rendered.headers,
|
||||
authentication: rendered.authentication,
|
||||
cookieJar: rendered.workspaceCookieJar,
|
||||
});
|
||||
};
|
||||
startListening();
|
||||
return;
|
||||
}
|
||||
handleSend();
|
||||
};
|
||||
|
||||
useInterval(send, currentInterval ? currentInterval : null);
|
||||
useTimeoutWhen(send, currentTimeout, !!currentTimeout);
|
||||
useInterval(sendOrConnect, currentInterval ? currentInterval : null);
|
||||
useTimeoutWhen(sendOrConnect, currentTimeout, !!currentTimeout);
|
||||
const patchRequest = useRequestPatcher();
|
||||
const handleStop = () => {
|
||||
if (isEventStreamRequest(activeRequest)) {
|
||||
window.main.curl.close({ requestId: activeRequest._id });
|
||||
return;
|
||||
}
|
||||
setCurrentInterval(null);
|
||||
setCurrentTimeout(undefined);
|
||||
};
|
||||
|
||||
const handleSendOnInterval = useCallback(() => {
|
||||
showPrompt({
|
||||
inputType: 'decimal',
|
||||
title: 'Send on Interval',
|
||||
label: 'Interval in seconds',
|
||||
defaultValue: '3',
|
||||
submitName: 'Start',
|
||||
onComplete: seconds => {
|
||||
setCurrentInterval(+seconds * 1000);
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleSendAfterDelay = () => {
|
||||
showPrompt({
|
||||
inputType: 'decimal',
|
||||
title: 'Send After Delay',
|
||||
label: 'Delay in seconds',
|
||||
defaultValue: '3',
|
||||
onComplete: seconds => {
|
||||
setCurrentTimeout(+seconds * 1000);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const downloadAfterSend = useCallback(async () => {
|
||||
const { canceled, filePaths } = await window.dialog.showOpenDialog({
|
||||
title: 'Select Download Location',
|
||||
buttonLabel: 'Select',
|
||||
properties: ['openDirectory'],
|
||||
});
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
patchRequestMeta(activeRequest._id, { downloadPath: filePaths[0] });
|
||||
}, [activeRequest._id, patchRequestMeta]);
|
||||
const handleClearDownloadLocation = () => patchRequestMeta(activeRequest._id, { downloadPath: null });
|
||||
|
||||
useDocBodyKeyboardShortcuts({
|
||||
request_focusUrl: () => {
|
||||
@ -312,7 +184,7 @@ export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
|
||||
},
|
||||
request_send: () => {
|
||||
if (activeRequest.url) {
|
||||
send();
|
||||
sendOrConnect();
|
||||
}
|
||||
},
|
||||
request_toggleHttpMethodMenu: () => {
|
||||
@ -399,107 +271,130 @@ export const RequestUrlBar = forwardRef<RequestUrlBarHandle, Props>(({
|
||||
defaultValue={url}
|
||||
onChange={handleUrlChange}
|
||||
onKeyDown={createKeybindingsHandler({
|
||||
'Enter': () => send(),
|
||||
'Enter': () => sendOrConnect(),
|
||||
})}
|
||||
/>
|
||||
{isCancellable ? (
|
||||
<button
|
||||
type="button"
|
||||
className="urlbar__send-btn"
|
||||
onClick={handleStop}
|
||||
onClick={() => {
|
||||
if (isEventStreamRequest(activeRequest)) {
|
||||
window.main.curl.close({ requestId: activeRequest._id });
|
||||
return;
|
||||
}
|
||||
setCurrentInterval(null);
|
||||
setCurrentTimeout(undefined);
|
||||
}}
|
||||
>
|
||||
{isEventStreamRequest(activeRequest) ? 'Disconnect' : 'Cancel'}
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
) : (<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => sendOrConnect()}
|
||||
className="urlbar__send-btn"
|
||||
onClick={send}
|
||||
type="button"
|
||||
|
||||
>
|
||||
{buttonText}</button>
|
||||
{isEventStreamRequest(activeRequest) ? null : (<Dropdown
|
||||
key="dropdown"
|
||||
className="tall"
|
||||
ref={dropdownRef}
|
||||
aria-label="Request Options"
|
||||
onClose={handleSendDropdownHide}
|
||||
closeOnSelect={false}
|
||||
triggerButton={
|
||||
<StyledDropdownButton
|
||||
className="urlbar__send-context"
|
||||
removeBorderRadius={true}
|
||||
>
|
||||
<i className="fa fa-caret-down" />
|
||||
</StyledDropdownButton>
|
||||
}
|
||||
{buttonText}</button>
|
||||
{isEventStreamRequest(activeRequest) ? null : (<Dropdown
|
||||
key="dropdown"
|
||||
className="tall"
|
||||
ref={dropdownRef}
|
||||
aria-label="Request Options"
|
||||
onClose={handleSendDropdownHide}
|
||||
closeOnSelect={false}
|
||||
triggerButton={
|
||||
<StyledDropdownButton
|
||||
className="urlbar__send-context"
|
||||
removeBorderRadius={true}
|
||||
>
|
||||
<i className="fa fa-caret-down" />
|
||||
</StyledDropdownButton>
|
||||
}
|
||||
>
|
||||
<DropdownSection
|
||||
aria-label="Basic Section"
|
||||
title="Basic"
|
||||
>
|
||||
<DropdownSection
|
||||
aria-label="Basic Section"
|
||||
title="Basic"
|
||||
>
|
||||
<DropdownItem aria-label="send-now">
|
||||
<DropdownItem aria-label="send-now">
|
||||
<ItemContent icon="arrow-circle-o-right" label="Send Now" hint={hotKeyRegistry.request_send} onClick={sendOrConnect} />
|
||||
</DropdownItem>
|
||||
<DropdownItem aria-label='Generate Client Code'>
|
||||
<ItemContent
|
||||
icon="code"
|
||||
label="Generate Client Code"
|
||||
onClick={() => showModal(GenerateCodeModal, { request: activeRequest })}
|
||||
/>
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
<DropdownSection
|
||||
aria-label="Advanced Section"
|
||||
title="Advanced"
|
||||
>
|
||||
<DropdownItem aria-label='Send After Delay'>
|
||||
<ItemContent
|
||||
icon="clock-o"
|
||||
label="Send After Delay"
|
||||
onClick={() => showPrompt({
|
||||
inputType: 'decimal',
|
||||
title: 'Send After Delay',
|
||||
label: 'Delay in seconds',
|
||||
defaultValue: '3',
|
||||
onComplete: seconds => {
|
||||
setCurrentTimeout(+seconds * 1000);
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem aria-label='Repeat on Interval'>
|
||||
<ItemContent
|
||||
icon="repeat"
|
||||
label="Repeat on Interval"
|
||||
onClick={() => showPrompt({
|
||||
inputType: 'decimal',
|
||||
title: 'Send on Interval',
|
||||
label: 'Interval in seconds',
|
||||
defaultValue: '3',
|
||||
submitName: 'Start',
|
||||
onComplete: seconds => {
|
||||
setCurrentInterval(+seconds * 1000);
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</DropdownItem>
|
||||
{downloadPath
|
||||
? (<DropdownItem aria-label='Stop Auto-Download'>
|
||||
<ItemContent
|
||||
icon="arrow-circle-o-right"
|
||||
label="Send Now"
|
||||
hint={hotKeyRegistry.request_send}
|
||||
onClick={send}
|
||||
icon="stop-circle"
|
||||
label="Stop Auto-Download"
|
||||
withPrompt
|
||||
onClick={() => patchRequestMeta(activeRequest._id, { downloadPath: null })}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem aria-label='Generate Client Code'>
|
||||
<ItemContent
|
||||
icon="code"
|
||||
label="Generate Client Code"
|
||||
onClick={handleGenerateCode}
|
||||
/>
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
<DropdownSection
|
||||
aria-label="Advanced Section"
|
||||
title="Advanced"
|
||||
>
|
||||
<DropdownItem aria-label='Send After Delay'>
|
||||
<ItemContent
|
||||
icon="clock-o"
|
||||
label="Send After Delay"
|
||||
onClick={handleSendAfterDelay}
|
||||
/>
|
||||
</DropdownItem>
|
||||
<DropdownItem aria-label='Repeat on Interval'>
|
||||
<ItemContent
|
||||
icon="repeat"
|
||||
label="Repeat on Interval"
|
||||
onClick={handleSendOnInterval}
|
||||
/>
|
||||
</DropdownItem>
|
||||
{downloadPath ? (
|
||||
<DropdownItem aria-label='Stop Auto-Download'>
|
||||
<ItemContent
|
||||
icon="stop-circle"
|
||||
label="Stop Auto-Download"
|
||||
withPrompt
|
||||
onClick={handleClearDownloadLocation}
|
||||
/>
|
||||
</DropdownItem>) :
|
||||
(<DropdownItem aria-label='Download After Send'>
|
||||
<ItemContent
|
||||
icon="download"
|
||||
label="Download After Send"
|
||||
onClick={downloadAfterSend}
|
||||
/>
|
||||
</DropdownItem>
|
||||
)}
|
||||
<DropdownItem aria-label='Send And Download'>
|
||||
</DropdownItem>)
|
||||
: (<DropdownItem aria-label='Download After Send'>
|
||||
<ItemContent
|
||||
icon="download"
|
||||
label="Send And Download"
|
||||
onClick={sendThenSetFilePath}
|
||||
label="Download After Send"
|
||||
onClick={async () => {
|
||||
const { canceled, filePaths } = await window.dialog.showOpenDialog({
|
||||
title: 'Select Download Location',
|
||||
buttonLabel: 'Select',
|
||||
properties: ['openDirectory'],
|
||||
});
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
patchRequestMeta(activeRequest._id, { downloadPath: filePaths[0] });
|
||||
}}
|
||||
/>
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
</Dropdown>)}
|
||||
</>
|
||||
)}
|
||||
</DropdownItem>)}
|
||||
<DropdownItem aria-label='Send And Download'>
|
||||
<ItemContent icon="download" label="Send And Download" onClick={() => sendOrConnect(true)} />
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
</Dropdown>)}
|
||||
</>)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,7 +1,6 @@
|
||||
import React, { ChangeEventHandler, FC, InputHTMLAttributes, useCallback } from 'react';
|
||||
import { useRouteLoaderData } from 'react-router-dom';
|
||||
|
||||
import { snapNumberToLimits } from '../../../common/misc';
|
||||
import { SettingsOfType } from '../../../common/settings';
|
||||
import { useSettingsPatcher } from '../../hooks/use-request';
|
||||
import { RootLoaderData } from '../../routes/root';
|
||||
@ -15,7 +14,17 @@ interface Props {
|
||||
setting: SettingsOfType<number>;
|
||||
step?: InputHTMLAttributes<HTMLInputElement>['step'];
|
||||
}
|
||||
|
||||
export function snapNumberToLimits(value: number, min?: number, max?: number) {
|
||||
const moreThanMax = max && !Number.isNaN(max) && value > max;
|
||||
if (moreThanMax) {
|
||||
return max;
|
||||
}
|
||||
const lessThanMin = min && !Number.isNaN(min) && value < min;
|
||||
if (lessThanMin) {
|
||||
return min;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
export const NumberSetting: FC<Props> = ({
|
||||
help,
|
||||
label,
|
||||
|
@ -1,20 +1,34 @@
|
||||
import React, { FC, memo } from 'react';
|
||||
|
||||
import * as util from '../../../common/misc';
|
||||
import { METHOD_DELETE, METHOD_OPTIONS } from '../../../common/constants';
|
||||
|
||||
interface Props {
|
||||
method: string;
|
||||
override?: string | null;
|
||||
fullNames?: boolean;
|
||||
}
|
||||
function removeVowels(str: string) {
|
||||
return str.replace(/[aeiouyAEIOUY]/g, '');
|
||||
}
|
||||
function formatMethodName(method: string) {
|
||||
let methodName = method || '';
|
||||
|
||||
if (method === METHOD_DELETE || method === METHOD_OPTIONS) {
|
||||
methodName = method.slice(0, 3);
|
||||
} else if (method.length > 4) {
|
||||
methodName = removeVowels(method).slice(0, 4);
|
||||
}
|
||||
|
||||
return methodName;
|
||||
}
|
||||
|
||||
export const MethodTag: FC<Props> = memo(({ method, override, fullNames }) => {
|
||||
let methodName = method;
|
||||
let overrideName = override;
|
||||
|
||||
if (!fullNames) {
|
||||
methodName = util.formatMethodName(method);
|
||||
overrideName = override ? util.formatMethodName(override) : override;
|
||||
methodName = formatMethodName(method);
|
||||
overrideName = override ? formatMethodName(override) : override;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -2,14 +2,19 @@ import { differenceInMinutes, formatDistanceToNowStrict } from 'date-fns';
|
||||
import React, { FC, useState } from 'react';
|
||||
import { useInterval } from 'react-use';
|
||||
|
||||
import { toTitleCase } from '../../common/misc';
|
||||
|
||||
interface Props {
|
||||
timestamp: number | Date | string;
|
||||
intervalSeconds?: number;
|
||||
className?: string;
|
||||
titleCase?: boolean;
|
||||
}
|
||||
const toTitleCase = (value: string) => (
|
||||
value
|
||||
.toLowerCase()
|
||||
.split(' ')
|
||||
.map(value => value.charAt(0).toUpperCase() + value.slice(1))
|
||||
.join(' ')
|
||||
);
|
||||
|
||||
function getTimeFromNow(timestamp: string | number | Date, titleCase: boolean): string {
|
||||
const date = new Date(timestamp);
|
||||
|
@ -11,7 +11,6 @@ import {
|
||||
PREVIEW_MODE_FRIENDLY,
|
||||
PREVIEW_MODE_RAW,
|
||||
} from '../../../common/constants';
|
||||
import { xmlDecode } from '../../../common/misc';
|
||||
import { CodeEditor, CodeEditorHandle } from '../codemirror/code-editor';
|
||||
import { useDocBodyKeyboardShortcuts } from '../keydown-binder';
|
||||
import { ResponseCSVViewer } from './response-csv-viewer';
|
||||
@ -25,7 +24,18 @@ let alwaysShowLargeResponses = false;
|
||||
export interface ResponseViewerHandle {
|
||||
refresh: () => void;
|
||||
}
|
||||
export function xmlDecode(input: string) {
|
||||
const ESCAPED_CHARACTERS_MAP = {
|
||||
'&': '&',
|
||||
'"': '"',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
};
|
||||
|
||||
return input.replace(/("|<|>|&)/g, (_: string, item: keyof typeof ESCAPED_CHARACTERS_MAP) => (
|
||||
ESCAPED_CHARACTERS_MAP[item])
|
||||
);
|
||||
}
|
||||
export interface ResponseViewerProps {
|
||||
bytes: number;
|
||||
contentType: string;
|
||||
|
@ -2,15 +2,13 @@ import React, { FC, useLayoutEffect, useRef } from 'react';
|
||||
import { useFetcher, useParams } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { getRenderContext, render, RENDER_PURPOSE_SEND } from '../../../common/render';
|
||||
import * as models from '../../../models';
|
||||
import { WebSocketRequest } from '../../../models/websocket-request';
|
||||
import { tryToInterpolateRequestOrShowRenderErrorModal } from '../../../utils/try-interpolate';
|
||||
import { buildQueryStringFromParams, joinUrlAndQueryString } from '../../../utils/url/querystring';
|
||||
import { ConnectActionParams } from '../../routes/request';
|
||||
import { OneLineEditor, OneLineEditorHandle } from '../codemirror/one-line-editor';
|
||||
import { createKeybindingsHandler, useDocBodyKeyboardShortcuts } from '../keydown-binder';
|
||||
import { showAlert, showModal } from '../modals';
|
||||
import { RequestRenderErrorModal } from '../modals/request-render-error-modal';
|
||||
import { DisconnectButton } from './disconnect-button';
|
||||
|
||||
const Button = styled.button<{ warning?: boolean }>(({ warning }) => ({
|
||||
@ -84,48 +82,32 @@ export const WebSocketActionBar: FC<ActionBarProps> = ({ request, environmentId,
|
||||
encType: 'application/json',
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (isOpen) {
|
||||
window.main.webSocket.close({ requestId: request._id });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const renderContext = await getRenderContext({ request, environmentId, purpose: RENDER_PURPOSE_SEND });
|
||||
// Render any nunjucks tags in the url/headers/authentication settings/cookies
|
||||
const workspaceCookieJar = await models.cookieJar.getOrCreateForParentId(workspaceId);
|
||||
const rendered = await render({
|
||||
// Render any nunjucks tags in the url/headers/authentication settings/cookies
|
||||
const workspaceCookieJar = await models.cookieJar.getOrCreateForParentId(workspaceId);
|
||||
const rendered = await tryToInterpolateRequestOrShowRenderErrorModal({
|
||||
request,
|
||||
environmentId,
|
||||
payload: {
|
||||
url: request.url,
|
||||
headers: request.headers,
|
||||
authentication: request.authentication,
|
||||
parameters: request.parameters.filter(p => !p.disabled),
|
||||
workspaceCookieJar,
|
||||
}, renderContext);
|
||||
connect({
|
||||
url: joinUrlAndQueryString(rendered.url, buildQueryStringFromParams(rendered.parameters)),
|
||||
headers: rendered.headers,
|
||||
authentication: rendered.authentication,
|
||||
cookieJar: rendered.workspaceCookieJar,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.type === 'render') {
|
||||
showModal(RequestRenderErrorModal, {
|
||||
request,
|
||||
error: err,
|
||||
});
|
||||
} else {
|
||||
showAlert({
|
||||
title: 'Unexpected Request Failure',
|
||||
message: (
|
||||
<div>
|
||||
<p>The request failed due to an unhandled error:</p>
|
||||
<code className="wide selectable">
|
||||
<pre>{err.message}</pre>
|
||||
</code>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
rendered && connect({
|
||||
url: joinUrlAndQueryString(rendered.url, buildQueryStringFromParams(rendered.parameters)),
|
||||
headers: rendered.headers,
|
||||
authentication: rendered.authentication,
|
||||
cookieJar: rendered.workspaceCookieJar,
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
useDocBodyKeyboardShortcuts({
|
||||
|
@ -3,10 +3,10 @@ import { useParams, useRouteLoaderData } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { AuthType, CONTENT_TYPE_JSON } from '../../../common/constants';
|
||||
import { getRenderContext, render, RENDER_PURPOSE_SEND } from '../../../common/render';
|
||||
import * as models from '../../../models';
|
||||
import { Environment } from '../../../models/environment';
|
||||
import { WebSocketRequest } from '../../../models/websocket-request';
|
||||
import { tryToInterpolateRequestOrShowRenderErrorModal } from '../../../utils/try-interpolate';
|
||||
import { buildQueryStringFromParams, joinUrlAndQueryString } from '../../../utils/url/querystring';
|
||||
import { useReadyState } from '../../hooks/use-ready-state';
|
||||
import { useRequestPatcher } from '../../hooks/use-request';
|
||||
@ -107,21 +107,25 @@ const WebSocketRequestForm: FC<FormProps> = ({
|
||||
|
||||
init();
|
||||
}, [request._id]);
|
||||
|
||||
// NOTE: Nunjucks interpolation can throw errors
|
||||
const interpolateOpenAndSend = async (payload: string) => {
|
||||
try {
|
||||
const renderContext = await getRenderContext({ request, environmentId, purpose: RENDER_PURPOSE_SEND });
|
||||
const renderedMessage = await render(payload, renderContext);
|
||||
const renderedMessage = await tryToInterpolateRequestOrShowRenderErrorModal({ request, environmentId, payload });
|
||||
const readyState = await window.main.webSocket.readyState.getCurrent({ requestId: request._id });
|
||||
if (!readyState) {
|
||||
const workspaceCookieJar = await models.cookieJar.getOrCreateForParentId(workspaceId);
|
||||
const rendered = await render({
|
||||
url: request.url,
|
||||
headers: request.headers,
|
||||
authentication: request.authentication,
|
||||
parameters: request.parameters.filter(p => !p.disabled),
|
||||
workspaceCookieJar,
|
||||
}, renderContext);
|
||||
const rendered = await tryToInterpolateRequestOrShowRenderErrorModal({
|
||||
request,
|
||||
environmentId,
|
||||
payload: {
|
||||
url: request.url,
|
||||
headers: request.headers,
|
||||
authentication: request.authentication,
|
||||
parameters: request.parameters.filter(p => !p.disabled),
|
||||
workspaceCookieJar,
|
||||
},
|
||||
});
|
||||
window.main.webSocket.open({
|
||||
requestId: request._id,
|
||||
workspaceId,
|
||||
|
@ -148,6 +148,10 @@ const router = createMemoryRouter(
|
||||
loader: async (...args) => (await import('./routes/request')).loader(...args),
|
||||
element: (<Outlet />),
|
||||
children: [
|
||||
{
|
||||
path: 'send',
|
||||
action: async (...args) => (await import('./routes/request')).sendAction(...args),
|
||||
},
|
||||
{
|
||||
path: 'connect',
|
||||
action: async (...args) => (await import('./routes/request')).connectAction(...args),
|
||||
|
@ -316,13 +316,7 @@ export const runAllTestsAction: ActionFunction = async ({
|
||||
|
||||
const src = generate([{ name: 'My Suite', suites: [], tests }]);
|
||||
|
||||
const workspaceMeta = await models.workspaceMeta.getOrCreateByParentId(
|
||||
workspaceId
|
||||
);
|
||||
|
||||
const sendRequest = getSendRequestCallback(
|
||||
workspaceMeta?.activeEnvironmentId || undefined
|
||||
);
|
||||
const sendRequest = getSendRequestCallback();
|
||||
|
||||
const results = await runTests(src, { sendRequest });
|
||||
|
||||
@ -435,13 +429,8 @@ export const runTestAction: ActionFunction = async ({ params }) => {
|
||||
},
|
||||
];
|
||||
const src = generate([{ name: 'My Suite', suites: [], tests }]);
|
||||
const workspaceMeta = await models.workspaceMeta.getOrCreateByParentId(
|
||||
unitTest.parentId
|
||||
);
|
||||
|
||||
const sendRequest = getSendRequestCallback(
|
||||
workspaceMeta?.activeEnvironmentId || undefined
|
||||
);
|
||||
const sendRequest = getSendRequestCallback();
|
||||
|
||||
const results = await runTests(src, { sendRequest });
|
||||
|
||||
|
@ -1,7 +1,15 @@
|
||||
import { createWriteStream } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import * as contentDisposition from 'content-disposition';
|
||||
import { extension as mimeExtension } from 'mime-types';
|
||||
import { ActionFunction, LoaderFunction, redirect } from 'react-router-dom';
|
||||
|
||||
import { CONTENT_TYPE_EVENT_STREAM, CONTENT_TYPE_GRAPHQL, CONTENT_TYPE_JSON, METHOD_GET, METHOD_POST } from '../../common/constants';
|
||||
import { ChangeBufferEvent, database } from '../../common/database';
|
||||
import { getContentDispositionHeader } from '../../common/misc';
|
||||
import { RenderedRequest } from '../../common/render';
|
||||
import { ResponsePatch } from '../../main/network/libcurl-promise';
|
||||
import * as models from '../../models';
|
||||
import { BaseModel } from '../../models';
|
||||
import { CookieJar } from '../../models/cookie-jar';
|
||||
@ -14,6 +22,7 @@ import { RequestVersion } from '../../models/request-version';
|
||||
import { Response } from '../../models/response';
|
||||
import { isWebSocketRequestId, WebSocketRequest } from '../../models/websocket-request';
|
||||
import { WebSocketResponse } from '../../models/websocket-response';
|
||||
import { fetchRequestData, responseTransform, sendCurlAndWriteTimeline } from '../../network/network';
|
||||
import { invariant } from '../../utils/invariant';
|
||||
import { SegmentEvent } from '../analytics';
|
||||
import { updateMimeType } from '../components/dropdowns/content-type-dropdown';
|
||||
@ -263,7 +272,91 @@ export const connectAction: ActionFunction = async ({ request, params }) => {
|
||||
});
|
||||
});
|
||||
};
|
||||
const writeToDownloadPath = (downloadPathAndName: string, responsePatch: ResponsePatch, requestMeta: RequestMeta, maxHistoryResponses: number) => {
|
||||
invariant(downloadPathAndName, 'filename should be set by now');
|
||||
|
||||
const to = createWriteStream(downloadPathAndName);
|
||||
const readStream = models.response.getBodyStream(responsePatch);
|
||||
if (!readStream || typeof readStream === 'string') {
|
||||
// setLoading(false);
|
||||
return null;
|
||||
}
|
||||
readStream.pipe(to);
|
||||
|
||||
return new Promise(resolve => {
|
||||
readStream.on('end', async () => {
|
||||
responsePatch.error = `Saved to ${downloadPathAndName}`;
|
||||
const response = await models.response.create(responsePatch, maxHistoryResponses);
|
||||
await models.requestMeta.update(requestMeta, { activeResponseId: response._id });
|
||||
// setLoading(false);
|
||||
resolve(null);
|
||||
});
|
||||
readStream.on('error', async err => {
|
||||
console.warn('Failed to download request after sending', responsePatch.bodyPath, err);
|
||||
const response = await models.response.create(responsePatch, maxHistoryResponses);
|
||||
await models.requestMeta.update(requestMeta, { activeResponseId: response._id });
|
||||
// setLoading(false);
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
|
||||
};
|
||||
export interface SendActionParams {
|
||||
renderedRequest: RenderedRequest;
|
||||
shouldPromptForPathAfterResponse?: boolean;
|
||||
context: Record<string, any>;
|
||||
}
|
||||
export const sendAction: ActionFunction = async ({ request, params }) => {
|
||||
const { requestId, workspaceId } = params;
|
||||
invariant(typeof requestId === 'string', 'Request ID is required');
|
||||
const req = await requestOperations.getById(requestId);
|
||||
invariant(req, 'Request not found');
|
||||
invariant(workspaceId, 'Workspace ID is required');
|
||||
const {
|
||||
settings,
|
||||
clientCertificates,
|
||||
caCert,
|
||||
activeEnvironmentId } = await fetchRequestData(requestId);
|
||||
const { renderedRequest, shouldPromptForPathAfterResponse, context } = await request.json() as SendActionParams;
|
||||
const response = await sendCurlAndWriteTimeline(
|
||||
renderedRequest,
|
||||
clientCertificates,
|
||||
caCert,
|
||||
settings,
|
||||
);
|
||||
const requestMeta = await models.requestMeta.getByParentId(requestId);
|
||||
invariant(requestMeta, 'RequestMeta not found');
|
||||
const responsePatch = await responseTransform(response, activeEnvironmentId, renderedRequest, context);
|
||||
const is2XXWithBodyPath = responsePatch.statusCode && responsePatch.statusCode >= 200 && responsePatch.statusCode < 300 && responsePatch.bodyPath;
|
||||
const shouldWriteToFile = shouldPromptForPathAfterResponse && is2XXWithBodyPath;
|
||||
if (!shouldWriteToFile) {
|
||||
const response = await models.response.create(responsePatch, settings.maxHistoryResponses);
|
||||
await models.requestMeta.update(requestMeta, { activeResponseId: response._id });
|
||||
// setLoading(false);
|
||||
return null;
|
||||
}
|
||||
if (requestMeta.downloadPath) {
|
||||
const header = getContentDispositionHeader(responsePatch.headers || []);
|
||||
const name = header
|
||||
? contentDisposition.parse(header.value).parameters.filename
|
||||
: `${req.name.replace(/\s/g, '-').toLowerCase()}.${responsePatch.contentType && mimeExtension(responsePatch.contentType) || 'unknown'}`;
|
||||
return writeToDownloadPath(path.join(requestMeta.downloadPath, name), responsePatch, requestMeta, settings.maxHistoryResponses);
|
||||
} else {
|
||||
const defaultPath = window.localStorage.getItem('insomnia.sendAndDownloadLocation');
|
||||
const { filePath } = await window.dialog.showSaveDialog({
|
||||
title: 'Select Download Location',
|
||||
buttonLabel: 'Save',
|
||||
// NOTE: An error will be thrown if defaultPath is supplied but not a String
|
||||
...(defaultPath ? { defaultPath } : {}),
|
||||
});
|
||||
if (!filePath) {
|
||||
// setLoading(false);
|
||||
return null;
|
||||
}
|
||||
window.localStorage.setItem('insomnia.sendAndDownloadLocation', filePath);
|
||||
return writeToDownloadPath(filePath, responsePatch, requestMeta, settings.maxHistoryResponses);
|
||||
};
|
||||
};
|
||||
export const deleteAllResponsesAction: ActionFunction = async ({ params }) => {
|
||||
const { workspaceId, requestId } = params;
|
||||
invariant(typeof requestId === 'string', 'Request ID is required');
|
||||
|
20
packages/insomnia/src/utils/try-interpolate.ts
Normal file
20
packages/insomnia/src/utils/try-interpolate.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { getRenderContext, render, RENDER_PURPOSE_SEND } from '../common/render';
|
||||
import { GrpcRequest } from '../models/grpc-request';
|
||||
import { Request } from '../models/request';
|
||||
import { WebSocketRequest } from '../models/websocket-request';
|
||||
import { showModal } from '../ui/components/modals';
|
||||
import { RequestRenderErrorModal } from '../ui/components/modals/request-render-error-modal';
|
||||
|
||||
// NOTE: template interpolation is tightly coupled with modal implementation
|
||||
export const tryToInterpolateRequestOrShowRenderErrorModal = async ({ request, environmentId, payload }: { request: Request | WebSocketRequest | GrpcRequest; environmentId: string; payload: any }): Promise<any> => {
|
||||
try {
|
||||
const renderContext = await getRenderContext({ request, environmentId, purpose: RENDER_PURPOSE_SEND });
|
||||
return await render(payload, renderContext);
|
||||
} catch (error) {
|
||||
if (error.type === 'render') {
|
||||
showModal(RequestRenderErrorModal, { request, error });
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
Loading…
Reference in New Issue
Block a user