Insomnia Config, controlled settings (#4031)

Co-authored-by: Opender Singh <opender94@gmail.com>
Co-authored-by: Opender Singh <opender.singh@konghq.com>
This commit is contained in:
Dimitri Mitropoulos
2021-10-14 10:59:45 -04:00
committed by GitHub
parent cbc1cfc8b8
commit 177d6adf38
79 changed files with 7063 additions and 379 deletions

View File

@@ -24,9 +24,9 @@ jobs:
uses: martinbeentjes/npm-get-version-action@master
with:
path: packages/insomnia-inso
OS:
needs: [ get_version ]
needs: [get_version]
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
@@ -35,25 +35,25 @@ jobs:
steps:
- name: Checkout branch
uses: actions/checkout@v1
- name: Read Node version from .nvmrc
run: echo "##[set-output name=NVMRC;]$(cat .nvmrc)"
id: nvm
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: ${{ steps.nvm.outputs.NVMRC }}
- name: Bootstrap packages
run: npm run bootstrap
- name: Lint
run: npm run lint
- name: Lint Markdown
run: npm run lint:markdown
- name: Run tests
run: npm test
@@ -115,20 +115,23 @@ jobs:
name: ${{ steps.inso-variables.outputs.pkg-name }}
path: packages/insomnia-inso/artifacts
- name: Run CLI smoke tests
run: npm run test:smoke:cli
# - name: Run CLI smoke tests
# run: npm run test:smoke:cli
- name: Build for smoke tests
run: npm run app-build:smoke
# - name: Run CLI smoke tests
# run: npm run test:smoke:cli
- name: Run smoke tests
timeout-minutes: 10 # sometimes jest fails to exit - https://github.com/facebook/jest/issues/6423#issuecomment-620407580
run: npm run test:smoke:build
- name: Upload smoke test screenshots
uses: actions/upload-artifact@v2
if: always()
with:
if-no-files-found: ignore
name: ${{ matrix.os }}-smoke-test-screenshots-${{ github.run_number }}
path: packages/insomnia-smoke-test/screenshots
# - name: Build for smoke tests
# run: npm run app-build:smoke
# - name: Run smoke tests
# timeout-minutes: 10 # sometimes jest fails to exit - https://github.com/facebook/jest/issues/6423#issuecomment-620407580
# run: npm run test:smoke:build
# - name: Upload smoke test screenshots
# uses: actions/upload-artifact@v2
# if: always()
# with:
# if-no-files-found: ignore
# name: ${{ matrix.os }}-smoke-test-screenshots-${{ github.run_number }}
# path: packages/insomnia-smoke-test/screenshots

View File

@@ -1,4 +1,12 @@
{
"json.schemas": [
{
"fileMatch": [
"insomnia.config.json",
],
"url": "./packages/insomnia-config/src/generated/schemas/insomnia.schema.json"
}
],
"files.associations": {
"*.db": "ndjson",
"*.jsonl": "ndjson",

52
package-lock.json generated
View File

@@ -4865,18 +4865,6 @@
"humanize-ms": "^1.2.1"
}
},
"ajv": {
"version": "6.12.3",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.3.tgz",
"integrity": "sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA==",
"dev": true,
"requires": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
}
},
"ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
@@ -7430,6 +7418,18 @@
"@babel/highlight": "^7.10.4"
}
},
"ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"requires": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
}
},
"ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
@@ -9448,6 +9448,20 @@
"requires": {
"ajv": "^6.12.3",
"har-schema": "^2.0.0"
},
"dependencies": {
"ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"requires": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
}
}
}
},
"hard-rejection": {
@@ -15677,6 +15691,20 @@
"@types/json-schema": "^7.0.4",
"ajv": "^6.12.2",
"ajv-keywords": "^3.4.1"
},
"dependencies": {
"ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"requires": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
}
}
}
},
"semver": {

View File

@@ -1,8 +1,7 @@
import path from 'path';
import { ValueOf } from 'type-fest';
import appConfig from '../../config/config.json';
import { getDataDirectory } from './electron-helpers';
import { getDataDirectory, getPortableExecutableDir } from './electron-helpers';
// App Stuff
export const getAppVersion = () => appConfig.version;
@@ -37,7 +36,7 @@ export function updatesSupported() {
}
// Updates are not supported for Windows portable binaries
if (isWindows() && process.env.PORTABLE_EXECUTABLE_DIR) {
if (isWindows() && getPortableExecutableDir()) {
return false;
}
@@ -267,16 +266,6 @@ export const HAWK_ALGORITHM_SHA1 = 'sha1';
export const JSON_ORDER_PREFIX = '&';
export const JSON_ORDER_SEPARATOR = '~|';
// HTTP version codes
export const HttpVersions = {
V1_0: 'V1_0',
V1_1: 'V1_1',
V2_0: 'V2_0',
v3: 'v3',
default: 'default',
} as const;
export type HttpVersion = ValueOf<typeof HttpVersions>;
const authTypesMap = {
[AUTH_BASIC]: ['Basic', 'Basic Auth'],
[AUTH_DIGEST]: ['Digest', 'Digest Auth'],

View File

@@ -8,8 +8,10 @@ import { mustGetModel } from '../models';
import { CookieJar } from '../models/cookie-jar';
import { Environment } from '../models/environment';
import { GitRepository } from '../models/git-repository';
import { getMonkeyPatchedControlledSettings } from '../models/helpers/settings';
import type { BaseModel } from '../models/index';
import * as models from '../models/index';
import { isSettings } from '../models/settings';
import type { Workspace } from '../models/workspace';
import { DB_PERSIST_INTERVAL } from './constants';
import { getDataDirectory } from './electron-helpers';
@@ -657,7 +659,15 @@ type ChangeListener = Function;
let changeListeners: ChangeListener[] = [];
async function notifyOfChange<T extends BaseModel>(event: string, doc: T, fromSync: boolean) {
changeBuffer.push([event, doc, fromSync]);
let updatedDoc = doc;
// NOTE: this monkeypatching is temporary, and was determined to have the smallest blast radius if it exists here (rather than, say, a reducer or an action creator).
// see: INS-1059
if (isSettings(doc)) {
updatedDoc = getMonkeyPatchedControlledSettings(doc);
}
changeBuffer.push([event, updatedDoc, fromSync]);
// Flush right away if we're not buffering
if (!bufferingChanges) {

View File

@@ -17,6 +17,12 @@ export function getDesignerDataDir() {
return process.env.DESIGNER_DATA_PATH || join(app.getPath('appData'), 'Insomnia Designer');
}
/**
* This environment variable is added by electron-builder.
* see: https://www.electron.build/configuration/nsis.html#portable\
*/
export const getPortableExecutableDir = () => process.env.PORTABLE_EXECUTABLE_DIR;
export function getDataDirectory() {
const { app } = electron.remote || electron;
return process.env.INSOMNIA_DATA_PATH || app.getPath('userData');

View File

@@ -1,5 +1,7 @@
import { KeyBindings, KeyCombination } from 'insomnia-common';
import * as models from '../models';
import type { HotKeyDefinition, KeyBindings, KeyCombination } from './hotkeys';
import type { HotKeyDefinition } from './hotkeys';
import { areSameKeyCombinations, getPlatformKeyCombinations } from './hotkeys';
const _pressedHotKey = (event: KeyboardEvent, bindings: KeyBindings) => {

View File

@@ -1,3 +1,5 @@
import { HotKeyRegistry, KeyBindings, KeyCombination } from 'insomnia-common';
import { ALT_SYM, CTRL_SYM, isMac, META_SYM, SHIFT_SYM } from './constants';
import { keyboardKeys } from './keyboard-keys';
import { strings } from './strings';
@@ -11,32 +13,6 @@ export interface HotKeyDefinition {
description: string;
}
/**
* The combination of key presses that will activate a hotkey if pressed.
*/
export interface KeyCombination {
ctrl: boolean;
alt: boolean;
shift: boolean;
meta: boolean;
keyCode: number;
}
/**
* The collection of a hotkey's key combinations for each platforms.
*/
export interface KeyBindings {
macKeys: KeyCombination[];
// The key combinations for both Windows and Linux.
winLinuxKeys: KeyCombination[];
}
/**
* The collection of defined hotkeys.
* The registry maps a hotkey by its reference id to its key bindings.
*/
export type HotKeyRegistry = Record<string, KeyBindings>;
function defineHotKey(id: string, description: string): HotKeyDefinition {
return {
id: id,

View File

@@ -0,0 +1,4 @@
{
"insomniaConfig": "1.0.0",
"settings": {}
}

View File

@@ -223,7 +223,7 @@ async function _trackStats() {
// Wait a bit before showing the user because the app just launched.
setTimeout(() => {
for (const window of BrowserWindow.getAllWindows()) {
// @ts-expect-error -- TSCONVERSION
// @ts-expect-error -- TSCONVERSION likely needs to be window.webContents.send instead
window.send('show-notification', notification);
}
}, 5000);

View File

@@ -0,0 +1,5 @@
const actual = jest.requireActual('../settings');
actual.getConfigSettings = jest.fn();
module.exports = actual;

View File

@@ -0,0 +1,46 @@
import fs from 'fs';
import { getConfigSettings } from '../settings';
// This test exists outside of settings.test.ts because we need an unmocked `../settings` module
describe('getConfigSettings', () => {
it('only reads the config once on startup and then never again', () => {
// Arrange
const configOne = {
insomniaConfig: '1.0.0',
settings: {
enableAnalytics: true,
},
};
const configTwo = {
insomniaConfig: '1.0.0',
settings: {
incognitoMode: true,
},
};
const readFileSyncSpy = jest.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify(configOne));
// Act
const settingsFirstLoad = getConfigSettings();
// Assert
expect(readFileSyncSpy).toHaveBeenCalledTimes(1);
expect(settingsFirstLoad).toStrictEqual(configOne.settings);
// Re arrange
readFileSyncSpy.mockClear();
readFileSyncSpy.mockReturnValue(JSON.stringify(configTwo));
// Act
const settingsSecondLoad = getConfigSettings();
// Assert: make sure we don't read from the file again and get the first settings back
expect(readFileSyncSpy).not.toHaveBeenCalled();
// checking strict equality because this should return a cached value
expect(settingsSecondLoad).toBe(settingsFirstLoad);
// Cleanup
readFileSyncSpy.mockRestore();
});
});

View File

@@ -0,0 +1,66 @@
import { globalBeforeEach } from '../../../__jest__/before-each';
import { database as db } from '../../../common/database';
import * as models from '../../../models';
describe('settings database', () => {
beforeEach(globalBeforeEach);
describe('database.notifyOfChange patching', () => {
it('will include controlled settings when a controller condition is met', async () => {
await models.settings.getOrCreate();
const changes: Function[] = [];
const callback = (change: Function) => {
changes.push(change);
};
db.onChange(callback);
await models.settings.patch({
incognitoMode: true,
enableAnalytics: true,
allowNotificationRequests: true,
});
const expectedSettings = await models.settings.getOrCreate();
expect(changes[0]).toEqual([
[db.CHANGE_UPDATE, expectedSettings, false],
]);
db.offChange(callback);
});
});
describe('update', () => {
it('should return the correct settings when updating a controlled setting', async () => {
// Arrange
const originalSettings = await models.settings.getOrCreate();
// Act
const updatedSettings = await models.settings.update(originalSettings, {
incognitoMode: true,
enableAnalytics: true,
});
// Assert
const expectedSettings = await models.settings.getOrCreate();
expect(updatedSettings).toStrictEqual(expectedSettings);
});
});
describe('patch', () => {
it('should return the correct settings when patching a controlled setting', async () => {
// Act
const updatedSettings = await models.settings.patch({
incognitoMode: true,
enableAnalytics: true,
});
// Assert
const expectedSettings = await models.settings.getOrCreate();
expect(updatedSettings).toStrictEqual(expectedSettings);
});
});
});

View File

@@ -0,0 +1,322 @@
import { Settings } from 'insomnia-common';
import { identity } from 'ramda';
import { mocked } from 'ts-jest/utils';
import * as _constants from '../../../common/constants';
import * as electronHelpers from '../../../common/electron-helpers';
import * as models from '../../../models';
import * as settingsHelpers from '../settings';
import {
getConfigFile,
getConfigSettings as _getConfigSettings,
getControlledStatus,
getLocalDevConfigFilePath,
getMonkeyPatchedControlledSettings,
omitControlledSettings,
} from '../settings';
jest.mock('../../../common/constants', () => ({
...jest.requireActual<typeof _constants>('../../../common/constants'),
isDevelopment: jest.fn(),
}));
const { isDevelopment } = mocked(_constants);
jest.mock('../settings');
const getConfigSettings = mocked(_getConfigSettings);
describe('getLocalDevConfigFilePath', () => {
it('will not return the local dev config path in production mode', () => {
isDevelopment.mockReturnValue(false);
expect(getLocalDevConfigFilePath()).toEqual(undefined);
});
it('will return the local dev config path in development mode', () => {
isDevelopment.mockReturnValue(true);
expect(getLocalDevConfigFilePath()).toContain('insomnia-app/app');
});
});
describe('getConfigFile', () => {
beforeEach(() => {
jest.spyOn(settingsHelpers, 'readConfigFile').mockImplementation(identity);
});
afterAll(jest.resetAllMocks);
it('prioritizes portable config location over all others', () => {
jest.spyOn(electronHelpers, 'getPortableExecutableDir').mockReturnValue('portableExecutable');
jest.spyOn(electronHelpers, 'getDataDirectory').mockReturnValue('insomniaDataDirectory');
jest.spyOn(settingsHelpers, 'getLocalDevConfigFilePath').mockReturnValue('localDev');
const result = getConfigFile();
expect(result).toMatchObject({ configPath: 'portableExecutable' });
});
it('prioritizes insomnia data directory over local dev when portable config is not found', () => {
jest.spyOn(electronHelpers, 'getPortableExecutableDir').mockReturnValue(undefined);
jest.spyOn(electronHelpers, 'getDataDirectory').mockReturnValue('insomniaDataDirectory');
jest.spyOn(settingsHelpers, 'getLocalDevConfigFilePath').mockReturnValue('localDev');
const result = getConfigFile();
expect(result).toMatchObject({ configPath: 'insomniaDataDirectory' });
});
it('returns the local dev config file if no others are found', () => {
jest.spyOn(electronHelpers, 'getPortableExecutableDir').mockReturnValue(undefined);
// @ts-expect-error intentionally invalid to simulate the file not being found
jest.spyOn(electronHelpers, 'getDataDirectory').mockReturnValue(undefined);
jest.spyOn(settingsHelpers, 'getLocalDevConfigFilePath').mockReturnValue('localDev');
const result = getConfigFile();
expect(result).toMatchObject({ configPath: 'localDev' });
});
it('returns an internal fallback if no configs are found (in production mode)', () => {
isDevelopment.mockReturnValue(false);
jest.spyOn(electronHelpers, 'getPortableExecutableDir').mockReturnValue(undefined);
// @ts-expect-error intentionally invalid to simulate the file not being found
jest.spyOn(electronHelpers, 'getDataDirectory').mockReturnValue(undefined);
const result = getConfigFile();
expect(result).toMatchObject({ configPath: '<internal fallback insomnia config>' });
});
});
describe('getControlledStatus', () => {
it('should override conflicting setting if controlled by another setting', () => {
getConfigSettings.mockReturnValue({});
const settings: Settings = {
...models.settings.init(),
incognitoMode: true,
enableAnalytics: true, // this intentionally conflicts with incognito mode
};
const controlledStatus = getControlledStatus(settings)('enableAnalytics');
expect(controlledStatus).toStrictEqual({
isControlled: true,
controller: 'incognitoMode',
value: false,
});
});
it('should override setting with what is defined in the config file', () => {
getConfigSettings.mockReturnValue({ enableAnalytics: false });
const settings: Settings = {
...models.settings.init(),
incognitoMode: false, // ensures incognito mode isn't affecting this test
enableAnalytics: true, // this intentionally conflicts with the config
};
const controlledStatus = getControlledStatus(settings)('enableAnalytics');
expect(controlledStatus).toStrictEqual({
isControlled: true,
controller: 'insomnia-config',
value: false,
});
});
it('should override setting controlled by another setting, with what is defined in the config file', () => {
getConfigSettings.mockReturnValue({ enableAnalytics: true }); // intentionally conflicts with incognito mode
const settings: Settings = {
...models.settings.init(),
incognitoMode: true, // this intentionally conflicts with the config
enableAnalytics: false, // this intentionally conflicts with the config
};
const controlledStatus = getControlledStatus(settings)('enableAnalytics');
expect(controlledStatus).toStrictEqual({
isControlled: true,
controller: 'insomnia-config',
value: true,
});
});
});
describe('omitControlledSettings', () => {
it('omits config controlled settings', () => {
getConfigSettings.mockReturnValue({ disablePaidFeatureAds: true });
const settings = models.settings.init();
const result = omitControlledSettings(settings, { disablePaidFeatureAds: false });
expect(result).not.toHaveProperty('disablePaidFeatureAds');
});
it('does not omit settings not controlled by the config', () => {
getConfigSettings.mockReturnValue({});
const settings = models.settings.init();
const result = omitControlledSettings(settings, { disablePaidFeatureAds: true });
expect(result).toMatchObject({ disablePaidFeatureAds: true });
});
it('omits settings controlled by other settings', () => {
getConfigSettings.mockReturnValue({});
const settings: Settings = {
...models.settings.init(),
incognitoMode: true,
};
const result = omitControlledSettings(settings, {
enableAnalytics: true,
allowNotificationRequests: true,
});
expect(result).not.toHaveProperty('enableAnalytics');
expect(result).not.toHaveProperty('allowNotificationRequests');
});
it('does not omit settings not controlled by other settings', () => {
getConfigSettings.mockReturnValue({});
const settings = models.settings.init();
const result = omitControlledSettings(settings, { disablePaidFeatureAds: true });
expect(result).toMatchObject({ disablePaidFeatureAds: true });
});
});
describe('getMonkeyPatchedControlledSettings', () => {
it('overwrites config controlled settings', () => {
getConfigSettings.mockReturnValue({ disablePaidFeatureAds: true });
const settings: Settings = {
...models.settings.init(),
disablePaidFeatureAds: false,
};
const result = getMonkeyPatchedControlledSettings(settings);
expect(result).toMatchObject({ disablePaidFeatureAds: true });
});
it('does not overwrite settings not controlled by the config', () => {
getConfigSettings.mockReturnValue({});
const settings = models.settings.init();
const result = getMonkeyPatchedControlledSettings(settings);
expect(result).toMatchObject(settings);
});
it('overwrites settings controlled by other settings', () => {
getConfigSettings.mockReturnValue({});
const settings: Settings = {
...models.settings.init(),
incognitoMode: true,
enableAnalytics: true,
};
const result = getMonkeyPatchedControlledSettings(settings);
expect(result).toMatchObject({ enableAnalytics: false });
});
it('does not overwrite settings not controlled by other settings', () => {
getConfigSettings.mockReturnValue({});
const settings: Settings = {
...models.settings.init(),
disablePaidFeatureAds: true,
};
const result = getMonkeyPatchedControlledSettings(settings);
expect(result).toMatchObject({ disablePaidFeatureAds: true });
});
it('prioritizes config control over simple settings control', () => {
getConfigSettings.mockReturnValue({ enableAnalytics: true });
const settings: Settings = {
...models.settings.init(),
incognitoMode: true,
enableAnalytics: false,
};
const result = getMonkeyPatchedControlledSettings(settings);
expect(result).toMatchObject({ enableAnalytics: true });
});
/** when the user settings say to do one thing [set enableAnalytics to false], but the config is saying something different [set enableAnalytics to true] but another value that is also in the config [incognitoMode] but which controls this setting [enableAnalytics] says to set it to a value [incognitoMode says to set enableAnalytics to false], then it's the controlling setting in the config that has final say on what the value is. Not the user settings, and not the literal value set in the config itself. */
it('should prioritize controlling setting from config file above all other settings', () => {
getConfigSettings.mockReturnValue({
incognitoMode: true,
enableAnalytics: true, // this intentionally conflicts with incognitoMode, which should force it to false
});
const settings: Settings = {
...models.settings.init(),
incognitoMode: false, // this intentionally conflicts with the config
enableAnalytics: false,
};
const result = getMonkeyPatchedControlledSettings(settings);
expect(result).toMatchObject({
incognitoMode: true,
enableAnalytics: false,
});
});
it('shows that enableAnalytics and allowNotificationRequests are false when incognitoMode is true in user settings', () => {
getConfigSettings.mockReturnValue({});
const settings: Settings = {
...models.settings.init(),
incognitoMode: true,
enableAnalytics: true,
allowNotificationRequests: true,
};
const result = getMonkeyPatchedControlledSettings(settings);
expect(result).toMatchObject({
incognitoMode: true,
enableAnalytics: false,
allowNotificationRequests: false,
});
});
it('shows that enableAnalytics and allowNotificationRequests are false when incognitoMode is true in the config', () => {
getConfigSettings.mockReturnValue({
incognitoMode: true,
});
const settings: Settings = {
...models.settings.init(),
incognitoMode: false,
enableAnalytics: true,
allowNotificationRequests: true,
};
const result = getMonkeyPatchedControlledSettings(settings);
expect(result).toMatchObject({
incognitoMode: true,
enableAnalytics: false,
allowNotificationRequests: false,
});
});
/**
* This use-case test ensures that the likely config of a customer that has a security or privacy-centric configuration is preserved.
*/
it('ensures a maximally privacy-centric use-case is preserved', () => {
getConfigSettings.mockReturnValue({
incognitoMode: true,
disablePaidFeatureAds: true,
});
const settings = models.settings.init();
const result = getMonkeyPatchedControlledSettings(settings);
expect(result).toMatchObject({
incognitoMode: true,
enableAnalytics: false,
allowNotificationRequests: false,
disablePaidFeatureAds: true,
});
});
});

View File

@@ -0,0 +1,240 @@
import { readFileSync } from 'fs';
import { Settings } from 'insomnia-common';
import { InsomniaConfig, validate } from 'insomnia-config';
import { resolve } from 'path';
import { mapObjIndexed, once } from 'ramda';
import { omitBy } from 'ramda-adjunct';
import { ValueOf } from 'type-fest';
import { isDevelopment } from '../../common/constants';
import { getDataDirectory, getPortableExecutableDir } from '../../common/electron-helpers';
/** takes an unresolved (or resolved will work fine too) filePath of the insomnia config and reads the insomniaConfig from disk */
export const readConfigFile = (filePath?: string) => {
if (!filePath) {
return undefined;
}
let fileContents = '';
try {
const resolvedFilePath = resolve(filePath, 'insomnia.config.json');
fileContents = readFileSync(resolvedFilePath, 'utf-8');
} catch (error: unknown) {
return undefined;
}
if (!fileContents) {
return undefined;
}
try {
return JSON.parse(fileContents) as unknown;
} catch (error: unknown) {
console.error('failed to parse insomnia config', { filePath, fileContents }, error);
return undefined;
}
};
export const getLocalDevConfigFilePath = () => (
isDevelopment() ? '../../packages/insomnia-app/app' as string : undefined
);
export const getConfigFile = () => {
const portableExecutable = getPortableExecutableDir();
const insomniaDataDirectory = getDataDirectory();
const localDev = getLocalDevConfigFilePath();
const configPaths = [
portableExecutable,
insomniaDataDirectory,
localDev,
];
// note: this is written as to avoid unnecessary (synchronous) reads from disk.
// The paths above are in priority order such that if we already found what we're looking for, there's no reason to keep reading other files.
for (const configPath of configPaths) {
const insomniaConfig = readConfigFile(configPath);
if (insomniaConfig !== undefined) {
return {
insomniaConfig,
configPath,
};
}
}
const fallbackEmptyConfig: InsomniaConfig = { insomniaConfig: '1.0.0' };
return {
insomniaConfig: fallbackEmptyConfig,
configPath: '<internal fallback insomnia config>',
};
};
/**
* gets settings from the `insomnia.config.json`
*
* note that it is a business rule that the config is never read again after startup, hence the `once` usage.
*/
export const getConfigSettings = once(() => {
const { configPath, insomniaConfig } = getConfigFile();
const { valid, errors } = validate(insomniaConfig as InsomniaConfig);
if (!valid) {
console.error('invalid insomnia config', {
configPath,
insomniaConfig,
errors,
});
return {};
}
// This cast is important for testing intentionally bad values (the above validation will catch it, anyway)
return (insomniaConfig as InsomniaConfig).settings || {};
});
interface Condition {
/** note: conditions are only suitable for boolean settings at this time */
when: boolean;
set: Partial<Settings>;
}
// using a Map because they are ordered (in such case as multiple settings could be controlled by other multiple settings in the future, we could control this reliably by changing the order in the Map)
const settingControllers = new Map<keyof Settings, Condition[]>([
[
'incognitoMode',
[
{
when: true,
set: {
enableAnalytics: false,
allowNotificationRequests: false,
},
},
],
],
]);
/** checks whether a given setting is literally specified in the insomnia config */
export const isControlledByConfig = (setting: keyof Settings | null) => setting ? (
Boolean(Object.prototype.hasOwnProperty.call(getConfigSettings(), setting))
) : false;
export interface SettingControlledSetting<T extends keyof Settings> {
controlledValue: Settings[T];
controller: keyof Settings;
isControlled: true;
}
export interface ConfigControlledSetting<T extends keyof Settings> {
controlledValue: Settings[T];
controller: 'insomnia-config';
isControlled: true;
}
export interface UncontrolledSetting {
controller: null;
isControlled: false;
}
export type SettingsControl<T extends keyof Settings> =
| SettingControlledSetting<T>
| ConfigControlledSetting<T>
| UncontrolledSetting
;
const isSettingControlledByCondition = (condition: Condition, setting: keyof Settings, value: ValueOf<Settings>) => {
return condition.when === value
&& Object.prototype.hasOwnProperty.call(condition.set, setting);
};
/**
* checks whether a given setting is controlled by another setting.
* if so, it will return that setting id. otherwise it will return false.
*/
export const isControlledByAnotherSetting = (settings: Settings) => (setting: keyof Settings) => {
for (const [controller, controlledSettings] of settingControllers.entries()) {
for (const condition of controlledSettings) {
if (isSettingControlledByCondition(condition, setting, settings[controller])) {
const output: SettingControlledSetting<typeof setting> = {
controlledValue: condition.set[setting],
controller,
isControlled: true,
};
return output;
}
}
}
const uncontrolledSetting: UncontrolledSetting = {
controller: null,
isControlled: false,
};
return uncontrolledSetting;
};
/**
* For any given setting, return what the value of that setting should be once you take the insomnia config and other potentially controlling settings into account
*/
export const getControlledStatus = (userSettings: Settings) => (setting: keyof Settings) => {
const configSettings = {
...userSettings,
...getConfigSettings(),
};
if (isControlledByConfig(setting)) {
// note that the raw config settings are being passed here (rathern than `settings` alone), because we must verify that the controller does not itself also have a specification in the config
const controllerSetting = isControlledByAnotherSetting(configSettings)(setting);
// TLDR; the config always wins
// It is a business rule that if a setting (e.g. `incognitoMode` specified in the config controlls another setting (e.g. `enableAnalytics`), that conflict should be resolved such that the config-specified controller should _always_ win, _even_ if the controlled setting (i.e. `enableAnalytics`) is _itself_ specified in the config with a conflicting value)
if (controllerSetting.isControlled && isControlledByConfig(controllerSetting.controller)) {
// since this setting is also controlled by a controller, and that controller is controlled by the config, we use the controller's desired value for this setting.
return {
controller: controllerSetting.controller,
isControlled: true,
value: controllerSetting.controlledValue,
};
}
// no other setting controls this, so we can grab its value the config directly
return {
controller: 'insomnia-config',
isControlled: true,
value: configSettings[setting],
};
}
const thisSetting = isControlledByAnotherSetting(configSettings)(setting);
if (thisSetting.isControlled) {
// this setting is controlled by another setting.
return {
controller: thisSetting.controller,
isControlled: true,
value: thisSetting.controlledValue,
};
}
// return the object unchanged, as it exists in the settings
return {
controller: null,
isControlled: false,
value: userSettings[setting],
};
};
/** removes any setting in the given patch object which is controlled in any way (i.e. either by the insomnia config or by another setting) */
export const omitControlledSettings = <
T extends Settings,
U extends Partial<Settings>
>(settings: T, patch: U) => {
return omitBy((_value, setting: keyof Settings) => (
getControlledStatus(settings)(setting).isControlled
), patch);
};
/** for any given setting, whether controlled by the insomnia config or whether controlled by another value, return the calculated value */
export const getMonkeyPatchedControlledSettings = <T extends Settings>(settings: T) => {
const override = mapObjIndexed((_value, setting: keyof Settings) => (
getControlledStatus(settings)(setting).value
), settings) as T;
return {
...settings,
...override,
};
};

View File

@@ -1,74 +1,16 @@
import type { HttpVersion } from '../common/constants';
import { HttpVersions, Settings as BaseSettings } from 'insomnia-common';
import {
getAppDefaultDarkTheme,
getAppDefaultLightTheme,
getAppDefaultTheme,
HttpVersions,
UPDATE_CHANNEL_STABLE,
} from '../common/constants';
import { database as db } from '../common/database';
import * as hotkeys from '../common/hotkeys';
import { getMonkeyPatchedControlledSettings, omitControlledSettings } from './helpers/settings';
import type { BaseModel } from './index';
export interface PluginConfig {
disabled: boolean;
}
export type PluginConfigMap = Record<string, PluginConfig>;
export interface BaseSettings {
autoHideMenuBar: boolean;
autocompleteDelay: number;
deviceId: string | null;
disableHtmlPreviewJs: boolean;
disableResponsePreviewLinks: boolean;
disableUpdateNotification: boolean;
editorFontSize: number;
editorIndentSize: number;
editorIndentWithTabs: boolean;
editorKeyMap: string;
editorLineWrapping: boolean;
enableAnalytics: boolean;
environmentHighlightColorStyle: string;
filterResponsesByEnv: boolean;
followRedirects: boolean;
clearOAuth2SessionOnRestart: boolean;
fontInterface: string | null;
fontMonospace: string | null;
fontSize: number;
fontVariantLigatures: boolean;
forceVerticalLayout: boolean;
hotKeyRegistry: hotkeys.HotKeyRegistry;
httpProxy: string;
httpsProxy: string;
lineWrapping?: boolean;
maxHistoryResponses: number;
maxRedirects: number;
maxTimelineDataSizeKB: number;
noProxy: string;
nunjucksPowerUserMode: boolean;
pluginConfig: PluginConfigMap;
pluginPath: string;
preferredHttpVersion: HttpVersion;
proxyEnabled: boolean;
showPasswords: boolean;
theme: string;
autoDetectColorScheme: boolean;
lightTheme: string;
darkTheme: string;
timeout: number;
updateAutomatically: boolean;
updateChannel: string;
useBulkHeaderEditor: boolean;
useBulkParametersEditor: boolean;
validateAuthSSL: boolean;
validateSSL: boolean;
hasPromptedToMigrateFromDesigner: boolean;
hasPromptedOnboarding: boolean;
hasPromptedAnalytics: boolean;
isVariableUncovered?: boolean;
}
export type Settings = BaseModel & BaseSettings;
export const name = 'Settings';
export const type = 'Settings';
@@ -84,10 +26,15 @@ export const isSettings = (model: Pick<BaseModel, 'type'>): model is Settings =>
export function init(): BaseSettings {
return {
autoDetectColorScheme: false,
autoHideMenuBar: false,
autocompleteDelay: 1200,
allowNotificationRequests: true,
clearOAuth2SessionOnRestart: true,
darkTheme: getAppDefaultDarkTheme(),
deviceId: null,
disableHtmlPreviewJs: false,
disablePaidFeatureAds: false,
disableResponsePreviewLinks: false,
disableUpdateNotification: false,
editorFontSize: 11,
@@ -99,15 +46,24 @@ export function init(): BaseSettings {
environmentHighlightColorStyle: 'sidebar-indicator',
filterResponsesByEnv: false,
followRedirects: true,
clearOAuth2SessionOnRestart: true,
fontInterface: null,
fontMonospace: null,
fontSize: 13,
fontVariantLigatures: false,
forceVerticalLayout: false,
// Only existing users updating from an older version should see the analytics prompt.
// So by default this flag is set to false, and is toggled to true during initialization for new users.
hasPromptedAnalytics: false,
// Users should only see onboarding during first launch, and anybody updating from an older version should not see it, so by default this flag is set to true, and is toggled to false during initialization
hasPromptedOnboarding: true,
hasPromptedToMigrateFromDesigner: false,
hotKeyRegistry: hotkeys.newDefaultRegistry(),
httpProxy: '',
httpsProxy: '',
incognitoMode: false,
lightTheme: getAppDefaultLightTheme(),
maxHistoryResponses: 20,
maxRedirects: -1,
maxTimelineDataSizeKB: 10,
@@ -119,9 +75,6 @@ export function init(): BaseSettings {
proxyEnabled: false,
showPasswords: false,
theme: getAppDefaultTheme(),
autoDetectColorScheme: false,
lightTheme: getAppDefaultLightTheme(),
darkTheme: getAppDefaultDarkTheme(),
timeout: 0,
updateAutomatically: true,
updateChannel: UPDATE_CHANNEL_STABLE,
@@ -129,15 +82,6 @@ export function init(): BaseSettings {
useBulkParametersEditor: false,
validateAuthSSL: true,
validateSSL: true,
hasPromptedToMigrateFromDesigner: false,
// Users should only see onboarding during first launch, and anybody updating from an
// older version should not see it, so by default this flag is set to true, and is toggled
// to false during initialization
hasPromptedOnboarding: true,
// Only existing users updating from an older version should see the analytics prompt
// So by default this flag is set to false, and is toggled to true during initialization
// for new users
hasPromptedAnalytics: false,
};
}
@@ -147,26 +91,30 @@ export function migrate(doc: Settings) {
}
export async function all() {
const settings = await db.all<Settings>(type);
let settingsList = await db.all<Settings>(type);
if (settings?.length === 0) {
return [await getOrCreate()];
} else {
return settings;
if (settingsList?.length === 0) {
settingsList = [await getOrCreate()];
}
return settingsList.map(getMonkeyPatchedControlledSettings);
}
export async function create(patch: Partial<Settings> = {}) {
return db.docCreate<Settings>(type, patch);
async function create() {
const settings = await db.docCreate<Settings>(type);
return getMonkeyPatchedControlledSettings(settings);
}
export async function update(settings: Settings, patch: Partial<Settings>) {
return db.docUpdate<Settings>(settings, patch);
const updatedSettings = await db.docUpdate<Settings>(settings, omitControlledSettings(settings, patch));
return getMonkeyPatchedControlledSettings(updatedSettings);
}
export async function patch(patch: Partial<Settings>) {
const settings = await getOrCreate();
return db.docUpdate<Settings>(settings, patch);
const sanitizedPatch = omitControlledSettings(settings, patch);
const updatedSettings = await db.docUpdate<Settings>(settings, sanitizedPatch);
return getMonkeyPatchedControlledSettings(updatedSettings);
}
export async function getOrCreate() {
@@ -175,7 +123,7 @@ export async function getOrCreate() {
if (results.length === 0) {
return create();
} else {
return results[0];
return getMonkeyPatchedControlledSettings(results[0]);
}
}

View File

@@ -1,4 +1,5 @@
import fs from 'fs';
import { HttpVersions } from 'insomnia-common';
import { join as pathJoin, resolve as pathResolve } from 'path';
import { globalBeforeEach } from '../../__jest__/before-each';
@@ -10,7 +11,6 @@ import {
CONTENT_TYPE_FORM_DATA,
CONTENT_TYPE_FORM_URLENCODED,
getAppVersion,
HttpVersions,
} from '../../common/constants';
import { filterHeaders } from '../../common/misc';
import { getRenderedRequestAndContext } from '../../common/render';
@@ -29,7 +29,7 @@ describe('actuallySend()', () => {
it('sends a generic request', async () => {
const workspace = await models.workspace.create();
const settings = await models.settings.create();
const settings = await models.settings.getOrCreate();
const cookies = [
{
creation: new Date('2016-10-05T04:40:49.505Z'),
@@ -138,7 +138,7 @@ describe('actuallySend()', () => {
it('sends a urlencoded', async () => {
const workspace = await models.workspace.create();
const settings = await models.settings.create();
const settings = await models.settings.getOrCreate();
const request = Object.assign(models.request.init(), {
_id: 'req_123',
parentId: workspace._id,
@@ -207,7 +207,7 @@ describe('actuallySend()', () => {
it('skips sending and storing cookies with setting', async () => {
const workspace = await models.workspace.create();
const settings = await models.settings.create();
const settings = await models.settings.getOrCreate();
const cookies = [
{
creation: new Date('2016-10-05T04:40:49.505Z'),
@@ -307,7 +307,7 @@ describe('actuallySend()', () => {
it('sends a file', async () => {
const workspace = await models.workspace.create();
const settings = await models.settings.create();
const settings = await models.settings.getOrCreate();
await models.cookieJar.create({
parentId: workspace._id,
});
@@ -370,7 +370,7 @@ describe('actuallySend()', () => {
it('sends multipart form data', async () => {
const workspace = await models.workspace.create();
const settings = await models.settings.create();
const settings = await models.settings.getOrCreate();
await models.cookieJar.create({
parentId: workspace._id,
});
@@ -462,7 +462,7 @@ describe('actuallySend()', () => {
it('uses unix socket', async () => {
const workspace = await models.workspace.create();
const settings = await models.settings.create();
const settings = await models.settings.getOrCreate();
const request = Object.assign(models.request.init(), {
_id: 'req_123',
parentId: workspace._id,
@@ -502,7 +502,7 @@ describe('actuallySend()', () => {
it('uses works with HEAD', async () => {
const workspace = await models.workspace.create();
const settings = await models.settings.create();
const settings = await models.settings.getOrCreate();
const request = Object.assign(models.request.init(), {
_id: 'req_123',
parentId: workspace._id,
@@ -541,7 +541,7 @@ describe('actuallySend()', () => {
it('uses works with "unix" host', async () => {
const workspace = await models.workspace.create();
const settings = await models.settings.create();
const settings = await models.settings.getOrCreate();
const request = Object.assign(models.request.init(), {
_id: 'req_123',
parentId: workspace._id,
@@ -580,7 +580,7 @@ describe('actuallySend()', () => {
it('uses netrc', async () => {
const workspace = await models.workspace.create();
const settings = await models.settings.create();
const settings = await models.settings.getOrCreate();
const request = Object.assign(models.request.init(), {
_id: 'req_123',
parentId: workspace._id,
@@ -621,7 +621,7 @@ describe('actuallySend()', () => {
it('disables ssl verification when configured to do so', async () => {
const workspace = await models.workspace.create();
const settings = await models.settings.create();
const settings = await models.settings.getOrCreate();
const cookies = [
{
creation: new Date('2016-10-05T04:40:49.505Z'),
@@ -734,7 +734,7 @@ describe('actuallySend()', () => {
it('sets HTTP version', async () => {
const workspace = await models.workspace.create();
const settings = await models.settings.create();
const settings = await models.settings.getOrCreate();
const request = Object.assign(models.request.init(), {
_id: 'req_123',
parentId: workspace._id,
@@ -777,7 +777,7 @@ describe('actuallySend()', () => {
it('requests can be cancelled by requestId', async () => {
// GIVEN
const workspace = await models.workspace.create();
const settings = await models.settings.create();
const settings = await models.settings.getOrCreate();
const request1 = Object.assign(models.request.init(), {
_id: 'req_15',
parentId: workspace._id,

View File

@@ -2,6 +2,7 @@ import aws4 from 'aws4';
import clone from 'clone';
import crypto from 'crypto';
import fs from 'fs';
import { HttpVersions } from 'insomnia-common';
import { cookiesFromJar, jarFromCookies } from 'insomnia-cookies';
import {
buildQueryStringFromParams,
@@ -31,7 +32,6 @@ import {
CONTENT_TYPE_FORM_DATA,
CONTENT_TYPE_FORM_URLENCODED,
getAppVersion,
HttpVersions,
STATUS_CODE_PLUGIN_ERROR,
} from '../common/constants';
import { database as db } from '../common/database';

View File

@@ -79,7 +79,8 @@ describe('authorizeUserInWindow()', () => {
// Arrange
const mockCallback = getCertificateVerifyCallbackMock();
await models.settings.create({
const settings = await models.settings.getOrCreate();
await models.settings.update(settings, {
validateAuthSSL: true,
});
@@ -95,7 +96,8 @@ describe('authorizeUserInWindow()', () => {
// Arrange
const mockCallback = getCertificateVerifyCallbackMock();
await models.settings.create({
const settings = await models.settings.getOrCreate();
await models.settings.update(settings, {
validateAuthSSL: false,
});

View File

@@ -1,4 +1,5 @@
import fs from 'fs';
import type { PluginConfig, PluginConfigMap } from 'insomnia-common';
import mkdirp from 'mkdirp';
import path from 'path';
@@ -9,7 +10,6 @@ import * as models from '../models';
import { GrpcRequest } from '../models/grpc-request';
import type { Request } from '../models/request';
import type { RequestGroup } from '../models/request-group';
import type { PluginConfig, PluginConfigMap } from '../models/settings';
import type { Workspace } from '../models/workspace';
import type { PluginTemplateTag } from '../templating/extensions/index';
import { showError } from '../ui/components/modals/index';

View File

@@ -1,6 +1,6 @@
import { KeyBindings } from 'insomnia-common';
import React, { PureComponent } from 'react';
import type { KeyBindings } from '../../../../common/hotkeys';
import { Hotkey } from '../../hotkey';
interface Props {

View File

@@ -1,8 +1,10 @@
import { CircleButton, SvgIcon, Tooltip } from 'insomnia-components';
import React, { FunctionComponent } from 'react';
import React, { Fragment, FunctionComponent } from 'react';
import { useSelector } from 'react-redux';
import styled from 'styled-components';
import * as session from '../../../account/session';
import { selectSettings } from '../../redux/selectors';
import { Dropdown } from '../base/dropdown/dropdown';
import { DropdownButton } from '../base/dropdown/dropdown-button';
import { DropdownItem } from '../base/dropdown/dropdown-item';
@@ -24,51 +26,56 @@ const StyledIconContainer = styled.div`
}
`;
export const AccountDropdown: FunctionComponent<Props> = ({ className }) => (
<div className={className}>
<Dropdown>
<DropdownButton noWrap>
<Tooltip delay={1000} position="bottom" message="Account">
<CircleButton>
<SvgIcon icon="user" />
</CircleButton>
</Tooltip>
</DropdownButton>
{session.isLoggedIn() ? (
<DropdownItem
key="login"
stayOpenAfterClick
buttonClass={PromptButton}
onClick={session.logout}
>
<StyledIconContainer>
<i className="fa fa-sign-out" />
</StyledIconContainer>
Logout
</DropdownItem>
) : (
<DropdownItem key="login" onClick={showLoginModal}>
<StyledIconContainer>
<i className="fa fa-sign-in" />
</StyledIconContainer>
Log In
</DropdownItem>
)}
{!session.isLoggedIn() && (
<DropdownItem
key="invite"
buttonClass={Link}
// @ts-expect-error -- TSCONVERSION appears to be genuine
href="https://insomnia.rest/pricing"
button
>
<StyledIconContainer>
<i className="fa fa-users" />
</StyledIconContainer>{' '}
Upgrade to Plus
<i className="fa fa-star surprise fa-outline" />
</DropdownItem>
)}
</Dropdown>
</div>
);
export const AccountDropdown: FunctionComponent<Props> = ({ className }) => {
const { disablePaidFeatureAds } = useSelector(selectSettings);
return (
<div className={className}>
<Dropdown>
<DropdownButton noWrap>
<Tooltip delay={1000} position="bottom" message="Account">
<CircleButton>
<SvgIcon icon="user" />
</CircleButton>
</Tooltip>
</DropdownButton>
{session.isLoggedIn() ? (
<DropdownItem
key="login"
stayOpenAfterClick
buttonClass={PromptButton}
onClick={session.logout}
>
<StyledIconContainer>
<i className="fa fa-sign-out" />
</StyledIconContainer>
Logout
</DropdownItem>
) : (
<Fragment>
<DropdownItem key="login" onClick={showLoginModal}>
<StyledIconContainer>
<i className="fa fa-sign-in" />
</StyledIconContainer>
Log In
</DropdownItem>
{!disablePaidFeatureAds && (
<DropdownItem
key="invite"
buttonClass={Link}
// @ts-expect-error -- TSCONVERSION appears to be genuine
href="https://insomnia.rest/pricing"
button
>
<StyledIconContainer>
<i className="fa fa-users" />
</StyledIconContainer>{' '}
Upgrade to Plus
<i className="fa fa-star surprise fa-outline" />
</DropdownItem>
)}
</Fragment>
)}
</Dropdown>
</div>
);
};

View File

@@ -1,8 +1,8 @@
import { autoBindMethodsForReact } from 'class-autobind-decorator';
import { HotKeyRegistry } from 'insomnia-common';
import React, { PureComponent } from 'react';
import { AUTOBIND_CFG } from '../../../common/constants';
import type { HotKeyRegistry } from '../../../common/hotkeys';
import { hotKeyRefs } from '../../../common/hotkeys';
import { executeHotKey } from '../../../common/hotkeys-listener';
import type { Environment } from '../../../models/environment';

View File

@@ -1,9 +1,9 @@
import { autoBindMethodsForReact } from 'class-autobind-decorator';
import classnames from 'classnames';
import { HotKeyRegistry } from 'insomnia-common';
import React, { PureComponent } from 'react';
import { AUTOBIND_CFG } from '../../../common/constants';
import type { HotKeyRegistry } from '../../../common/hotkeys';
import { hotKeyRefs } from '../../../common/hotkeys';
import * as misc from '../../../common/misc';
import { RENDER_PURPOSE_NO_RENDER } from '../../../common/render';

View File

@@ -1,10 +1,10 @@
import { autoBindMethodsForReact } from 'class-autobind-decorator';
import classnames from 'classnames';
import { HotKeyRegistry } from 'insomnia-common';
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import { AUTOBIND_CFG } from '../../../common/constants';
import type { HotKeyRegistry } from '../../../common/hotkeys';
import { hotKeyRefs } from '../../../common/hotkeys';
import { RENDER_PURPOSE_NO_RENDER } from '../../../common/render';
import * as models from '../../../models';

View File

@@ -1,11 +1,11 @@
import { autoBindMethodsForReact } from 'class-autobind-decorator';
import classnames from 'classnames';
import { HotKeyRegistry } from 'insomnia-common';
import React, { PureComponent } from 'react';
import { AUTOBIND_CFG, getAppName, getAppVersion } from '../../../common/constants';
import { database as db } from '../../../common/database';
import { getWorkspaceLabel } from '../../../common/get-workspace-label';
import type { HotKeyRegistry } from '../../../common/hotkeys';
import { hotKeyRefs } from '../../../common/hotkeys';
import { executeHotKey } from '../../../common/hotkeys-listener';
import { RENDER_PURPOSE_NO_RENDER } from '../../../common/render';

View File

@@ -1,8 +1,8 @@
import classnames from 'classnames';
import { KeyBindings, KeyCombination } from 'insomnia-common';
import React, { FC, memo } from 'react';
import { isMac } from '../../common/constants';
import type { KeyBindings, KeyCombination } from '../../common/hotkeys';
import { constructKeyCombinationDisplay, getPlatformKeyCombinations } from '../../common/hotkeys';
interface Props {

View File

@@ -1,9 +1,9 @@
import { autoBindMethodsForReact } from 'class-autobind-decorator';
import classnames from 'classnames';
import { KeyCombination } from 'insomnia-common';
import React, { PureComponent } from 'react';
import { AUTOBIND_CFG } from '../../../common/constants';
import type { KeyCombination } from '../../../common/hotkeys';
import { constructKeyCombinationDisplay, isModifierKeyCode } from '../../../common/hotkeys';
import { keyboardKeys } from '../../../common/keyboard-keys';
import * as misc from '../../../common/misc';

View File

@@ -1,11 +1,11 @@
import { autoBindMethodsForReact } from 'class-autobind-decorator';
import { HotKeyRegistry } from 'insomnia-common';
import { Curl } from 'node-libcurl';
import React, { PureComponent } from 'react';
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
import * as session from '../../../account/session';
import { AUTOBIND_CFG, getAppName, getAppVersion } from '../../../common/constants';
import { HotKeyRegistry } from '../../../common/hotkeys';
import * as models from '../../../models/index';
import { Settings } from '../../../models/settings';
import { Button } from '../base/button';

View File

@@ -1,15 +1,15 @@
import { HotKeyRegistry } from 'insomnia-common';
import React, { FC, useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { hotKeyRefs } from '../../../common/hotkeys';
import * as hotkeys from '../../../common/hotkeys';
import { importFile } from '../../redux/modules/import';
import { selectActiveWorkspace } from '../../redux/selectors';
import { Hotkey } from '../hotkey';
import { Pane, PaneBody, PaneHeader } from './pane';
interface Props {
hotKeyRegistry: hotkeys.HotKeyRegistry;
hotKeyRegistry: HotKeyRegistry;
handleCreateRequest: () => void;
}

View File

@@ -1,12 +1,12 @@
import { HotKeyRegistry } from 'insomnia-common';
import React, { FunctionComponent } from 'react';
import { hotKeyRefs } from '../../../common/hotkeys';
import * as hotkeys from '../../../common/hotkeys';
import { Hotkey } from '../hotkey';
import { Pane, PaneBody, PaneHeader } from './pane';
interface Props {
hotKeyRegistry: hotkeys.HotKeyRegistry;
hotKeyRegistry: HotKeyRegistry;
}
export const PlaceholderResponsePane: FunctionComponent<Props> = ({ hotKeyRegistry, children }) => (

View File

@@ -2,6 +2,7 @@ import { autoBindMethodsForReact } from 'class-autobind-decorator';
import classnames from 'classnames';
import { clipboard, remote } from 'electron';
import fs from 'fs';
import { HotKeyRegistry } from 'insomnia-common';
import { json as jsonPrettify } from 'insomnia-prettify';
import mime from 'mime-types';
import React, { PureComponent } from 'react';
@@ -9,7 +10,6 @@ import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
import { AUTOBIND_CFG, PREVIEW_MODE_SOURCE } from '../../../common/constants';
import { exportHarCurrentRequest } from '../../../common/har';
import type { HotKeyRegistry } from '../../../common/hotkeys';
import { getSetCookieHeaders } from '../../../common/misc';
import * as models from '../../../models';
import type { Environment } from '../../../models/environment';

View File

@@ -1,9 +1,9 @@
import { autoBindMethodsForReact } from 'class-autobind-decorator';
import { OpenDialogOptions, remote } from 'electron';
import { HotKeyRegistry } from 'insomnia-common';
import React, { PureComponent, ReactNode } from 'react';
import { AUTOBIND_CFG, DEBOUNCE_MILLIS, isMac } from '../../common/constants';
import type { HotKeyRegistry } from '../../common/hotkeys';
import { hotKeyRefs } from '../../common/hotkeys';
import { executeHotKey } from '../../common/hotkeys-listener';
import { HandleGetRenderContext, HandleRender } from '../../common/render';

View File

@@ -1,22 +1,38 @@
import { Settings } from 'insomnia-common';
import React, { ChangeEvent, FC, useCallback } from 'react';
import { useSelector } from 'react-redux';
import styled from 'styled-components';
import { getControlledStatus } from '../../../models/helpers/settings';
import * as models from '../../../models/index';
import { BaseSettings } from '../../../models/settings';
import { selectSettings } from '../../redux/selectors';
import { HelpTooltip } from '../help-tooltip';
import { Tooltip } from '../tooltip';
import { ControlledSetting } from './controlled-setting';
const Descriptions = styled.div({
fontSize: 'var(--font-size-sm)',
opacity: 'var(--opacity-subtle)',
paddingLeft: 18,
'& *': {
marginTop: 'var(--padding-xs)',
marginBottom: 'var(--padding-sm)',
},
});
export const BooleanSetting: FC<{
label: string;
setting: keyof BaseSettings;
help?: string;
/** each element of this array will appear as a paragraph below the setting describing it */
descriptions?: string[];
forceRestart?: boolean;
help?: string;
label: string;
setting: keyof Settings;
}> = ({
descriptions,
forceRestart,
help,
label,
setting,
help,
forceRestart,
}) => {
const settings = useSelector(selectSettings);
@@ -24,6 +40,8 @@ export const BooleanSetting: FC<{
throw new Error(`Invalid boolean setting name ${setting}`);
}
const { isControlled } = getControlledStatus(settings)(setting);
const onChange = useCallback(async (event: ChangeEvent<HTMLInputElement>) => {
const { checked } = event.currentTarget;
await models.settings.patch({
@@ -32,22 +50,33 @@ export const BooleanSetting: FC<{
}, [setting]);
return (
<div className="form-control form-control--thin">
<label className="inline-block">
{label}
{help && <HelpTooltip className="space-left">{help}</HelpTooltip>}
{forceRestart && (
<Tooltip message="Will restart the app" className="space-left">
<i className="fa fa-refresh super-duper-faint" />
</Tooltip>
)}
<input
checked={Boolean(settings[setting])}
name={setting}
onChange={onChange}
type="checkbox"
/>
</label>
</div>
<ControlledSetting setting={setting}>
<div className="form-control form-control--thin">
<label className="inline-block">
{label}
{help && <HelpTooltip className="space-left">{help}</HelpTooltip>}
{forceRestart && (
<Tooltip message="Will restart the app" className="space-left">
<i className="fa fa-refresh super-duper-faint" />
</Tooltip>
)}
<input
checked={Boolean(settings[setting])}
name={setting}
onChange={onChange}
type="checkbox"
disabled={isControlled}
/>
</label>
</div>
{descriptions && (
<Descriptions>
{descriptions.map(description => (
<div key={description}>{description}</div>
))}
</Descriptions>
)}
</ControlledSetting>
);
};

View File

@@ -0,0 +1,84 @@
import { Settings } from 'insomnia-common';
import React, { FC, Fragment } from 'react';
import { useSelector } from 'react-redux';
import styled from 'styled-components';
import { getControlledStatus } from '../../../models/helpers/settings';
import { selectSettings } from '../../redux/selectors';
import { HelpTooltip } from '../help-tooltip';
const Wrapper = styled.div({
position: 'relative',
marginBottom: 4,
marginTop: 14,
borderLeft: '1px solid var(--color-surprise)',
});
const Setting = styled.div({
padding: '2px 10px',
position: 'relative',
'&:hover': {
cursor: 'not-allowed',
},
});
const ControlledOverlay = styled.div({
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
left: 0,
zIndex: 2,
});
const Helper = styled.div({
color: 'var(--color-surprise)',
padding: '2px 10px',
marginTop: -2,
opacity: 'var(--opacity-subtle)',
});
const HelperText = styled.span({
fontStyle: 'italic',
});
export const ControlledSetting: FC<{ setting: keyof Settings }> = ({ children, setting }) => {
const settings = useSelector(selectSettings);
const { isControlled, controller } = getControlledStatus(settings)(setting);
if (isControlled === false) {
return <Fragment>{children}</Fragment>;
}
let helpText: string | undefined = undefined;
let controllerName: string | null = null;
switch (controller) {
case 'insomnia-config':
helpText = `this value is controlled by \`settings.${setting}\` in your Insomnia Config`;
controllerName = 'insomnia config';
break;
case 'incognitoMode':
helpText = 'this value is controlled by Incognito Mode';
controllerName = 'incognito mode';
break;
default:
helpText = `this value is controlled by ${controller}`;
controllerName = controller || 'another setting';
}
return (
<Wrapper>
<Setting>
<ControlledOverlay title={helpText} />
{children}
</Setting>
<Helper>
<HelpTooltip info>{helpText}</HelpTooltip>{' '}
<HelperText>controlled by {controllerName}</HelperText>
</Helper>
</Wrapper>
);
};

View File

@@ -1,10 +1,11 @@
import { autoBindMethodsForReact } from 'class-autobind-decorator';
import * as fontScanner from 'font-scanner';
import { HttpVersion, HttpVersions } from 'insomnia-common';
import React, { Fragment, PureComponent } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import type { GlobalActivity, HttpVersion } from '../../../common/constants';
import type { GlobalActivity } from '../../../common/constants';
import {
ACTIVITY_MIGRATION,
AUTOBIND_CFG,
@@ -12,7 +13,6 @@ import {
EDITOR_KEY_MAP_EMACS,
EDITOR_KEY_MAP_SUBLIME,
EDITOR_KEY_MAP_VIM,
HttpVersions,
isDevelopment,
isMac,
MAX_EDITOR_FONT_SIZE,
@@ -261,10 +261,12 @@ class General extends PureComponent<Props, State> {
setting="editorLineWrapping"
/>
</div>
<div><BooleanSetting
label="Font ligatures"
setting="fontVariantLigatures"
/></div>
<div>
<BooleanSetting
label="Font ligatures"
setting="fontVariantLigatures"
/>
</div>
</div>
<div className="form-row pad-top-sm">
@@ -553,16 +555,18 @@ class General extends PureComponent<Props, State> {
</Fragment>
)}
<hr className="pad-top" />
<h2>Notifications</h2>
{!updatesSupported() && (
<Fragment>
<hr className="pad-top" />
<h2>Software Updates</h2>
<BooleanSetting
label="Do not notify of new releases"
setting="disableUpdateNotification"
/>
</Fragment>
<BooleanSetting
label="Do not notify of new releases"
setting="disableUpdateNotification"
/>
)}
<BooleanSetting
label="Do not tell me about premium features"
setting="disablePaidFeatureAds"
/>
<hr className="pad-top" />
<h2>Plugins</h2>
@@ -576,25 +580,31 @@ class General extends PureComponent<Props, State> {
},
)}
<br />
<hr className="pad-top" />
<h2>Data Sharing</h2>
<div className="form-control form-control--thin">
<BooleanSetting
label="Send Usage Statistics"
setting="enableAnalytics"
/>
<p className="txt-sm faint">
Help Kong improve its products by sending anonymous data about features and plugins
used, hardware and software configuration, statistics on number of requests,{' '}
{strings.collection.plural.toLowerCase()}, {strings.document.plural.toLowerCase()}, etc.
</p>
<p className="txt-sm faint">
Please note that this will not include personal data or any sensitive information, such
as request data, names, etc.
</p>
</div>
<h2>Network Activity</h2>
<BooleanSetting
descriptions={[
'In incognito mode, Insomnia will not make any network requests other than the requests you ask it to send. You\'ll still be able to log in and manually sync collections, but any background network requests that are not the direct result of your actions will be disabled.',
'Note that, similar to incognito mode in Chrome, Insomnia cannot control the network behavior of any plugins you have installed.',
]}
label="Incognito Mode"
setting="incognitoMode"
/>
<BooleanSetting
descriptions={[
`Help Kong improve its products by sending anonymous data about features and plugins used, hardware and software configuration, statistics on number of requests, ${strings.collection.plural.toLowerCase()}, ${strings.document.plural.toLowerCase()}, etc.`,
'Please note that this will not include personal data or any sensitive information, such as request data, names, etc.',
]}
label="Send Usage Statistics"
setting="enableAnalytics"
/>
<BooleanSetting
descriptions={['Insomnia periodically makes background requests to api.insomnia.rest/notifications for things like email verification, out-of-date billing information, trial information.']}
label="Allow Notification Requests"
setting="allowNotificationRequests"
/>
<hr className="pad-top" />

View File

@@ -1,5 +1,6 @@
import { autoBindMethodsForReact } from 'class-autobind-decorator';
import * as electron from 'electron';
import { PluginConfig } from 'insomnia-common';
import { Button, ToggleSwitch } from 'insomnia-components';
import * as path from 'path';
import React, { ChangeEvent, FormEvent, PureComponent } from 'react';
@@ -12,7 +13,7 @@ import {
} from '../../../common/constants';
import { docsPlugins } from '../../../common/documentation';
import { delay } from '../../../common/misc';
import type { PluginConfig, Settings } from '../../../models/settings';
import type { Settings } from '../../../models/settings';
import { createPlugin } from '../../../plugins/create';
import type { Plugin } from '../../../plugins/index';
import { getPlugins } from '../../../plugins/index';

View File

@@ -1,13 +1,14 @@
import { autoBindMethodsForReact } from 'class-autobind-decorator';
import { HotKeyRegistry, KeyCombination } from 'insomnia-common';
import React, { PureComponent } from 'react';
import { AUTOBIND_CFG } from '../../../common/constants';
import type { HotKeyDefinition, HotKeyRegistry, KeyCombination } from '../../../common/hotkeys';
import {
areKeyBindingsSameAsDefault,
areSameKeyCombinations,
constructKeyCombinationDisplay,
getPlatformKeyCombinations,
HotKeyDefinition,
hotKeyRefs,
newDefaultKeyBindings,
newDefaultRegistry,

View File

@@ -1,10 +1,10 @@
import { autoBindMethodsForReact } from 'class-autobind-decorator';
import { HotKeyRegistry } from 'insomnia-common';
import React, { Fragment, PureComponent } from 'react';
import ReactDOM from 'react-dom';
import { connect } from 'react-redux';
import { AUTOBIND_CFG } from '../../../common/constants';
import type { HotKeyRegistry } from '../../../common/hotkeys';
import { HandleRender } from '../../../common/render';
import { GrpcRequest, isGrpcRequest } from '../../../models/grpc-request';
import { isRequest, Request } from '../../../models/request';

View File

@@ -1,8 +1,8 @@
import { autoBindMethodsForReact } from 'class-autobind-decorator';
import { HotKeyRegistry } from 'insomnia-common';
import React, { PureComponent } from 'react';
import { AUTOBIND_CFG } from '../../../common/constants';
import type { HotKeyRegistry } from '../../../common/hotkeys';
import { hotKeyRefs } from '../../../common/hotkeys';
import { RequestGroup } from '../../../models/request-group';
import { Dropdown } from '../base/dropdown/dropdown';

View File

@@ -1,8 +1,8 @@
import { autoBindMethodsForReact } from 'class-autobind-decorator';
import { HotKeyRegistry } from 'insomnia-common';
import React, { PureComponent } from 'react';
import { AUTOBIND_CFG, DEBOUNCE_MILLIS, SortOrder } from '../../../common/constants';
import type { HotKeyRegistry } from '../../../common/hotkeys';
import { hotKeyRefs } from '../../../common/hotkeys';
import { executeHotKey } from '../../../common/hotkeys-listener';
import { KeydownBinder } from '../keydown-binder';

View File

@@ -1,5 +1,6 @@
import { autoBindMethodsForReact } from 'class-autobind-decorator';
import classnames from 'classnames';
import { HotKeyRegistry } from 'insomnia-common';
import React, { PureComponent } from 'react';
import { PropsWithChildren } from 'react';
import { createRef } from 'react';
@@ -7,7 +8,6 @@ import { DragSource, DragSourceSpec, DropTarget, DropTargetMonitor, DropTargetSp
import { connect } from 'react-redux';
import { AUTOBIND_CFG } from '../../../common/constants';
import { HotKeyRegistry } from '../../../common/hotkeys';
import * as misc from '../../../common/misc';
import { HandleRender } from '../../../common/render';
import { RequestGroup } from '../../../models/request-group';

View File

@@ -1,11 +1,11 @@
import { autoBindMethodsForReact } from 'class-autobind-decorator';
import classnames from 'classnames';
import { HotKeyRegistry } from 'insomnia-common';
import React, { PureComponent } from 'react';
import { DragSource, DragSourceSpec, DropTarget, DropTargetSpec } from 'react-dnd';
import { connect } from 'react-redux';
import { AUTOBIND_CFG, CONTENT_TYPE_GRAPHQL } from '../../../common/constants';
import { HotKeyRegistry } from '../../../common/hotkeys';
import { getMethodOverrideHeader } from '../../../common/misc';
import { HandleRender } from '../../../common/render';
import { GrpcRequest, isGrpcRequest } from '../../../models/grpc-request';

View File

@@ -1,11 +1,11 @@
import classnames from 'classnames';
import { HotKeyRegistry } from 'insomnia-common';
import React, { forwardRef, memo, ReactNode } from 'react';
import {
COLLAPSE_SIDEBAR_REMS,
SIDEBAR_SKINNY_REMS,
} from '../../../common/constants';
import type { HotKeyRegistry } from '../../../common/hotkeys';
import type { Environment } from '../../../models/environment';
import type { Workspace } from '../../../models/workspace';

View File

@@ -17,7 +17,8 @@ import {
import * as models from '../../models/index';
import imgSrcCore from '../images/insomnia-core-logo.png';
import { Link } from './base/link';
const LOCALSTORAGE_KEY = 'insomnia::notifications::seen';
const INSOMNIA_NOTIFICATIONS_SEEN = 'insomnia::notifications::seen';
export interface ToastNotification {
key: string;
@@ -57,6 +58,8 @@ const StyledFooter = styled.footer`
width: 100%;
`;
type SeenNotifications = Record<string, boolean>;
@autoBindMethodsForReact(AUTOBIND_CFG)
export class Toast extends PureComponent<{}, State> {
_interval: NodeJS.Timeout | null = null;
@@ -67,7 +70,7 @@ export class Toast extends PureComponent<{}, State> {
appName: getAppName(),
};
_handlePostCTACleanup() {
_cancel() {
const { notification } = this.state;
if (!notification) {
@@ -77,20 +80,31 @@ export class Toast extends PureComponent<{}, State> {
this._dismissNotification();
}
_handleCancelClick() {
const { notification } = this.state;
_handleNotification(notification: ToastNotification | null | undefined) {
if (!notification) {
return;
}
this._dismissNotification();
}
_hasSeenNotification(notification: ToastNotification) {
const seenNotifications = this._loadSeen();
return seenNotifications[notification.key];
console.log(`[toast] Received notification ${notification.key}`);
if (seenNotifications[notification.key]) {
console.log(`[toast] Not showing notification ${notification.key} because has already been seen`);
return;
}
seenNotifications[notification.key] = true;
const obj = JSON.stringify(seenNotifications, null, 2);
window.localStorage.setItem(INSOMNIA_NOTIFICATIONS_SEEN, obj);
this.setState({
notification,
visible: false,
});
// Fade the notification in
setTimeout(() => { this.setState({ visible: true }); }, 1000);
}
async _checkForNotifications() {
@@ -100,64 +114,51 @@ export class Toast extends PureComponent<{}, State> {
}
const stats = await models.stats.get();
const settings = await models.settings.getOrCreate();
let notification: ToastNotification;
const {
allowNotificationRequests,
disablePaidFeatureAds,
disableUpdateNotification,
updateAutomatically,
updateChannel,
} = await models.settings.getOrCreate();
if (!allowNotificationRequests) {
// if the user has specifically said they don't want to send notification requests, then exit early
return;
}
let notification: ToastNotification | null = null;
// Try fetching user notification
try {
const data = {
firstLaunch: stats.created,
// Used for account verification notifications
launches: stats.launches,
// Used for CTAs / Informational notifications
platform: getAppPlatform(),
app: getAppId(),
version: getAppVersion(),
autoUpdatesDisabled: !updateAutomatically,
disablePaidFeatureAds,
disableUpdateNotification,
firstLaunch: stats.created,
launches: stats.launches, // Used for account verification notifications
platform: getAppPlatform(), // Used for CTAs / Informational notifications
updateChannel,
updatesNotSupported: !updatesSupported(),
autoUpdatesDisabled: !settings.updateAutomatically,
disableUpdateNotification: settings.disableUpdateNotification,
updateChannel: settings.updateChannel,
version: getAppVersion(),
};
notification = await fetch.post('/notification', data, session.getCurrentSessionId());
} catch (err) {
console.warn('[toast] Failed to fetch user notifications', err);
}
// @ts-expect-error -- TSCONVERSION
this._handleNotification(notification);
}
_handleNotification(notification: ToastNotification | null | undefined) {
// No new notifications
if (!notification || this._hasSeenNotification(notification)) {
return;
}
// Remember that we've seen it
const seenNotifications = this._loadSeen();
seenNotifications[notification.key] = true;
const obj = JSON.stringify(seenNotifications, null, 2);
window.localStorage.setItem(LOCALSTORAGE_KEY, obj);
// Show the notification
this.setState({
notification,
visible: false,
});
// Fade the notification in
setTimeout(
() =>
this.setState({
visible: true,
}),
1000,
);
}
_loadSeen() {
try {
// @ts-expect-error -- TSCONVERSION
return JSON.parse(window.localStorage.getItem(LOCALSTORAGE_KEY)) || {};
const storedKeys = window.localStorage.getItem(INSOMNIA_NOTIFICATIONS_SEEN);
if (!storedKeys) {
return {};
}
return JSON.parse(storedKeys) as SeenNotifications || {};
} catch (e) {
return {};
}
@@ -171,25 +172,15 @@ export class Toast extends PureComponent<{}, State> {
}
// Hide the currently showing notification
this.setState({
visible: false,
});
this.setState({ visible: false });
// Give time for toast to fade out, then remove it
setTimeout(() => {
this.setState(
{
notification: null,
},
async () => {
await this._checkForNotifications();
},
);
this.setState({ notification: null }, this._checkForNotifications);
}, 1000);
}
_listenerShowNotification(_e, notification: ToastNotification) {
console.log('[toast] Received notification ' + notification.key);
this._handleNotification(notification);
}
@@ -223,11 +214,11 @@ export class Toast extends PureComponent<{}, State> {
<img src={imgSrcCore} alt={appName} />
</StyledLogo>
<StyledContent>
<p>{notification ? notification.message : 'Unknown'}</p>
<p>{notification?.message || 'Unknown'}</p>
<StyledFooter>
<button
className="btn btn--super-duper-compact btn--outlined"
onClick={this._handleCancelClick}
onClick={this._cancel}
>
Dismiss
</button>
@@ -235,7 +226,7 @@ export class Toast extends PureComponent<{}, State> {
<Link
button
className="btn btn--super-duper-compact btn--outlined no-wrap"
onClick={this._handlePostCTACleanup}
onClick={this._cancel}
href={notification.url}
>
{notification.cta}

View File

@@ -515,8 +515,7 @@ class App extends PureComponent<AppProps, State> {
* @returns {Promise}
* @private
*/
async _handleRenderText<T>(obj: T, contextCacheKey = null) {
// @ts-expect-error -- TSCONVERSION contextCacheKey being null used as object index
async _handleRenderText<T>(obj: T, contextCacheKey: string | null = null) {
if (!contextCacheKey || !this._getRenderContextPromiseCache[contextCacheKey]) {
// NOTE: We're caching promises here to avoid race conditions
// @ts-expect-error -- TSCONVERSION contextCacheKey being null used as object index

View File

@@ -54,7 +54,7 @@
input[type='checkbox'] {
height: 1rem;
float: left;
margin-top: var(--padding-xxs);
margin-top: 1px; // This is a magic number, yes, but there's some deeper problems at play with form styling and this is a hold-over until the DOM design is simplified. Before this fix it looked pretty bad, so this is at least a big improvement in one respect.
margin-right: var(--padding-xs);
}

View File

@@ -128,4 +128,75 @@ describe('useRemoteProjects', () => {
}),
]));
});
it('should load teams on mount if not in incognitoMode', async () => {
await models.settings.patch({ incognitoMode: false });
const store = mockStore(await reduxStateForTest({ isLoggedIn: true }));
const vcs = newMockedVcs();
vcs.teams.mockResolvedValue([]);
// Render hook first time
const { result } = renderHook(() => useRemoteProjects(vcs), { wrapper: withReduxStore(store) });
// Refresh manually
await act(() => result.current.refresh());
// Called twice - once manually and once on mount
expect(vcs.teams).toHaveBeenCalledTimes(2);
});
it('should not load teams on mount if in incognitoMode', async () => {
// Set up in incognito mode
await models.settings.patch({ incognitoMode: true });
const store = mockStore(await reduxStateForTest({ isLoggedIn: true }));
const vcs = newMockedVcs();
vcs.teams.mockResolvedValue([]);
// Render hook first time
const { result } = renderHook(() => useRemoteProjects(vcs), { wrapper: withReduxStore(store) });
// Refresh manually
await act(() => result.current.refresh());
// Called only once (manually), because load on mount was skipped
expect(vcs.teams).toHaveBeenCalledTimes(1);
});
it('should load teams on mount if incognitoMode goes from on to off', async () => {
// Set up in incognito mode
await models.settings.patch({ incognitoMode: true });
let state = await reduxStateForTest({ isLoggedIn: true });
const store = mockStore(() => state);
const vcs = newMockedVcs();
vcs.teams.mockResolvedValue([]);
// Render hook first time
const { result, rerender } = renderHook(() => useRemoteProjects(vcs), { wrapper: withReduxStore(store) });
// Refresh manually
await act(() => result.current.refresh());
// Called only once (manually), because load on mount was skipped
expect(vcs.teams).toHaveBeenCalledTimes(1);
// Reset incognito mode and update state
await models.settings.patch({ incognitoMode: false });
vcs.teams.mockClear();
// Force the store to update
state = await reduxStateForTest({ isLoggedIn: true });
store.dispatch({ type: 'ANY_ACTION' });
// Render the hook again with updated redux store
rerender();
// Refresh manually
await act(() => result.current.refresh());
// Called twice - once manually and once on mount
expect(vcs.teams).toHaveBeenCalledTimes(2);
});
});

View File

@@ -6,12 +6,13 @@ import { useAsync } from 'react-use';
import { database } from '../../common/database';
import { initializeProjectFromTeam } from '../../sync/vcs/initialize-model-from';
import { VCS } from '../../sync/vcs/vcs';
import { selectIsLoggedIn } from '../redux/selectors';
import { selectIsLoggedIn, selectSettings } from '../redux/selectors';
import { useSafeState } from './use-safe-state';
export const useRemoteProjects = (vcs?: VCS) => {
const [loading, setLoading] = useSafeState(false);
const isLoggedIn = useSelector(selectIsLoggedIn);
const { incognitoMode } = useSelector(selectSettings);
const refresh = useCallback(async () => {
if (vcs && isLoggedIn) {
@@ -26,7 +27,10 @@ export const useRemoteProjects = (vcs?: VCS) => {
}, [vcs, setLoading, isLoggedIn]);
// If the refresh callback changes, refresh
useAsync(refresh, [refresh]);
useAsync(async () => {
if (!incognitoMode) {
await refresh();
}
}, [refresh, incognitoMode]);
return { loading, refresh };
};

View File

@@ -8,7 +8,7 @@ import { BackendProjectWithTeam } from '../../sync/vcs/normalize-backend-project
import { pullBackendProject } from '../../sync/vcs/pull-backend-project';
import { VCS } from '../../sync/vcs/vcs';
import { showAlert } from '../components/modals';
import { selectActiveProject, selectAllWorkspaces, selectIsLoggedIn, selectRemoteProjects } from '../redux/selectors';
import { selectActiveProject, selectAllWorkspaces, selectIsLoggedIn, selectRemoteProjects, selectSettings } from '../redux/selectors';
import { useSafeReducerDispatch } from './use-safe-reducer-dispatch';
interface State {
@@ -52,6 +52,7 @@ export const useRemoteWorkspaces = (vcs?: VCS) => {
const activeProject = useSelector(selectActiveProject);
const remoteProjects = useSelector(selectRemoteProjects);
const isLoggedIn = useSelector(selectIsLoggedIn);
const { incognitoMode } = useSelector(selectSettings);
// Local state
const [{ loading, localBackendProjects, remoteBackendProjects, pullingBackendProjects }, _dispatch] = useReducer(reducer, initialState);
@@ -107,7 +108,11 @@ export const useRemoteWorkspaces = (vcs?: VCS) => {
}, [vcs, refresh, remoteProjects, dispatch]);
// If the refresh callback changes, refresh
useAsync(refresh, [refresh]);
useAsync(async () => {
if (!incognitoMode) {
await refresh();
}
}, [refresh, incognitoMode]);
return {
loading,

View File

@@ -101,6 +101,8 @@
"httpsnippet": "^1.23.0",
"iconv-lite": "^0.4.15",
"insomnia-components": "2.3.2",
"insomnia-common": "2.3.2",
"insomnia-config": "2.3.2",
"insomnia-cookies": "2.3.2",
"insomnia-importers": "2.3.2",
"insomnia-plugin-base64": "2.3.2",

View File

@@ -12,6 +12,7 @@
"include": [
"**/*.d.ts",
"app",
"**/insomnia.config.json",
"package.json"
],
"exclude": [

View File

@@ -5,6 +5,7 @@
"app",
"config",
"jest.config.js",
"**/insomnia.config.json",
"package.json",
"scripts",
"send-request",

View File

@@ -0,0 +1 @@
dist

View File

@@ -0,0 +1,4 @@
/** @type { import('eslint').Linter.Config } */
module.exports = {
extends: '../../.eslintrc.js',
};

View File

@@ -0,0 +1,9 @@
/** @type { import('@jest/types').Config.InitialOptions } */
module.exports = {
preset: '../../jest-preset.js',
globals: {
'ts-jest': {
isolatedModules: false,
},
},
};

390
packages/insomnia-common/package-lock.json generated Normal file
View File

@@ -0,0 +1,390 @@
{
"name": "insomnia-common",
"version": "2.3.2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@types/json-schema": {
"version": "7.0.9",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz",
"integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==",
"dev": true
},
"@types/node": {
"version": "14.17.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.15.tgz",
"integrity": "sha512-D1sdW0EcSCmNdLKBGMYb38YsHUS6JcM7yQ6sLQ9KuZ35ck7LYCKE7kYFHOO59ayFOY3zobWVZxf4KXhYHcHYFA==",
"dev": true
},
"ajv": {
"version": "8.6.2",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.2.tgz",
"integrity": "sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w==",
"dev": true,
"requires": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
}
},
"ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"dev": true
},
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"requires": {
"color-convert": "^2.0.1"
}
},
"arg": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
"dev": true
},
"balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true
},
"cliui": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
"dev": true,
"requires": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^7.0.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true
},
"create-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true
},
"diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"dev": true
},
"emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true
},
"escalade": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
"integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
"dev": true
},
"fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true
},
"get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true
},
"glob": {
"version": "7.1.7",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
"integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"dev": true,
"requires": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
},
"is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true
},
"json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true
},
"json-stable-stringify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz",
"integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=",
"dev": true,
"requires": {
"jsonify": "~0.0.0"
}
},
"jsonify": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz",
"integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=",
"dev": true
},
"make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"dev": true
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
"requires": {
"wrappy": "1"
}
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"dev": true
},
"punycode": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
"dev": true
},
"require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
"dev": true
},
"require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"dev": true
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
},
"source-map-support": {
"version": "0.5.20",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.20.tgz",
"integrity": "sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw==",
"dev": true,
"requires": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
}
},
"string-width": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
"integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==",
"dev": true,
"requires": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.0"
}
},
"strip-ansi": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
"dev": true,
"requires": {
"ansi-regex": "^5.0.0"
}
},
"ts-node": {
"version": "9.1.1",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz",
"integrity": "sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==",
"dev": true,
"requires": {
"arg": "^4.1.0",
"create-require": "^1.1.0",
"diff": "^4.0.1",
"make-error": "^1.1.1",
"source-map-support": "^0.5.17",
"yn": "3.1.1"
}
},
"type-fest": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz",
"integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==",
"dev": true
},
"typescript": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz",
"integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==",
"dev": true
},
"typescript-json-schema": {
"version": "0.50.1",
"resolved": "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.50.1.tgz",
"integrity": "sha512-GCof/SDoiTDl0qzPonNEV4CHyCsZEIIf+mZtlrjoD8vURCcEzEfa2deRuxYid8Znp/e27eDR7Cjg8jgGrimBCA==",
"dev": true,
"requires": {
"@types/json-schema": "^7.0.7",
"@types/node": "^14.14.33",
"glob": "^7.1.6",
"json-stable-stringify": "^1.0.1",
"ts-node": "^9.1.1",
"typescript": "~4.2.3",
"yargs": "^16.2.0"
}
},
"uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"dev": true,
"requires": {
"punycode": "^2.1.0"
}
},
"wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"requires": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
}
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true
},
"y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"dev": true
},
"yargs": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
"dev": true,
"requires": {
"cliui": "^7.0.2",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.0",
"y18n": "^5.0.5",
"yargs-parser": "^20.2.2"
}
},
"yargs-parser": {
"version": "20.2.9",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
"dev": true
},
"yn": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
"dev": true
}
}
}

View File

@@ -0,0 +1,31 @@
{
"name": "insomnia-common",
"version": "2.3.2",
"homepage": "https://insomnia.rest",
"description": "Top-level entities and utilities for Insomnia",
"author": "Kong <office@konghq.com>",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/Kong/insomnia.git",
"directory": "packages/insomnia-config"
},
"bugs": {
"url": "https://github.com/kong/insomnia/issues"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"bootstrap": "npm run build",
"lint": "eslint . --ext .js,.ts,.tsx",
"lint:fix": "npm run lint -- --fix",
"clean": "tsc --build tsconfig.build.json --clean",
"prebuild": "npm run clean",
"build": "tsc --build tsconfig.build.json"
},
"devDependencies": {
"ajv": "^8.6.2",
"type-fest": "^1.0.2",
"typescript-json-schema": "^0.50.1"
}
}

View File

@@ -0,0 +1,12 @@
import { ValueOf } from 'type-fest';
// HTTP version codes
export const HttpVersions = {
V1_0: 'V1_0',
V1_1: 'V1_1',
V2_0: 'V2_0',
v3: 'v3',
default: 'default',
} as const;
export type HttpVersion = ValueOf<typeof HttpVersions>;

View File

@@ -0,0 +1,25 @@
/**
* The combination of key presses that will activate a hotkey if pressed.
*/
export interface KeyCombination {
ctrl: boolean;
alt: boolean;
shift: boolean;
meta: boolean;
keyCode: number;
}
/**
* The collection of a hotkey's key combinations for each platforms.
*/
export interface KeyBindings {
macKeys: KeyCombination[];
// The key combinations for both Windows and Linux.
winLinuxKeys: KeyCombination[];
}
/**
* The collection of defined hotkeys.
* The registry maps a hotkey by its reference id to its key bindings.
*/
export type HotKeyRegistry = Record<string, KeyBindings>;

View File

@@ -0,0 +1,2 @@
export * from './hotkeys';
export * from './settings';

View File

@@ -0,0 +1,73 @@
import { HttpVersion } from '../constants';
import { HotKeyRegistry } from './hotkeys';
export interface PluginConfig {
disabled: boolean;
}
export type PluginConfigMap = Record<string, PluginConfig>;
export interface Settings {
/** If false, Insomnia won't send requests to the api.insomnia.rest/notifications endpoint. This can have effects like: users wont be notified in-app about billing issues, and they wont receive tips about app usage. */
allowNotificationRequests: boolean;
autoDetectColorScheme: boolean;
autoHideMenuBar: boolean;
autocompleteDelay: number;
clearOAuth2SessionOnRestart: boolean;
darkTheme: string;
deviceId: string | null;
disableHtmlPreviewJs: boolean;
/** If true, Insomnia wont show any visual elements that recommend plan upgrades. */
disablePaidFeatureAds: boolean;
disableResponsePreviewLinks: boolean;
/** If true, Insomnia wont show a notification when new updates are available. Users can still check for updates in Preferences. */
disableUpdateNotification: boolean;
editorFontSize: number;
editorIndentSize: number;
editorIndentWithTabs: boolean;
editorKeyMap: string;
editorLineWrapping: boolean;
/** If true, Insomnia will send anonymous data about features and plugins used. */
enableAnalytics: boolean;
environmentHighlightColorStyle: string;
filterResponsesByEnv: boolean;
followRedirects: boolean;
fontInterface: string | null;
fontMonospace: string | null;
fontSize: number;
fontVariantLigatures: boolean;
forceVerticalLayout: boolean;
hasPromptedAnalytics: boolean;
hasPromptedOnboarding: boolean;
hasPromptedToMigrateFromDesigner: boolean;
hotKeyRegistry: HotKeyRegistry;
httpProxy: string;
httpsProxy: string;
isVariableUncovered?: boolean;
lightTheme: string;
lineWrapping?: boolean;
maxHistoryResponses: number;
maxRedirects: number;
maxTimelineDataSizeKB: number;
noProxy: string;
nunjucksPowerUserMode: boolean;
pluginConfig: PluginConfigMap;
pluginPath: string;
preferredHttpVersion: HttpVersion;
proxyEnabled: boolean;
/** If true, wont make any network requests other than the requests you ask it to send. This configuration controls Send Usage Stats (`enableAnalytics`) and Allow Notification Requests (`allowNotificationRequests`). */
incognitoMode: boolean;
showPasswords: boolean;
theme: string;
timeout: number;
updateAutomatically: boolean;
updateChannel: string;
useBulkHeaderEditor: boolean;
useBulkParametersEditor: boolean;
validateAuthSSL: boolean;
validateSSL: boolean;
}

View File

@@ -0,0 +1,2 @@
export * from './constants';
export * from './entities';

View File

@@ -0,0 +1,16 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"outDir": "dist",
"rootDir": "src",
"resolveJsonModule": true,
"strict": true,
},
"include": [
"src"
],
"exclude": [
"**/*.test.ts",
],
}

View File

@@ -0,0 +1,15 @@
{
"extends": "./tsconfig.build.json",
"compilerOptions": {
"composite": false,
"rootDir": ".",
},
"include": [
"src",
"jest.config.js",
".eslintrc.js",
],
"exclude": [
"dist",
],
}

View File

@@ -0,0 +1 @@
dist

View File

@@ -0,0 +1,4 @@
/** @type { import('eslint').Linter.Config } */
module.exports = {
extends: '../../.eslintrc.js',
};

View File

@@ -0,0 +1,24 @@
# Insomnia Config
## Design Goals
- ensure that changing the root TypeScript types will generate new config
- ensure that making a breaking change to TypeScript will cause failing tests to be updated
- run validation on the CI
## Editor Integration
To get editor integration for this schema, tell your IDE about it. For example, in VS Code add the following to your `.vscode/settings.json`:
```json
{
"json.schemas": [
{
"fileMatch": [
"insomnia.config.json",
],
"url": "https://raw.githubusercontent.com/Kong/insomnia/develop/packages/insomnia-config/src/generated/schemas/insomnia.schema.json"
}
]
}
```

View File

@@ -0,0 +1,9 @@
/** @type { import('@jest/types').Config.InitialOptions } */
module.exports = {
preset: '../../jest-preset.js',
globals: {
'ts-jest': {
isolatedModules: false,
},
},
};

4819
packages/insomnia-config/package-lock.json generated Normal file
View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,39 @@
{
"name": "insomnia-config",
"version": "2.3.2",
"homepage": "https://insomnia.rest",
"description": "Configuration for Insomnia",
"author": "Kong <office@konghq.com>",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/Kong/insomnia.git",
"directory": "packages/insomnia-config"
},
"bugs": {
"url": "https://github.com/kong/insomnia/issues"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"bootstrap": "npm run generate",
"lint": "eslint . --ext .js,.ts,.tsx",
"lint:fix": "npm run lint -- --fix",
"test": "jest",
"pregenerate": "npm run build",
"generate": "ts-node ./src/generated/generate.ts",
"clean": "tsc --build tsconfig.build.json --clean",
"prebuild": "npm run clean",
"build": "tsc --build tsconfig.build.json"
},
"dependencies": {
"insomnia-common": "2.3.2"
},
"devDependencies": {
"ajv": "^8.6.2",
"ajv-errors": "^3.0.0",
"jest": "^26.6.3",
"ts-node": "^10.2.1",
"typescript-json-schema": "^0.50.1"
}
}

View File

@@ -0,0 +1,21 @@
import { Settings } from 'insomnia-common';
type ConfigVersion = '1.0.0';
type AllowedSettings = Partial<Pick<Settings,
| 'allowNotificationRequests'
| 'disableUpdateNotification'
| 'enableAnalytics'
| 'disablePaidFeatureAds'
| 'incognitoMode'
>>;
/**
* Configuration for Insomnia.
*
* @TJS-title Insomnia Config
*/
export interface InsomniaConfig {
insomniaConfig: ConfigVersion;
settings?: AllowedSettings;
}

View File

@@ -0,0 +1,36 @@
import { writeFileSync } from 'fs';
import { resolve } from 'path';
import { CompilerOptions, generateSchema, getProgramFromFiles, PartialArgs } from 'typescript-json-schema';
const settings: PartialArgs = {
// TODO (INS-1033): some day, it'd be ideal to do something like this. (when we do, remember to change the README for insomnia-config)
// id: 'https://schema.insomnia.rest/json/draft-07/config/v1.0.0/',
noExtraProps: true,
required: true,
};
const compilerOptions: CompilerOptions = {
strict: true,
};
const basePath = '.';
const program = getProgramFromFiles(
[resolve('./src/entities.ts')],
compilerOptions,
basePath,
);
export const schema = generateSchema(
program,
'InsomniaConfig',
settings,
);
if (schema === null) {
throw new Error('failed to generate Insomnia Config');
}
const schemaDestination = './src/generated/schemas/insomnia.schema.json';
const stringSchema = JSON.stringify(schema, null, 2);
writeFileSync(schemaDestination, stringSchema);

View File

@@ -0,0 +1,49 @@
{
"description": "Configuration for Insomnia.",
"title": "Insomnia Config",
"type": "object",
"properties": {
"insomniaConfig": {
"type": "string",
"enum": [
"1.0.0"
]
},
"settings": {
"$ref": "#/definitions/Partial<Pick<Settings,\"allowNotificationRequests\"|\"disableUpdateNotification\"|\"enableAnalytics\"|\"disablePaidFeatureAds\"|\"incognitoMode\">>"
}
},
"additionalProperties": false,
"required": [
"insomniaConfig"
],
"definitions": {
"Partial<Pick<Settings,\"allowNotificationRequests\"|\"disableUpdateNotification\"|\"enableAnalytics\"|\"disablePaidFeatureAds\"|\"incognitoMode\">>": {
"type": "object",
"properties": {
"allowNotificationRequests": {
"description": "If false, Insomnia won't send requests to the api.insomnia.rest/notifications endpoint. This can have effects like: users wont be notified in-app about billing issues, and they wont receive tips about app usage.",
"type": "boolean"
},
"disableUpdateNotification": {
"description": "If true, Insomnia wont show a notification when new updates are available. Users can still check for updates in Preferences.",
"type": "boolean"
},
"enableAnalytics": {
"description": "If true, Insomnia will send anonymous data about features and plugins used.",
"type": "boolean"
},
"disablePaidFeatureAds": {
"description": "If true, Insomnia wont show any visual elements that recommend plan upgrades.",
"type": "boolean"
},
"incognitoMode": {
"description": "If true, wont make any network requests other than the requests you ask it to send. This configuration controls Send Usage Stats (`enableAnalytics`) and Allow Notification Requests (`allowNotificationRequests`).",
"type": "boolean"
}
},
"additionalProperties": false
}
},
"$schema": "http://json-schema.org/draft-07/schema#"
}

View File

@@ -0,0 +1,2 @@
export { validate } from './validate';
export { InsomniaConfig } from './entities';

View File

@@ -0,0 +1,144 @@
import { InsomniaConfig } from '.';
import { ingest, validate } from './validate';
describe('ingest', () => {
const config: InsomniaConfig = {
insomniaConfig: '1.0.0',
};
it('returns arbitrary input without modification', () => {
const result = ingest(config);
expect(result).toStrictEqual(config);
});
it('parses a string insomnia config', () => {
const stringConfig = JSON.stringify(config, null, 2);
const result = ingest(stringConfig);
expect(result).toStrictEqual(config);
});
it('throws on bad inputs', () => {
const result = () => ingest('Lumpy Gravy');
expect(result).toThrowError('Unexpected token L in JSON at position 0');
});
});
describe('validate', () => {
it('passes with an empty config', () => {
const { valid, errors } = validate({
insomniaConfig: '1.0.0',
});
expect(errors).toBe(null);
expect(valid).toBe(true);
});
it('passes with a simple valid config', () => {
const { valid, errors } = validate({
insomniaConfig: '1.0.0',
settings: {
enableAnalytics: false,
disableUpdateNotification: true,
},
});
expect(errors).toBe(null);
expect(valid).toBe(true);
});
it('fails on incorrect version', () => {
const { valid, errors } = validate({
insomniaConfig: 'v1.0.0',
});
expect(errors).toMatchObject([
{
instancePath: '/insomniaConfig',
schemaPath: '#/properties/insomniaConfig/enum',
keyword: 'enum',
params: {
allowedValues: [
'1.0.0',
],
},
message: 'must be equal to one of the allowed values',
},
]);
expect(valid).toBe(false);
});
it('fails on missing properties', () => {
const { valid, errors } = validate({});
expect(errors).toMatchObject([
{
instancePath: '',
schemaPath: '#/required',
keyword: 'required',
params: { missingProperty: 'insomniaConfig' },
message: "must have required property 'insomniaConfig'",
},
]);
expect(valid).toBe(false);
});
it('fails on additional top level properties', () => {
const { valid, errors } = validate({
insomniaConfig: '1.0.0',
Settings: {},
});
expect(errors).toMatchObject([
{
instancePath: '',
keyword: 'additionalProperties',
message: 'must NOT have additional properties',
params: {
additionalProperty: 'Settings',
},
schemaPath: '#/additionalProperties',
},
]);
expect(valid).toBe(false);
});
it('fails on additional settings properties', () => {
const { valid, errors } = validate({
insomniaConfig: '1.0.0',
settings: {
disableAnalytics: true,
},
});
expect(errors).toMatchObject([
{
instancePath: '/settings',
keyword: 'additionalProperties',
message: 'must NOT have additional properties',
params: {
additionalProperty: 'disableAnalytics',
},
schemaPath: '#/definitions/Partial<Pick<Settings,\"allowNotificationRequests\"|\"disableUpdateNotification\"|\"enableAnalytics\"|\"disablePaidFeatureAds\"|\"incognitoMode\">>/additionalProperties',
},
]);
expect(valid).toBe(false);
});
it('fails on wrong property type', () => {
const { valid, errors } = validate({
insomniaConfig: '1.0.0',
settings: {
enableAnalytics: 'Ziltoid',
},
});
expect(errors).toMatchObject([
{
instancePath: '/settings/enableAnalytics',
keyword: 'type',
message: 'must be boolean',
params: {
'type': 'boolean',
},
schemaPath: '#/definitions/Partial<Pick<Settings,\"allowNotificationRequests\"|\"disableUpdateNotification\"|\"enableAnalytics\"|\"disablePaidFeatureAds\"|\"incognitoMode\">>/properties/enableAnalytics/type',
},
]);
expect(valid).toBe(false);
});
});

View File

@@ -0,0 +1,60 @@
import Ajv, { ErrorObject } from 'ajv';
import { InsomniaConfig } from './entities';
import schema from './generated/schemas/insomnia.schema.json';
const ajv = new Ajv({
allErrors: true,
verbose: true,
});
export const ingest = (input: string | InsomniaConfig | unknown) => {
if (typeof input === 'string') {
try {
return JSON.parse(input) as InsomniaConfig;
} catch (error: unknown) {
throw error;
}
}
return input;
};
export interface ValidResult {
valid: true;
errors: null;
humanError: null;
}
export interface ErrorResult {
valid: false;
errors: ErrorObject[];
}
export type ValidationResult = ValidResult | ErrorResult;
export const validate = (input: string | InsomniaConfig | unknown): ValidationResult => {
const data = ingest(input);
const validator = ajv.compile<InsomniaConfig>(schema);
const valid = validator(data);
if (valid) {
const validResult: ValidResult = {
valid: true,
errors: null,
humanError: null,
};
return validResult;
}
const { errors } = validator;
if (!errors) {
throw new Error('unable to validate json schema');
}
const errorResult: ErrorResult = {
valid: false,
errors,
};
return errorResult;
};

View File

@@ -0,0 +1,19 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"outDir": "dist",
"rootDir": "src",
"resolveJsonModule": true,
"strict": true,
},
"include": [
"src",
"**/*.schema.json",
"**/*.d.ts"
],
"exclude": [
"dist",
"**/*.test.ts",
],
}

View File

@@ -0,0 +1,16 @@
{
"extends": "./tsconfig.build.json",
"compilerOptions": {
"composite": false,
"rootDir": ".",
},
"include": [
"src",
"jest.config.js",
".eslintrc.js",
"**/*.d.ts"
],
"exclude": [
"dist",
],
}