mirror of
https://github.com/Kong/insomnia.git
synced 2026-10-27 09:09:14 -04:00
Fix Vim escape blocke
This commit is contained in:
commit
7f17494520
259 files changed
+36009
-11152
No files matched your search
+1
-1
@@ -9,7 +9,7 @@ test: off
|
||||
shallow_clone: true
|
||||
platform: x64
|
||||
environment:
|
||||
NODEJS_VERSION: "10"
|
||||
NODEJS_VERSION: '10'
|
||||
CSC_LINK: '%WINDOWS_CSC_LINK%'
|
||||
CSC_KEY_PASSWORD: '%WINDOWS_CSC_KEY_PASSWORD%'
|
||||
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
[*.{js,json,html,md}]
|
||||
[*.{js,json,html,md,graphql,gql}]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
end_of_line = lf
|
||||
end_of_line = lf
|
||||
+5
-1
@@ -23,4 +23,8 @@ node_modules/
|
||||
.idea
|
||||
.DS_Store
|
||||
*test-plugins
|
||||
.vscode
|
||||
.vscode
|
||||
graphql.config.json
|
||||
.cache
|
||||
.graphqlconfig
|
||||
schema.graphql
|
||||
@@ -1,4 +1,4 @@
|
||||
# Insomnia REST Client
|
||||
# Insomnia REST Client
|
||||
|
||||
[](https://insomnia.rest)
|
||||
[](https://travis-ci.org/getinsomnia/insomnia)
|
||||
@@ -12,26 +12,26 @@ Insomnia is a cross-platform _REST client_, built on top of [Electron](http://el
|
||||
|
||||
## Download
|
||||
|
||||
Insomnia is available for Mac, Windows, and Linux and can be downloaded
|
||||
Insomnia is available for Mac, Windows, and Linux and can be downloaded
|
||||
from the website.
|
||||
|
||||
**[https://insomnia.rest/download](https://insomnia.rest/download/)**
|
||||
|
||||
## Bugs and Feature Requests
|
||||
|
||||
Have a bug or a feature request? First, read the
|
||||
[issue guidelines](CONTRIBUTING.md#using-the-issue-tracker) and search for existing and
|
||||
Have a bug or a feature request? First, read the
|
||||
[issue guidelines](CONTRIBUTING.md#using-the-issue-tracker) and search for existing and
|
||||
closed issues. If your problem or idea is not addressed yet, [please open a new issue](/issues).
|
||||
|
||||
For more generic product questions and feedback, join the [Slack Team](https://chat.insomnia.rest) or email
|
||||
For more generic product questions and feedback, join the [Slack Team](https://chat.insomnia.rest) or email
|
||||
[support@insomnia.rest](mailto:support@insomnia.rest)
|
||||
|
||||
## Contributing
|
||||
|
||||
Please read through our [contributing guidelines](CONTRIBUTING.md) and [code of conduct](CODE_OF_CONDUCT.md). Included are directions
|
||||
Please read through our [contributing guidelines](CONTRIBUTING.md) and [code of conduct](CODE_OF_CONDUCT.md). Included are directions
|
||||
for opening issues, coding standards, and notes on development.
|
||||
|
||||
Editor preferences are available in the [editor config](.editorconfig) for easy use in
|
||||
Editor preferences are available in the [editor config](.editorconfig) for easy use in
|
||||
common text editors. Read more and download plugins at [editorconfig.org](http://editorconfig.org).
|
||||
|
||||
## Developing
|
||||
@@ -94,6 +94,7 @@ Here is a list of plugins available for installation via NPM.
|
||||
- [XDebug](https://www.npmjs.com/package/insomnia-plugin-xdebug) – Enable Xdebug debugging by adding an `XDEBUG_SESSION` cookie to the request
|
||||
|
||||
## Community Projects
|
||||
|
||||
- [GitHub API Spec Importer](https://github.com/swinton/github-rest-apis-for-insomnia) – A complete set of GitHub REST API route specifications that can be imported straight into Insomnia
|
||||
- [Swaggymnia](https://github.com/mlabouardy/swaggymnia) – Generate [Swagger](https://swagger.io/) documentation for your existing API in Insomnia.
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"lerna": "3.4.3",
|
||||
"lerna": "3.13.1",
|
||||
"version": "independent",
|
||||
"packages": ["plugins/*", "packages/*"]
|
||||
}
|
||||
Generated
+9311
-1729
File diff suppressed because it is too large.
Load diff
+22
-11
@@ -8,13 +8,13 @@
|
||||
"url": "https://github.com/getinsomnia/insomnia"
|
||||
},
|
||||
"scripts": {
|
||||
"bootstrap": "npm install && lerna bootstrap && lerna run bootstrap",
|
||||
"bootstrap": "npm install && lerna bootstrap && lerna run --parallel --stream bootstrap",
|
||||
"publish": "lerna publish",
|
||||
"clean": "lerna clean --yes && rimraf node_modules",
|
||||
"test": "lerna run test --stream",
|
||||
"app-start": "lerna run start --stream --scope=insomnia-app",
|
||||
"app-build": "lerna run build --stream --scope=insomnia-app",
|
||||
"app-package": "lerna run package --stream --scope=insomnia-app",
|
||||
"test": "lerna run --stream --parallel test",
|
||||
"app-start": "lerna run start --stream --parallel --bail",
|
||||
"app-build": "lerna run build --stream --parallel --bail",
|
||||
"app-package": "lerna run package --stream --parallel --bail",
|
||||
"format-code": "prettier --write \"**/*.js\""
|
||||
},
|
||||
"husky": {
|
||||
@@ -23,11 +23,22 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.4.3",
|
||||
"@babel/core": "^7.4.3",
|
||||
"@babel/plugin-proposal-decorators": "^7.4.0",
|
||||
"@babel/preset-env": "^7.4.3",
|
||||
"@babel/preset-flow": "^7.0.0",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"babel-eslint": "^8.2.3",
|
||||
"babel-jest": "^22.4.3",
|
||||
"babel-loader": "^8.0.5",
|
||||
"flow-bin": "^0.96.0",
|
||||
"husky": "^1.0.0-rc.9",
|
||||
"jest": "^23.6.0",
|
||||
"lerna": "^3.4.3",
|
||||
"prettier": "^1.13.6",
|
||||
"pretty-quick": "^1.6.0",
|
||||
"rimraf": "^2.6.2"
|
||||
}
|
||||
"jest": "^24.7.1",
|
||||
"lerna": "^3.13.1",
|
||||
"prettier": "^1.16.4",
|
||||
"pretty-quick": "^1.10.0",
|
||||
"rimraf": "^2.6.3"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
@@ -1,11 +1,16 @@
|
||||
{
|
||||
"presets": [
|
||||
"react",
|
||||
"flow"
|
||||
"@babel/preset-react",
|
||||
"@babel/preset-flow"
|
||||
],
|
||||
"plugins": [
|
||||
"transform-object-rest-spread",
|
||||
"transform-decorators-legacy"
|
||||
[
|
||||
"@babel/plugin-proposal-decorators",
|
||||
{
|
||||
"legacy": true
|
||||
}
|
||||
],
|
||||
["@babel/plugin-proposal-class-properties", { "loose" : true }]
|
||||
],
|
||||
"env": {
|
||||
"development": {
|
||||
@@ -16,13 +21,15 @@
|
||||
"test": {
|
||||
"presets": [
|
||||
[
|
||||
"env", {
|
||||
"targets": {
|
||||
"node": "8.4"
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"targets": {
|
||||
"node": "10"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"flow" // We need to add this again because it has to run before es2015
|
||||
// We need to add this again because it has to run before es2015
|
||||
"@babel/preset-flow"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,8 +36,10 @@
|
||||
"indent": "off",
|
||||
"no-var": "error",
|
||||
"no-duplicate-imports": "off",
|
||||
"flowtype/space-after-type-colon": "off",
|
||||
"react/jsx-uses-react": "error",
|
||||
"react/jsx-uses-vars": "error",
|
||||
"space-in-parens": "off",
|
||||
"space-before-function-paren": [
|
||||
"error",
|
||||
{
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import * as db from '../common/database';
|
||||
import * as models from '../models';
|
||||
import * as fetch from '../account/fetch';
|
||||
|
||||
export async function globalBeforeEach() {
|
||||
// Setup the local database in case it's used
|
||||
|
||||
fetch.setup('insomnia-tests', 'http://localhost:8000');
|
||||
await db.init(models.types(), { inMemoryOnly: true }, true);
|
||||
}
|
||||
@@ -9,14 +9,10 @@ module.exports = {
|
||||
jsbn: forge.jsbn,
|
||||
util: forge.util,
|
||||
pkcs5: {
|
||||
pbkdf2() {},
|
||||
pbkdf2: forge.pkcs5.pbkdf2,
|
||||
},
|
||||
md: {
|
||||
sha256: {
|
||||
create() {
|
||||
return 'TODO';
|
||||
},
|
||||
},
|
||||
sha256: forge.md.sha256,
|
||||
},
|
||||
rsa: {
|
||||
setPublicKey() {
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import * as crypt from '../crypt';
|
||||
|
||||
describe('crypt', () => {
|
||||
describe('deriveKey()', () => {
|
||||
it('derives a key properly', async () => {
|
||||
const result = await crypt.deriveKey('Password', 'email', 'salt');
|
||||
const expected = 'fb058595c02ae9660ed7098273bf50e49407942ecc437bf317638d76c4578eae';
|
||||
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('AES', () => {
|
||||
it('encrypts and decrypts', () => {
|
||||
const key = {
|
||||
kty: 'oct',
|
||||
alg: 'A256GCM',
|
||||
ext: true,
|
||||
key_ops: ['encrypt', 'decrypt'],
|
||||
k: '5hs1f2xuiNPHUp11i6SWlsqYpWe_hWPcEKucZlwBfFE',
|
||||
};
|
||||
|
||||
const resultEncrypted = crypt.encryptAES(key, 'Hello World!', 'additional data');
|
||||
const resultDecrypted = crypt.decryptAES(key, resultEncrypted);
|
||||
|
||||
expect(resultDecrypted).toEqual('Hello World!');
|
||||
});
|
||||
});
|
||||
|
||||
describe('AES Buffer', () => {
|
||||
it('encrypts and decrypts', () => {
|
||||
const key = {
|
||||
kty: 'oct',
|
||||
alg: 'A256GCM',
|
||||
ext: true,
|
||||
key_ops: ['encrypt', 'decrypt'],
|
||||
k: '5hs1f2xuiNPHUp11i6SWlsqYpWe_hWPcEKucZlwBfFE',
|
||||
};
|
||||
|
||||
const resultEncrypted = crypt.encryptAESBuffer(key, Buffer.from('Hello World!', 'utf8'));
|
||||
const resultDecrypted = crypt.decryptAESToBuffer(key, resultEncrypted);
|
||||
|
||||
expect(resultDecrypted).toEqual(Buffer.from('Hello World!', 'utf8'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('RSA', () => {
|
||||
it('encrypts and decrypts', () => {
|
||||
const privateKey = {
|
||||
alg: 'RSA-OAEP-256',
|
||||
kty: 'RSA',
|
||||
key_ops: ['decrypt'],
|
||||
ext: true,
|
||||
d:
|
||||
'UkouuQID2o9Q6VyiRMmK8ETPsAHWEL2HMYwy34c4nTpM7KfqlNeMzs6HmbuEfx-bwUvTqOO4Tz7FZw4ILD6s5sE9' +
|
||||
'xqIxmV-fIiqiBI4aWKozxgf9OJZWKqru3loSd923O3fI3oa9ZCTaKc1U0bYOB-XP2Q_hB2M64Hb63-McXAM0RQwN' +
|
||||
'R5vh_TweqcaBiXAyhuYl2NarOwrbSlSttkhZzy4i-otulPGkW61I5rNflsSmnYEijmD7zl9EouEOYcHlJGmNLHjG' +
|
||||
'nHlb-avvwvER5NZVDwd6vT61QR1wwpnYSjVH_Z_OrqJu8U2J64J_MaZPJog3KbPYqZnGcxJ9ldnSYQ',
|
||||
dp:
|
||||
'SnO3kTAogveLWkqSuDVxQLOo4QXEq-Us_lM00dfGIuZrfWnyqOd6_NJKu-G62PCQgUMBy7r_f3N3sOseRVl_5fy3' +
|
||||
'kd21n7WmnQcMGBm-VRathT0nOz-fhzbwCJwtuI1g36YtaCQEQiC4pYxMYmXaX1WGPhL4rGSNz5SMHrPvyTU',
|
||||
dq:
|
||||
'eaK2-w2Jb7rWWYhLon-RKlzWXTPHjnt1JfLkD_D12FNE6KAcbyIETDid8_sXnqj3oCr3HrO-AuNI87zZJ4UVsy2J' +
|
||||
'mvNNVFTn0s8R6TZ9_o9LCXBdBcRST5qpAax0t-duHRaKMuWPG3xAb2Uub6T42C7yd-mpOIRo7uSHFGQGzWk',
|
||||
e: 'AQAB',
|
||||
n:
|
||||
'5wOecCVWaDB2s-ybsCp1BskBW5fj3iH4xja5hNTfW_s-ERgbEoBA_PSF60PC1se4r_oWwFBfIRL9OUTYwYOuQOuC' +
|
||||
'rgd7ODa_YBeOjzHUA1b0K5kWXaqPxkmN8kJPISyLdLjCCOwHtTFnqxL9JjnU4aIxy4OU1S5KR6v-XVeLOZtUOm4k' +
|
||||
'AhLVfmdzYB1nZmq9xH1O8_acIUoWDbAWX6fIXhPhn7jCuCO4WZQVDxZ5_bc27UVhR4VYe2Our7aESUQ5ZyMtYNym' +
|
||||
'o9Oy0y_m3OS6W_JR_feXBbxRCBuGf7fjnvV9ohx1ZqLpJFx9_xL7naoVCQhBDfVE31iYz3L6KTIhFQ',
|
||||
p:
|
||||
'_Pyx-puBM_BQubDg8BZsrssgmECHuieKEwlR19fckczS4dlgQVUemUTr-RqmItj7k-WMG7mWuRgbIrGO1sigpuDy' +
|
||||
'uKWkg3KyqoeIvgsaJV05xu8pneVblTXJSCtNMXvYMa6mPNYudUSy8-TlLyLg_w5lUnuA8Mq8xehzCsPrM-0',
|
||||
q:
|
||||
'6cPuxuW0zaMIVAJpOcLf5D0dA-mLXxmmJoMDodamM7t7_oGYgFR3Os9gtB5TYxsUjjRCWGkDy7ru8arcbTNCnNFd' +
|
||||
'PAayzTdWCW-GBs3xswggjmugGupjOnrtD-N1_fG040Ka8UqwMyI1lUapxaKhViR9TNYUz6EAOjxycf8MTMk',
|
||||
qi:
|
||||
'FMVx12_ioueu052xgFxWdIS_lImUGTrw8Iiw_kp-KKsONtofs91A5GVRtyg_wdXpG2qyomaet1hTlHhLnoI23L2a' +
|
||||
'EkQ87SokIpoR9lR8jfIRwLwKKXMc33_bRRQXvWop0yvTzmSGaC0gULcqj0OHiUR1u9Ver1ZvgGz2jh4mP_E',
|
||||
};
|
||||
|
||||
const publicKey = {
|
||||
alg: 'RSA-OAEP-256',
|
||||
kty: 'RSA',
|
||||
key_ops: ['encrypt'],
|
||||
e: 'AQAB',
|
||||
n:
|
||||
'5wOecCVWaDB2s-ybsCp1BskBW5fj3iH4xja5hNTfW_s-ERgbEoBA_PSF60PC1se4r_oWwFBfIRL9OUTYwYOuQO' +
|
||||
'uCrgd7ODa_YBeOjzHUA1b0K5kWXaqPxkmN8kJPISyLdLjCCOwHtTFnqxL9JjnU4aIxy4OU1S5KR6v-XVeLOZtU' +
|
||||
'Om4kAhLVfmdzYB1nZmq9xH1O8_acIUoWDbAWX6fIXhPhn7jCuCO4WZQVDxZ5_bc27UVhR4VYe2Our7aESUQ5Zy' +
|
||||
'MtYNymo9Oy0y_m3OS6W_JR_feXBbxRCBuGf7fjnvV9ohx1ZqLpJFx9_xL7naoVCQhBDfVE31iYz3L6KTIhFQ',
|
||||
};
|
||||
|
||||
const resultEncrypted = crypt.encryptRSAWithJWK(publicKey, 'aaaaaaaaa');
|
||||
const resultDecrypted = crypt.decryptRSAWithJWK(privateKey, resultEncrypted);
|
||||
|
||||
const expectedDecrypted = 'aaaaaaaaa';
|
||||
|
||||
expect(resultDecrypted.toString()).toEqual(expectedDecrypted);
|
||||
});
|
||||
});
|
||||
});
|
||||
+62
-25
@@ -1,6 +1,6 @@
|
||||
import HKDF from 'hkdf';
|
||||
import srp from 'srp-js';
|
||||
import * as forge from 'node-forge';
|
||||
import forge from 'node-forge';
|
||||
|
||||
const DEFAULT_BYTE_LENGTH = 32;
|
||||
const DEFAULT_PBKDF2_ITERATIONS = 1e5; // 100,000
|
||||
@@ -64,9 +64,32 @@ export function decryptRSAWithJWK(privateJWK, encryptedBlob) {
|
||||
return decodeURIComponent(decrypted);
|
||||
}
|
||||
|
||||
export function recryptRSAWithJWK(privateJWK, publicJWK, encryptedBlob) {
|
||||
const decrypted = decryptRSAWithJWK(privateJWK, encryptedBlob);
|
||||
return encryptRSAWithJWK(publicJWK, decrypted);
|
||||
/**
|
||||
* Encrypt data using symmetric key
|
||||
*
|
||||
* @param jwkOrKey JWK or string representing symmetric key
|
||||
* @param buff data to encrypt
|
||||
* @param additionalData any additional public data to attach
|
||||
* @returns {{iv, t, d, ad}}
|
||||
*/
|
||||
export function encryptAESBuffer(jwkOrKey, buff, additionalData = '') {
|
||||
// TODO: Add assertion checks for JWK
|
||||
const rawKey = typeof jwkOrKey === 'string' ? jwkOrKey : _b64UrlToHex(jwkOrKey.k);
|
||||
const key = forge.util.hexToBytes(rawKey);
|
||||
|
||||
const iv = forge.random.getBytesSync(12);
|
||||
const cipher = forge.cipher.createCipher('AES-GCM', key);
|
||||
|
||||
cipher.start({ additionalData, iv, tagLength: 128 });
|
||||
cipher.update(forge.util.createBuffer(buff));
|
||||
cipher.finish();
|
||||
|
||||
return {
|
||||
iv: forge.util.bytesToHex(iv),
|
||||
t: forge.util.bytesToHex(cipher.mode.tag),
|
||||
ad: forge.util.bytesToHex(additionalData),
|
||||
d: forge.util.bytesToHex(cipher.output),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -95,8 +118,8 @@ export function encryptAES(jwkOrKey, plaintext, additionalData = '') {
|
||||
return {
|
||||
iv: forge.util.bytesToHex(iv),
|
||||
t: forge.util.bytesToHex(cipher.mode.tag),
|
||||
d: forge.util.bytesToHex(cipher.output),
|
||||
ad: forge.util.bytesToHex(additionalData),
|
||||
d: forge.util.bytesToHex(cipher.output),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -104,10 +127,10 @@ export function encryptAES(jwkOrKey, plaintext, additionalData = '') {
|
||||
* Decrypt AES using a key
|
||||
*
|
||||
* @param jwkOrKey JWK or string representing symmetric key
|
||||
* @param message
|
||||
* @param encryptedResult encryption data
|
||||
* @returns String
|
||||
*/
|
||||
export function decryptAES(jwkOrKey, message) {
|
||||
export function decryptAES(jwkOrKey, encryptedResult) {
|
||||
// TODO: Add assertion checks for JWK
|
||||
const rawKey = typeof jwkOrKey === 'string' ? jwkOrKey : _b64UrlToHex(jwkOrKey.k);
|
||||
const key = forge.util.hexToBytes(rawKey);
|
||||
@@ -118,13 +141,13 @@ export function decryptAES(jwkOrKey, message) {
|
||||
|
||||
const decipher = forge.cipher.createDecipher('AES-GCM', key);
|
||||
decipher.start({
|
||||
iv: forge.util.hexToBytes(message.iv),
|
||||
tagLength: message.t.length * 4,
|
||||
tag: forge.util.hexToBytes(message.t),
|
||||
additionalData: forge.util.hexToBytes(message.ad),
|
||||
iv: forge.util.hexToBytes(encryptedResult.iv),
|
||||
tagLength: encryptedResult.t.length * 4,
|
||||
tag: forge.util.hexToBytes(encryptedResult.t),
|
||||
additionalData: forge.util.hexToBytes(encryptedResult.ad),
|
||||
});
|
||||
|
||||
decipher.update(forge.util.createBuffer(forge.util.hexToBytes(message.d)));
|
||||
decipher.update(forge.util.createBuffer(forge.util.hexToBytes(encryptedResult.d)));
|
||||
|
||||
if (decipher.finish()) {
|
||||
return decodeURIComponent(decipher.output.toString());
|
||||
@@ -134,21 +157,35 @@ export function decryptAES(jwkOrKey, message) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random salt in hex
|
||||
*
|
||||
* Decrypts AES using a key to buffer
|
||||
* @param jwkOrKey
|
||||
* @param encryptedResult
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getRandomHex(bytes = DEFAULT_BYTE_LENGTH) {
|
||||
return forge.util.bytesToHex(forge.random.getBytesSync(bytes));
|
||||
}
|
||||
export function decryptAESToBuffer(jwkOrKey, encryptedResult) {
|
||||
// TODO: Add assertion checks for JWK
|
||||
const rawKey = typeof jwkOrKey === 'string' ? jwkOrKey : _b64UrlToHex(jwkOrKey.k);
|
||||
const key = forge.util.hexToBytes(rawKey);
|
||||
|
||||
/**
|
||||
* Generate a random account Id
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
export function generateAccountId() {
|
||||
return `act_${getRandomHex(DEFAULT_BYTE_LENGTH)}`;
|
||||
// ~~~~~~~~~~~~~~~~~~~~ //
|
||||
// Decrypt with AES-GCM //
|
||||
// ~~~~~~~~~~~~~~~~~~~~ //
|
||||
|
||||
const decipher = forge.cipher.createDecipher('AES-GCM', key);
|
||||
decipher.start({
|
||||
iv: forge.util.hexToBytes(encryptedResult.iv),
|
||||
tagLength: encryptedResult.t.length * 4,
|
||||
tag: forge.util.hexToBytes(encryptedResult.t),
|
||||
additionalData: forge.util.hexToBytes(encryptedResult.ad),
|
||||
});
|
||||
|
||||
decipher.update(forge.util.createBuffer(forge.util.hexToBytes(encryptedResult.d)));
|
||||
|
||||
if (decipher.finish()) {
|
||||
return Buffer.from(forge.util.bytesToHex(decipher.output), 'hex');
|
||||
} else {
|
||||
throw new Error('Failed to decrypt data');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -264,7 +301,7 @@ export async function generateKeyPairJWK() {
|
||||
* @param rawEmail
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function _hkdfSalt(rawSalt, rawEmail) {
|
||||
async function _hkdfSalt(rawSalt, rawEmail) {
|
||||
return new Promise(resolve => {
|
||||
const hkdf = new HKDF('sha256', rawSalt, rawEmail);
|
||||
hkdf.derive('', DEFAULT_BYTE_LENGTH, buffer => resolve(buffer.toString('hex')));
|
||||
@@ -0,0 +1,104 @@
|
||||
import { delay } from '../common/misc';
|
||||
import { parse as urlParse } from 'url';
|
||||
import zlib from 'zlib';
|
||||
|
||||
let _userAgent = '';
|
||||
let _baseUrl = '';
|
||||
let _commandListeners = [];
|
||||
|
||||
export function setup(userAgent, baseUrl) {
|
||||
_userAgent = userAgent;
|
||||
_baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
export function onCommand(callback) {
|
||||
_commandListeners.push(callback);
|
||||
}
|
||||
|
||||
export async function post(path, obj, sessionId, compressBody = false) {
|
||||
return _fetch('POST', path, obj, sessionId, compressBody);
|
||||
}
|
||||
|
||||
export async function put(path, obj, sessionId, compressBody = false) {
|
||||
return _fetch('PUT', path, obj, sessionId, compressBody);
|
||||
}
|
||||
|
||||
export async function get(path, sessionId) {
|
||||
return _fetch('GET', path, null, sessionId);
|
||||
}
|
||||
|
||||
async function _fetch(method, path, obj, sessionId, compressBody = false, retries = 0) {
|
||||
if (sessionId === undefined) {
|
||||
throw new Error(`No session ID provided to ${method}:${path}`);
|
||||
}
|
||||
|
||||
const config = {
|
||||
method: method,
|
||||
headers: {},
|
||||
};
|
||||
|
||||
// Set some client information
|
||||
if (_userAgent) {
|
||||
config.headers['X-Insomnia-Client'] = _userAgent;
|
||||
}
|
||||
|
||||
if (obj && compressBody) {
|
||||
config.body = zlib.gzipSync(JSON.stringify(obj));
|
||||
config.headers['Content-Type'] = 'application/json';
|
||||
config.headers['Content-Encoding'] = 'gzip';
|
||||
} else if (obj) {
|
||||
config.body = JSON.stringify(obj);
|
||||
config.headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
|
||||
if (sessionId) {
|
||||
config.headers['X-Session-Id'] = sessionId;
|
||||
}
|
||||
|
||||
let response;
|
||||
const url = _getUrl(path);
|
||||
try {
|
||||
response = await window.fetch(url, config);
|
||||
|
||||
// Exponential backoff for 502 errors
|
||||
if (response.status === 502 && retries < 5) {
|
||||
retries++;
|
||||
await delay(retries * 200);
|
||||
return this._fetch(method, path, obj, sessionId, compressBody, retries);
|
||||
}
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to fetch '${url}'`);
|
||||
}
|
||||
const uri = response.headers.get('x-insomnia-command');
|
||||
uri && _notifyCommandListeners(uri);
|
||||
|
||||
if (!response.ok) {
|
||||
const err = new Error(`Response ${response.status} for ${path}`);
|
||||
err.message = await response.text();
|
||||
err.statusCode = response.status;
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (response.headers.get('content-type') === 'application/json' || path.match(/\.json$/)) {
|
||||
return response.json();
|
||||
} else {
|
||||
return response.text();
|
||||
}
|
||||
}
|
||||
|
||||
function _getUrl(path) {
|
||||
if (!_baseUrl) {
|
||||
throw new Error('API base URL not configured!');
|
||||
}
|
||||
|
||||
return `${_baseUrl}${path}`;
|
||||
}
|
||||
|
||||
function _notifyCommandListeners(uri) {
|
||||
const parsed = urlParse(uri, true);
|
||||
|
||||
const command = `${parsed.hostname}${parsed.pathname}`;
|
||||
const args = JSON.parse(JSON.stringify(parsed.query));
|
||||
|
||||
_commandListeners.map(fn => fn(command, args));
|
||||
}
|
||||
+83
-135
@@ -1,6 +1,22 @@
|
||||
import srp from 'srp-js';
|
||||
import * as srp from 'srp-js';
|
||||
import * as crypt from './crypt';
|
||||
import * as util from '../common/fetch';
|
||||
import * as fetch from './fetch';
|
||||
|
||||
const loginCallbacks = [];
|
||||
|
||||
function _callCallbacks() {
|
||||
const loggedIn = isLoggedIn();
|
||||
console.log('[session] Sync state changed loggedIn=' + loggedIn);
|
||||
for (const cb of loginCallbacks) {
|
||||
if (typeof cb === 'function') {
|
||||
cb(loggedIn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function onLoginLogout(callback) {
|
||||
loginCallbacks.push(callback);
|
||||
}
|
||||
|
||||
/** Create a new session for the user */
|
||||
export async function login(rawEmail, rawPassphrase) {
|
||||
@@ -15,7 +31,7 @@ export async function login(rawEmail, rawPassphrase) {
|
||||
// Fetch Salt and Submit A To Server //
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
|
||||
|
||||
const { saltKey, saltAuth } = await util.post('/auth/login-s', { email });
|
||||
const { saltKey, saltAuth } = await fetch.post('/auth/login-s', { email }, null);
|
||||
const authSecret = await crypt.deriveKey(passphrase, email, saltKey);
|
||||
const secret1 = await crypt.srpGenKey();
|
||||
const c = new srp.Client(
|
||||
@@ -26,10 +42,14 @@ export async function login(rawEmail, rawPassphrase) {
|
||||
Buffer.from(secret1, 'hex'),
|
||||
);
|
||||
const srpA = c.computeA().toString('hex');
|
||||
const { sessionStarterId, srpB } = await util.post('/auth/login-a', {
|
||||
srpA,
|
||||
email,
|
||||
});
|
||||
const { sessionStarterId, srpB } = await fetch.post(
|
||||
'/auth/login-a',
|
||||
{
|
||||
srpA,
|
||||
email,
|
||||
},
|
||||
null,
|
||||
);
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~~~~~ //
|
||||
// Compute and Submit M1 //
|
||||
@@ -37,10 +57,14 @@ export async function login(rawEmail, rawPassphrase) {
|
||||
|
||||
c.setB(Buffer.from(srpB, 'hex'));
|
||||
const srpM1 = c.computeM1().toString('hex');
|
||||
const { srpM2 } = await util.post('/auth/login-m1', {
|
||||
srpM1,
|
||||
sessionStarterId,
|
||||
});
|
||||
const { srpM2 } = await fetch.post(
|
||||
'/auth/login-m1',
|
||||
{
|
||||
srpM1,
|
||||
sessionStarterId,
|
||||
},
|
||||
null,
|
||||
);
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~ //
|
||||
// Verify Server Identity M2 //
|
||||
@@ -64,7 +88,7 @@ export async function login(rawEmail, rawPassphrase) {
|
||||
accountId,
|
||||
firstName,
|
||||
lastName,
|
||||
} = await whoami(sessionId);
|
||||
} = await _whoami(sessionId);
|
||||
|
||||
const derivedSymmetricKey = await crypt.deriveKey(passphrase, email, saltEnc);
|
||||
const symmetricKeyStr = await crypt.decryptAES(derivedSymmetricKey, JSON.parse(encSymmetricKey));
|
||||
@@ -80,83 +104,16 @@ export async function login(rawEmail, rawPassphrase) {
|
||||
JSON.parse(publicKey),
|
||||
JSON.parse(encPrivateKey),
|
||||
);
|
||||
}
|
||||
|
||||
export function syncCreateResourceGroup(parentResourceId, name, encSymmetricKey) {
|
||||
return util.post('/api/resource_groups', {
|
||||
parentResourceId,
|
||||
name,
|
||||
encSymmetricKey,
|
||||
});
|
||||
}
|
||||
|
||||
export function syncGetResourceGroup(id) {
|
||||
return util.get(`/api/resource_groups/${id}`);
|
||||
}
|
||||
|
||||
export function syncPull(body) {
|
||||
return util.post('/sync/pull', body);
|
||||
}
|
||||
|
||||
export function syncPush(body) {
|
||||
return util.post('/sync/push', body);
|
||||
}
|
||||
|
||||
export function syncResetData() {
|
||||
return util.post('/auth/reset');
|
||||
}
|
||||
|
||||
export function syncFixDupes(resourceGroupIds) {
|
||||
return util.post('/sync/fix-dupes', { ids: resourceGroupIds });
|
||||
}
|
||||
|
||||
export function unshareWithAllTeams(resourceGroupId) {
|
||||
return util.put(`/api/resource_groups/${resourceGroupId}/unshare`);
|
||||
}
|
||||
|
||||
export async function shareWithTeam(resourceGroupId, teamId, rawPassphrase) {
|
||||
// Ask the server what we need to do to invite the member
|
||||
const instructions = await util.post(`/api/resource_groups/${resourceGroupId}/share-a`, {
|
||||
teamId,
|
||||
});
|
||||
|
||||
// Compute keys necessary to invite the member
|
||||
const passPhrase = _sanitizePassphrase(rawPassphrase);
|
||||
const { email, saltEnc, encPrivateKey, encSymmetricKey } = await whoami();
|
||||
const secret = await crypt.deriveKey(passPhrase, email, saltEnc);
|
||||
let symmetricKey;
|
||||
try {
|
||||
symmetricKey = crypt.decryptAES(secret, JSON.parse(encSymmetricKey));
|
||||
} catch (err) {
|
||||
throw new Error('Invalid password');
|
||||
}
|
||||
const privateKey = crypt.decryptAES(JSON.parse(symmetricKey), JSON.parse(encPrivateKey));
|
||||
const privateKeyJWK = JSON.parse(privateKey);
|
||||
const resourceGroupSymmetricKey = crypt.decryptRSAWithJWK(
|
||||
privateKeyJWK,
|
||||
instructions.encSymmetricKey,
|
||||
);
|
||||
|
||||
// Build the invite data request
|
||||
const newKeys = {};
|
||||
for (const accountId of Object.keys(instructions.keys)) {
|
||||
const accountPublicKeyJWK = JSON.parse(instructions.keys[accountId]);
|
||||
newKeys[accountId] = crypt.encryptRSAWithJWK(accountPublicKeyJWK, resourceGroupSymmetricKey);
|
||||
}
|
||||
|
||||
// Actually share it with the team
|
||||
await util.post(`/api/resource_groups/${resourceGroupId}/share-b`, {
|
||||
teamId,
|
||||
keys: newKeys,
|
||||
});
|
||||
_callCallbacks();
|
||||
}
|
||||
|
||||
export function getPublicKey() {
|
||||
return getSessionData().publicKey;
|
||||
return _getSessionData().publicKey;
|
||||
}
|
||||
|
||||
export function getPrivateKey() {
|
||||
const { symmetricKey, encPrivateKey } = getSessionData();
|
||||
const { symmetricKey, encPrivateKey } = _getSessionData();
|
||||
const privateKeyStr = crypt.decryptAES(symmetricKey, encPrivateKey);
|
||||
return JSON.parse(privateKeyStr);
|
||||
}
|
||||
@@ -165,39 +122,39 @@ export function getCurrentSessionId() {
|
||||
if (window) {
|
||||
return window.localStorage.getItem('currentSessionId');
|
||||
} else {
|
||||
return false;
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export function getAccountId() {
|
||||
return getSessionData().accountId;
|
||||
return _getSessionData().accountId;
|
||||
}
|
||||
|
||||
export function getEmail() {
|
||||
return getSessionData().email;
|
||||
return _getSessionData().email;
|
||||
}
|
||||
|
||||
export function getFirstName() {
|
||||
return getSessionData().firstName;
|
||||
return _getSessionData().firstName;
|
||||
}
|
||||
|
||||
export function getFullName() {
|
||||
const { firstName, lastName } = getSessionData();
|
||||
return `${firstName || ''} ${lastName || ''}`.trim();
|
||||
/** Check if we (think) we have a session */
|
||||
export function isLoggedIn() {
|
||||
return !!getCurrentSessionId();
|
||||
}
|
||||
|
||||
/**
|
||||
* get Data about the current session
|
||||
* @returns Object
|
||||
*/
|
||||
export function getSessionData() {
|
||||
const sessionId = getCurrentSessionId();
|
||||
if (!sessionId || !window) {
|
||||
return {};
|
||||
/** Log out and delete session data */
|
||||
export async function logout() {
|
||||
try {
|
||||
await fetch.post('/auth/logout', null, getCurrentSessionId());
|
||||
} catch (e) {
|
||||
// Not a huge deal if this fails, but we don't want it to prevent the
|
||||
// user from signing out.
|
||||
console.warn('Failed to logout', e);
|
||||
}
|
||||
|
||||
const dataStr = window.localStorage.getItem(getSessionKey(sessionId));
|
||||
return JSON.parse(dataStr);
|
||||
_unsetSessionData();
|
||||
_callCallbacks();
|
||||
}
|
||||
|
||||
/** Set data for the new session and store it encrypted with the sessionId */
|
||||
@@ -222,57 +179,48 @@ export function setSessionData(
|
||||
lastName: lastName,
|
||||
});
|
||||
|
||||
window.localStorage.setItem(getSessionKey(sessionId), dataStr);
|
||||
window.localStorage.setItem(_getSessionKey(sessionId), dataStr);
|
||||
|
||||
// NOTE: We're setting this last because the stuff above might fail
|
||||
window.localStorage.setItem('currentSessionId', sessionId);
|
||||
}
|
||||
|
||||
/** Unset the session data (log out) */
|
||||
export function unsetSessionData() {
|
||||
const sessionId = getCurrentSessionId();
|
||||
window.localStorage.removeItem(getSessionKey(sessionId));
|
||||
window.localStorage.removeItem(`currentSessionId`);
|
||||
}
|
||||
|
||||
/** Check if we (think) we have a session */
|
||||
export function isLoggedIn() {
|
||||
return getCurrentSessionId();
|
||||
}
|
||||
|
||||
/** Log out and delete session data */
|
||||
export async function logout() {
|
||||
try {
|
||||
await util.post('/auth/logout');
|
||||
} catch (e) {
|
||||
// Not a huge deal if this fails, but we don't want it to prevent the
|
||||
// user from signing out.
|
||||
console.warn('Failed to logout', e);
|
||||
}
|
||||
|
||||
unsetSessionData();
|
||||
}
|
||||
|
||||
export async function listTeams() {
|
||||
return util.get('/api/teams');
|
||||
return fetch.get('/api/teams', getCurrentSessionId());
|
||||
}
|
||||
|
||||
export async function endTrial() {
|
||||
await util.put('/api/billing/end-trial');
|
||||
}
|
||||
|
||||
export function whoami(sessionId = null) {
|
||||
return util.get('/auth/whoami', sessionId);
|
||||
}
|
||||
|
||||
export function getSessionKey(sessionId) {
|
||||
return `session__${(sessionId || '').slice(0, 10)}`;
|
||||
await fetch.put('/api/billing/end-trial', getCurrentSessionId());
|
||||
}
|
||||
|
||||
// ~~~~~~~~~~~~~~~~ //
|
||||
// Helper Functions //
|
||||
// ~~~~~~~~~~~~~~~~ //
|
||||
|
||||
function _whoami(sessionId = null) {
|
||||
return fetch.get('/auth/whoami', sessionId || getCurrentSessionId());
|
||||
}
|
||||
|
||||
function _getSessionData() {
|
||||
const sessionId = getCurrentSessionId();
|
||||
if (!sessionId || !window) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const dataStr = window.localStorage.getItem(_getSessionKey(sessionId));
|
||||
return JSON.parse(dataStr);
|
||||
}
|
||||
|
||||
function _unsetSessionData() {
|
||||
const sessionId = getCurrentSessionId();
|
||||
window.localStorage.removeItem(_getSessionKey(sessionId));
|
||||
window.localStorage.removeItem(`currentSessionId`);
|
||||
}
|
||||
|
||||
function _getSessionKey(sessionId) {
|
||||
return `session__${(sessionId || '').slice(0, 10)}`;
|
||||
}
|
||||
|
||||
function _getSrpParams() {
|
||||
return srp.params[2048];
|
||||
}
|
||||
@@ -2,10 +2,11 @@ import * as models from '../../models';
|
||||
import * as importUtil from '../import';
|
||||
import { getAppVersion } from '../constants';
|
||||
import { globalBeforeEach } from '../../__jest__/before-each';
|
||||
import YAML from 'yaml';
|
||||
|
||||
describe('exportHAR()', () => {
|
||||
describe('exportWorkspacesHAR() and exportRequestsHAR()', () => {
|
||||
beforeEach(globalBeforeEach);
|
||||
it('exports a single workspace as an HTTP Archive', async () => {
|
||||
it('exports a single workspace and some requests only as an HTTP Archive', async () => {
|
||||
const wrk1 = await models.workspace.create({
|
||||
_id: 'wrk_1',
|
||||
name: 'Workspace 1',
|
||||
@@ -52,10 +53,11 @@ describe('exportHAR()', () => {
|
||||
});
|
||||
|
||||
const includePrivateDocs = true;
|
||||
const json = await importUtil.exportHAR(wrk1, includePrivateDocs);
|
||||
const data = JSON.parse(json);
|
||||
|
||||
expect(data).toMatchObject({
|
||||
// Test export whole workspace.
|
||||
const exportWorkspacesJson = await importUtil.exportWorkspacesHAR(wrk1, includePrivateDocs);
|
||||
const exportWorkspacesData = JSON.parse(exportWorkspacesJson);
|
||||
expect(exportWorkspacesData).toMatchObject({
|
||||
log: {
|
||||
entries: [
|
||||
{
|
||||
@@ -70,7 +72,24 @@ describe('exportHAR()', () => {
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(data.log.entries.length).toBe(2);
|
||||
expect(exportWorkspacesData.log.entries.length).toBe(2);
|
||||
|
||||
// Test export some requests only.
|
||||
const exportRequestsJson = await importUtil.exportRequestsHAR([req1], includePrivateDocs);
|
||||
const exportRequestsData = JSON.parse(exportRequestsJson);
|
||||
expect(exportRequestsData).toMatchObject({
|
||||
log: {
|
||||
entries: [
|
||||
{
|
||||
request: {
|
||||
headers: [{ name: 'X-Environment', value: 'private1' }],
|
||||
},
|
||||
comment: req1.name,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(exportRequestsData.log.entries.length).toBe(1);
|
||||
});
|
||||
it('exports all workspaces as an HTTP Archive', async () => {
|
||||
const wrk1 = await models.workspace.create({
|
||||
@@ -132,39 +151,47 @@ describe('exportHAR()', () => {
|
||||
});
|
||||
|
||||
const includePrivateDocs = false;
|
||||
const json = await importUtil.exportHAR(null, includePrivateDocs);
|
||||
const json = await importUtil.exportWorkspacesHAR(null, includePrivateDocs);
|
||||
const data = JSON.parse(json);
|
||||
|
||||
expect(data).toMatchObject({
|
||||
log: {
|
||||
entries: [
|
||||
{
|
||||
request: {
|
||||
entries: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
request: expect.objectContaining({
|
||||
headers: [{ name: 'X-Environment', value: 'public1' }],
|
||||
},
|
||||
}),
|
||||
comment: 'Request 1',
|
||||
},
|
||||
{
|
||||
request: {
|
||||
}),
|
||||
expect.objectContaining({
|
||||
request: expect.objectContaining({
|
||||
headers: [{ name: 'X-Environment', value: 'base2' }],
|
||||
},
|
||||
}),
|
||||
comment: 'Request 2',
|
||||
},
|
||||
],
|
||||
}),
|
||||
]),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('exportJSON()', () => {
|
||||
describe('export', () => {
|
||||
beforeEach(globalBeforeEach);
|
||||
it('exports all workspaces', async () => {
|
||||
it('exports all workspaces and some requests only', async () => {
|
||||
const w = await models.workspace.create({ name: 'Workspace' });
|
||||
const jar = await models.cookieJar.getOrCreateForParentId(w._id);
|
||||
const r1 = await models.request.create({
|
||||
name: 'Request',
|
||||
name: 'Request 1',
|
||||
parentId: w._id,
|
||||
});
|
||||
const f2 = await models.requestGroup.create({
|
||||
name: 'Folder 2',
|
||||
parentId: w._id,
|
||||
});
|
||||
const r2 = await models.request.create({
|
||||
name: 'Request 2',
|
||||
parentId: f2._id,
|
||||
});
|
||||
const eBase = await models.environment.getOrCreateForWorkspace(w);
|
||||
const ePub = await models.environment.create({
|
||||
name: 'Public',
|
||||
@@ -176,18 +203,56 @@ describe('exportJSON()', () => {
|
||||
parentId: eBase._id,
|
||||
});
|
||||
|
||||
const json = await importUtil.exportJSON();
|
||||
const data = JSON.parse(json);
|
||||
// Test export whole workspace.
|
||||
const exportedWorkspacesJson = await importUtil.exportWorkspacesData(null, false, 'json');
|
||||
const exportedWorkspacesYaml = await importUtil.exportWorkspacesData(null, false, 'yaml');
|
||||
const exportWorkspacesDataJson = JSON.parse(exportedWorkspacesJson);
|
||||
const exportWorkspacesDataYaml = YAML.parse(exportedWorkspacesYaml);
|
||||
|
||||
expect(data._type).toBe('export');
|
||||
expect(data.__export_format).toBe(3);
|
||||
expect(data.__export_date).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/);
|
||||
expect(data.__export_source).toBe(`insomnia.desktop.app:v${getAppVersion()}`);
|
||||
expect(data.resources[0]._id).toBe(w._id);
|
||||
expect(data.resources[1]._id).toBe(eBase._id);
|
||||
expect(data.resources[2]._id).toBe(jar._id);
|
||||
expect(data.resources[3]._id).toBe(r1._id);
|
||||
expect(data.resources[4]._id).toBe(ePub._id);
|
||||
expect(data.resources.length).toBe(5);
|
||||
// Ensure JSON is the same as YAML
|
||||
expect(exportWorkspacesDataJson.resources).toEqual(exportWorkspacesDataYaml.resources);
|
||||
|
||||
expect(exportWorkspacesDataJson).toMatchObject({
|
||||
_type: 'export',
|
||||
__export_format: 4,
|
||||
__export_date: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/),
|
||||
__export_source: `insomnia.desktop.app:v${getAppVersion()}`,
|
||||
resources: expect.arrayContaining([
|
||||
expect.objectContaining({ _id: w._id }),
|
||||
expect.objectContaining({ _id: eBase._id }),
|
||||
expect.objectContaining({ _id: jar._id }),
|
||||
expect.objectContaining({ _id: r1._id }),
|
||||
expect.objectContaining({ _id: f2._id }),
|
||||
expect.objectContaining({ _id: r2._id }),
|
||||
expect.objectContaining({ _id: ePub._id }),
|
||||
]),
|
||||
});
|
||||
expect(exportWorkspacesDataJson.resources.length).toBe(7);
|
||||
|
||||
// Test export some requests only.
|
||||
const exportRequestsJson = await importUtil.exportRequestsData([r1], false, 'json');
|
||||
const exportRequestsYaml = await importUtil.exportRequestsData([r1], false, 'yaml');
|
||||
const exportRequestsDataJSON = JSON.parse(exportRequestsJson);
|
||||
const exportRequestsDataYAML = YAML.parse(exportRequestsYaml);
|
||||
|
||||
expect(exportRequestsDataJSON).toMatchObject({
|
||||
_type: 'export',
|
||||
__export_format: 4,
|
||||
__export_date: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/),
|
||||
__export_source: `insomnia.desktop.app:v${getAppVersion()}`,
|
||||
resources: expect.arrayContaining([
|
||||
expect.objectContaining({ _id: w._id }),
|
||||
expect.objectContaining({ _id: eBase._id }),
|
||||
expect.objectContaining({ _id: jar._id }),
|
||||
expect.objectContaining({ _id: r1._id }),
|
||||
expect.objectContaining({ _id: ePub._id }),
|
||||
]),
|
||||
});
|
||||
|
||||
expect(exportRequestsDataJSON.resources.length).toBe(5);
|
||||
expect(exportRequestsDataYAML.resources.length).toBe(5);
|
||||
|
||||
// Ensure JSON and YAML are the same
|
||||
expect(exportRequestsDataJSON.resources).toEqual(exportRequestsDataYAML.resources);
|
||||
});
|
||||
});
|
||||
@@ -171,3 +171,25 @@ describe('fuzzyMatchAll()', () => {
|
||||
expect(misc.fuzzyMatchAll('wrong this ou', ['testing', 'this', 'out'])).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('chunkArray()', () => {
|
||||
it('works with exact divisor', () => {
|
||||
const chunks = misc.chunkArray([1, 2, 3, 4, 5, 6], 3);
|
||||
expect(chunks).toEqual([[1, 2, 3], [4, 5, 6]]);
|
||||
});
|
||||
|
||||
it('works with weird divisor', () => {
|
||||
const chunks = misc.chunkArray([1, 2, 3, 4, 5, 6], 4);
|
||||
expect(chunks).toEqual([[1, 2, 3, 4], [5, 6]]);
|
||||
});
|
||||
|
||||
it('works with empty', () => {
|
||||
const chunks = misc.chunkArray([], 4);
|
||||
expect(chunks).toEqual([]);
|
||||
});
|
||||
|
||||
it('works with less than one chunk', () => {
|
||||
const chunks = misc.chunkArray([1, 2], 4);
|
||||
expect(chunks).toEqual([[1, 2]]);
|
||||
});
|
||||
});
|
||||
@@ -57,7 +57,6 @@ export function getClientString() {
|
||||
// Global Stuff
|
||||
export const DB_PERSIST_INTERVAL = 1000 * 60 * 60 * 2; // Compact every couple hour
|
||||
export const DEBOUNCE_MILLIS = 100;
|
||||
export const MAX_RESPONSES = 20;
|
||||
export const REQUEST_TIME_TO_SHOW_COUNTER = 1; // Seconds
|
||||
export const GA_ID = 'UA-86416787-1';
|
||||
export const GA_LOCATION = 'https://desktop.insomnia.rest/';
|
||||
@@ -69,6 +68,12 @@ export const FLEXIBLE_URL_REGEX = /^(http|https):\/\/[\wàâäèéêëîïôóœ
|
||||
export const CHECK_FOR_UPDATES_INTERVAL = 1000 * 60 * 60 * 3; // 3 hours
|
||||
export const PLUGIN_PATH = path.join(getDataDirectory(), 'plugins');
|
||||
|
||||
// Available editor key maps
|
||||
export const EDITOR_KEY_MAP_DEFAULT = 'default';
|
||||
export const EDITOR_KEY_MAP_EMACS = 'emacs';
|
||||
export const EDITOR_KEY_MAP_SUBLIME = 'sublime';
|
||||
export const EDITOR_KEY_MAP_VIM = 'vim';
|
||||
|
||||
// Hotkeys
|
||||
export const MNEMONIC_SYM = isMac() ? '' : '&';
|
||||
export const CTRL_SYM = isMac() ? '⌃' : 'Ctrl';
|
||||
@@ -82,6 +87,9 @@ export const UPDATE_CHANNEL_BETA = 'beta';
|
||||
export const UPDATE_URL_MAC = 'https://updates.insomnia.rest/builds/check/mac';
|
||||
export const UPDATE_URL_WINDOWS = 'https://updates.insomnia.rest/updates/win';
|
||||
|
||||
// API
|
||||
export const API_BASE_URL = 'https://api.insomnia.rest';
|
||||
|
||||
// UI Stuff
|
||||
export const MAX_SIDEBAR_REMS = 45;
|
||||
export const MIN_SIDEBAR_REMS = 0.75;
|
||||
|
||||
@@ -77,8 +77,12 @@ export async function init(types: Array<string>, config: Object = {}, forceReset
|
||||
delete db._empty;
|
||||
|
||||
electron.ipcMain.on('db.fn', async (e, fnName, replyChannel, ...args) => {
|
||||
const result = await database[fnName](...args);
|
||||
e.sender.send(replyChannel, result);
|
||||
try {
|
||||
const result = await database[fnName](...args);
|
||||
e.sender.send(replyChannel, null, result);
|
||||
} catch (err) {
|
||||
e.sender.send(replyChannel, { message: err.message, stack: err.stack });
|
||||
}
|
||||
});
|
||||
|
||||
// NOTE: Only repair the DB if we're not running in memory. Repairing here causes tests to
|
||||
@@ -345,7 +349,13 @@ export const insert = (database.insert = async function<T: BaseModel>(
|
||||
if (db._empty) return _send('insert', ...arguments);
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const docWithDefaults = await models.initModel(doc.type, doc);
|
||||
let docWithDefaults;
|
||||
try {
|
||||
docWithDefaults = await models.initModel(doc.type, doc);
|
||||
} catch (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
db[doc.type].insert(docWithDefaults, (err, newDoc) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
@@ -366,7 +376,13 @@ export const update = (database.update = async function<T: BaseModel>(
|
||||
if (db._empty) return _send('update', ...arguments);
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const docWithDefaults = await models.initModel(doc.type, doc);
|
||||
let docWithDefaults;
|
||||
try {
|
||||
docWithDefaults = await models.initModel(doc.type, doc);
|
||||
} catch (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
db[doc.type].update({ _id: docWithDefaults._id }, docWithDefaults, err => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
@@ -433,6 +449,31 @@ export const removeWhere = (database.removeWhere = async function(
|
||||
await database.flushChanges(flushId);
|
||||
});
|
||||
|
||||
export const batchModifyDocs = (database.batchModifyDocs = async function(operations: {
|
||||
upsert: Array<Object>,
|
||||
remove: Array<Object>,
|
||||
}): Promise<void> {
|
||||
if (db._empty) return _send('batchModifyDocs', ...arguments);
|
||||
|
||||
const flushId = await bufferChanges();
|
||||
|
||||
const promisesUpserted = [];
|
||||
const promisesDeleted = [];
|
||||
for (const doc: BaseModel of operations.upsert) {
|
||||
promisesUpserted.push(upsert(doc, true));
|
||||
}
|
||||
|
||||
for (const doc: BaseModel of operations.remove) {
|
||||
promisesDeleted.push(unsafeRemove(doc, true));
|
||||
}
|
||||
|
||||
// Perform from least to most dangerous
|
||||
await Promise.all(promisesUpserted);
|
||||
await Promise.all(promisesDeleted);
|
||||
|
||||
await flushChanges(flushId);
|
||||
});
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~~~ //
|
||||
// DEFAULT MODEL STUFF //
|
||||
// ~~~~~~~~~~~~~~~~~~~ //
|
||||
@@ -595,8 +636,12 @@ async function _send<T>(fnName: string, ...args: Array<any>): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const replyChannel = `db.fn.reply:${uuid.v4()}`;
|
||||
electron.ipcRenderer.send('db.fn', fnName, replyChannel, ...args);
|
||||
electron.ipcRenderer.once(replyChannel, (e, result) => {
|
||||
resolve(result);
|
||||
electron.ipcRenderer.once(replyChannel, (e, err, result) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
import 'whatwg-fetch';
|
||||
import { parse as urlParse } from 'url';
|
||||
import { getClientString } from './constants';
|
||||
import * as session from '../sync/session';
|
||||
import * as zlib from 'zlib';
|
||||
|
||||
let commandListeners = [];
|
||||
|
||||
export function onCommand(callback) {
|
||||
commandListeners.push(callback);
|
||||
}
|
||||
|
||||
export function offCommand(callback) {
|
||||
commandListeners = commandListeners.filter(l => l !== callback);
|
||||
}
|
||||
|
||||
export function post(path, obj) {
|
||||
return _fetch('POST', path, obj);
|
||||
}
|
||||
|
||||
export function get(path, sessionId = null) {
|
||||
return _fetch('GET', path, null, sessionId);
|
||||
}
|
||||
|
||||
export function del(path, sessionId = null) {
|
||||
return _fetch('DELETE', path, null, sessionId);
|
||||
}
|
||||
|
||||
export function put(path, sessionId = null) {
|
||||
return _fetch('PUT', path, null, sessionId);
|
||||
}
|
||||
|
||||
export function rawFetch(...args) {
|
||||
return window.fetch(...args);
|
||||
}
|
||||
|
||||
async function _fetch(method, path, obj, sessionId = null) {
|
||||
const config = {
|
||||
method: method,
|
||||
headers: new window.Headers(),
|
||||
};
|
||||
|
||||
// Set some client information
|
||||
config.headers.set('X-Insomnia-Client', getClientString());
|
||||
|
||||
if (obj) {
|
||||
config.body = zlib.gzipSync(JSON.stringify(obj));
|
||||
config.headers.set('Content-Type', 'application/json');
|
||||
config.headers.set('Content-Encoding', 'gzip');
|
||||
}
|
||||
|
||||
sessionId = sessionId || session.getCurrentSessionId();
|
||||
if (sessionId) {
|
||||
config.headers.set('X-Session-Id', sessionId);
|
||||
}
|
||||
|
||||
const response = await window.fetch(_getUrl(path), config);
|
||||
const uri = response.headers.get('x-insomnia-command');
|
||||
uri && _notifyCommandListeners(uri);
|
||||
|
||||
if (!response.ok) {
|
||||
const err = new Error(`Response ${response.status} for ${path}`);
|
||||
err.message = await response.text();
|
||||
err.statusCode = response.status;
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (response.headers.get('content-type') === 'application/json' || path.match(/\.json$/)) {
|
||||
return response.json();
|
||||
} else {
|
||||
return response.text();
|
||||
}
|
||||
}
|
||||
|
||||
function _getUrl(path) {
|
||||
const baseUrl = process.env.INSOMNIA_SYNC_URL || 'https://api.insomnia.rest';
|
||||
return `${baseUrl}${path}`;
|
||||
}
|
||||
|
||||
function _notifyCommandListeners(uri) {
|
||||
const parsed = urlParse(uri, true);
|
||||
|
||||
const command = `${parsed.hostname}${parsed.pathname}`;
|
||||
const args = JSON.parse(JSON.stringify(parsed.query));
|
||||
|
||||
commandListeners.map(fn => fn(command, args));
|
||||
}
|
||||
@@ -441,21 +441,25 @@ function getResponseContent(response: ResponseModel): HarContent {
|
||||
}
|
||||
|
||||
function getResponseHeaders(response: ResponseModel): Array<HarHeader> {
|
||||
return response.headers.filter(header => header.name).map(h => {
|
||||
return {
|
||||
name: h.name,
|
||||
value: h.value,
|
||||
};
|
||||
});
|
||||
return response.headers
|
||||
.filter(header => header.name)
|
||||
.map(h => {
|
||||
return {
|
||||
name: h.name,
|
||||
value: h.value,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function getRequestHeaders(renderedRequest: RenderedRequest): Array<HarHeader> {
|
||||
return renderedRequest.headers.filter(header => header.name).map(header => {
|
||||
return {
|
||||
name: header.name,
|
||||
value: header.value,
|
||||
};
|
||||
});
|
||||
return renderedRequest.headers
|
||||
.filter(header => header.name)
|
||||
.map(header => {
|
||||
return {
|
||||
name: header.name,
|
||||
value: header.value,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function getRequestQueryString(renderedRequest: RenderedRequest): Array<HarQueryString> {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// @flow
|
||||
import { convert } from 'insomnia-importers';
|
||||
import clone from 'clone';
|
||||
import * as db from './database';
|
||||
import * as har from './har';
|
||||
import type { BaseModel } from '../models/index';
|
||||
@@ -7,13 +8,13 @@ import * as models from '../models/index';
|
||||
import { getAppVersion } from './constants';
|
||||
import { showModal } from '../ui/components/modals/index';
|
||||
import AlertModal from '../ui/components/modals/alert-modal';
|
||||
import * as fetch from './fetch';
|
||||
import fs from 'fs';
|
||||
import type { Workspace } from '../models/workspace';
|
||||
import type { Environment } from '../models/environment';
|
||||
import { fnOrString, generateId } from './misc';
|
||||
import YAML from 'yaml';
|
||||
|
||||
const EXPORT_FORMAT = 3;
|
||||
const EXPORT_FORMAT = 4;
|
||||
|
||||
const EXPORT_TYPE_REQUEST = 'request';
|
||||
const EXPORT_TYPE_REQUEST_GROUP = 'request_group';
|
||||
@@ -35,7 +36,7 @@ const MODELS = {
|
||||
export async function importUri(workspaceId: string | null, uri: string): Promise<void> {
|
||||
let rawText;
|
||||
if (uri.match(/^(http|https):\/\//)) {
|
||||
const response = await fetch.rawFetch(uri);
|
||||
const response = await window.fetch(uri);
|
||||
rawText = await response.text();
|
||||
} else if (uri.match(/^(file):\/\//)) {
|
||||
const path = uri.replace(/^(file):\/\//, '');
|
||||
@@ -188,52 +189,81 @@ export async function importRaw(
|
||||
};
|
||||
}
|
||||
|
||||
export async function exportHAR(
|
||||
export async function exportWorkspacesHAR(
|
||||
parentDoc: BaseModel | null = null,
|
||||
includePrivateDocs: boolean = false,
|
||||
): Promise<string> {
|
||||
let workspaces;
|
||||
if (parentDoc) {
|
||||
workspaces = [parentDoc];
|
||||
} else {
|
||||
workspaces = await models.workspace.all();
|
||||
const docs: Array<BaseModel> = await getDocWithDescendants(parentDoc, includePrivateDocs);
|
||||
const requests: Array<BaseModel> = docs.filter(doc => doc.type === models.request.type);
|
||||
return exportRequestsHAR(requests, includePrivateDocs);
|
||||
}
|
||||
|
||||
export async function exportRequestsHAR(
|
||||
requests: Array<BaseModel>,
|
||||
includePrivateDocs: boolean = false,
|
||||
): Promise<string> {
|
||||
const workspaces: Array<BaseModel> = [];
|
||||
const mapRequestIdToWorkspace: Object = {};
|
||||
const workspaceLookup: Object = {};
|
||||
for (const request of requests) {
|
||||
const ancestors: Array<BaseModel> = await db.withAncestors(request, [
|
||||
models.workspace.type,
|
||||
models.requestGroup.type,
|
||||
]);
|
||||
const workspace = ancestors.find(ancestor => ancestor.type === models.workspace.type);
|
||||
mapRequestIdToWorkspace[request._id] = workspace;
|
||||
if (workspace == null || workspaceLookup.hasOwnProperty(workspace._id)) {
|
||||
continue;
|
||||
}
|
||||
workspaceLookup[workspace._id] = true;
|
||||
workspaces.push(workspace);
|
||||
}
|
||||
|
||||
const workspaceEnvironmentLookup = {};
|
||||
for (let workspace of workspaces) {
|
||||
const mapWorkspaceIdToEnvironmentId: Object = {};
|
||||
for (const workspace of workspaces) {
|
||||
const workspaceMeta = await models.workspaceMeta.getByParentId(workspace._id);
|
||||
let environmentId = workspaceMeta ? workspaceMeta.activeEnvironmentId : null;
|
||||
const environment = await models.environment.getById(environmentId || 'n/a');
|
||||
if (!environment || (environment.isPrivate && !includePrivateDocs)) {
|
||||
environmentId = 'n/a';
|
||||
}
|
||||
workspaceEnvironmentLookup[workspace._id] = environmentId;
|
||||
mapWorkspaceIdToEnvironmentId[workspace._id] = environmentId;
|
||||
}
|
||||
|
||||
const requests = [];
|
||||
for (let workspace of workspaces) {
|
||||
const docs: Array<BaseModel> = await getDocWithDescendants(workspace, includePrivateDocs);
|
||||
const workspaceRequests = docs
|
||||
.filter(d => d.type === models.request.type)
|
||||
.sort((a: Object, b: Object) => (a.metaSortKey < b.metaSortKey ? -1 : 1))
|
||||
.map((request: BaseModel) => {
|
||||
return {
|
||||
requestId: request._id,
|
||||
environmentId: workspaceEnvironmentLookup[workspace._id],
|
||||
};
|
||||
});
|
||||
|
||||
requests.push(...workspaceRequests);
|
||||
requests = requests.sort((a: Object, b: Object) => (a.metaSortKey < b.metaSortKey ? -1 : 1));
|
||||
const harRequests: Array<Object> = [];
|
||||
for (const request of requests) {
|
||||
const workspace = mapRequestIdToWorkspace[request._id];
|
||||
if (workspace == null) {
|
||||
// Workspace not found for request, so don't export it.
|
||||
continue;
|
||||
}
|
||||
const environmentId = mapWorkspaceIdToEnvironmentId[workspace._id];
|
||||
harRequests.push({
|
||||
requestId: request._id,
|
||||
environmentId: environmentId,
|
||||
});
|
||||
}
|
||||
|
||||
const data = await har.exportHar(requests);
|
||||
const data = await har.exportHar(harRequests);
|
||||
|
||||
return JSON.stringify(data, null, '\t');
|
||||
}
|
||||
|
||||
export async function exportJSON(
|
||||
parentDoc: BaseModel | null = null,
|
||||
includePrivateDocs: boolean = false,
|
||||
export async function exportWorkspacesData(
|
||||
parentDoc: BaseModel | null,
|
||||
includePrivateDocs: boolean,
|
||||
format: 'json' | 'yaml',
|
||||
): Promise<string> {
|
||||
const docs: Array<BaseModel> = await getDocWithDescendants(parentDoc, includePrivateDocs);
|
||||
const requests: Array<BaseModel> = docs.filter(doc => doc.type === models.request.type);
|
||||
return exportRequestsData(requests, includePrivateDocs, format);
|
||||
}
|
||||
|
||||
export async function exportRequestsData(
|
||||
requests: Array<BaseModel>,
|
||||
includePrivateDocs: boolean,
|
||||
format: 'json' | 'yaml',
|
||||
): Promise<string> {
|
||||
const data = {
|
||||
_type: 'export',
|
||||
@@ -243,18 +273,49 @@ export async function exportJSON(
|
||||
resources: [],
|
||||
};
|
||||
|
||||
const docs: Array<BaseModel> = await getDocWithDescendants(parentDoc, includePrivateDocs);
|
||||
const docs: Array<BaseModel> = [];
|
||||
const workspaces: Array<BaseModel> = [];
|
||||
const mapTypeAndIdToDoc: Object = {};
|
||||
for (const req of requests) {
|
||||
const ancestors: Array<BaseModel> = clone(await db.withAncestors(req));
|
||||
for (const ancestor of ancestors) {
|
||||
const key = ancestor.type + '___' + ancestor._id;
|
||||
if (mapTypeAndIdToDoc.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
mapTypeAndIdToDoc[key] = ancestor;
|
||||
docs.push(ancestor);
|
||||
if (ancestor.type === models.workspace.type) {
|
||||
workspaces.push(ancestor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const workspace of workspaces) {
|
||||
const descendants: Array<BaseModel> = (await db.withDescendants(workspace)).filter(d => {
|
||||
// Only interested in these additional model types.
|
||||
return d.type === models.cookieJar.type || d.type === models.environment.type;
|
||||
});
|
||||
docs.push(...descendants);
|
||||
}
|
||||
|
||||
data.resources = docs
|
||||
.filter(
|
||||
d =>
|
||||
// Only export these model types
|
||||
d.type === models.request.type ||
|
||||
d.type === models.requestGroup.type ||
|
||||
d.type === models.workspace.type ||
|
||||
d.type === models.cookieJar.type ||
|
||||
d.type === models.environment.type,
|
||||
)
|
||||
.filter(d => {
|
||||
// Only export these model types.
|
||||
if (
|
||||
!(
|
||||
d.type === models.request.type ||
|
||||
d.type === models.requestGroup.type ||
|
||||
d.type === models.workspace.type ||
|
||||
d.type === models.cookieJar.type ||
|
||||
d.type === models.environment.type
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
// BaseModel doesn't have isPrivate, so cast it first.
|
||||
return !(d: Object).isPrivate || includePrivateDocs;
|
||||
})
|
||||
.map((d: Object) => {
|
||||
if (d.type === models.workspace.type) {
|
||||
d._type = EXPORT_TYPE_WORKSPACE;
|
||||
@@ -273,7 +334,13 @@ export async function exportJSON(
|
||||
return d;
|
||||
});
|
||||
|
||||
return JSON.stringify(data, null, '\t');
|
||||
if (format.toLowerCase() === 'yaml') {
|
||||
return YAML.stringify(data);
|
||||
} else if (format.toLowerCase() === 'json') {
|
||||
return JSON.stringify(data);
|
||||
} else {
|
||||
throw new Error(`Invalid export format ${format}. Must be "json" or "yaml"`);
|
||||
}
|
||||
}
|
||||
|
||||
async function getDocWithDescendants(
|
||||
|
||||
@@ -339,3 +339,12 @@ export function getDataDirectory(): string {
|
||||
const { app } = electron.remote || electron;
|
||||
return process.env.INSOMNIA_DATA_PATH || app.getPath('userData');
|
||||
}
|
||||
|
||||
export function chunkArray<T>(arr: Array<T>, chunkSize: number): Array<Array<T>> {
|
||||
const chunks = [];
|
||||
for (let i = 0, j = arr.length; i < j; i += chunkSize) {
|
||||
chunks.push(arr.slice(i, i + chunkSize));
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
@@ -80,8 +80,19 @@ function _launchApp() {
|
||||
});
|
||||
|
||||
// Called when second instance launched with args (Windows)
|
||||
app.makeSingleInstance(args => {
|
||||
args.length && window.send('run-command', args[0]);
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
|
||||
if (!gotTheLock) {
|
||||
console.error('[app] Failed to get instance lock');
|
||||
return;
|
||||
}
|
||||
|
||||
app.on('second-instance', (event, commandLine, workingDirectory) => {
|
||||
// Someone tried to run a second instance, we should focus our window.
|
||||
if (window) {
|
||||
if (window.isMinimized()) window.restore();
|
||||
window.focus();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle URLs when app already open
|
||||
|
||||
@@ -149,27 +149,27 @@ export function createWindow() {
|
||||
label: `${MNEMONIC_SYM}Actual Size`,
|
||||
accelerator: 'CmdOrCtrl+0',
|
||||
click: () => {
|
||||
const window = BrowserWindow.getFocusedWindow();
|
||||
if (!window || !window.webContents) {
|
||||
const w = BrowserWindow.getFocusedWindow();
|
||||
if (!w || !w.webContents) {
|
||||
return;
|
||||
}
|
||||
|
||||
const zoomFactor = 1;
|
||||
window.webContents.setZoomFactor(zoomFactor);
|
||||
w.webContents.setZoomFactor(zoomFactor);
|
||||
saveZoomFactor(zoomFactor);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: `Zoom ${MNEMONIC_SYM}In`,
|
||||
accelerator: isMac() ? 'CmdOrCtrl+Plus' : 'CmdOrCtrl+=',
|
||||
accelerator: 'CmdOrCtrl+=',
|
||||
click: () => {
|
||||
const window = BrowserWindow.getFocusedWindow();
|
||||
if (!window || !window.webContents) {
|
||||
const w = BrowserWindow.getFocusedWindow();
|
||||
if (!w || !w.webContents) {
|
||||
return;
|
||||
}
|
||||
|
||||
const zoomFactor = Math.min(1.8, getZoomFactor() + 0.05);
|
||||
window.webContents.setZoomFactor(zoomFactor);
|
||||
w.webContents.setZoomFactor(zoomFactor);
|
||||
|
||||
saveZoomFactor(zoomFactor);
|
||||
},
|
||||
@@ -178,27 +178,16 @@ export function createWindow() {
|
||||
label: `Zoom ${MNEMONIC_SYM}Out`,
|
||||
accelerator: 'CmdOrCtrl+-',
|
||||
click: () => {
|
||||
const window = BrowserWindow.getFocusedWindow();
|
||||
if (!window || !window.webContents) {
|
||||
const w = BrowserWindow.getFocusedWindow();
|
||||
if (!w || !w.webContents) {
|
||||
return;
|
||||
}
|
||||
|
||||
const zoomFactor = Math.max(0.5, getZoomFactor() - 0.05);
|
||||
window.webContents.setZoomFactor(zoomFactor);
|
||||
w.webContents.setZoomFactor(zoomFactor);
|
||||
saveZoomFactor(zoomFactor);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: `Toggle ${MNEMONIC_SYM}Sidebar`,
|
||||
click: () => {
|
||||
const window = BrowserWindow.getFocusedWindow();
|
||||
if (!window || !window.webContents) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.webContents.send('toggle-sidebar');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: `Toggle ${MNEMONIC_SYM}DevTools`,
|
||||
click: () => mainWindow.toggleDevTools(),
|
||||
@@ -228,16 +217,17 @@ export function createWindow() {
|
||||
},
|
||||
{
|
||||
label: `${MNEMONIC_SYM}Keyboard Shortcuts`,
|
||||
click: (menuItem, window, e) => {
|
||||
if (!window || !window.webContents) {
|
||||
accelerator: 'CmdOrCtrl+Shift+?',
|
||||
click: (menuItem, w, e) => {
|
||||
if (!w || !w.webContents) {
|
||||
return;
|
||||
}
|
||||
window.webContents.send('toggle-preferences-shortcuts');
|
||||
w.webContents.send('toggle-preferences-shortcuts');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: `Show App ${MNEMONIC_SYM}Data Folder`,
|
||||
click: (menuItem, window, e) => {
|
||||
click: (menuItem, w, e) => {
|
||||
const directory = misc.getDataDirectory();
|
||||
shell.showItemInFolder(directory);
|
||||
},
|
||||
@@ -317,12 +307,12 @@ export function createWindow() {
|
||||
{
|
||||
label: `${MNEMONIC_SYM}Reload Plugins`,
|
||||
click: () => {
|
||||
const window = BrowserWindow.getFocusedWindow();
|
||||
if (!window || !window.webContents) {
|
||||
const w = BrowserWindow.getFocusedWindow();
|
||||
if (!w || !w.webContents) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.webContents.send('reload-plugins');
|
||||
w.webContents.send('reload-plugins');
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -6,6 +6,7 @@ export const name = 'Client Certificate';
|
||||
export const type = 'ClientCertificate';
|
||||
export const prefix = 'crt';
|
||||
export const canDuplicate = true;
|
||||
export const canSync = false;
|
||||
|
||||
type BaseClientCertificate = {
|
||||
parentId: string,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// @flow
|
||||
import crypto from 'crypto';
|
||||
import * as db from '../common/database';
|
||||
import type { BaseModel } from './index';
|
||||
|
||||
@@ -6,6 +7,7 @@ export const name = 'Cookie Jar';
|
||||
export const type = 'CookieJar';
|
||||
export const prefix = 'jar';
|
||||
export const canDuplicate = true;
|
||||
export const canSync = false;
|
||||
|
||||
export type Cookie = {
|
||||
id: string,
|
||||
@@ -44,7 +46,7 @@ export function migrate(doc: CookieJar): CookieJar {
|
||||
return doc;
|
||||
}
|
||||
|
||||
export function create(patch: Object = {}) {
|
||||
export async function create(patch: Object = {}) {
|
||||
if (!patch.parentId) {
|
||||
throw new Error(`New CookieJar missing \`parentId\`: ${JSON.stringify(patch)}`);
|
||||
}
|
||||
@@ -55,21 +57,30 @@ export function create(patch: Object = {}) {
|
||||
export async function getOrCreateForParentId(parentId: string) {
|
||||
const cookieJars = await db.find(type, { parentId });
|
||||
if (cookieJars.length === 0) {
|
||||
return create({ parentId });
|
||||
return create({
|
||||
parentId,
|
||||
|
||||
// Deterministic ID. It helps reduce sync complexity since we won't have to
|
||||
// de-duplicate environments.
|
||||
_id: `${prefix}_${crypto
|
||||
.createHash('sha1')
|
||||
.update(parentId)
|
||||
.digest('hex')}`,
|
||||
});
|
||||
} else {
|
||||
return cookieJars[0];
|
||||
}
|
||||
}
|
||||
|
||||
export function all() {
|
||||
export async function all(): Promise<Array<BaseModel>> {
|
||||
return db.all(type);
|
||||
}
|
||||
|
||||
export function getById(id: string) {
|
||||
export async function getById(id: string) {
|
||||
return db.get(type, id);
|
||||
}
|
||||
|
||||
export function update(cookieJar: CookieJar, patch: Object = {}) {
|
||||
export async function update(cookieJar: CookieJar, patch: Object = {}) {
|
||||
return db.docUpdate(cookieJar, patch);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// @flow
|
||||
import * as crypto from 'crypto';
|
||||
import * as db from '../common/database';
|
||||
import type { BaseModel } from './index';
|
||||
import type { Workspace } from './workspace';
|
||||
@@ -7,6 +8,7 @@ export const name = 'Environment';
|
||||
export const type = 'Environment';
|
||||
export const prefix = 'env';
|
||||
export const canDuplicate = true;
|
||||
export const canSync = true;
|
||||
|
||||
type BaseEnvironment = {
|
||||
name: string,
|
||||
@@ -57,6 +59,13 @@ export async function getOrCreateForWorkspaceId(workspaceId: string): Promise<En
|
||||
return create({
|
||||
parentId: workspaceId,
|
||||
name: 'Base Environment',
|
||||
|
||||
// Deterministic base env ID. It helps reduce sync complexity since we won't have to
|
||||
// de-duplicate environments.
|
||||
_id: `${prefix}_${crypto
|
||||
.createHash('sha1')
|
||||
.update(workspaceId)
|
||||
.digest('hex')}`,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -61,10 +61,23 @@ export function all() {
|
||||
];
|
||||
}
|
||||
|
||||
export function types() {
|
||||
export function types(): Array<any> {
|
||||
return all().map(model => model.type);
|
||||
}
|
||||
|
||||
export function canSync(d: BaseModel): boolean {
|
||||
if ((d: any).isPrivate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const m = getModel(d.type);
|
||||
if (!m) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return m.canSync || false;
|
||||
}
|
||||
|
||||
export function getModel(type: string): Object | null {
|
||||
return all().find(m => m.type === type) || null;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ export const name = 'OAuth 2.0 Token';
|
||||
export const type = 'OAuth2Token';
|
||||
export const prefix = 'oa2';
|
||||
export const canDuplicate = false;
|
||||
export const canSync = false;
|
||||
|
||||
export function init(): BaseOAuth2Token {
|
||||
return {
|
||||
|
||||
@@ -6,6 +6,7 @@ export const name = 'PluginData';
|
||||
export const type = 'PluginData';
|
||||
export const prefix = 'plg';
|
||||
export const canDuplicate = false;
|
||||
export const canSync = false;
|
||||
|
||||
type BasePluginData = {
|
||||
plugin: string,
|
||||
|
||||
@@ -4,6 +4,7 @@ export const name = 'Folder Meta';
|
||||
export const type = 'RequestGroupMeta';
|
||||
export const prefix = 'fldm';
|
||||
export const canDuplicate = false;
|
||||
export const canSync = false;
|
||||
|
||||
export function init() {
|
||||
return {
|
||||
|
||||
@@ -6,6 +6,7 @@ export const name = 'Folder';
|
||||
export const type = 'RequestGroup';
|
||||
export const prefix = 'fld';
|
||||
export const canDuplicate = true;
|
||||
export const canSync = true;
|
||||
|
||||
type BaseRequestGroup = {
|
||||
name: string,
|
||||
|
||||
@@ -5,6 +5,7 @@ export const name = 'Request Meta';
|
||||
export const type = 'RequestMeta';
|
||||
export const prefix = 'reqm';
|
||||
export const canDuplicate = false;
|
||||
export const canSync = false;
|
||||
|
||||
export function init() {
|
||||
return {
|
||||
|
||||
@@ -6,6 +6,7 @@ export const name = 'Request Version';
|
||||
export const type = 'RequestVersion';
|
||||
export const prefix = 'rvr';
|
||||
export const canDuplicate = false;
|
||||
export const canSync = false;
|
||||
|
||||
const FIELDS_TO_IGNORE = [
|
||||
'_id',
|
||||
|
||||
@@ -32,6 +32,7 @@ export const name = 'Request';
|
||||
export const type = 'Request';
|
||||
export const prefix = 'req';
|
||||
export const canDuplicate = true;
|
||||
export const canSync = true;
|
||||
|
||||
export type RequestAuthentication = Object;
|
||||
|
||||
@@ -357,7 +358,7 @@ export function remove(request: Request): Promise<void> {
|
||||
return db.remove(request);
|
||||
}
|
||||
|
||||
export function all() {
|
||||
export async function all(): Promise<Array<Request>> {
|
||||
return db.all(type);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import crypto from 'crypto';
|
||||
import path from 'path';
|
||||
import zlib from 'zlib';
|
||||
import mkdirp from 'mkdirp';
|
||||
import { MAX_RESPONSES } from '../common/constants';
|
||||
import * as db from '../common/database';
|
||||
import { getDataDirectory } from '../common/misc';
|
||||
|
||||
@@ -16,6 +15,7 @@ export const name = 'Response';
|
||||
export const type = 'Response';
|
||||
export const prefix = 'res';
|
||||
export const canDuplicate = false;
|
||||
export const canSync = false;
|
||||
|
||||
export type ResponseHeader = {
|
||||
name: string,
|
||||
@@ -97,7 +97,7 @@ export function getById(id: string) {
|
||||
return db.get(type, id);
|
||||
}
|
||||
|
||||
export function all() {
|
||||
export async function all(): Promise<Array<Response>> {
|
||||
return db.all(type);
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ export async function getLatestForRequest(requestId: string): Promise<Response |
|
||||
return response || null;
|
||||
}
|
||||
|
||||
export async function create(patch: Object = {}) {
|
||||
export async function create(patch: Object = {}, maxResponses: number = 20) {
|
||||
if (!patch.parentId) {
|
||||
throw new Error('New Response missing `parentId`');
|
||||
}
|
||||
@@ -136,7 +136,11 @@ export async function create(patch: Object = {}) {
|
||||
patch.requestVersionId = requestVersion ? requestVersion._id : null;
|
||||
|
||||
// Delete all other responses before creating the new one
|
||||
const allResponses = await db.findMostRecentlyModified(type, { parentId }, MAX_RESPONSES);
|
||||
const allResponses = await db.findMostRecentlyModified(
|
||||
type,
|
||||
{ parentId },
|
||||
Math.max(1, maxResponses),
|
||||
);
|
||||
const recentIds = allResponses.map(r => r._id);
|
||||
await db.removeWhere(type, { parentId, _id: { $nin: recentIds } });
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ type BaseSettings = {
|
||||
autoHideMenuBar: boolean,
|
||||
theme: string,
|
||||
maxRedirects: number,
|
||||
maxHistoryResponses: number,
|
||||
pluginPath: string,
|
||||
nunjucksPowerUserMode: boolean,
|
||||
deviceId: string | null,
|
||||
@@ -35,6 +36,9 @@ type BaseSettings = {
|
||||
fontInterface: string | null,
|
||||
fontSize: number,
|
||||
fontVariantLigatures: boolean,
|
||||
|
||||
// Feature flags
|
||||
enableSyncBeta: boolean,
|
||||
hotKeyRegistry: hotkeys.HotKeyRegistry,
|
||||
};
|
||||
|
||||
@@ -44,6 +48,7 @@ export const name = 'Settings';
|
||||
export const type = 'Settings';
|
||||
export const prefix = 'set';
|
||||
export const canDuplicate = false;
|
||||
export const canSync = false;
|
||||
|
||||
export function init(): BaseSettings {
|
||||
return {
|
||||
@@ -59,6 +64,7 @@ export function init(): BaseSettings {
|
||||
httpsProxy: '',
|
||||
noProxy: '',
|
||||
maxRedirects: -1,
|
||||
maxHistoryResponses: 20,
|
||||
proxyEnabled: false,
|
||||
timeout: 0,
|
||||
validateSSL: true,
|
||||
@@ -77,6 +83,7 @@ export function init(): BaseSettings {
|
||||
fontInterface: null,
|
||||
fontSize: 13,
|
||||
fontVariantLigatures: false,
|
||||
enableSyncBeta: false,
|
||||
hotKeyRegistry: hotkeys.newDefaultRegistry(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ export const name = 'Stats';
|
||||
export const type = 'Stats';
|
||||
export const prefix = 'sta';
|
||||
export const canDuplicate = false;
|
||||
export const canSync = false;
|
||||
|
||||
type BaseStats = {
|
||||
currentLaunch: number | null,
|
||||
|
||||
@@ -11,6 +11,7 @@ export const name = 'Workspace Meta';
|
||||
export const type = 'WorkspaceMeta';
|
||||
export const prefix = 'wrkm';
|
||||
export const canDuplicate = false;
|
||||
export const canSync = false;
|
||||
|
||||
type BaseWorkspaceMeta = {
|
||||
activeRequestId: string | null,
|
||||
|
||||
@@ -7,6 +7,7 @@ export const name = 'Workspace';
|
||||
export const type = 'Workspace';
|
||||
export const prefix = 'wrk';
|
||||
export const canDuplicate = true;
|
||||
export const canSync = true;
|
||||
|
||||
type BaseWorkspace = {
|
||||
name: string,
|
||||
|
||||
@@ -626,19 +626,21 @@ export async function _actuallySend(
|
||||
}
|
||||
|
||||
// NOTE: This is last because headers might be modified multiple times
|
||||
const headerStrings = headers.filter(h => h.name).map(h => {
|
||||
const value = h.value || '';
|
||||
if (value === '') {
|
||||
// Curl needs a semicolon suffix to send empty header values
|
||||
return `${h.name};`;
|
||||
} else if (value === DISABLE_HEADER_VALUE) {
|
||||
// Tell Curl NOT to send the header if value is null
|
||||
return `${h.name}:`;
|
||||
} else {
|
||||
// Send normal header value
|
||||
return `${h.name}: ${value}`;
|
||||
}
|
||||
});
|
||||
const headerStrings = headers
|
||||
.filter(h => h.name)
|
||||
.map(h => {
|
||||
const value = h.value || '';
|
||||
if (value === '') {
|
||||
// Curl needs a semicolon suffix to send empty header values
|
||||
return `${h.name};`;
|
||||
} else if (value === DISABLE_HEADER_VALUE) {
|
||||
// Tell Curl NOT to send the header if value is null
|
||||
return `${h.name}:`;
|
||||
} else {
|
||||
// Send normal header value
|
||||
return `${h.name}: ${value}`;
|
||||
}
|
||||
});
|
||||
setOpt(Curl.option.HTTPHEADER, headerStrings);
|
||||
|
||||
let responseBodyBytes = 0;
|
||||
|
||||
@@ -10,6 +10,7 @@ export const RESPONSE_TYPE_TOKEN = 'token';
|
||||
export const RESPONSE_TYPE_ID_TOKEN_TOKEN = 'id_token token';
|
||||
|
||||
export const P_ACCESS_TOKEN = 'access_token';
|
||||
export const P_ID_TOKEN = 'id_token';
|
||||
export const P_CLIENT_ID = 'client_id';
|
||||
export const P_CLIENT_SECRET = 'client_secret';
|
||||
export const P_AUDIENCE = 'audience';
|
||||
|
||||
@@ -36,12 +36,17 @@ export default async function(
|
||||
const qs = buildQueryStringFromParams(params);
|
||||
const finalUrl = joinUrlAndQueryString(authorizationUrl, qs);
|
||||
|
||||
const redirectedTo = await authorizeUserInWindow(finalUrl, /(access_token=)/, /(error=)/);
|
||||
const redirectedTo = await authorizeUserInWindow(
|
||||
finalUrl,
|
||||
/(access_token=|id_token=)/,
|
||||
/(error=)/,
|
||||
);
|
||||
const fragment = redirectedTo.split('#')[1];
|
||||
|
||||
if (fragment) {
|
||||
return responseToObject(fragment, [
|
||||
const results = responseToObject(fragment, [
|
||||
c.P_ACCESS_TOKEN,
|
||||
c.P_ID_TOKEN,
|
||||
c.P_TOKEN_TYPE,
|
||||
c.P_EXPIRES_IN,
|
||||
c.P_SCOPE,
|
||||
@@ -50,6 +55,9 @@ export default async function(
|
||||
c.P_ERROR_DESCRIPTION,
|
||||
c.P_ERROR_URI,
|
||||
]);
|
||||
results[c.P_ACCESS_TOKEN] = results[c.P_ACCESS_TOKEN] || results[c.P_ID_TOKEN];
|
||||
delete results[c.P_ID_TOKEN];
|
||||
return results;
|
||||
} else {
|
||||
// Bad redirect
|
||||
return {};
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
{
|
||||
"_type": "export",
|
||||
"__export_format": 3,
|
||||
"__export_date": "2017-11-24T16:14:14.805Z",
|
||||
"__export_source": "insomnia.desktop.app:v5.12.0-beta.3",
|
||||
"resources": [
|
||||
{
|
||||
"_id": "wrk_imported_1",
|
||||
"created": 888,
|
||||
"_type": "export",
|
||||
"__export_format": 4,
|
||||
"__export_date": "2017-11-24T16:14:14.805Z",
|
||||
"__export_source": "insomnia.desktop.app:v5.12.0-beta.3",
|
||||
"resources": [
|
||||
{
|
||||
"_id": "wrk_imported_1",
|
||||
"created": 888,
|
||||
"modified": 999,
|
||||
"description": "",
|
||||
"name": "New",
|
||||
"parentId": null,
|
||||
"_type": "workspace"
|
||||
},
|
||||
{
|
||||
"_id": "req_imported_1",
|
||||
"authentication": {},
|
||||
"body": {},
|
||||
"description": "",
|
||||
"name": "New",
|
||||
"parentId": null,
|
||||
"_type": "workspace"
|
||||
},
|
||||
{
|
||||
"_id": "req_imported_1",
|
||||
"authentication": {},
|
||||
"body": {},
|
||||
"created": 111,
|
||||
"modified": 222,
|
||||
"metaSortKey": 0,
|
||||
"description": "",
|
||||
"headers": [],
|
||||
"method": "GET",
|
||||
"name": "Test",
|
||||
"parameters": [],
|
||||
"parentId": "wrk_imported_1",
|
||||
"settingDisableRenderRequestBody": false,
|
||||
"settingEncodeUrl": true,
|
||||
"settingSendCookies": true,
|
||||
"settingStoreCookies": true,
|
||||
"url": "https://insomnia.rest",
|
||||
"_type": "request"
|
||||
}
|
||||
]
|
||||
"description": "",
|
||||
"headers": [],
|
||||
"method": "GET",
|
||||
"name": "Test",
|
||||
"parameters": [],
|
||||
"parentId": "wrk_imported_1",
|
||||
"settingDisableRenderRequestBody": false,
|
||||
"settingEncodeUrl": true,
|
||||
"settingSendCookies": true,
|
||||
"settingStoreCookies": true,
|
||||
"url": "https://insomnia.rest",
|
||||
"_type": "request"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -168,10 +168,10 @@ describe('app.export.*', () => {
|
||||
exportedData.__export_date = '2017-11-24T18:09:18.480Z';
|
||||
expect(exportedData).toEqual({
|
||||
__export_date: '2017-11-24T18:09:18.480Z',
|
||||
__export_format: 3,
|
||||
__export_format: 4,
|
||||
__export_source: `insomnia.desktop.app:v${getAppVersion()}`,
|
||||
_type: 'export',
|
||||
resources: [
|
||||
resources: expect.arrayContaining([
|
||||
{
|
||||
_id: 'wrk_1',
|
||||
_type: 'workspace',
|
||||
@@ -204,7 +204,7 @@ describe('app.export.*', () => {
|
||||
settingMaxTimelineDataSize: 1000,
|
||||
url: 'https://insomnia.rest',
|
||||
},
|
||||
],
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
// @flow
|
||||
import { exportHAR, exportJSON, importRaw, importUri } from '../../common/import';
|
||||
import {
|
||||
exportWorkspacesHAR,
|
||||
exportWorkspacesData,
|
||||
importRaw,
|
||||
importUri,
|
||||
} from '../../common/import';
|
||||
|
||||
export function init(): { import: Object, export: Object } {
|
||||
return {
|
||||
@@ -12,12 +17,14 @@ export function init(): { import: Object, export: Object } {
|
||||
},
|
||||
},
|
||||
export: {
|
||||
async insomnia(options: { includePrivate?: boolean } = {}): Promise<string> {
|
||||
async insomnia(
|
||||
options: { includePrivate?: boolean, format?: 'json' | 'yaml' } = {},
|
||||
): Promise<string> {
|
||||
options = options || {};
|
||||
return exportJSON(null, options.includePrivate);
|
||||
return exportWorkspacesData(null, !!options.includePrivate, options.format || 'json');
|
||||
},
|
||||
async har(options: { includePrivate?: boolean } = {}): Promise<string> {
|
||||
return exportHAR(null, options.includePrivate);
|
||||
return exportWorkspacesHAR(null, !!options.includePrivate);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -78,21 +78,23 @@ async function _isInsomniaPlugin(lookupName: string): Promise<Object> {
|
||||
},
|
||||
},
|
||||
(err, stdout, stderr) => {
|
||||
if (err) {
|
||||
reject(new Error(`${lookupName} npm error: ${err.message}`));
|
||||
return;
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
reject(new Error(`Yarn error ${stderr.toString('utf8')}`));
|
||||
reject(new Error(`Yarn error ${stderr.toString()}`));
|
||||
return;
|
||||
}
|
||||
|
||||
let yarnOutput;
|
||||
try {
|
||||
yarnOutput = JSON.parse(stdout.toString('utf8'));
|
||||
} catch (err) {
|
||||
reject(new Error(`Yarn response not JSON: ${err.message}`));
|
||||
yarnOutput = JSON.parse(stdout.toString());
|
||||
} catch (ex) {
|
||||
// Output is not JSON. Check if yarn/electron terminated with non-zero exit code.
|
||||
// In certain environments electron can exit with error even if output is OK.
|
||||
// Parsing is attemted before checking exit code as workaround for false errors.
|
||||
if (err) {
|
||||
reject(new Error(`${lookupName} npm error: ${err.message}`));
|
||||
} else {
|
||||
reject(new Error(`Yarn response not JSON: ${ex.message}`));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -149,13 +151,16 @@ async function _installPluginToTmpDir(lookupName: string): Promise<{ tmpDir: str
|
||||
},
|
||||
},
|
||||
(err, stdout, stderr) => {
|
||||
if (err) {
|
||||
// Check yarn/electron process exit code.
|
||||
// In certain environments electron can exit with error even if the command was perfomed sucesfully.
|
||||
// Checking for sucess message in output is a workaround for false errors.
|
||||
if (err && !stdout.toString().includes('success')) {
|
||||
reject(new Error(`${lookupName} install error: ${err.message}`));
|
||||
return;
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
reject(new Error(`Yarn error ${stderr.toString('utf8')}`));
|
||||
reject(new Error(`Yarn error ${stderr.toString()}`));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+41
-40
@@ -1,10 +1,11 @@
|
||||
import * as sync from '../index';
|
||||
import * as session from '../session';
|
||||
import * as models from '../../models';
|
||||
import * as db from '../../common/database';
|
||||
import * as syncStorage from '../storage';
|
||||
import * as crypt from '../crypt';
|
||||
import * as network from '../network';
|
||||
import { globalBeforeEach } from '../../__jest__/before-each';
|
||||
import * as session from '../../account/session';
|
||||
import * as crypt from '../../account/crypt';
|
||||
|
||||
describe('Test push/pull behaviour', () => {
|
||||
beforeEach(async () => {
|
||||
@@ -59,16 +60,16 @@ describe('Test push/pull behaviour', () => {
|
||||
await sync.push(resourceRequest.resourceGroupId); // Force push rg_1
|
||||
await sync.push(resourceRequest2.resourceGroupId); // Force push rg_2
|
||||
|
||||
expect(session.syncPush.mock.calls.length).toBe(3);
|
||||
expect(session.syncPush.mock.calls[0][0].length).toBe(2);
|
||||
expect(session.syncPush.mock.calls[0][0][0].id).toBe('wrk_1');
|
||||
expect(session.syncPush.mock.calls[0][0][1].id).toBe('req_1');
|
||||
expect(session.syncPush.mock.calls[1][0].length).toBe(2);
|
||||
expect(session.syncPush.mock.calls[1][0][0].id).toBe('wrk_1');
|
||||
expect(session.syncPush.mock.calls[1][0][1].id).toBe('req_1');
|
||||
expect(session.syncPush.mock.calls[2][0].length).toBe(2);
|
||||
expect(session.syncPush.mock.calls[2][0][0].id).toBe('wrk_2');
|
||||
expect(session.syncPush.mock.calls[2][0][1].id).toBe('req_2');
|
||||
expect(network.syncPush.mock.calls.length).toBe(3);
|
||||
expect(network.syncPush.mock.calls[0][0].length).toBe(2);
|
||||
expect(network.syncPush.mock.calls[0][0][0].id).toBe('wrk_1');
|
||||
expect(network.syncPush.mock.calls[0][0][1].id).toBe('req_1');
|
||||
expect(network.syncPush.mock.calls[1][0].length).toBe(2);
|
||||
expect(network.syncPush.mock.calls[1][0][0].id).toBe('wrk_1');
|
||||
expect(network.syncPush.mock.calls[1][0][1].id).toBe('req_1');
|
||||
expect(network.syncPush.mock.calls[2][0].length).toBe(2);
|
||||
expect(network.syncPush.mock.calls[2][0][0].id).toBe('wrk_2');
|
||||
expect(network.syncPush.mock.calls[2][0][1].id).toBe('req_2');
|
||||
});
|
||||
|
||||
it('Updates dirty flag for push response', async () => {
|
||||
@@ -78,7 +79,7 @@ describe('Test push/pull behaviour', () => {
|
||||
syncMode: syncStorage.SYNC_MODE_ON,
|
||||
});
|
||||
|
||||
session.syncPush.mockReturnValueOnce({
|
||||
network.syncPush.mockReturnValueOnce({
|
||||
updated: [],
|
||||
created: [{ id: request._id, version: 'new-version' }],
|
||||
removed: [],
|
||||
@@ -89,10 +90,10 @@ describe('Test push/pull behaviour', () => {
|
||||
await sync.push(resourceRequest.resourceGroupId);
|
||||
const resourceAfter = await syncStorage.getResourceByDocId(request._id);
|
||||
|
||||
expect(session.syncPush.mock.calls.length).toBe(1);
|
||||
expect(session.syncPush.mock.calls[0][0].length).toBe(2);
|
||||
expect(session.syncPush.mock.calls[0][0][0].id).toBe('wrk_1');
|
||||
expect(session.syncPush.mock.calls[0][0][1].id).toBe('req_1');
|
||||
expect(network.syncPush.mock.calls.length).toBe(1);
|
||||
expect(network.syncPush.mock.calls[0][0].length).toBe(2);
|
||||
expect(network.syncPush.mock.calls[0][0][0].id).toBe('wrk_1');
|
||||
expect(network.syncPush.mock.calls[0][0][1].id).toBe('req_1');
|
||||
expect(resourceBefore.dirty).toBe(true);
|
||||
expect(resourceAfter.dirty).toBe(false);
|
||||
});
|
||||
@@ -122,7 +123,7 @@ describe('Test push/pull behaviour', () => {
|
||||
encContent: await sync.encryptDoc(resourceBefore.resourceGroupId, requestNew),
|
||||
});
|
||||
|
||||
session.syncPull.mockReturnValueOnce({
|
||||
network.syncPull.mockReturnValueOnce({
|
||||
updatedResources: [updatedResource],
|
||||
createdResources: [createdResourceNew],
|
||||
idsToPush: [],
|
||||
@@ -165,7 +166,7 @@ describe('Test push/pull behaviour', () => {
|
||||
lastEdited: resourceRequest.lastEdited - 1000, // Same edited time
|
||||
});
|
||||
|
||||
session.syncPush.mockReturnValueOnce({
|
||||
network.syncPush.mockReturnValueOnce({
|
||||
updated: [],
|
||||
created: [],
|
||||
removed: [],
|
||||
@@ -180,8 +181,8 @@ describe('Test push/pull behaviour', () => {
|
||||
const requestAfter = await models.request.getById(requestClient._id);
|
||||
|
||||
// Assert
|
||||
expect(session.syncPush.mock.calls.length).toBe(1);
|
||||
expect(session.syncPush.mock.calls[0][0].length).toBe(2);
|
||||
expect(network.syncPush.mock.calls.length).toBe(1);
|
||||
expect(network.syncPush.mock.calls[0][0].length).toBe(2);
|
||||
// Even when local wins, local resource gets the remove resource version
|
||||
expect(resourceAfter.version).toBe(resourceConflict.version);
|
||||
// Local resource gets marked as dirty so it's pushed right away
|
||||
@@ -202,7 +203,7 @@ describe('Test push/pull behaviour', () => {
|
||||
lastEdited: resourceRequest.lastEdited, // Same edited time
|
||||
});
|
||||
|
||||
session.syncPush.mockReturnValueOnce({
|
||||
network.syncPush.mockReturnValueOnce({
|
||||
updated: [],
|
||||
created: [],
|
||||
removed: [],
|
||||
@@ -217,8 +218,8 @@ describe('Test push/pull behaviour', () => {
|
||||
const requestAfter = await models.request.getById(requestClient._id);
|
||||
|
||||
// Assert
|
||||
expect(session.syncPush.mock.calls.length).toBe(1);
|
||||
expect(session.syncPush.mock.calls[0][0].length).toBe(2);
|
||||
expect(network.syncPush.mock.calls.length).toBe(1);
|
||||
expect(network.syncPush.mock.calls[0][0].length).toBe(2);
|
||||
// Even when local wins, local resource gets the remove resource version
|
||||
expect(resourceAfter.version).toBe(resourceConflict.version);
|
||||
// Local resource gets marked as dirty so it's pushed right away
|
||||
@@ -239,7 +240,7 @@ describe('Test push/pull behaviour', () => {
|
||||
lastEdited: resourceRequest.lastEdited + 1000,
|
||||
});
|
||||
|
||||
session.syncPush.mockReturnValueOnce({
|
||||
network.syncPush.mockReturnValueOnce({
|
||||
updated: [],
|
||||
created: [],
|
||||
removed: [],
|
||||
@@ -254,8 +255,8 @@ describe('Test push/pull behaviour', () => {
|
||||
const requestAfter = await models.request.getById(requestClient._id);
|
||||
|
||||
// Assert
|
||||
expect(session.syncPush.mock.calls.length).toBe(1);
|
||||
expect(session.syncPush.mock.calls[0][0].length).toBe(2);
|
||||
expect(network.syncPush.mock.calls.length).toBe(1);
|
||||
expect(network.syncPush.mock.calls[0][0].length).toBe(2);
|
||||
expect(resourceAfter.lastEdited).toBeGreaterThan(resourceRequest.lastEdited);
|
||||
expect(resourceAfter.version).toBe(resourceConflict.version);
|
||||
// Local resource gets marked as dirty so it's pushed right away
|
||||
@@ -365,10 +366,10 @@ describe('Integration tests for creating Resources and pushing', () => {
|
||||
expect(_decryptResource(resource).url).toBe('https://google.com');
|
||||
expect(resource.removed).toBe(false);
|
||||
|
||||
expect(session.syncPush.mock.calls.length).toBe(1);
|
||||
expect(session.syncPush.mock.calls[0][0].length).toBe(6);
|
||||
expect(network.syncPush.mock.calls.length).toBe(1);
|
||||
expect(network.syncPush.mock.calls[0][0].length).toBe(6);
|
||||
|
||||
expect(session.syncPull.mock.calls).toEqual([]);
|
||||
expect(network.syncPull.mock.calls).toEqual([]);
|
||||
});
|
||||
|
||||
it('Resources revived on DB change', async () => {
|
||||
@@ -425,10 +426,10 @@ describe('Integration tests for creating Resources and pushing', () => {
|
||||
expect(_decryptResource(updatedResource).name).toBe('New Name');
|
||||
expect(resource.removed).toBe(false);
|
||||
|
||||
expect(session.syncPush.mock.calls.length).toBe(1);
|
||||
expect(session.syncPush.mock.calls[0][0].length).toBe(5);
|
||||
expect(network.syncPush.mock.calls.length).toBe(1);
|
||||
expect(network.syncPush.mock.calls[0][0].length).toBe(5);
|
||||
|
||||
expect(session.syncPull.mock.calls).toEqual([]);
|
||||
expect(network.syncPull.mock.calls).toEqual([]);
|
||||
});
|
||||
|
||||
it('Resources removed on DB change', async () => {
|
||||
@@ -448,10 +449,10 @@ describe('Integration tests for creating Resources and pushing', () => {
|
||||
expect(resource.removed).toBe(false);
|
||||
expect(updatedResource.removed).toBe(true);
|
||||
|
||||
expect(session.syncPush.mock.calls.length).toBe(1);
|
||||
expect(session.syncPush.mock.calls[0][0].length).toBe(5);
|
||||
expect(network.syncPush.mock.calls.length).toBe(1);
|
||||
expect(network.syncPush.mock.calls[0][0].length).toBe(5);
|
||||
|
||||
expect(session.syncPull.mock.calls).toEqual([]);
|
||||
expect(network.syncPull.mock.calls).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -508,7 +509,7 @@ async function _setSessionData() {
|
||||
async function _setupSessionMocks() {
|
||||
const resourceGroups = {};
|
||||
|
||||
session.syncCreateResourceGroup = jest.fn((parentId, name, _) => {
|
||||
network.syncCreateResourceGroup = jest.fn((parentId, name, _) => {
|
||||
const id = `rg_${Object.keys(resourceGroups).length + 1}`;
|
||||
|
||||
// Generate a public key and use a symmetric equal to it's Id for
|
||||
@@ -530,7 +531,7 @@ async function _setupSessionMocks() {
|
||||
return resourceGroups[id];
|
||||
});
|
||||
|
||||
session.syncGetResourceGroup = jest.fn(id => {
|
||||
network.syncGetResourceGroup = jest.fn(id => {
|
||||
if (resourceGroups[id]) {
|
||||
return resourceGroups[id];
|
||||
}
|
||||
@@ -540,14 +541,14 @@ async function _setupSessionMocks() {
|
||||
throw err;
|
||||
});
|
||||
|
||||
session.syncPull = jest.fn(body => ({
|
||||
network.syncPull = jest.fn(body => ({
|
||||
updatedResources: [],
|
||||
createdResources: [],
|
||||
idsToPush: [],
|
||||
idsToRemove: [],
|
||||
}));
|
||||
|
||||
session.syncPush = jest.fn(body => ({
|
||||
network.syncPush = jest.fn(body => ({
|
||||
conflicts: [],
|
||||
updated: [],
|
||||
created: [],
|
||||
+46
-14
@@ -1,11 +1,19 @@
|
||||
import * as db from '../common/database';
|
||||
import * as models from '../models';
|
||||
import * as crypt from './crypt';
|
||||
import * as session from './session';
|
||||
import * as store from './storage';
|
||||
import * as misc from '../common/misc';
|
||||
import Logger from './logger';
|
||||
import * as zlib from 'zlib';
|
||||
import {
|
||||
syncCreateResourceGroup,
|
||||
syncFixDupes,
|
||||
syncGetResourceGroup,
|
||||
syncPull,
|
||||
syncPush,
|
||||
syncResetData,
|
||||
} from './network';
|
||||
import * as crypt from '../account/crypt';
|
||||
import * as session from '../account/session';
|
||||
|
||||
export const START_DELAY = 1e3;
|
||||
export const PULL_PERIOD = 15e3;
|
||||
@@ -32,7 +40,19 @@ let _writeChangesInterval = null;
|
||||
let _pendingDBChanges = {};
|
||||
let _isInitialized = false;
|
||||
|
||||
// Used to mark whether or not the new sync system is enabled
|
||||
let _disabledForSession = false;
|
||||
|
||||
export function disableForSession() {
|
||||
_disabledForSession = true;
|
||||
}
|
||||
|
||||
export async function init() {
|
||||
if (_disabledForSession) {
|
||||
logger.debug('Legacy sync is disabled for current session');
|
||||
return;
|
||||
}
|
||||
|
||||
if (_isInitialized) {
|
||||
logger.debug('Already enabled');
|
||||
return;
|
||||
@@ -42,8 +62,8 @@ export async function init() {
|
||||
_isInitialized = true;
|
||||
db.onChange(async changes => {
|
||||
// To help prevent bugs, put Workspaces first
|
||||
const sortedChanges = changes.sort(
|
||||
([event, doc, fromSync]) => (doc.type === models.workspace.type ? 1 : -1),
|
||||
const sortedChanges = changes.sort(([event, doc, fromSync]) =>
|
||||
doc.type === models.workspace.type ? 1 : -1,
|
||||
);
|
||||
|
||||
for (const [event, doc, fromSync] of sortedChanges) {
|
||||
@@ -154,7 +174,7 @@ export async function fixDuplicateResourceGroups() {
|
||||
|
||||
// Fix duplicates
|
||||
const ids = resources.map(r => r.resourceGroupId);
|
||||
const { deleteResourceGroupIds } = await session.syncFixDupes(ids);
|
||||
const { deleteResourceGroupIds } = await syncFixDupes(ids);
|
||||
|
||||
for (const idToDelete of deleteResourceGroupIds) {
|
||||
await store.removeResourceGroup(idToDelete);
|
||||
@@ -187,6 +207,11 @@ export async function writePendingChanges() {
|
||||
}
|
||||
|
||||
export async function push(resourceGroupId = null) {
|
||||
if (_disabledForSession) {
|
||||
logger.debug('Legacy sync is disabled for current session');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!session.isLoggedIn()) {
|
||||
return;
|
||||
}
|
||||
@@ -199,7 +224,7 @@ export async function push(resourceGroupId = null) {
|
||||
}
|
||||
|
||||
if (!allDirtyResources.length) {
|
||||
logger.debug('No changes to push');
|
||||
// No changes to push
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -223,7 +248,7 @@ export async function push(resourceGroupId = null) {
|
||||
|
||||
let responseBody;
|
||||
try {
|
||||
responseBody = await session.syncPush(dirtyResources);
|
||||
responseBody = await syncPush(dirtyResources);
|
||||
} catch (e) {
|
||||
logger.error('Failed to push changes', e);
|
||||
return;
|
||||
@@ -298,6 +323,11 @@ export async function push(resourceGroupId = null) {
|
||||
}
|
||||
|
||||
export async function pull(resourceGroupId = null, createMissingResources = true) {
|
||||
if (_disabledForSession) {
|
||||
logger.debug('Legacy sync is disabled for current session');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!session.isLoggedIn()) {
|
||||
return;
|
||||
}
|
||||
@@ -337,11 +367,13 @@ export async function pull(resourceGroupId = null, createMissingResources = true
|
||||
blacklist: blacklistedResourceGroupIds,
|
||||
};
|
||||
|
||||
logger.debug(`Pulling with ${resources.length} resources`);
|
||||
if (resources.length) {
|
||||
logger.debug(`Pulling with ${resources.length} resources`);
|
||||
}
|
||||
|
||||
let responseBody;
|
||||
try {
|
||||
responseBody = await session.syncPull(body);
|
||||
responseBody = await syncPull(body);
|
||||
} catch (e) {
|
||||
logger.error('Failed to sync changes', e, body);
|
||||
return;
|
||||
@@ -535,7 +567,7 @@ export async function resetLocalData() {
|
||||
}
|
||||
|
||||
export async function resetRemoteData() {
|
||||
await session.syncResetData();
|
||||
await syncResetData();
|
||||
}
|
||||
|
||||
// ~~~~~~~ //
|
||||
@@ -587,7 +619,7 @@ export async function fetchResourceGroup(resourceGroupId, invalidateCache = fals
|
||||
|
||||
if (!resourceGroup) {
|
||||
try {
|
||||
resourceGroup = await session.syncGetResourceGroup(resourceGroupId);
|
||||
resourceGroup = await syncGetResourceGroup(resourceGroupId);
|
||||
} catch (e) {
|
||||
if (e.statusCode === 404) {
|
||||
await store.removeResourceGroup(resourceGroupId);
|
||||
@@ -716,7 +748,7 @@ export async function createResourceGroup(parentId, name) {
|
||||
// Create the new ResourceGroup
|
||||
let resourceGroup;
|
||||
try {
|
||||
resourceGroup = await session.syncCreateResourceGroup(parentId, name, encRGSymmetricJWK);
|
||||
resourceGroup = await syncCreateResourceGroup(parentId, name, encRGSymmetricJWK);
|
||||
} catch (e) {
|
||||
logger.error(`Failed to create ResourceGroup: ${e}`);
|
||||
throw e;
|
||||
@@ -802,8 +834,8 @@ export async function getOrCreateAllActiveResources(resourceGroupId = null) {
|
||||
}
|
||||
|
||||
// Make sure Workspace is first, because the loop below depends on it
|
||||
const modelTypes = Object.keys(WHITE_LIST).sort(
|
||||
(a, b) => (a.type === models.workspace.type ? 1 : -1),
|
||||
const modelTypes = Object.keys(WHITE_LIST).sort((a, b) =>
|
||||
a.type === models.workspace.type ? 1 : -1,
|
||||
);
|
||||
|
||||
let created = 0;
|
||||
File renamed without changes.
@@ -0,0 +1,76 @@
|
||||
import * as fetch from '../account/fetch';
|
||||
import * as session from '../account/session';
|
||||
import * as crypt from '../account/crypt';
|
||||
|
||||
export async function syncCreateResourceGroup(parentResourceId, name, encSymmetricKey) {
|
||||
return fetch.post(
|
||||
'/api/resource_groups',
|
||||
{
|
||||
parentResourceId,
|
||||
name,
|
||||
encSymmetricKey,
|
||||
},
|
||||
session.getCurrentSessionId(),
|
||||
);
|
||||
}
|
||||
|
||||
export async function syncGetResourceGroup(id) {
|
||||
return fetch.get(`/api/resource_groups/${id}`, session.getCurrentSessionId());
|
||||
}
|
||||
|
||||
export async function syncPull(body) {
|
||||
return fetch.post('/sync/pull', body, session.getCurrentSessionId(), true);
|
||||
}
|
||||
|
||||
export async function syncPush(body) {
|
||||
return fetch.post('/sync/push', body, session.getCurrentSessionId(), true);
|
||||
}
|
||||
|
||||
export async function syncResetData() {
|
||||
return fetch.post('/auth/reset', session.getCurrentSessionId());
|
||||
}
|
||||
|
||||
export async function syncFixDupes(resourceGroupIds) {
|
||||
return fetch.post('/sync/fix-dupes', { ids: resourceGroupIds }, session.getCurrentSessionId());
|
||||
}
|
||||
|
||||
export async function unshareWithAllTeams(resourceGroupId) {
|
||||
return fetch.put(
|
||||
`/api/resource_groups/${resourceGroupId}/unshare`,
|
||||
session.getCurrentSessionId(),
|
||||
);
|
||||
}
|
||||
|
||||
export async function shareWithTeam(resourceGroupId, teamId) {
|
||||
// Ask the server what we need to do to invite the member
|
||||
const instructions = await fetch.post(
|
||||
`/api/resource_groups/${resourceGroupId}/share-a`,
|
||||
{
|
||||
teamId,
|
||||
},
|
||||
session.getCurrentSessionId(),
|
||||
);
|
||||
|
||||
const privateKeyJWK = session.getPrivateKey();
|
||||
const resourceGroupSymmetricKey = crypt.decryptRSAWithJWK(
|
||||
privateKeyJWK,
|
||||
instructions.encSymmetricKey,
|
||||
);
|
||||
|
||||
// Build the invite data request
|
||||
const newKeys = {};
|
||||
for (const accountId of Object.keys(instructions.keys)) {
|
||||
const accountPublicKeyJWK = JSON.parse(instructions.keys[accountId]);
|
||||
newKeys[accountId] = crypt.encryptRSAWithJWK(accountPublicKeyJWK, resourceGroupSymmetricKey);
|
||||
}
|
||||
|
||||
// Actually share it with the team
|
||||
await fetch.post(
|
||||
`/api/resource_groups/${resourceGroupId}/share-b`,
|
||||
{
|
||||
teamId,
|
||||
keys: newKeys,
|
||||
},
|
||||
session.getCurrentSessionId(),
|
||||
);
|
||||
}
|
||||
File renamed without changes.
@@ -1,33 +0,0 @@
|
||||
import * as crypt from '../crypt';
|
||||
import { globalBeforeEach } from '../../__jest__/before-each';
|
||||
|
||||
describe('deriveKey()', () => {
|
||||
beforeEach(globalBeforeEach);
|
||||
it('derives a key properly', async () => {
|
||||
const result = await crypt.deriveKey('Password', 'email', 'salt');
|
||||
|
||||
const expected = '';
|
||||
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('encryptRSA', () => {
|
||||
beforeEach(globalBeforeEach);
|
||||
it('encrypts and decrypts', () => {
|
||||
const resultEncrypted = crypt.encryptAES('rawkey', 'Hello World!', 'additional data');
|
||||
const resultDecrypted = crypt.decryptAES('rawkey', resultEncrypted);
|
||||
|
||||
const expectedEncrypted = {
|
||||
ad: '6164646974696f6e616c2064617461',
|
||||
d: '48656c6c6f253230576f726c6421',
|
||||
iv: '616161616161616161616161',
|
||||
t: '746167',
|
||||
};
|
||||
|
||||
const expectedDecrypted = 'Hello World!';
|
||||
|
||||
expect(resultEncrypted).toEqual(expectedEncrypted);
|
||||
expect(resultDecrypted).toEqual(expectedDecrypted);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import { diff, __internal } from '../diff';
|
||||
|
||||
describe('diff()', () => {
|
||||
it('creates block map', () => {
|
||||
const result = __internal.getBlockMap('Hello World!!', 3);
|
||||
expect(result).toEqual({
|
||||
dbc2d1fed0dc37a70aea0f376958c802eddc0559: [
|
||||
{
|
||||
start: 0,
|
||||
len: 3,
|
||||
hash: 'dbc2d1fed0dc37a70aea0f376958c802eddc0559',
|
||||
},
|
||||
],
|
||||
'263b8ac41bd2ffb775af535c54d8b0a0e6b9e743': [
|
||||
{
|
||||
start: 3,
|
||||
len: 3,
|
||||
hash: '263b8ac41bd2ffb775af535c54d8b0a0e6b9e743',
|
||||
},
|
||||
],
|
||||
'3603dd999f7dd952041d3bdb27f74e511cfd6b2a': [
|
||||
{
|
||||
start: 6,
|
||||
len: 3,
|
||||
hash: '3603dd999f7dd952041d3bdb27f74e511cfd6b2a',
|
||||
},
|
||||
],
|
||||
c20d168802bbae1c84f90b9b0495e0d918da3aea: [
|
||||
{
|
||||
start: 9,
|
||||
len: 3,
|
||||
hash: 'c20d168802bbae1c84f90b9b0495e0d918da3aea',
|
||||
},
|
||||
],
|
||||
'0ab8318acaf6e678dd02e2b5c343ed41111b393d': [
|
||||
{
|
||||
start: 12,
|
||||
len: 1,
|
||||
hash: '0ab8318acaf6e678dd02e2b5c343ed41111b393d',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('creates operations', () => {
|
||||
// hel|lo |Wor|ld!|!
|
||||
const result = diff('Hello World!!', 'Hello there World!!', 3);
|
||||
expect(result).toEqual([
|
||||
{ type: 'COPY', start: 0, len: 6 },
|
||||
{ type: 'INSERT', content: 'there ' },
|
||||
{ type: 'COPY', start: 6, len: 7 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('creates operations 2', () => {
|
||||
const result2 = diff(
|
||||
'Hello, this is a pretty long sentence about not much at all.',
|
||||
'Hello, this is a pretty short sentence.',
|
||||
2,
|
||||
);
|
||||
expect(result2).toEqual([
|
||||
{ len: 24, start: 0, type: 'COPY' },
|
||||
{ content: 'shor', type: 'INSERT' },
|
||||
{ len: 2, start: 42, type: 'COPY' },
|
||||
{ content: 's', type: 'INSERT' },
|
||||
{ len: 7, start: 30, type: 'COPY' },
|
||||
{ content: '.', type: 'INSERT' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import { diff } from '../diff';
|
||||
import { patch } from '../patch';
|
||||
|
||||
describe('patch()', () => {
|
||||
it('works on many examples', () => {
|
||||
const things = [
|
||||
[
|
||||
'Hello, this is a pretty long sentence about not much at all.',
|
||||
'Hello, this is a pretty short sentence.',
|
||||
],
|
||||
['xxxxxxxxxxyyyyyyyyyyyyyyyyzzzzzzzzzzzzzzzz', 'abc'],
|
||||
['xyz', 'xyz'],
|
||||
];
|
||||
|
||||
for (const thing of things) {
|
||||
// Test 100 chunk sizes
|
||||
for (let i = 1; i < 5; i++) {
|
||||
// Test both directions
|
||||
for (let j = 0; j < 2; j++) {
|
||||
let a, b;
|
||||
if (j === 0) {
|
||||
a = thing[0];
|
||||
b = thing[1];
|
||||
} else {
|
||||
a = thing[1];
|
||||
b = thing[0];
|
||||
}
|
||||
|
||||
// Diff the result
|
||||
const diffResult = diff(a, b, i * 5);
|
||||
|
||||
// Use the diff to patch it and make sure it equals the same
|
||||
expect(patch(a, diffResult)).toBe(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,130 @@
|
||||
// @flow
|
||||
import crypto from 'crypto';
|
||||
|
||||
type InsertOperation = {|
|
||||
type: 'INSERT',
|
||||
content: string,
|
||||
|};
|
||||
|
||||
type CopyOperation = {|
|
||||
type: 'COPY',
|
||||
start: number,
|
||||
len: number,
|
||||
|};
|
||||
|
||||
export type Operation = InsertOperation | CopyOperation;
|
||||
|
||||
type Block = {|
|
||||
start: number,
|
||||
len: number,
|
||||
hash: string,
|
||||
|};
|
||||
|
||||
export function diff(source: string, target: string, blockSize: number): Array<Operation> {
|
||||
const operations: Array<Operation> = [];
|
||||
|
||||
const sourceBlockMap = getBlockMap(source, blockSize);
|
||||
|
||||
// Iterate over source blocks in order and match them to target
|
||||
let lastTargetMatch = 0;
|
||||
for (let targetPosition = 0; targetPosition < target.length; ) {
|
||||
const targetBlock = getBlock(target, targetPosition, blockSize);
|
||||
const sourceBlocks = sourceBlockMap[targetBlock.hash] || [];
|
||||
|
||||
if (sourceBlocks.length === 0) {
|
||||
targetPosition++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: Try all blocks
|
||||
const sourceBlock = sourceBlocks[0];
|
||||
|
||||
// Try to match as far as possible
|
||||
let sourceIndex = sourceBlock.start + sourceBlock.len;
|
||||
let targetIndex = targetBlock.start + targetBlock.len;
|
||||
|
||||
while (targetIndex < target.length && sourceIndex < source.length) {
|
||||
if (source[sourceIndex] === target[targetIndex]) {
|
||||
targetIndex++;
|
||||
sourceIndex++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
while (
|
||||
source[sourceIndex++] === target[targetIndex] &&
|
||||
targetIndex < target.length &&
|
||||
sourceIndex < source.length
|
||||
) {
|
||||
targetIndex++;
|
||||
}
|
||||
|
||||
sourceIndex--;
|
||||
|
||||
// Found unknown bytes, INSERT them
|
||||
if (targetBlock.start > lastTargetMatch) {
|
||||
operations.push({
|
||||
type: 'INSERT',
|
||||
content: target.slice(lastTargetMatch, targetBlock.start),
|
||||
});
|
||||
}
|
||||
|
||||
// Source block found in target
|
||||
operations.push({
|
||||
type: 'COPY',
|
||||
start: sourceBlock.start,
|
||||
len: sourceIndex - sourceBlock.start,
|
||||
});
|
||||
|
||||
targetPosition = lastTargetMatch = targetIndex;
|
||||
}
|
||||
|
||||
// Add the target suffix if there's still some left
|
||||
if (lastTargetMatch < target.length) {
|
||||
operations.push({
|
||||
type: 'INSERT',
|
||||
content: target.slice(lastTargetMatch),
|
||||
});
|
||||
}
|
||||
|
||||
return operations;
|
||||
}
|
||||
|
||||
function getBlock(value: string, start: number, blockSize: number): Block {
|
||||
if (start >= value.length) {
|
||||
throw new Error('Invalid block index');
|
||||
}
|
||||
|
||||
const blockSlice = value.slice(start, start + blockSize);
|
||||
return {
|
||||
start,
|
||||
len: blockSlice.length,
|
||||
hash: crypto
|
||||
.createHash('sha1')
|
||||
.update(blockSlice)
|
||||
.digest('hex'),
|
||||
};
|
||||
}
|
||||
|
||||
function getBlockMap(value: string, blockSize: number): { [string]: Block } {
|
||||
const map = {};
|
||||
|
||||
for (let i = 0; i < value.length; ) {
|
||||
const block = getBlock(value, i, blockSize);
|
||||
|
||||
if (map[block.hash]) {
|
||||
map[block.hash].push(block);
|
||||
} else {
|
||||
map[block.hash] = [block];
|
||||
}
|
||||
|
||||
i += block.len;
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
export const __internal = {
|
||||
getBlockMap,
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
// @flow
|
||||
import type { Operation } from './diff';
|
||||
|
||||
export function patch(a: string, operations: Array<Operation>): string {
|
||||
let result = '';
|
||||
|
||||
for (const op of operations) {
|
||||
if (op.type === 'COPY') {
|
||||
result += a.slice(op.start, op.start + op.len);
|
||||
} else if (op.type === 'INSERT') {
|
||||
result += op.content;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { deterministicStringify } from '../deterministicStringify';
|
||||
|
||||
describe('deterministicStringify()', () => {
|
||||
it('sorts object keys', () => {
|
||||
const result = deterministicStringify({
|
||||
c: 'c',
|
||||
a: 'a',
|
||||
b: 'b',
|
||||
});
|
||||
|
||||
expect(result).toBe('{"a":"a","b":"b","c":"c"}');
|
||||
});
|
||||
|
||||
it('works recursively', () => {
|
||||
const result = deterministicStringify({
|
||||
arr: [{ b: 'b', a: 'a' }],
|
||||
obj: {
|
||||
obj2: { b: 'b', a: 'a' },
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBe('{"arr":[{"a":"a","b":"b"}],"obj":{"obj2":{"a":"a","b":"b"}}}');
|
||||
});
|
||||
|
||||
it('works with strange types', () => {
|
||||
const sDate = deterministicStringify(new Date(1541178019555));
|
||||
expect(sDate).toBe('"2018-11-02T17:00:19.555Z"');
|
||||
|
||||
const sNull = deterministicStringify(null);
|
||||
expect(sNull).toBe('null');
|
||||
});
|
||||
|
||||
it('skips non-json types', () => {
|
||||
const sFunc = deterministicStringify({
|
||||
a: [0, () => null],
|
||||
fn: () => null,
|
||||
});
|
||||
expect(sFunc).toBe('{"a":[0]}');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
// @flow
|
||||
|
||||
export function deterministicStringify(value: any): string {
|
||||
const t = Object.prototype.toString.call(value);
|
||||
if (t === '[object Object]') {
|
||||
const pairs = [];
|
||||
for (const key of Object.keys(value).sort()) {
|
||||
const k = deterministicStringify(key);
|
||||
const v = deterministicStringify(value[key]);
|
||||
if (v !== '' && k !== '') {
|
||||
pairs.push(`${k}:${v}`);
|
||||
}
|
||||
}
|
||||
return `{${pairs.join(',')}}`;
|
||||
} else if (t === '[object Array]') {
|
||||
const items = [];
|
||||
for (const v of value) {
|
||||
const vStr = deterministicStringify(v);
|
||||
if (vStr !== '') {
|
||||
items.push(vStr);
|
||||
}
|
||||
}
|
||||
return `[${items.join(',')}]`;
|
||||
}
|
||||
|
||||
const str = JSON.stringify(value);
|
||||
|
||||
// Only return valid stringifyable things
|
||||
return str === undefined ? '' : str;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import Store from '../index';
|
||||
import MemoryDriver from '../drivers/memory-driver';
|
||||
|
||||
describe('store', () => {
|
||||
for (const Driver of [MemoryDriver]) {
|
||||
describe(Driver.name, () => {
|
||||
it('supports CRUD operations', async () => {
|
||||
const s = new Store(new Driver());
|
||||
|
||||
expect(await s.hasItem('404')).toBe(false);
|
||||
expect(await s.getItem('404')).toBe(null);
|
||||
|
||||
expect(await s.setItem('foo', 'bar')).toBe(undefined);
|
||||
expect(await s.getItem('foo')).toBe('bar');
|
||||
|
||||
expect(await s.setItem('null', null)).toBe(undefined);
|
||||
expect(await s.getItem('null')).toBe(null);
|
||||
|
||||
expect(await s.setItem('obj', { foo: 'bar' })).toBe(undefined);
|
||||
expect(await s.getItem('obj')).toEqual({ foo: 'bar' });
|
||||
|
||||
expect(await s.removeItem('foo')).toBe(undefined);
|
||||
expect(await s.hasItem('foo')).toBe(false);
|
||||
expect(await s.getItem('foo')).toBe(null);
|
||||
});
|
||||
|
||||
it('clears all values', async () => {
|
||||
const s = new Store(new Driver());
|
||||
|
||||
await s.setItem('a', 'aaa');
|
||||
await s.setItem('b', 'bbb');
|
||||
await s.clear();
|
||||
|
||||
expect(await s.hasItem('a')).toBe(false);
|
||||
expect(await s.hasItem('b')).toBe(false);
|
||||
});
|
||||
|
||||
it('stores buffers directly', async () => {
|
||||
const s = new Store(new Driver());
|
||||
|
||||
await s.setItem('buff', Buffer.from(`{"hi": "there"}`, 'utf8'));
|
||||
await s.setItem('json', { hi: 'there' });
|
||||
|
||||
expect(await s.getItem('buff')).toEqual({ hi: 'there' });
|
||||
expect(await s.getItem('json')).toEqual({ hi: 'there' });
|
||||
|
||||
expect((await s._driver.getItem('buff')).toString('utf8')).toEqual(`{"hi": "there"}`);
|
||||
expect((await s._driver.getItem('json')).toString('utf8')).toEqual(`{\n "hi": "there"\n}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
it('supports hooks', async () => {
|
||||
const s = new Store(new MemoryDriver(), [
|
||||
{
|
||||
// Just some dumb hooks to test with
|
||||
read: (ext, buff) => Buffer.from(buff.toString('utf8').replace('WORLD', 'World')),
|
||||
write: (ext, buff) => Buffer.from(buff.toString('utf8').replace('World', 'WORLD')),
|
||||
},
|
||||
]);
|
||||
|
||||
await s.setItem('foo', { Hello: 'World!' });
|
||||
expect((await s._driver.getItem('foo')).toString('utf8')).toBe('{\n "Hello": "WORLD!"\n}');
|
||||
expect(await s.getItem('foo')).toEqual({ Hello: 'World!' });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
// @flow
|
||||
|
||||
export interface BaseDriver {
|
||||
constructor(config: { [string]: any }): void;
|
||||
|
||||
hasItem(key: string): Promise<boolean>;
|
||||
|
||||
setItem(key: string, value: Buffer): Promise<void>;
|
||||
|
||||
getItem(key: string): Promise<Buffer | null>;
|
||||
|
||||
removeItem(key: string): Promise<void>;
|
||||
|
||||
keys(prefix: string, recursive: boolean): Promise<Array<string>>;
|
||||
|
||||
clear(): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
// @flow
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import mkdirp from 'mkdirp';
|
||||
import type { BaseDriver } from './base';
|
||||
|
||||
export default class FileSystemDriver implements BaseDriver {
|
||||
_directory: string;
|
||||
|
||||
constructor(config: { directory: string }) {
|
||||
this._directory = config.directory;
|
||||
console.log(`[FileSystemDriver] Initialized in "${this._directory}"`);
|
||||
}
|
||||
|
||||
async hasItem(key: string): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.stat(this._getKeyPath(key), (err, result) => {
|
||||
if (err && err.code === 'ENOENT') {
|
||||
resolve(false);
|
||||
} else if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setItem(key: string, value: Buffer): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const finalPath = this._getKeyPath(key);
|
||||
|
||||
// Temp path contains randomness to avoid race-condition collisions. This
|
||||
// doesn't actually avoid race conditions but at least it won't fail.
|
||||
const tmpPath = `${finalPath}.${Math.random()}.tmp`;
|
||||
|
||||
// This method implements atomic writes by first writing to a temporary
|
||||
// file (non-atomic) then renaming the file to the final value (atomic)
|
||||
fs.writeFile(tmpPath, value, 'utf8', err => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
fs.rename(tmpPath, finalPath, err => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getItem(key: string): Promise<Buffer | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(this._getKeyPath(key), (err, data) => {
|
||||
if (err && err.code === 'ENOENT') {
|
||||
resolve(null);
|
||||
} else if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
removeItem(key: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.unlink(this._getKeyPath(key), err => {
|
||||
if (err && err.code === 'ENOENT') {
|
||||
resolve();
|
||||
} else if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
clear(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readdir(this._directory, (err, names) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
for (const name of names) {
|
||||
fs.unlinkSync(this._getKeyPath(name));
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async keys(prefix: string, recursive: boolean): Promise<Array<string>> {
|
||||
const next = dir => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let keys: Array<string> = [];
|
||||
let names = [];
|
||||
try {
|
||||
names = fs.readdirSync(dir);
|
||||
} catch (err) {
|
||||
if (err.code !== 'ENOENT') {
|
||||
reject(err);
|
||||
}
|
||||
}
|
||||
|
||||
for (const name of names) {
|
||||
if (name.indexOf('.') === 0) {
|
||||
// Skip any non-vcs files
|
||||
continue;
|
||||
}
|
||||
|
||||
const p = path.join(dir, name);
|
||||
const isDir = fs.statSync(p).isDirectory();
|
||||
if (isDir && recursive) {
|
||||
const more = await next(p);
|
||||
keys = [...keys, ...more];
|
||||
} else if (!recursive && isDir) {
|
||||
keys.push(p);
|
||||
} else if (!isDir) {
|
||||
keys.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
resolve(keys);
|
||||
});
|
||||
};
|
||||
|
||||
const rawKeys = await next(this._getKeyPath(prefix));
|
||||
|
||||
const keys = [];
|
||||
for (const rawKey of rawKeys) {
|
||||
keys.push(rawKey.substring(this._directory.length));
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
_getKeyPath(key: string): string {
|
||||
const p = path.join(this._directory, key);
|
||||
|
||||
// Create base directory
|
||||
mkdirp.sync(path.dirname(p));
|
||||
|
||||
return p;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// @flow
|
||||
|
||||
import type { BaseDriver } from './base';
|
||||
|
||||
export default class MemoryDriver implements BaseDriver {
|
||||
_db: { [string]: Buffer };
|
||||
|
||||
constructor() {
|
||||
this._init();
|
||||
}
|
||||
|
||||
async hasItem(key: string): Promise<boolean> {
|
||||
return this._db[String(key)] instanceof Buffer;
|
||||
}
|
||||
|
||||
async setItem(key: string, value: Buffer): Promise<void> {
|
||||
this._db[String(key)] = value;
|
||||
}
|
||||
|
||||
async getItem(key: string): Promise<Buffer | null> {
|
||||
let value = null;
|
||||
|
||||
if (await this.hasItem(key)) {
|
||||
value = this._db[key];
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
async removeItem(key: string): Promise<void> {
|
||||
delete this._db[String(key)];
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
this._init();
|
||||
}
|
||||
|
||||
async keys(prefix: string, recursive: boolean): Promise<Array<string>> {
|
||||
const keys = [];
|
||||
const baseLevels = prefix.split('/').length;
|
||||
|
||||
for (const key of Object.keys(this._db)) {
|
||||
if (key.indexOf(prefix) !== 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const levels = key.split('/').length;
|
||||
const isOnSameLevel = levels === baseLevels;
|
||||
|
||||
if (!recursive && !isOnSameLevel) {
|
||||
continue;
|
||||
}
|
||||
|
||||
keys.push(key);
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
_init() {
|
||||
this._db = {};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import hook from '../compress';
|
||||
|
||||
describe('compress hook', () => {
|
||||
it('compresses non-extension keys', async () => {
|
||||
const compressed = await hook.write('', 'hello');
|
||||
const uncompressed = await hook.read('', compressed);
|
||||
expect(uncompressed.toString()).toBe('hello');
|
||||
});
|
||||
|
||||
it('writes raw data for extensions', async () => {
|
||||
const compressed = await hook.write('.json', 'hello');
|
||||
expect(compressed.toString('base64')).toBe('hello');
|
||||
});
|
||||
|
||||
it('reads compressed data', async () => {
|
||||
const data = Buffer.from('hello', 'utf8');
|
||||
const compressed = await hook.read('.json', data);
|
||||
expect(compressed.toString('utf8')).toBe('hello');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
// @flow
|
||||
import type { HookFn } from '../index';
|
||||
import zlib from 'zlib';
|
||||
|
||||
const read: HookFn = async function read(extension: string, value: Buffer): Promise<Buffer> {
|
||||
if (extension) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
zlib.gunzip(value, (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const write: HookFn = async function read(extension: string, value: Buffer): Promise<Buffer> {
|
||||
if (extension) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
zlib.gzip(value, (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export default {
|
||||
read,
|
||||
write,
|
||||
};
|
||||
@@ -0,0 +1,101 @@
|
||||
// @flow
|
||||
|
||||
import path from 'path';
|
||||
import type { BaseDriver } from './drivers/base';
|
||||
|
||||
// Can't really make this any more specific unfortunately
|
||||
type JSONValue = any;
|
||||
export type HookFn = (extension: string, value: Buffer) => Promise<Buffer>;
|
||||
export type Hook = { read: HookFn, write: HookFn };
|
||||
|
||||
export default class Store {
|
||||
_driver: BaseDriver;
|
||||
_hooks: Array<Hook>;
|
||||
|
||||
constructor(driver: BaseDriver, hooks?: Array<Hook>) {
|
||||
this._driver = driver;
|
||||
this._hooks = hooks || [];
|
||||
}
|
||||
|
||||
async hasItem(key: string): Promise<boolean> {
|
||||
return this._driver.hasItem(key);
|
||||
}
|
||||
|
||||
async setItem(key: string, value: JSONValue | Buffer): Promise<void> {
|
||||
const ext = path.extname(key);
|
||||
let serializedValue;
|
||||
try {
|
||||
serializedValue = await this._serialize(ext, value);
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to serialize key=${key} err=${err}`);
|
||||
}
|
||||
|
||||
return this._driver.setItem(key, serializedValue);
|
||||
}
|
||||
|
||||
async setItemRaw(key: string, value: Buffer): Promise<void> {
|
||||
return this._driver.setItem(key, value);
|
||||
}
|
||||
|
||||
async getItem(key: string): Promise<JSONValue | null> {
|
||||
const rawValue = await this.getItemRaw(key);
|
||||
if (rawValue === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ext = path.extname(key);
|
||||
|
||||
let value;
|
||||
try {
|
||||
// Without the `await` here, the catch won't get called
|
||||
value = await this._deserialize(ext, rawValue);
|
||||
} catch (err) {
|
||||
console.log('Failed to deserialize', rawValue.toString('base64'));
|
||||
throw new Error(`Failed to deserialize key=${key} err=${err}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
async getItemRaw(key: string): Promise<Buffer | null> {
|
||||
return this._driver.getItem(key);
|
||||
}
|
||||
|
||||
async removeItem(key: string): Promise<void> {
|
||||
return this._driver.removeItem(key);
|
||||
}
|
||||
|
||||
async keys(prefix: string, recursive: boolean = true): Promise<Array<string>> {
|
||||
return this._driver.keys(prefix, recursive);
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
return this._driver.clear();
|
||||
}
|
||||
|
||||
async _serialize(ext: string, raw: JSONValue | Buffer): Promise<Buffer> {
|
||||
let buff = raw instanceof Buffer ? raw : Buffer.from(JSON.stringify(raw, null, 2), 'utf8');
|
||||
|
||||
for (const hook of this._hooks) {
|
||||
if (!hook.write) {
|
||||
continue;
|
||||
}
|
||||
|
||||
buff = await hook.write(ext, buff);
|
||||
}
|
||||
|
||||
return buff;
|
||||
}
|
||||
|
||||
async _deserialize(ext: string, value: Buffer): Promise<JSONValue | null> {
|
||||
for (const hook of this._hooks) {
|
||||
if (!hook.read) {
|
||||
continue;
|
||||
}
|
||||
|
||||
value = await hook.read(ext, value);
|
||||
}
|
||||
|
||||
return JSON.parse(value.toString('utf8'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
// @flow
|
||||
|
||||
export type Team = {
|
||||
id: string,
|
||||
name: string,
|
||||
};
|
||||
|
||||
export type Project = {
|
||||
id: string,
|
||||
name: string,
|
||||
rootDocumentId: string,
|
||||
};
|
||||
|
||||
export type DocumentKey = string;
|
||||
export type BlobId = string;
|
||||
|
||||
export type Head = {|
|
||||
branch: string,
|
||||
|};
|
||||
|
||||
export type SnapshotStateEntry = {|
|
||||
key: DocumentKey,
|
||||
blob: BlobId,
|
||||
name: string,
|
||||
|};
|
||||
|
||||
export type SnapshotState = Array<SnapshotStateEntry>;
|
||||
export type SnapshotStateMap = { [DocumentKey]: SnapshotStateEntry };
|
||||
export type SnapshotId = string;
|
||||
|
||||
export type Snapshot = {|
|
||||
id: SnapshotId,
|
||||
created: Date,
|
||||
parent: string,
|
||||
author: string,
|
||||
name: string,
|
||||
description: string,
|
||||
state: Array<SnapshotStateEntry>,
|
||||
|};
|
||||
|
||||
export type Branch = {|
|
||||
name: string,
|
||||
created: Date,
|
||||
modified: Date,
|
||||
snapshots: Array<string>,
|
||||
|};
|
||||
|
||||
export type StageEntryDelete = {|
|
||||
deleted: true,
|
||||
key: string,
|
||||
name: string,
|
||||
blobId: string,
|
||||
|};
|
||||
|
||||
export type StageEntryAdd = {|
|
||||
added: true,
|
||||
key: string,
|
||||
name: string,
|
||||
blobId: string,
|
||||
blobContent: string,
|
||||
|};
|
||||
|
||||
export type StageEntryModify = {|
|
||||
modified: true,
|
||||
key: string,
|
||||
name: string,
|
||||
blobId: string,
|
||||
blobContent: string,
|
||||
|};
|
||||
|
||||
export type StageEntry = StageEntryDelete | StageEntryAdd | StageEntryModify;
|
||||
|
||||
export type MergeConflict = {|
|
||||
name: string,
|
||||
key: DocumentKey,
|
||||
message: string,
|
||||
mineBlob: BlobId | null,
|
||||
theirsBlob: BlobId | null,
|
||||
choose: BlobId | null,
|
||||
|};
|
||||
|
||||
export type Stage = {
|
||||
[DocumentKey]: StageEntry,
|
||||
};
|
||||
|
||||
export type StatusCandidate = {|
|
||||
key: DocumentKey,
|
||||
name: string,
|
||||
document: Object,
|
||||
|};
|
||||
|
||||
export type StatusCandidateMap = { [DocumentKey]: StatusCandidate };
|
||||
|
||||
export type Status = {|
|
||||
key: string,
|
||||
stage: Stage,
|
||||
unstaged: {
|
||||
[DocumentKey]: StageEntry,
|
||||
},
|
||||
|};
|
||||
@@ -0,0 +1,678 @@
|
||||
import VCS from '../index';
|
||||
import MemoryDriver from '../../store/drivers/memory-driver';
|
||||
|
||||
function newDoc(id) {
|
||||
return { id };
|
||||
}
|
||||
|
||||
async function vcs(branch) {
|
||||
const v = new VCS(new MemoryDriver());
|
||||
await v.switchProject('workspace_1', 'Test Workspace');
|
||||
await v.checkout([], branch);
|
||||
return v;
|
||||
}
|
||||
|
||||
describe('VCS', () => {
|
||||
beforeEach(() => {
|
||||
let ts = 1000000000000;
|
||||
Date.now = jest.fn(() => ts++);
|
||||
});
|
||||
|
||||
describe('status()', () => {
|
||||
it('returns status with no commits', async () => {
|
||||
const v = await vcs('master');
|
||||
const status = await v.status(
|
||||
[
|
||||
{
|
||||
key: 'foo',
|
||||
name: 'Foo',
|
||||
document: newDoc('bar'),
|
||||
},
|
||||
{
|
||||
key: 'baz',
|
||||
name: 'Baz',
|
||||
document: newDoc('qux'),
|
||||
},
|
||||
],
|
||||
{},
|
||||
);
|
||||
|
||||
expect(status).toEqual({
|
||||
key: '0cffac636df909fb4f8e9a25570e5012846b46fd',
|
||||
stage: {},
|
||||
unstaged: {
|
||||
foo: {
|
||||
added: true,
|
||||
key: 'foo',
|
||||
blobContent: '{"id":"bar"}',
|
||||
blobId: 'b933e3dbd1d218e2763de8e3ece6147527d046af',
|
||||
name: 'Foo',
|
||||
},
|
||||
baz: {
|
||||
added: true,
|
||||
key: 'baz',
|
||||
blobContent: '{"id":"qux"}',
|
||||
blobId: 'dc61fb5cb183286293dc7f0e8499b4e1e09eef05',
|
||||
name: 'Baz',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns add/modify/delete operations', async () => {
|
||||
const v = await vcs('master');
|
||||
|
||||
const status1 = await v.status(
|
||||
[
|
||||
{ key: 'a', name: 'A', document: newDoc('aaa') },
|
||||
{ key: 'b', name: 'B', document: newDoc('bbb') },
|
||||
{ key: 'c', name: 'C', document: newDoc('ccc') },
|
||||
],
|
||||
{},
|
||||
);
|
||||
expect(Object.keys(status1.unstaged)).toEqual(['a', 'b', 'c']);
|
||||
|
||||
await v.stage(status1.stage, [
|
||||
status1.unstaged['a'],
|
||||
status1.unstaged['b'],
|
||||
status1.unstaged['c'],
|
||||
]);
|
||||
|
||||
await v.takeSnapshot(status1.stage, 'Add a/b/c');
|
||||
const history = await v.getHistory();
|
||||
expect(history.length).toBe(1);
|
||||
expect(history).toEqual([
|
||||
{
|
||||
created: expect.any(Date),
|
||||
description: '',
|
||||
author: '',
|
||||
id: 'a5fb7270c060c0387967c6db725fdf07e9594911',
|
||||
name: 'Add a/b/c',
|
||||
parent: '0000000000000000000000000000000000000000',
|
||||
state: [
|
||||
{
|
||||
blob: '210be55f7998bb55805f9d65c5345103e7957929',
|
||||
key: 'a',
|
||||
name: 'A',
|
||||
},
|
||||
{
|
||||
blob: '8ce1aded8fa999d1c5632ff993b56dd9aa1f4880',
|
||||
key: 'b',
|
||||
name: 'B',
|
||||
},
|
||||
{
|
||||
blob: 'ce29c03e80866a215004d55f160a9a7b510ceacb',
|
||||
key: 'c',
|
||||
name: 'C',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
// Should get every operation type
|
||||
const status = await v.status(
|
||||
[
|
||||
{ key: 'notA', name: 'Not A', document: newDoc('aaa') },
|
||||
{ key: 'b', name: 'B', document: newDoc('bbb') },
|
||||
{ key: 'c', name: 'C', document: newDoc('modified') },
|
||||
{ key: 'd', name: 'D', document: newDoc('ddd') },
|
||||
],
|
||||
{},
|
||||
);
|
||||
|
||||
expect(status).toEqual({
|
||||
key: '80834d815272c73192291477f70dce8a7c8bc424',
|
||||
stage: {},
|
||||
unstaged: {
|
||||
a: {
|
||||
deleted: true,
|
||||
blobId: '210be55f7998bb55805f9d65c5345103e7957929',
|
||||
key: 'a',
|
||||
name: 'A',
|
||||
},
|
||||
notA: {
|
||||
added: true,
|
||||
key: 'notA',
|
||||
name: 'Not A',
|
||||
blobId: '210be55f7998bb55805f9d65c5345103e7957929',
|
||||
blobContent: '{"id":"aaa"}',
|
||||
},
|
||||
c: {
|
||||
modified: true,
|
||||
key: 'c',
|
||||
name: 'C',
|
||||
blobId: 'c32090250612936632fa4a3127507aa0694ba375',
|
||||
blobContent: '{"id":"modified"}',
|
||||
},
|
||||
d: {
|
||||
added: true,
|
||||
key: 'd',
|
||||
name: 'D',
|
||||
blobId: 'd4d10ce251d7184fa14587796aed6e06c549f7a4',
|
||||
blobContent: '{"id":"ddd"}',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const newStage = await v.stage(status.stage, [
|
||||
status.unstaged['a'],
|
||||
status.unstaged['notA'],
|
||||
status.unstaged['c'],
|
||||
status.unstaged['d'],
|
||||
]);
|
||||
|
||||
const status2 = await v.status(
|
||||
[
|
||||
{ key: 'notA', name: 'Not A', document: newDoc('aaa') },
|
||||
{ key: 'b', name: 'B', document: newDoc('bbb') },
|
||||
{ key: 'c', name: 'C', document: newDoc('modified') },
|
||||
{ key: 'd', name: 'D', document: newDoc('ddd') },
|
||||
],
|
||||
newStage,
|
||||
);
|
||||
|
||||
expect(status2).toEqual({
|
||||
key: '4a0e8fd9ee30acb9c8ff4eb2f73e413ff7175a8c',
|
||||
stage: {
|
||||
a: {
|
||||
deleted: true,
|
||||
blobId: '210be55f7998bb55805f9d65c5345103e7957929',
|
||||
key: 'a',
|
||||
name: 'A',
|
||||
},
|
||||
notA: {
|
||||
added: true,
|
||||
blobId: '210be55f7998bb55805f9d65c5345103e7957929',
|
||||
key: 'notA',
|
||||
name: 'Not A',
|
||||
blobContent: '{"id":"aaa"}',
|
||||
},
|
||||
c: {
|
||||
modified: true,
|
||||
blobId: 'c32090250612936632fa4a3127507aa0694ba375',
|
||||
key: 'c',
|
||||
name: 'C',
|
||||
blobContent: '{"id":"modified"}',
|
||||
},
|
||||
d: {
|
||||
added: true,
|
||||
blobId: 'd4d10ce251d7184fa14587796aed6e06c549f7a4',
|
||||
key: 'd',
|
||||
name: 'D',
|
||||
blobContent: '{"id":"ddd"}',
|
||||
},
|
||||
},
|
||||
unstaged: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('can appear both staged and unstaged', async () => {
|
||||
const v = await vcs('master');
|
||||
|
||||
const status = await v.status(
|
||||
[
|
||||
{ key: 'a', name: 'A', document: newDoc('aaa') },
|
||||
{ key: 'b', name: 'B', document: newDoc('bbb') },
|
||||
],
|
||||
{},
|
||||
);
|
||||
const stage = await v.stage(status.stage, [status.unstaged['a']]);
|
||||
|
||||
const status2 = await v.status(
|
||||
[
|
||||
{ key: 'a', name: 'A', document: newDoc('modified') },
|
||||
{ key: 'b', name: 'B', document: newDoc('bbb') },
|
||||
],
|
||||
stage,
|
||||
);
|
||||
|
||||
expect(status2).toEqual({
|
||||
key: '886337817e3c1ff7df3fcfa7c30192eb854b45cf',
|
||||
stage: {
|
||||
a: {
|
||||
added: true,
|
||||
blobId: '210be55f7998bb55805f9d65c5345103e7957929',
|
||||
name: 'A',
|
||||
key: 'a',
|
||||
blobContent: '{"id":"aaa"}',
|
||||
},
|
||||
},
|
||||
unstaged: {
|
||||
a: {
|
||||
added: true,
|
||||
blobId: 'c32090250612936632fa4a3127507aa0694ba375',
|
||||
key: 'a',
|
||||
name: 'A',
|
||||
blobContent: '{"id":"modified"}',
|
||||
},
|
||||
b: {
|
||||
added: true,
|
||||
blobId: '8ce1aded8fa999d1c5632ff993b56dd9aa1f4880',
|
||||
name: 'B',
|
||||
key: 'b',
|
||||
blobContent: '{"id":"bbb"}',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should not show committed entities', async () => {
|
||||
const v = await vcs('master');
|
||||
|
||||
const status = await v.status([{ key: 'foo', name: 'Foo', document: newDoc('bar') }], {});
|
||||
const stage2 = await v.stage(status.stage, [status.unstaged['foo']]);
|
||||
await v.takeSnapshot(stage2, 'Add foo');
|
||||
|
||||
const status2 = await v.status([{ key: 'foo', name: 'Foo', document: newDoc('bar') }], {});
|
||||
expect(status2).toEqual({
|
||||
key: 'ca455b43c1e992812d81032e79e037a8db85fa8b',
|
||||
stage: {},
|
||||
unstaged: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('stage()', () => {
|
||||
it('stages entity', async () => {
|
||||
const v = await vcs('master');
|
||||
|
||||
const status = await v.status(
|
||||
[
|
||||
{ key: 'foo', name: 'Foo', document: newDoc('bar') },
|
||||
{ key: 'baz', name: 'Baz', document: newDoc('qux') },
|
||||
],
|
||||
{},
|
||||
);
|
||||
|
||||
const stage = await v.stage(status.stage, [status.unstaged['foo']]);
|
||||
expect(stage).toEqual({
|
||||
foo: {
|
||||
key: 'foo',
|
||||
name: 'Foo',
|
||||
blobContent: '{"id":"bar"}',
|
||||
blobId: 'b933e3dbd1d218e2763de8e3ece6147527d046af',
|
||||
added: true,
|
||||
},
|
||||
});
|
||||
|
||||
const status2 = await v.status(
|
||||
[
|
||||
{ key: 'foo', name: 'Foo', document: newDoc('bar') },
|
||||
{ key: 'baz', name: 'Baz', document: newDoc('qux') },
|
||||
],
|
||||
stage,
|
||||
);
|
||||
expect(status2).toEqual({
|
||||
key: '19c281e6bbbebec0143491bf14f9a013a4114125',
|
||||
stage: {
|
||||
foo: {
|
||||
name: 'Foo',
|
||||
key: 'foo',
|
||||
blobContent: '{"id":"bar"}',
|
||||
blobId: 'b933e3dbd1d218e2763de8e3ece6147527d046af',
|
||||
added: true,
|
||||
},
|
||||
},
|
||||
unstaged: {
|
||||
baz: {
|
||||
key: 'baz',
|
||||
name: 'Baz',
|
||||
blobContent: '{"id":"qux"}',
|
||||
blobId: 'dc61fb5cb183286293dc7f0e8499b4e1e09eef05',
|
||||
added: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('takeSnapshot()', () => {
|
||||
it('commits basic entity', async () => {
|
||||
const v = await vcs('master');
|
||||
|
||||
const status = await v.status([{ key: 'foo', name: 'Foo', document: newDoc('bar') }], {});
|
||||
const stage = await v.stage(status.stage, [status.unstaged['foo']]);
|
||||
await v.takeSnapshot(stage, 'Add foo');
|
||||
|
||||
const history = await v.getHistory();
|
||||
expect(history).toEqual([
|
||||
{
|
||||
id: '24b4888f4cecc5a1ac272eb1641cb5aa102c1675',
|
||||
author: '',
|
||||
created: expect.any(Date),
|
||||
name: 'Add foo',
|
||||
description: '',
|
||||
parent: '0000000000000000000000000000000000000000',
|
||||
state: [
|
||||
{
|
||||
blob: 'b933e3dbd1d218e2763de8e3ece6147527d046af',
|
||||
key: 'foo',
|
||||
name: 'Foo',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('commits deleted entity', async () => {
|
||||
const v = await vcs('master');
|
||||
|
||||
const status = await v.status([{ key: 'foo', name: 'Foo', document: newDoc('bar') }], {});
|
||||
const stage = await v.stage(status.stage, [status.unstaged['foo']]);
|
||||
await v.takeSnapshot(stage, 'Add foo');
|
||||
|
||||
const history = await v.getHistory();
|
||||
expect(history).toEqual([
|
||||
{
|
||||
id: '365c4341f6d57e18994df2429387b78f2c9a31cb',
|
||||
author: '',
|
||||
created: expect.any(Date),
|
||||
name: 'Add foo',
|
||||
description: '',
|
||||
parent: '0000000000000000000000000000000000000000',
|
||||
state: [
|
||||
{
|
||||
blob: 'b933e3dbd1d218e2763de8e3ece6147527d046af',
|
||||
key: 'foo',
|
||||
name: 'Foo',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const status2 = await v.status([], {});
|
||||
const stage2 = await v.stage(status2.stage, [status2.unstaged['foo']]);
|
||||
await v.takeSnapshot(stage2, 'Delete foo');
|
||||
const history2 = await v.getHistory();
|
||||
|
||||
expect(history2).toEqual([
|
||||
{
|
||||
id: '365c4341f6d57e18994df2429387b78f2c9a31cb',
|
||||
author: '',
|
||||
created: expect.any(Date),
|
||||
name: 'Add foo',
|
||||
description: '',
|
||||
parent: '0000000000000000000000000000000000000000',
|
||||
state: [
|
||||
{
|
||||
blob: 'b933e3dbd1d218e2763de8e3ece6147527d046af',
|
||||
key: 'foo',
|
||||
name: 'Foo',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '42f5b05d004f99e875a7eb3fe24ad7f94ad3e0c5',
|
||||
author: '',
|
||||
created: expect.any(Date),
|
||||
name: 'Delete foo',
|
||||
description: '',
|
||||
parent: '365c4341f6d57e18994df2429387b78f2c9a31cb',
|
||||
state: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBranches()', () => {
|
||||
it('lists branches', async () => {
|
||||
const v = await vcs('master');
|
||||
|
||||
await v.checkout([], 'branch-1');
|
||||
await v.checkout([], 'branch-2');
|
||||
|
||||
const branches = await v.getBranches();
|
||||
expect(branches).toEqual(['master', 'branch-1', 'branch-2']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeBranch()', () => {
|
||||
it('cannot remove empty branch', async () => {
|
||||
const v = await vcs('master');
|
||||
|
||||
let didError = false;
|
||||
try {
|
||||
await v.removeBranch();
|
||||
} catch (err) {
|
||||
didError = true;
|
||||
}
|
||||
|
||||
expect(didError).toBe(true);
|
||||
});
|
||||
|
||||
it('cannot remove current branch', async () => {
|
||||
const v = await vcs('master');
|
||||
|
||||
let didError = false;
|
||||
try {
|
||||
await v.removeBranch('master');
|
||||
} catch (err) {
|
||||
didError = true;
|
||||
}
|
||||
|
||||
expect(didError).toBe(true);
|
||||
});
|
||||
|
||||
it('remove branch', async () => {
|
||||
const v = await vcs('master');
|
||||
|
||||
// Add something to master
|
||||
const status1 = await v.status([{ key: 'foo', name: 'Foo', document: newDoc('bar') }], {});
|
||||
const stage1 = await v.stage(status1.stage, [status1.unstaged['foo']]);
|
||||
await v.takeSnapshot(stage1, 'Add foo');
|
||||
|
||||
// Checkout branch
|
||||
await v.checkout([], 'new-branch');
|
||||
expect(await v.getBranches()).toEqual(['master', 'new-branch']);
|
||||
|
||||
// Back to master and delete other branch
|
||||
await v.checkout([], 'master');
|
||||
await v.removeBranch('new-branch');
|
||||
expect(await v.getBranches()).toEqual(['master']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fork()', () => {
|
||||
it('forks to a new branch', async () => {
|
||||
const v = await vcs('master');
|
||||
|
||||
// Add something to master
|
||||
const status1 = await v.status([{ key: 'foo', name: 'Foo', document: newDoc('bar') }], {});
|
||||
const stage1 = await v.stage(status1.stage, [status1.unstaged['foo']]);
|
||||
await v.takeSnapshot(stage1, 'Add foo');
|
||||
|
||||
// Checkout branch
|
||||
await v.fork('new-branch');
|
||||
await v.checkout([], 'new-branch');
|
||||
const history = await v.getHistory();
|
||||
expect(await v.getBranch()).toBe('new-branch');
|
||||
expect(history).toEqual([
|
||||
{
|
||||
created: expect.any(Date),
|
||||
id: 'e1afd0f5607d8da06b4360eb22d41650262f6dc5',
|
||||
author: '',
|
||||
parent: '0000000000000000000000000000000000000000',
|
||||
name: 'Add foo',
|
||||
description: '',
|
||||
state: [
|
||||
{
|
||||
blob: 'b933e3dbd1d218e2763de8e3ece6147527d046af',
|
||||
key: 'foo',
|
||||
name: 'Foo',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
const name = 'merge()';
|
||||
describe(name, () => {
|
||||
it('performs fast-forward merge', async () => {
|
||||
const v = await vcs('master');
|
||||
|
||||
const status1 = await v.status(
|
||||
[
|
||||
{ key: 'a', name: 'A', document: newDoc('aaa') },
|
||||
{ key: 'b', name: 'B', document: newDoc('bbb') },
|
||||
],
|
||||
{},
|
||||
);
|
||||
const stage1 = await v.stage(status1.stage, [status1.unstaged['a'], status1.unstaged['b']]);
|
||||
await v.takeSnapshot(stage1, 'Add A and B');
|
||||
expect((await v.getHistory())[0].state).toEqual([
|
||||
expect.objectContaining({ key: 'a' }),
|
||||
expect.objectContaining({ key: 'b' }),
|
||||
]);
|
||||
|
||||
await v.fork('feature-a');
|
||||
await v.checkout([], 'feature-a');
|
||||
const status2 = await v.status(
|
||||
[
|
||||
{ key: 'a', name: 'A', document: newDoc('aaa') },
|
||||
{ key: 'b', name: 'B', document: newDoc('bbbbbbb') },
|
||||
{ key: 'c', name: 'C', document: newDoc('ccc') },
|
||||
],
|
||||
status1.stage,
|
||||
);
|
||||
const stage2 = await v.stage(status2.stage, [status2.unstaged['b'], status2.unstaged['c']]);
|
||||
await v.takeSnapshot(stage2, 'Add C, modify B');
|
||||
expect((await v.getHistory())[1].state).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ key: 'a' }),
|
||||
expect.objectContaining({ key: 'b' }),
|
||||
expect.objectContaining({ key: 'c' }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('merges even if no common root', async () => {
|
||||
const v = await vcs('master');
|
||||
|
||||
const status1 = await v.status(
|
||||
[
|
||||
{ key: 'a', name: 'A', document: newDoc('aaa') },
|
||||
{ key: 'b', name: 'B', document: newDoc('bbb') },
|
||||
],
|
||||
{},
|
||||
);
|
||||
const stage1 = await v.stage(status1.stage, [status1.unstaged['a'], status1.unstaged['b']]);
|
||||
await v.takeSnapshot(stage1, 'message');
|
||||
});
|
||||
|
||||
it('does something', async () => {
|
||||
const v = await vcs('master');
|
||||
|
||||
// Add a file to master
|
||||
expect(await v.getBranch()).toBe('master');
|
||||
const status1 = await v.status([{ key: 'a', name: 'A', document: newDoc('aaa') }], {});
|
||||
const stage = await v.stage(status1.stage, [status1.unstaged['a']]);
|
||||
await v.takeSnapshot(stage, 'Add A');
|
||||
expect(await v.getHistory()).toEqual([
|
||||
{
|
||||
id: 'c0b75a0cb27c946e376326b963ce544af08b680a',
|
||||
author: '',
|
||||
created: expect.any(Date),
|
||||
parent: '0000000000000000000000000000000000000000',
|
||||
name: 'Add A',
|
||||
description: '',
|
||||
state: [
|
||||
{
|
||||
blob: '210be55f7998bb55805f9d65c5345103e7957929',
|
||||
key: 'a',
|
||||
name: 'A',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
// Checkout new branch and add file
|
||||
await v.fork('new-branch');
|
||||
await v.checkout([], 'new-branch');
|
||||
expect(await v.getBranch()).toBe('new-branch');
|
||||
|
||||
const status2 = await v.status([{ key: 'b', name: 'B', document: newDoc('bbb') }], {});
|
||||
const stage2 = await v.stage(status2.stage, [status2.unstaged['b']]);
|
||||
await v.takeSnapshot(stage2, 'Add B');
|
||||
expect(await v.getHistory()).toEqual([
|
||||
{
|
||||
id: 'c0b75a0cb27c946e376326b963ce544af08b680a',
|
||||
author: '',
|
||||
created: expect.any(Date),
|
||||
parent: '0000000000000000000000000000000000000000',
|
||||
name: 'Add A',
|
||||
description: '',
|
||||
state: [
|
||||
{
|
||||
blob: '210be55f7998bb55805f9d65c5345103e7957929',
|
||||
key: 'a',
|
||||
name: 'A',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'b0fb09c8ac80b34f94dc6751da6b5aa634682f45',
|
||||
author: '',
|
||||
created: expect.any(Date),
|
||||
parent: 'c0b75a0cb27c946e376326b963ce544af08b680a',
|
||||
name: 'Add B',
|
||||
description: '',
|
||||
state: [
|
||||
{
|
||||
blob: '210be55f7998bb55805f9d65c5345103e7957929',
|
||||
key: 'a',
|
||||
name: 'A',
|
||||
},
|
||||
{
|
||||
blob: '8ce1aded8fa999d1c5632ff993b56dd9aa1f4880',
|
||||
key: 'b',
|
||||
name: 'B',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
// Merge new branch back into master
|
||||
await v.checkout([], 'master');
|
||||
expect(await v.getBranch()).toBe('master');
|
||||
await v.merge([], 'new-branch');
|
||||
expect(await v.getHistory()).toEqual([
|
||||
{
|
||||
id: 'c0b75a0cb27c946e376326b963ce544af08b680a',
|
||||
author: '',
|
||||
created: expect.any(Date),
|
||||
parent: '0000000000000000000000000000000000000000',
|
||||
name: 'Add A',
|
||||
description: '',
|
||||
state: [
|
||||
{
|
||||
blob: '210be55f7998bb55805f9d65c5345103e7957929',
|
||||
key: 'a',
|
||||
name: 'A',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'b0fb09c8ac80b34f94dc6751da6b5aa634682f45',
|
||||
author: '',
|
||||
created: expect.any(Date),
|
||||
parent: 'c0b75a0cb27c946e376326b963ce544af08b680a',
|
||||
name: 'Add B',
|
||||
description: '',
|
||||
state: [
|
||||
{
|
||||
blob: '210be55f7998bb55805f9d65c5345103e7957929',
|
||||
key: 'a',
|
||||
name: 'A',
|
||||
},
|
||||
{
|
||||
blob: '8ce1aded8fa999d1c5632ff993b56dd9aa1f4880',
|
||||
key: 'b',
|
||||
name: 'B',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import * as paths from '../paths';
|
||||
|
||||
describe('paths', () => {
|
||||
it('successes', async () => {
|
||||
expect(paths.projectBase('p1')).toBe('/projects/p1/');
|
||||
expect(paths.head('p1')).toBe('/projects/p1/head.json');
|
||||
expect(paths.project('p1')).toBe('/projects/p1/meta.json');
|
||||
expect(paths.blobs('p1')).toBe('/projects/p1/blobs/');
|
||||
expect(paths.blob('p1', 'b1c1c2')).toBe('/projects/p1/blobs/b1/c1c2');
|
||||
expect(paths.snapshots('p1')).toBe('/projects/p1/snapshots/');
|
||||
expect(paths.snapshot('p1', 's1')).toBe('/projects/p1/snapshots/s1.json');
|
||||
expect(paths.branches('p1')).toBe('/projects/p1/branches/');
|
||||
expect(paths.branch('p1', 'master')).toBe('/projects/p1/branches/master.json');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,893 @@
|
||||
import {
|
||||
combinedMapKeys,
|
||||
compareBranches,
|
||||
generateCandidateMap,
|
||||
generateSnapshotStateMap,
|
||||
getRootSnapshot,
|
||||
getStagable,
|
||||
hashDocument,
|
||||
preMergeCheck,
|
||||
stateDelta,
|
||||
threeWayMerge,
|
||||
updateStateWithConflictResolutions,
|
||||
} from '../util';
|
||||
|
||||
describe('util', () => {
|
||||
describe('generateSnapshotStateMap()', () => {
|
||||
it('generates from simple states', async () => {
|
||||
const snapshot = newSnapshot(1, [
|
||||
newStateEntry('doc_1', 'blob_1'),
|
||||
newStateEntry('doc_2', 'blob_2'),
|
||||
]);
|
||||
|
||||
const map = generateSnapshotStateMap(snapshot);
|
||||
expect(Object.keys(map).sort()).toEqual(['doc_1', 'doc_2']);
|
||||
});
|
||||
|
||||
it('works with duplicates', async () => {
|
||||
const snapshot = newSnapshot(1, [
|
||||
newStateEntry('doc_1', 'blob_1'),
|
||||
newStateEntry('doc_2', 'blob_2'),
|
||||
newStateEntry('doc_2', 'blob_2'),
|
||||
newStateEntry('doc_2', 'blob_2'),
|
||||
]);
|
||||
|
||||
const map = generateSnapshotStateMap(snapshot);
|
||||
expect(Object.keys(map).sort()).toEqual(['doc_1', 'doc_2']);
|
||||
});
|
||||
|
||||
it('works with null snapshots', async () => {
|
||||
const map = generateSnapshotStateMap(null);
|
||||
expect(map).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateCandidateMap()', () => {
|
||||
it('generates from simple candidates', async () => {
|
||||
const candidates = [
|
||||
newCandidate('doc_2', 2),
|
||||
newCandidate('doc_1', 1),
|
||||
newCandidate('doc_2', 2),
|
||||
];
|
||||
|
||||
const map = generateCandidateMap(candidates);
|
||||
expect(Object.keys(map).sort()).toEqual(['doc_1', 'doc_2']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('combineMapKeys()', () => {
|
||||
it('generates from simple states', async () => {
|
||||
const map1 = generateSnapshotStateMap(
|
||||
newSnapshot(1, [newStateEntry('doc_1', 'blob_1'), newStateEntry('doc_2', 'blob_2')]),
|
||||
);
|
||||
const map2 = generateSnapshotStateMap(
|
||||
newSnapshot(1, [newStateEntry('doc_4', 'blob_4'), newStateEntry('doc_1', 'blob_1')]),
|
||||
);
|
||||
const map3 = generateSnapshotStateMap(newSnapshot(1, []));
|
||||
|
||||
const keys = combinedMapKeys(map1, map2, map3);
|
||||
expect(keys.sort()).toEqual(['doc_1', 'doc_2', 'doc_4']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('threeWayMerge()', () => {
|
||||
const A1 = { key: 'a', blob: 'a.1', name: '' };
|
||||
const A2 = { key: 'a', blob: 'a.2', name: '' };
|
||||
const A3 = { key: 'a', blob: 'a.3', name: '' };
|
||||
const B1 = { key: 'b', blob: 'b.1', name: '' };
|
||||
const B2 = { key: 'b', blob: 'b.2', name: '' };
|
||||
const C1 = { key: 'c', blob: 'c.1', name: '' };
|
||||
|
||||
it('does nothing with empty states', () => {
|
||||
const newState = threeWayMerge([], [], []);
|
||||
expect(newState).toEqual({
|
||||
conflicts: [],
|
||||
state: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('adds new object from other', () => {
|
||||
const root = [A1];
|
||||
const trunk = [A1];
|
||||
const other = [A1, B1];
|
||||
expect(threeWayMerge(root, trunk, other)).toEqual({
|
||||
conflicts: [],
|
||||
state: [A1, B1],
|
||||
});
|
||||
});
|
||||
|
||||
it('removes object from trunk', () => {
|
||||
const root = [A1, C1];
|
||||
const trunk = [A1, C1];
|
||||
const other = [A1];
|
||||
expect(threeWayMerge(root, trunk, other)).toEqual({
|
||||
conflicts: [],
|
||||
state: [A1],
|
||||
});
|
||||
});
|
||||
|
||||
it('removes object from both', () => {
|
||||
const root = [A1, C1];
|
||||
const trunk = [A1];
|
||||
const other = [A1];
|
||||
expect(threeWayMerge(root, trunk, other)).toEqual({
|
||||
conflicts: [],
|
||||
state: [A1],
|
||||
});
|
||||
});
|
||||
|
||||
it('modifies object from other', () => {
|
||||
const root = [A1];
|
||||
const trunk = [A1];
|
||||
const other = [A2];
|
||||
expect(threeWayMerge(root, trunk, other)).toEqual({
|
||||
conflicts: [],
|
||||
state: [A2],
|
||||
});
|
||||
});
|
||||
|
||||
it('modifies object from trunk', () => {
|
||||
const root = [A1];
|
||||
const trunk = [A2];
|
||||
const other = [A1];
|
||||
expect(threeWayMerge(root, trunk, other)).toEqual({
|
||||
conflicts: [],
|
||||
state: [A2],
|
||||
});
|
||||
});
|
||||
|
||||
it('conflicts when both modify', () => {
|
||||
const root = [A1];
|
||||
const trunk = [A2];
|
||||
const other = [A3];
|
||||
|
||||
expect(threeWayMerge(root, trunk, other)).toEqual({
|
||||
conflicts: [
|
||||
{
|
||||
key: A2.key,
|
||||
name: A2.name,
|
||||
choose: A3.blob,
|
||||
mineBlob: A2.blob,
|
||||
theirsBlob: A3.blob,
|
||||
message: 'both modified',
|
||||
},
|
||||
],
|
||||
state: [A2],
|
||||
});
|
||||
});
|
||||
|
||||
it('works if both add the same document', () => {
|
||||
const root = [A1];
|
||||
const trunk = [A1, B1];
|
||||
const other = [A1, B1];
|
||||
|
||||
expect(threeWayMerge(root, trunk, other)).toEqual({
|
||||
conflicts: [],
|
||||
state: [A1, B1],
|
||||
});
|
||||
});
|
||||
|
||||
it('conflicts if both add the same document but different', () => {
|
||||
const root = [A1];
|
||||
const trunk = [A1, B1];
|
||||
const other = [A1, B2];
|
||||
|
||||
expect(threeWayMerge(root, trunk, other)).toEqual({
|
||||
conflicts: [
|
||||
{
|
||||
key: B1.key,
|
||||
name: B1.name,
|
||||
choose: B2.blob,
|
||||
mineBlob: B1.blob,
|
||||
theirsBlob: B2.blob,
|
||||
message: 'both added',
|
||||
},
|
||||
],
|
||||
state: [A1, B1],
|
||||
});
|
||||
});
|
||||
|
||||
it('conflicts if modified in trunk and deleted in other', () => {
|
||||
const root = [A1, B1];
|
||||
const trunk = [A1, B2];
|
||||
const other = [A1];
|
||||
|
||||
expect(threeWayMerge(root, trunk, other)).toEqual({
|
||||
conflicts: [
|
||||
{
|
||||
key: B1.key,
|
||||
name: B1.name,
|
||||
choose: B2.blob,
|
||||
mineBlob: B2.blob,
|
||||
theirsBlob: null,
|
||||
message: 'they deleted and you modified',
|
||||
},
|
||||
],
|
||||
state: [A1, B2],
|
||||
});
|
||||
});
|
||||
|
||||
it('conflicts if modified in other and deleted in trunk', () => {
|
||||
const root = [A1, B1];
|
||||
const trunk = [A1];
|
||||
const other = [A1, B2];
|
||||
|
||||
expect(threeWayMerge(root, trunk, other)).toEqual({
|
||||
conflicts: [
|
||||
{
|
||||
key: B1.key,
|
||||
name: B1.name,
|
||||
choose: B2.blob,
|
||||
mineBlob: null,
|
||||
theirsBlob: B2.blob,
|
||||
message: 'you deleted and they modified',
|
||||
},
|
||||
],
|
||||
state: [A1, B2],
|
||||
});
|
||||
});
|
||||
|
||||
it('works with no root state', () => {
|
||||
const root = [];
|
||||
const trunk = [A1, C1];
|
||||
const other = [A1, B2];
|
||||
|
||||
expect(threeWayMerge(root, trunk, other)).toEqual({
|
||||
conflicts: [],
|
||||
state: expect.arrayContaining([A1, B2, C1]),
|
||||
});
|
||||
});
|
||||
|
||||
it('works with no root state and conflict', () => {
|
||||
const root = [];
|
||||
const trunk = [A1];
|
||||
const other = [A2];
|
||||
|
||||
expect(threeWayMerge(root, trunk, other)).toEqual({
|
||||
conflicts: [
|
||||
{
|
||||
key: A1.key,
|
||||
name: A1.name,
|
||||
choose: A2.blob,
|
||||
mineBlob: A1.blob,
|
||||
theirsBlob: A2.blob,
|
||||
message: 'both added',
|
||||
},
|
||||
],
|
||||
state: [A1],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('stateDelta()', () => {
|
||||
const A1 = { key: 'a', blob: 'a.1', name: '' };
|
||||
const A2 = { key: 'a', blob: 'a.2', name: '' };
|
||||
const B1 = { key: 'b', blob: 'b.1', name: '' };
|
||||
const B2 = { key: 'b', blob: 'b.2', name: '' };
|
||||
const C1 = { key: 'c', blob: 'c.1', name: '' };
|
||||
|
||||
it('modifies an entry', () => {
|
||||
const base = [A1];
|
||||
const dsrd = [A2];
|
||||
|
||||
expect(stateDelta(base, dsrd)).toEqual({
|
||||
add: [],
|
||||
update: [A2],
|
||||
remove: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('adds an entry', () => {
|
||||
const base = [];
|
||||
const dsrd = [B1];
|
||||
|
||||
expect(stateDelta(base, dsrd)).toEqual({
|
||||
add: [B1],
|
||||
update: [],
|
||||
remove: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('deletes an entry', () => {
|
||||
const base = [A1];
|
||||
const dsrd = [];
|
||||
|
||||
expect(stateDelta(base, dsrd)).toEqual({
|
||||
add: [],
|
||||
update: [],
|
||||
remove: [A1],
|
||||
});
|
||||
});
|
||||
|
||||
it('complex combo', () => {
|
||||
const base = [A1, B1, C1];
|
||||
const dsrd = [B2, C1];
|
||||
|
||||
expect(stateDelta(base, dsrd)).toEqual({
|
||||
add: [],
|
||||
update: [B2],
|
||||
remove: [A1],
|
||||
});
|
||||
});
|
||||
|
||||
it('invalid duplicate key state', () => {
|
||||
const base = [A1, A2];
|
||||
const dsrd = [A2];
|
||||
|
||||
expect(stateDelta(base, dsrd)).toEqual({
|
||||
add: [],
|
||||
update: [],
|
||||
remove: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStagable()', () => {
|
||||
const A1 = { key: 'a', blob: '1932cc749f57a77ba4a231d62559c47ee931ee2c', name: '' };
|
||||
const A2 = { key: 'a', blob: 'cf2ec18c2e686b4c966725f30050120d42d4b2e5', name: '' };
|
||||
const B1 = { key: 'b', blob: '05ab2ec03c7e32eff27305b4a11c573a3843fa01', name: '' };
|
||||
const B2 = { key: 'b', blob: 'eb157a78ebd367a004c5a32a18d6d8c18e60ce00', name: '' };
|
||||
const C1 = { key: 'c', blob: 'db7d35b96291b823e674bf23ac59e6e116b1890e', name: '' };
|
||||
const DA1 = { key: 'a', document: { id: 'a', v: 1 }, name: '' };
|
||||
const DA2 = { key: 'a', document: { id: 'a', v: 2 }, name: '' };
|
||||
const DB1 = { key: 'b', document: { id: 'b', v: 1 }, name: '' };
|
||||
const DB2 = { key: 'b', document: { id: 'b', v: 2 }, name: '' };
|
||||
const DC1 = { key: 'c', document: { id: 'c', v: 1 }, name: '' };
|
||||
|
||||
it('works with empty state', () => {
|
||||
const state = [];
|
||||
const dsrd = [DA1];
|
||||
|
||||
expect(getStagable(state, dsrd)).toEqual([
|
||||
{
|
||||
blobContent: '{"id":"a","v":1}',
|
||||
blobId: A1.blob,
|
||||
key: 'a',
|
||||
name: '',
|
||||
added: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('stays the same', () => {
|
||||
const state = [A1, B1, C1];
|
||||
const dsrd = [DA1, DB1, DC1];
|
||||
|
||||
expect(getStagable(state, dsrd)).toEqual([]);
|
||||
});
|
||||
|
||||
it('adds a document', () => {
|
||||
const state = [A1];
|
||||
const dsrd = [DA1, DB1];
|
||||
|
||||
expect(getStagable(state, dsrd)).toEqual([
|
||||
{
|
||||
key: 'b',
|
||||
blobId: B1.blob,
|
||||
blobContent: '{"id":"b","v":1}',
|
||||
name: '',
|
||||
added: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('modifies a document', () => {
|
||||
const state = [B1];
|
||||
const dsrd = [DB2];
|
||||
|
||||
expect(getStagable(state, dsrd)).toEqual([
|
||||
{
|
||||
key: 'b',
|
||||
blobId: B2.blob,
|
||||
blobContent: '{"id":"b","v":2}',
|
||||
name: '',
|
||||
modified: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('deletes a document', () => {
|
||||
const state = [B1];
|
||||
const dsrd = [];
|
||||
|
||||
expect(getStagable(state, dsrd)).toEqual([
|
||||
{
|
||||
key: 'b',
|
||||
blobId: '05ab2ec03c7e32eff27305b4a11c573a3843fa01',
|
||||
name: '',
|
||||
deleted: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('does a lot of things', () => {
|
||||
const state = [A1, C1];
|
||||
const dsrd = [DA2, DB1];
|
||||
|
||||
expect(getStagable(state, dsrd)).toEqual([
|
||||
{
|
||||
blobContent: '{"id":"a","v":2}',
|
||||
blobId: A2.blob,
|
||||
key: 'a',
|
||||
modified: true,
|
||||
name: '',
|
||||
},
|
||||
{
|
||||
key: 'c',
|
||||
blobId: 'db7d35b96291b823e674bf23ac59e6e116b1890e',
|
||||
name: '',
|
||||
deleted: true,
|
||||
},
|
||||
{
|
||||
key: 'b',
|
||||
blobId: B1.blob,
|
||||
blobContent: '{"id":"b","v":1}',
|
||||
name: '',
|
||||
added: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRootSnapshot()', () => {
|
||||
it('works with empty states', () => {
|
||||
const a = newBranch([]);
|
||||
const b = newBranch([]);
|
||||
|
||||
expect(getRootSnapshot(a, b)).toBeNull();
|
||||
});
|
||||
|
||||
it('works with different states', () => {
|
||||
const a = newBranch(['s1', 's2']);
|
||||
const b = newBranch(['s3', 's5']);
|
||||
|
||||
expect(getRootSnapshot(a, b)).toBeNull();
|
||||
});
|
||||
|
||||
it('works with same states', () => {
|
||||
const a = newBranch(['s1', 's2', 's3']);
|
||||
const b = newBranch(['s1', 's2', 's3']);
|
||||
|
||||
expect(getRootSnapshot(a, b)).toBe('s3');
|
||||
});
|
||||
|
||||
it('works with subset', () => {
|
||||
const a = newBranch(['s1']);
|
||||
const b = newBranch(['s1', 's2', 's3']);
|
||||
|
||||
expect(getRootSnapshot(a, b)).toBe('s1');
|
||||
expect(getRootSnapshot(b, a)).toBe('s1');
|
||||
});
|
||||
|
||||
it('works with missing middle', () => {
|
||||
const a = newBranch(['s1', 's3', 's4']);
|
||||
const b = newBranch(['s1', 's2', 's3', 's6']);
|
||||
|
||||
expect(getRootSnapshot(a, b)).toBe('s3');
|
||||
expect(getRootSnapshot(b, a)).toBe('s3');
|
||||
});
|
||||
|
||||
it('works with missing middle', () => {
|
||||
const a = newBranch(['s1', 's3']);
|
||||
const b = newBranch(['s1', 's2', 's3']);
|
||||
|
||||
expect(getRootSnapshot(a, b)).toBe('s3');
|
||||
expect(getRootSnapshot(b, a)).toBe('s3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateStateWithConflictResolutions()', () => {
|
||||
const A1 = { key: 'a', blob: '1932cc749f57a77ba4a231d62559c47ee931ee2c', name: '' };
|
||||
const A2 = { key: 'a', blob: 'cf2ec18c2e686b4c966725f30050120d42d4b2e5', name: '' };
|
||||
const B1 = { key: 'b', blob: '05ab2ec03c7e32eff27305b4a11c573a3843fa01', name: '' };
|
||||
const C1 = { key: 'c', blob: 'db7d35b96291b823e674bf23ac59e6e116b1890e', name: '' };
|
||||
const D1 = { key: 'd', blob: 'f23ac59e6e11db7d35b96291b823e674b6b1890e', name: '' };
|
||||
const E1 = { key: 'e', blob: '1db7d35b96291f23ac59e6e1b823e674b6b1890e', name: '' };
|
||||
const F1 = { key: 'f', blob: '6291b823e674b6b137172d35b9f23ac59e6e190e', name: '' };
|
||||
|
||||
it('does it', () => {
|
||||
const state = [A1, B1, E1, F1];
|
||||
const conflicts = [
|
||||
{
|
||||
key: A1.key,
|
||||
name: A1.name,
|
||||
mineBlob: A1.blob,
|
||||
theirsBlob: A2.blob,
|
||||
choose: A1.blob,
|
||||
},
|
||||
{
|
||||
key: B1.key,
|
||||
name: B1.name,
|
||||
mineBlob: B1.blob,
|
||||
theirsBlob: null,
|
||||
choose: null,
|
||||
},
|
||||
{
|
||||
key: C1.key,
|
||||
name: C1.name,
|
||||
mineBlob: null,
|
||||
theirsBlob: C1.blob,
|
||||
choose: C1.blob,
|
||||
},
|
||||
{
|
||||
key: D1.key,
|
||||
name: D1.name,
|
||||
mineBlob: D1.blob,
|
||||
theirsBlob: null,
|
||||
choose: null,
|
||||
},
|
||||
];
|
||||
|
||||
expect(updateStateWithConflictResolutions(state, conflicts)).toEqual([A1, E1, F1, C1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('preMergeCheck()', () => {
|
||||
const A1 = { key: 'a', blob: '1932cc749f57a77ba4a231d62559c47ee931ee2c', name: '' };
|
||||
const A2 = { key: 'a', blob: 'cf2ec18c2e686b4c966725f30050120d42d4b2e5', name: '' };
|
||||
const B1 = { key: 'b', blob: '05ab2ec03c7e32eff27305b4a11c573a3843fa01', name: '' };
|
||||
const B2 = { key: 'b', blob: 'eb157a78ebd367a004c5a32a18d6d8c18e60ce00', name: '' };
|
||||
const DA1 = { key: 'a', document: { id: 'a', v: 1 }, name: '' };
|
||||
const DA2 = { key: 'a', document: { id: 'a', v: 2 }, name: '' };
|
||||
const DB1 = { key: 'b', document: { id: 'b', v: 1 }, name: '' };
|
||||
const DC1 = { key: 'c', document: { id: 'c', v: 1 }, name: '' };
|
||||
|
||||
it('no changes', () => {
|
||||
const trunk = [A1];
|
||||
const other = [A1];
|
||||
const cands = [DA1];
|
||||
|
||||
expect(preMergeCheck(trunk, other, cands)).toEqual({
|
||||
conflicts: [],
|
||||
dirty: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('candidates same as trunk but not other', () => {
|
||||
const trunk = [A1, B1];
|
||||
const other = [A2, B2];
|
||||
const cands = [DA1, DB1];
|
||||
|
||||
expect(preMergeCheck(trunk, other, cands)).toEqual({
|
||||
conflicts: [],
|
||||
dirty: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('candidate does not exist but trunk and other do', () => {
|
||||
const trunk = [A1];
|
||||
const other = [A1];
|
||||
const cands = [];
|
||||
|
||||
expect(preMergeCheck(trunk, other, cands)).toEqual({
|
||||
conflicts: [],
|
||||
dirty: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('candidate different than trunk and other does not exist', () => {
|
||||
const trunk = [A1];
|
||||
const other = [];
|
||||
const cands = [DA2];
|
||||
|
||||
expect(preMergeCheck(trunk, other, cands)).toEqual({
|
||||
conflicts: [DA2],
|
||||
dirty: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('candidate different than other and trunk does not exist', () => {
|
||||
const trunk = [];
|
||||
const other = [A1];
|
||||
const cands = [DA2];
|
||||
|
||||
expect(preMergeCheck(trunk, other, cands)).toEqual({
|
||||
conflicts: [DA2],
|
||||
dirty: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('candidate modified but trunk and other are same', () => {
|
||||
const trunk = [A1];
|
||||
const other = [A1];
|
||||
const cands = [DA2];
|
||||
|
||||
expect(preMergeCheck(trunk, other, cands)).toEqual({
|
||||
conflicts: [],
|
||||
dirty: [DA2],
|
||||
});
|
||||
});
|
||||
|
||||
it('candidate not same as trunk but same as other', () => {
|
||||
const trunk = [A1];
|
||||
const other = [A2];
|
||||
const cands = [DA2];
|
||||
|
||||
expect(preMergeCheck(trunk, other, cands)).toEqual({
|
||||
conflicts: [],
|
||||
dirty: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('candidate not same as other but same as trunk', () => {
|
||||
const trunk = [A1];
|
||||
const other = [A2];
|
||||
const cands = [DA1];
|
||||
|
||||
expect(preMergeCheck(trunk, other, cands)).toEqual({
|
||||
conflicts: [],
|
||||
dirty: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('candidate not same as trunk and not in other', () => {
|
||||
const trunk = [A1];
|
||||
const other = [];
|
||||
const cands = [DA2];
|
||||
|
||||
expect(preMergeCheck(trunk, other, cands)).toEqual({
|
||||
conflicts: [DA2],
|
||||
dirty: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('candidate outside of history', () => {
|
||||
const trunk = [A1];
|
||||
const other = [A2];
|
||||
const cands = [DC1];
|
||||
|
||||
expect(preMergeCheck(trunk, other, cands)).toEqual({
|
||||
conflicts: [],
|
||||
dirty: [DC1],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('compareBranches()', () => {
|
||||
it('empty states are the same', () => {
|
||||
const a = newBranch([]);
|
||||
const b = newBranch([]);
|
||||
expect(compareBranches(a, b)).toEqual({
|
||||
ahead: 0,
|
||||
behind: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('works with other empty', () => {
|
||||
const a = newBranch([]);
|
||||
const b = newBranch(['1']);
|
||||
expect(compareBranches(a, b)).toEqual({
|
||||
ahead: 0,
|
||||
behind: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('works with both null', () => {
|
||||
const a = null;
|
||||
const b = null;
|
||||
expect(compareBranches(a, b)).toEqual({
|
||||
ahead: 0,
|
||||
behind: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('works with null', () => {
|
||||
const a = null;
|
||||
const b = newBranch(['1']);
|
||||
expect(compareBranches(a, b)).toEqual({
|
||||
ahead: 0,
|
||||
behind: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('works with other null', () => {
|
||||
const a = newBranch(['1']);
|
||||
const b = null;
|
||||
expect(compareBranches(a, b)).toEqual({
|
||||
ahead: 1,
|
||||
behind: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('works with one empty', () => {
|
||||
const a = newBranch(['1']);
|
||||
const b = newBranch([]);
|
||||
expect(compareBranches(a, b)).toEqual({
|
||||
ahead: 1,
|
||||
behind: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('same states are the same', () => {
|
||||
const a = newBranch(['1']);
|
||||
const b = newBranch(['1']);
|
||||
expect(compareBranches(a, b)).toEqual({
|
||||
ahead: 0,
|
||||
behind: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('same final states are the same', () => {
|
||||
const a = newBranch(['1', '2']);
|
||||
const b = newBranch(['0', '2']);
|
||||
expect(compareBranches(a, b)).toEqual({
|
||||
ahead: 0,
|
||||
behind: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('subset of B is ahead', () => {
|
||||
const a = newBranch(['1', '2', '3']);
|
||||
const b = newBranch(['0', '2']);
|
||||
expect(compareBranches(a, b)).toEqual({
|
||||
ahead: 1,
|
||||
behind: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('subset of A is behind', () => {
|
||||
const a = newBranch(['1', '2']);
|
||||
const b = newBranch(['0', '2', '3']);
|
||||
expect(compareBranches(a, b)).toEqual({
|
||||
ahead: 0,
|
||||
behind: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('states have diverged', () => {
|
||||
const a = newBranch(['1', '2', '3', '5']);
|
||||
const b = newBranch(['0', '2', '3', '4']);
|
||||
expect(compareBranches(a, b)).toEqual({
|
||||
ahead: 1,
|
||||
behind: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('states have diverged scenario 2', () => {
|
||||
const a = newBranch(['1', '2', '3', '5', '9']);
|
||||
const b = newBranch(['0', '2', '3', '4', '8', '11']);
|
||||
expect(compareBranches(a, b)).toEqual({
|
||||
ahead: 2,
|
||||
behind: 3,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('hashDocument()', () => {
|
||||
it('ignore object key order', () => {
|
||||
const result1 = hashDocument({ c: 'c', a: 'a', b: 'b' });
|
||||
const result2 = hashDocument({ a: 'a', b: 'b', c: 'c' });
|
||||
|
||||
expect(result1.hash).toBe(result2.hash);
|
||||
expect(result1.hash).toBe('fed03259e8027e1fab2e2e57b402671bef7f3eb9');
|
||||
expect(result2.hash).toBe('fed03259e8027e1fab2e2e57b402671bef7f3eb9');
|
||||
expect(result1.content.toString('utf8')).toBe('{"a":"a","b":"b","c":"c"}');
|
||||
expect(result2.content.toString('utf8')).toBe('{"a":"a","b":"b","c":"c"}');
|
||||
});
|
||||
|
||||
it('works on recursive things', () => {
|
||||
const result1 = hashDocument({
|
||||
arr: [{ b: 'b', a: 'a' }],
|
||||
obj: {
|
||||
obj2: { b: 'b', a: 'a' },
|
||||
},
|
||||
});
|
||||
|
||||
const result2 = hashDocument({
|
||||
arr: [{ b: 'b', a: 'a' }],
|
||||
obj: {
|
||||
obj2: { a: 'a', b: 'b' },
|
||||
},
|
||||
});
|
||||
|
||||
expect(result1.hash).toBe(result2.hash);
|
||||
expect(result1.hash).toBe('7eaaa8a03bada54b403aeada681aad6892a28ab3');
|
||||
expect(result2.hash).toBe('7eaaa8a03bada54b403aeada681aad6892a28ab3');
|
||||
expect(result1.content.toString('utf8')).toBe(
|
||||
'{"arr":[{"a":"a","b":"b"}],"obj":{"obj2":{"a":"a","b":"b"}}}',
|
||||
);
|
||||
expect(result2.content.toString('utf8')).toBe(
|
||||
'{"arr":[{"a":"a","b":"b"}],"obj":{"obj2":{"a":"a","b":"b"}}}',
|
||||
);
|
||||
});
|
||||
|
||||
it('array order matters', () => {
|
||||
const result1 = hashDocument(['c', 'a', 'b']);
|
||||
const result2 = hashDocument(['a', 'b', 'c']);
|
||||
|
||||
expect(result1.hash).not.toBe(result2.hash);
|
||||
expect(result1.hash).toBe('edf94aad27c26bdcfe7477e0ed68991cbaedf8d8');
|
||||
expect(result2.hash).toBe('e13460afb1e68af030bb9bee8344c274494661fa');
|
||||
expect(result1.content.toString('utf8')).toBe('["c","a","b"]');
|
||||
expect(result2.content.toString('utf8')).toBe('["a","b","c"]');
|
||||
});
|
||||
|
||||
it('works with strange types', () => {
|
||||
const sDate1 = hashDocument(new Date(1541178019555));
|
||||
const sDate2 = hashDocument('2018-11-02T17:00:19.555Z');
|
||||
|
||||
expect(sDate1.hash).toBe(sDate2.hash);
|
||||
expect(sDate1.hash).toBe('ec0a18c47fb39de00204c57b5b492fd408394a01');
|
||||
expect(sDate2.hash).toBe('ec0a18c47fb39de00204c57b5b492fd408394a01');
|
||||
expect(sDate1.content.toString('utf8')).toBe('"2018-11-02T17:00:19.555Z"');
|
||||
expect(sDate2.content.toString('utf8')).toBe('"2018-11-02T17:00:19.555Z"');
|
||||
|
||||
const sNull2 = hashDocument('null');
|
||||
expect(sNull2.hash).toBe('8c1030365643f1f4b7f00e3d88c0a3c555522b60');
|
||||
expect(sNull2.content.toString('utf8')).toBe('"null"');
|
||||
});
|
||||
|
||||
it('skips non-json types', () => {
|
||||
const sFunc1 = hashDocument({
|
||||
a: [0, () => null],
|
||||
fn: () => null,
|
||||
});
|
||||
|
||||
const sFunc2 = hashDocument({
|
||||
a: [0],
|
||||
});
|
||||
|
||||
expect(sFunc1.hash).toBe(sFunc2.hash);
|
||||
expect(sFunc1.hash).toBe('38b742facb1034438d82cf1e294d9e71051bf120');
|
||||
expect(sFunc2.hash).toBe('38b742facb1034438d82cf1e294d9e71051bf120');
|
||||
expect(sFunc1.content.toString('utf8')).toBe('{"a":[0]}');
|
||||
expect(sFunc2.content.toString('utf8')).toBe('{"a":[0]}');
|
||||
});
|
||||
|
||||
it('fails on undefined', () => {
|
||||
try {
|
||||
hashDocument(undefined);
|
||||
hashDocument();
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
throw new Error('Expected hashDocument(undefined) to fail');
|
||||
});
|
||||
|
||||
it('ignores object keys that do not matter', () => {
|
||||
const result1 = hashDocument({ a: 'a', modified: 456 });
|
||||
const result2 = hashDocument({ a: 'a', modified: 123 });
|
||||
const result3 = hashDocument({ a: 'b', modified: 123 });
|
||||
|
||||
expect(result1.hash).toBe(result2.hash);
|
||||
expect(result1.hash).not.toBe(result3.hash);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function newSnapshot(n, state) {
|
||||
return {
|
||||
id: `snapshot_${n}`,
|
||||
created: new Date(),
|
||||
parent: '00000',
|
||||
author: 'author_1',
|
||||
name: `Snapshot ${n}`,
|
||||
description: `Test snapshot #${n}`,
|
||||
state,
|
||||
};
|
||||
}
|
||||
|
||||
function newStateEntry(key, blob) {
|
||||
return {
|
||||
key,
|
||||
blob,
|
||||
name: `Blob ${key}.${blob}`,
|
||||
};
|
||||
}
|
||||
|
||||
function newCandidate(key, n) {
|
||||
return {
|
||||
key,
|
||||
name: `Candidate ${n}`,
|
||||
content: `Content for candidate ${key}.${n}`,
|
||||
};
|
||||
}
|
||||
|
||||
function newBranch(snapshots) {
|
||||
return {
|
||||
name: '',
|
||||
created: new Date(0),
|
||||
modified: new Date(0),
|
||||
snapshots,
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,42 @@
|
||||
// @flow
|
||||
|
||||
export function projects(): string {
|
||||
return `/projects/`;
|
||||
}
|
||||
|
||||
export function projectBase(projectId: string): string {
|
||||
return `${projects()}${projectId}/`;
|
||||
}
|
||||
|
||||
export function head(projectId: string): string {
|
||||
return `${projectBase(projectId)}head.json`;
|
||||
}
|
||||
|
||||
export function project(projectId: string): string {
|
||||
return `${projectBase(projectId)}meta.json`;
|
||||
}
|
||||
|
||||
export function blobs(projectId: string): string {
|
||||
return `${projectBase(projectId)}blobs/`;
|
||||
}
|
||||
|
||||
export function blob(projectId: string, blobId: string): string {
|
||||
const subPath = `${blobId.slice(0, 2)}/${blobId.slice(2)}`;
|
||||
return `${blobs(projectId)}${subPath}`;
|
||||
}
|
||||
|
||||
export function snapshots(projectId: string): string {
|
||||
return `${projectBase(projectId)}snapshots/`;
|
||||
}
|
||||
|
||||
export function snapshot(projectId: string, snapshotId: string): string {
|
||||
return `${snapshots(projectId)}${snapshotId}.json`;
|
||||
}
|
||||
|
||||
export function branches(projectId: string): string {
|
||||
return `${projectBase(projectId)}branches/`;
|
||||
}
|
||||
|
||||
export function branch(projectId: string, branchName: string): string {
|
||||
return `${branches(projectId)}${branchName}.json`;
|
||||
}
|
||||
@@ -0,0 +1,478 @@
|
||||
// @flow
|
||||
import clone from 'clone';
|
||||
import crypto from 'crypto';
|
||||
import { deterministicStringify } from '../lib/deterministicStringify';
|
||||
import type {
|
||||
Branch,
|
||||
DocumentKey,
|
||||
MergeConflict,
|
||||
Snapshot,
|
||||
SnapshotState,
|
||||
SnapshotStateEntry,
|
||||
SnapshotStateMap,
|
||||
StageEntry,
|
||||
StatusCandidate,
|
||||
StatusCandidateMap,
|
||||
} from '../types';
|
||||
|
||||
export function generateSnapshotStateMap(snapshot: Snapshot | null): SnapshotStateMap {
|
||||
if (!snapshot) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return generateStateMap(snapshot.state);
|
||||
}
|
||||
|
||||
export function generateStateMap(state: SnapshotState | null): SnapshotStateMap {
|
||||
if (!state) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const map = {};
|
||||
for (const entry of state) {
|
||||
map[entry.key] = entry;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
export function generateCandidateMap(candidates: Array<StatusCandidate>): StatusCandidateMap {
|
||||
const map = {};
|
||||
for (const candidate of candidates) {
|
||||
map[candidate.key] = candidate;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
export function combinedMapKeys<T: SnapshotStateMap | StatusCandidateMap>(
|
||||
...maps: Array<T>
|
||||
): Array<DocumentKey> {
|
||||
const keyMap = {};
|
||||
for (const map of maps) {
|
||||
for (const key of Object.keys(map)) {
|
||||
keyMap[key] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(keyMap);
|
||||
}
|
||||
|
||||
export function threeWayMerge(
|
||||
root: SnapshotState,
|
||||
trunk: SnapshotState,
|
||||
other: SnapshotState,
|
||||
): { state: SnapshotState, conflicts: Array<MergeConflict> } {
|
||||
const stateRoot = generateStateMap(root);
|
||||
const stateTrunk = generateStateMap(trunk);
|
||||
const stateOther = generateStateMap(other);
|
||||
const allKeys = combinedMapKeys(stateRoot, stateTrunk, stateOther);
|
||||
|
||||
const newState: SnapshotState = [];
|
||||
const conflicts: Array<MergeConflict> = [];
|
||||
|
||||
for (const key of allKeys) {
|
||||
const root = stateRoot[key] || null;
|
||||
const trunk = stateTrunk[key] || null;
|
||||
const other = stateOther[key] || null;
|
||||
|
||||
// This condition tree checks every possible case that root, trunk, other can be in
|
||||
// separately. YES, it could be simplified but we want to expand every case to make
|
||||
// it as bulletproof and readable as possible.
|
||||
//
|
||||
// Here are the possible combinations:
|
||||
// root => [ exists, missing ]
|
||||
// trunk => [ exists, missing, modified ]
|
||||
// other => [ exists, missing, modified ]
|
||||
//
|
||||
// Therefore, the total number of cases is equal to 2 * 3! = 12
|
||||
|
||||
// ~~~~~~~~~~ //
|
||||
// Unmodified //
|
||||
// ~~~~~~~~~~ //
|
||||
|
||||
// (1/12)
|
||||
// Unmodified
|
||||
if (root && trunk && other && root.blob === trunk.blob && root.blob === other.blob) {
|
||||
newState.push(trunk);
|
||||
continue;
|
||||
}
|
||||
|
||||
// ~~~~~~~~~ //
|
||||
// Deletions //
|
||||
// ~~~~~~~~~ //
|
||||
|
||||
// (2/12)
|
||||
// Deleted in both
|
||||
if (root && trunk === null && other === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// (3/12)
|
||||
// Deleted in trunk
|
||||
if (root && trunk === null && other && other.blob === root.blob) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// (4/12)
|
||||
// Deleted in other
|
||||
if (root && trunk && other === null && trunk.blob === root.blob) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// ~~~~~~~~~ //
|
||||
// Additions //
|
||||
// ~~~~~~~~~ //
|
||||
|
||||
// (5/12)
|
||||
// Added in both
|
||||
if (root === null && trunk && other) {
|
||||
if (trunk.blob !== other.blob) {
|
||||
conflicts.push({
|
||||
key,
|
||||
name: other.name,
|
||||
message: 'both added',
|
||||
mineBlob: trunk.blob,
|
||||
theirsBlob: other.blob,
|
||||
choose: other.blob,
|
||||
});
|
||||
}
|
||||
|
||||
newState.push(trunk || other);
|
||||
continue;
|
||||
}
|
||||
|
||||
// (6/12)
|
||||
// Added in trunk
|
||||
if (root === null && trunk && other === null) {
|
||||
newState.push(trunk);
|
||||
continue;
|
||||
}
|
||||
|
||||
// (7/12)
|
||||
// Added in other
|
||||
if (root === null && trunk === null && other) {
|
||||
newState.push(other);
|
||||
continue;
|
||||
}
|
||||
|
||||
// ~~~~~~~~~~~~~ //
|
||||
// Modifications //
|
||||
// ~~~~~~~~~~~~~ //
|
||||
|
||||
// (8/12)
|
||||
// Modified in both
|
||||
if (root && trunk && other && root.blob !== trunk.blob && root.blob !== other.blob) {
|
||||
if (trunk.blob !== other.blob) {
|
||||
conflicts.push({
|
||||
key,
|
||||
name: other.name,
|
||||
message: 'both modified',
|
||||
mineBlob: trunk.blob,
|
||||
theirsBlob: other.blob,
|
||||
choose: other.blob,
|
||||
});
|
||||
}
|
||||
|
||||
newState.push(trunk);
|
||||
continue;
|
||||
}
|
||||
|
||||
// (9/12)
|
||||
// Modified in trunk
|
||||
if (root && trunk && other && root.blob !== trunk.blob && root.blob === other.blob) {
|
||||
newState.push(trunk);
|
||||
continue;
|
||||
}
|
||||
|
||||
// (10/12)
|
||||
// Modified in other
|
||||
if (root && trunk && other && root.blob === trunk.blob && root.blob !== other.blob) {
|
||||
newState.push(other);
|
||||
continue;
|
||||
}
|
||||
|
||||
// ~~~~~~ //
|
||||
// Combos //
|
||||
// ~~~~~~ //
|
||||
|
||||
// (11/12)
|
||||
// Deleted in trunk and modified in other
|
||||
if (root && trunk === null && other && other.blob !== root.blob) {
|
||||
conflicts.push({
|
||||
key,
|
||||
name: other.name,
|
||||
message: 'you deleted and they modified',
|
||||
mineBlob: null,
|
||||
theirsBlob: other.blob,
|
||||
choose: other.blob,
|
||||
});
|
||||
newState.push(other);
|
||||
continue;
|
||||
}
|
||||
|
||||
// (12/12)
|
||||
// Deleted in other and modified in trunk
|
||||
if (root && trunk && other === null && trunk.blob !== root.blob) {
|
||||
conflicts.push({
|
||||
key,
|
||||
name: root.name,
|
||||
message: 'they deleted and you modified',
|
||||
mineBlob: trunk.blob,
|
||||
theirsBlob: null,
|
||||
choose: trunk.blob,
|
||||
});
|
||||
newState.push(trunk);
|
||||
continue;
|
||||
}
|
||||
|
||||
// This should never actually happen, but let's error just to be safe
|
||||
throw new Error('3-way merge hit impossible state');
|
||||
}
|
||||
|
||||
return {
|
||||
state: newState,
|
||||
conflicts: conflicts,
|
||||
};
|
||||
}
|
||||
|
||||
export function compareBranches(
|
||||
a: Branch | null,
|
||||
b: Branch | null,
|
||||
): { ahead: number, behind: number } {
|
||||
const snapshotsA = a ? a.snapshots : [];
|
||||
const snapshotsB = b ? b.snapshots : [];
|
||||
const latestA = snapshotsA[snapshotsA.length - 1] || null;
|
||||
const latestB = snapshotsB[snapshotsB.length - 1] || null;
|
||||
|
||||
const result = {
|
||||
ahead: 0,
|
||||
behind: 0,
|
||||
};
|
||||
|
||||
if (latestA === latestB) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (latestA === null) {
|
||||
result.behind = snapshotsB.length;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (latestB === null) {
|
||||
result.ahead = snapshotsA.length;
|
||||
return result;
|
||||
}
|
||||
|
||||
const root = getRootSnapshot(a, b);
|
||||
if (root === null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const indexOfRootInA = snapshotsA.indexOf(root);
|
||||
const indexOfRootInB = snapshotsB.indexOf(root);
|
||||
|
||||
result.ahead = snapshotsA.length - indexOfRootInA - 1;
|
||||
result.behind = snapshotsB.length - indexOfRootInB - 1;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function stateDelta(
|
||||
base: SnapshotState,
|
||||
desired: SnapshotState,
|
||||
): {
|
||||
add: Array<SnapshotStateEntry>,
|
||||
update: Array<SnapshotStateEntry>,
|
||||
remove: Array<SnapshotStateEntry>,
|
||||
} {
|
||||
const result = {
|
||||
add: [],
|
||||
update: [],
|
||||
remove: [],
|
||||
};
|
||||
|
||||
const stateMapStart = generateStateMap(base);
|
||||
const stateMapFinish = generateStateMap(desired);
|
||||
|
||||
for (const key of combinedMapKeys(stateMapStart, stateMapFinish)) {
|
||||
const start = stateMapStart[key];
|
||||
const finish = stateMapFinish[key];
|
||||
|
||||
if (!start && finish) {
|
||||
result.add.push(finish);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (start && !finish) {
|
||||
result.remove.push(start);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (start && finish && start.blob !== finish.blob) {
|
||||
result.update.push(finish);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getStagable(
|
||||
state: SnapshotState,
|
||||
candidates: Array<StatusCandidate>,
|
||||
): Array<StageEntry> {
|
||||
const stagable: Array<StageEntry> = [];
|
||||
|
||||
const stateMap = generateStateMap(state);
|
||||
const candidateMap = generateCandidateMap(candidates);
|
||||
for (const key of combinedMapKeys(stateMap, candidateMap)) {
|
||||
const entry = stateMap[key];
|
||||
const candidate = candidateMap[key];
|
||||
|
||||
if (!entry && candidate) {
|
||||
const { name, document } = candidate;
|
||||
const { hash: blobId, content: blobContent } = hashDocument(document);
|
||||
stagable.push({ key, name, blobId, blobContent, added: true });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry && !candidate) {
|
||||
const { name, blob: blobId } = entry;
|
||||
stagable.push({ key, name, blobId, deleted: true });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry && candidate) {
|
||||
const { document, name } = candidate;
|
||||
const { hash: blobId, content: blobContent } = hashDocument(document);
|
||||
if (entry.blob !== blobId) {
|
||||
stagable.push({ key, name, blobId, blobContent, modified: true });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return stagable;
|
||||
}
|
||||
|
||||
export function getRootSnapshot(a: Branch | null, b: Branch | null): string | null {
|
||||
const snapshotsA = a ? a.snapshots : [];
|
||||
const snapshotsB = b ? b.snapshots : [];
|
||||
|
||||
let rootSnapshotId = '';
|
||||
for (let ai = snapshotsA.length - 1; ai >= 0; ai--) {
|
||||
for (let bi = snapshotsB.length - 1; bi >= 0; bi--) {
|
||||
if (snapshotsA[ai] === snapshotsB[bi]) {
|
||||
return snapshotsA[ai];
|
||||
}
|
||||
}
|
||||
}
|
||||
return rootSnapshotId || null;
|
||||
}
|
||||
|
||||
export function preMergeCheck(
|
||||
trunkState: SnapshotState,
|
||||
otherState: SnapshotState,
|
||||
candidates: Array<StatusCandidate>,
|
||||
): {
|
||||
conflicts: Array<StatusCandidate>,
|
||||
dirty: Array<StatusCandidate>,
|
||||
} {
|
||||
const result = {
|
||||
conflicts: [],
|
||||
dirty: [],
|
||||
};
|
||||
|
||||
const trunkMap = generateStateMap(trunkState);
|
||||
const otherMap = generateStateMap(otherState);
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const { key } = candidate;
|
||||
const trunk = trunkMap[key];
|
||||
const other = otherMap[key];
|
||||
|
||||
// Candidate is not in trunk or other (not yet in version control)
|
||||
if (!trunk && !other) {
|
||||
result.dirty.push(candidate);
|
||||
continue;
|
||||
}
|
||||
|
||||
const { hash: blobId } = hashDocument(candidate.document);
|
||||
|
||||
// Candidate is same as trunk (unchanged) so anything goes
|
||||
if (trunk && trunk.blob === blobId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Candidate is the same as trunk (nothing to do)
|
||||
if (trunk && blobId === trunk.blob) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Candidate is the same as other (would update to same value)
|
||||
if (other && blobId === other.blob) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Candidate is different but trunk and other are the same (preserve safe change)
|
||||
if (
|
||||
other &&
|
||||
trunk &&
|
||||
other.blob === trunk.blob &&
|
||||
blobId !== other.blob &&
|
||||
blobId !== trunk.blob
|
||||
) {
|
||||
result.dirty.push(candidate);
|
||||
continue;
|
||||
}
|
||||
|
||||
// All other cases result in conflict
|
||||
result.conflicts.push(candidate);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function hashDocument(doc: Object): { content: string, hash: string } {
|
||||
if (!doc) {
|
||||
throw new Error('Cannot hash undefined value');
|
||||
}
|
||||
|
||||
// Remove fields we don't care about for sync purposes
|
||||
const newDoc = clone(doc);
|
||||
delete newDoc.modified;
|
||||
|
||||
const content = deterministicStringify(newDoc);
|
||||
const hash = crypto
|
||||
.createHash('sha1')
|
||||
.update(content)
|
||||
.digest('hex');
|
||||
|
||||
return { hash, content };
|
||||
}
|
||||
|
||||
export function updateStateWithConflictResolutions(
|
||||
state: SnapshotState,
|
||||
conflicts: Array<MergeConflict>,
|
||||
): SnapshotState {
|
||||
const newStateMap = generateStateMap(state);
|
||||
for (const { choose, key, name } of conflicts) {
|
||||
const stateEntry = state.find(e => e.key === key);
|
||||
|
||||
// Not in the state, but we choose the conflict
|
||||
if (!stateEntry && choose !== null) {
|
||||
newStateMap[key] = { key, name, blob: choose };
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add the conflict
|
||||
if (choose !== null) {
|
||||
newStateMap[key] = { ...stateEntry, blob: choose };
|
||||
continue;
|
||||
}
|
||||
|
||||
// Chose to delete it, so don't add to state
|
||||
delete newStateMap[key];
|
||||
}
|
||||
|
||||
return Object.keys(newStateMap).map(k => newStateMap[k]);
|
||||
}
|
||||
@@ -1,7 +1,18 @@
|
||||
// @flow
|
||||
|
||||
import type { PluginArgumentEnumOption } from './extensions';
|
||||
|
||||
export type NunjucksParsedTagArg = {
|
||||
type: 'string' | 'number' | 'boolean' | 'number' | 'variable' | 'expression',
|
||||
type:
|
||||
| 'string'
|
||||
| 'number'
|
||||
| 'boolean'
|
||||
| 'number'
|
||||
| 'variable'
|
||||
| 'expression'
|
||||
| 'enum'
|
||||
| 'file'
|
||||
| 'model',
|
||||
value: string | number | boolean,
|
||||
defaultValue?: string | number | boolean,
|
||||
forceVariable?: boolean,
|
||||
@@ -11,6 +22,10 @@ export type NunjucksParsedTagArg = {
|
||||
quotedBy?: '"' | "'",
|
||||
validate?: (value: any) => string,
|
||||
hide?: (Array<NunjucksParsedTagArg>) => boolean,
|
||||
model?: string,
|
||||
options?: Array<PluginArgumentEnumOption>,
|
||||
itemTypes?: Array<string>,
|
||||
extensions?: Array<string>,
|
||||
};
|
||||
|
||||
export type NunjucksParsedTag = {
|
||||
@@ -172,7 +187,8 @@ export function getDefaultFill(name: string, args: Array<NunjucksParsedTagArg>):
|
||||
switch (argDefinition.type) {
|
||||
case 'enum':
|
||||
const { defaultValue, options } = argDefinition;
|
||||
const value = defaultValue !== undefined ? defaultValue : options[0].value;
|
||||
const fallback = options && options.length ? options[0].value : '';
|
||||
const value = defaultValue !== undefined ? String(defaultValue) : String(fallback);
|
||||
return `'${value}'`;
|
||||
case 'number':
|
||||
return `${parseFloat(argDefinition.defaultValue) || 0}`;
|
||||
|
||||
@@ -147,7 +147,7 @@ class Dropdown extends PureComponent {
|
||||
this._dropdownList.style.minWidth = 'initial';
|
||||
this._dropdownList.style.maxWidth = 'initial';
|
||||
|
||||
const screenMargin = 5;
|
||||
const screenMargin = 6;
|
||||
|
||||
const { right, wide } = this.props;
|
||||
if (right || wide) {
|
||||
@@ -350,7 +350,11 @@ class Dropdown extends PureComponent {
|
||||
finalChildren = [
|
||||
dropdownButtons[0],
|
||||
ReactDOM.createPortal(
|
||||
<div key="item" className={menuClasses} ref={this._addDropdownMenuRef}>
|
||||
<div
|
||||
key="item"
|
||||
className={menuClasses}
|
||||
ref={this._addDropdownMenuRef}
|
||||
aria-hidden={!open}>
|
||||
<div className="dropdown__backdrop theme--transparent-overlay" />
|
||||
<div
|
||||
key={uniquenessKey}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import autobind from 'autobind-decorator';
|
||||
|
||||
type Props = {
|
||||
indeterminate: boolean,
|
||||
};
|
||||
|
||||
@autobind
|
||||
class IndeterminateCheckbox extends React.PureComponent<Props> {
|
||||
input: ?HTMLInputElement;
|
||||
|
||||
_setRef(n: ?HTMLInputElement) {
|
||||
this.input = n;
|
||||
}
|
||||
|
||||
_update() {
|
||||
if (this.input) {
|
||||
this.input.indeterminate = this.props.indeterminate;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._update();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this._update();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
indeterminate, // eslint-disable-line no-unused-vars
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return <input ref={this._setRef} type="checkbox" {...otherProps} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default IndeterminateCheckbox;
|
||||
@@ -77,7 +77,7 @@ class Modal extends PureComponent {
|
||||
this._node = n;
|
||||
}
|
||||
|
||||
show() {
|
||||
show(options) {
|
||||
const { freshState } = this.props;
|
||||
const { forceRefreshCounter } = this.state;
|
||||
|
||||
@@ -91,6 +91,9 @@ class Modal extends PureComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow instance-based onHide method
|
||||
this.onHide = options ? options.onHide : null;
|
||||
|
||||
setTimeout(() => this._node && this._node.focus());
|
||||
}
|
||||
|
||||
@@ -109,6 +112,7 @@ class Modal extends PureComponent {
|
||||
hide() {
|
||||
this.setState({ open: false });
|
||||
this.props.onHide && this.props.onHide();
|
||||
this.onHide && this.onHide();
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -140,6 +144,7 @@ class Modal extends PureComponent {
|
||||
tabIndex="-1"
|
||||
className={classes}
|
||||
style={styles}
|
||||
aria-hidden={!open}
|
||||
onClick={this._handleClick}>
|
||||
<div className="modal__backdrop overlay theme--transparent-overlay" data-close-modal />
|
||||
<div className="modal__content__wrapper">
|
||||
|
||||
@@ -81,7 +81,10 @@ class PromptButton extends PureComponent {
|
||||
|
||||
const { state } = this.state;
|
||||
|
||||
const finalConfirmMessage = (confirmMessage || 'Click to confirm').trim();
|
||||
const finalConfirmMessage = (typeof confirmMessage === 'string'
|
||||
? confirmMessage
|
||||
: 'Click to confirm'
|
||||
).trim();
|
||||
const finalDoneMessage = doneMessage || 'Done';
|
||||
|
||||
let innerMsg;
|
||||
|
||||
@@ -10,7 +10,8 @@ import { showModal } from '../modals/index';
|
||||
import FilterHelpModal from '../modals/filter-help-modal';
|
||||
import * as misc from '../../../common/misc';
|
||||
import prettify from 'insomnia-prettify';
|
||||
import { DEBOUNCE_MILLIS, isMac } from '../../../common/constants';
|
||||
import { DEBOUNCE_MILLIS, EDITOR_KEY_MAP_VIM, isMac } from '../../../common/constants';
|
||||
import { keyboardKeys as keyCodes } from '../../../common/keyboard-keys';
|
||||
import './base-imports';
|
||||
import { getTagDefinitions } from '../../../templating/index';
|
||||
import Dropdown from '../base/dropdown/dropdown';
|
||||
@@ -235,6 +236,7 @@ class CodeEditor extends React.Component {
|
||||
return;
|
||||
}
|
||||
|
||||
window.codeMirror = this.codeMirror;
|
||||
editorStates[uniquenessKey] = {
|
||||
scroll: this.codeMirror.getScrollInfo(),
|
||||
selections: this.codeMirror.listSelections(),
|
||||
@@ -287,6 +289,7 @@ class CodeEditor extends React.Component {
|
||||
this.codeMirror.on('blur', this._codemirrorBlur);
|
||||
this.codeMirror.on('paste', this._codemirrorPaste);
|
||||
this.codeMirror.on('scroll', this._codemirrorScroll);
|
||||
this.codeMirror.on('keyHandled', this._codemirrorKeyHandled);
|
||||
|
||||
// Prevent these things if we're type === "password"
|
||||
this.codeMirror.on('copy', this._codemirrorPreventWhenTypePassword);
|
||||
@@ -664,6 +667,18 @@ class CodeEditor extends React.Component {
|
||||
this._persistState();
|
||||
}
|
||||
|
||||
_codemirrorKeyHandled(codeMirror, keyName, event) {
|
||||
const { keyMap } = this.props;
|
||||
const { keyCode } = event;
|
||||
|
||||
const isVimKeyMap = keyMap === EDITOR_KEY_MAP_VIM;
|
||||
const pressedEscape = keyCode === keyCodes.esc.keyCode;
|
||||
|
||||
if (isVimKeyMap && pressedEscape) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
_codemirrorValueBeforeChange(doc, change) {
|
||||
// If we're in single-line mode, merge all changed lines into one
|
||||
if (this.props.singleLine && change.text && change.text.length > 1) {
|
||||
|
||||
@@ -88,7 +88,7 @@ class CookieList extends React.PureComponent<Props> {
|
||||
<PromptButton
|
||||
className="btn btn--super-compact btn--outlined"
|
||||
addIcon
|
||||
confirmMessage=" "
|
||||
confirmMessage=""
|
||||
onClick={e => this._handleDeleteCookie(cookie)}
|
||||
title="Delete cookie">
|
||||
<i className="fa fa-trash-o" />
|
||||
|
||||
@@ -69,12 +69,12 @@ class AuthDropdown extends React.PureComponent<Props> {
|
||||
}
|
||||
|
||||
renderAuthType(type: string, nameOverride: string | null = null) {
|
||||
const {authentication} = this.props.request;
|
||||
const { authentication } = this.props.request;
|
||||
const currentType = authentication.type || AUTH_NONE;
|
||||
|
||||
return (
|
||||
<DropdownItem onClick={this._handleTypeChange} value={type}>
|
||||
{currentType === type ? <i className="fa fa-check"/> : <i className="fa fa-empty"/>}{' '}
|
||||
{currentType === type ? <i className="fa fa-check" /> : <i className="fa fa-empty" />}{' '}
|
||||
{nameOverride || getAuthTypeName(type, true)}
|
||||
</DropdownItem>
|
||||
);
|
||||
|
||||
@@ -93,15 +93,14 @@ class EnvironmentsDropdown extends React.PureComponent<Props> {
|
||||
<Dropdown ref={this._setDropdownRef} {...other} className={className}>
|
||||
<DropdownButton className="btn btn--super-compact no-wrap">
|
||||
<div className="sidebar__menu__thing">
|
||||
{!activeEnvironment &&
|
||||
subEnvironments.length > 0 && (
|
||||
<Tooltip
|
||||
message="No environments active. Please select one to use."
|
||||
className="space-right"
|
||||
position="right">
|
||||
<i className="fa fa-exclamation-triangle notice" />
|
||||
</Tooltip>
|
||||
)}
|
||||
{!activeEnvironment && subEnvironments.length > 0 && (
|
||||
<Tooltip
|
||||
message="No environments active. Please select one to use."
|
||||
className="space-right"
|
||||
position="right">
|
||||
<i className="fa fa-exclamation-triangle notice" />
|
||||
</Tooltip>
|
||||
)}
|
||||
<div className="sidebar__menu__thing__text">
|
||||
{activeEnvironment &&
|
||||
activeEnvironment.color &&
|
||||
|
||||
@@ -40,7 +40,7 @@ class ResponseHistoryDropdown extends PureComponent {
|
||||
}
|
||||
|
||||
renderDropdownItem(response, i) {
|
||||
const { activeResponse } = this.props;
|
||||
const { activeResponse, requestMethod } = this.props;
|
||||
const activeResponseId = activeResponse ? activeResponse._id : 'n/a';
|
||||
const active = response._id === activeResponseId;
|
||||
const message =
|
||||
@@ -59,7 +59,7 @@ class ResponseHistoryDropdown extends PureComponent {
|
||||
statusCode={response.statusCode}
|
||||
statusMessage={response.statusMessage || null}
|
||||
/>
|
||||
<URLTag small url={response.url} />
|
||||
<URLTag small url={response.url} method={requestMethod} />
|
||||
<TimeTag milliseconds={response.elapsedTime} small />
|
||||
<SizeTag bytesRead={response.bytesRead} bytesContent={response.bytesContent} small />
|
||||
{!response.requestVersionId && <i className="icon fa fa-info-circle" title={message} />}
|
||||
@@ -116,6 +116,7 @@ ResponseHistoryDropdown.propTypes = {
|
||||
handleDeleteResponse: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
requestId: PropTypes.string.isRequired,
|
||||
requestMethod: PropTypes.string.isRequired,
|
||||
responses: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
|
||||
// Optional
|
||||
|
||||
@@ -1,218 +1,464 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import autobind from 'autobind-decorator';
|
||||
import classnames from 'classnames';
|
||||
import { Dropdown, DropdownButton, DropdownDivider, DropdownItem } from '../base/dropdown';
|
||||
import { showModal } from '../modals';
|
||||
import * as syncStorage from '../../../sync/storage';
|
||||
import * as session from '../../../sync/session';
|
||||
import * as sync from '../../../sync';
|
||||
import WorkspaceShareSettingsModal from '../modals/workspace-share-settings-modal';
|
||||
import SetupSyncModal from '../modals/setup-sync-modal';
|
||||
import type { Workspace } from '../../../models/workspace';
|
||||
import { showAlert, showModal } from '../modals';
|
||||
import SyncStagingModal from '../modals/sync-staging-modal';
|
||||
import { batchModifyDocs } from '../../../common/database';
|
||||
import HelpTooltip from '../help-tooltip';
|
||||
import Link from '../base/link';
|
||||
import SyncHistoryModal from '../modals/sync-history-modal';
|
||||
import SyncShareModal from '../modals/sync-share-modal';
|
||||
import SyncBranchesModal from '../modals/sync-branches-modal';
|
||||
import VCS from '../../../sync/vcs';
|
||||
import type { Snapshot, Status, StatusCandidate } from '../../../sync/types';
|
||||
import ErrorModal from '../modals/error-modal';
|
||||
import Tooltip from '../tooltip';
|
||||
import LoginModal from '../modals/login-modal';
|
||||
import * as session from '../../../account/session';
|
||||
import PromptButton from '../base/prompt-button';
|
||||
|
||||
// Stop refreshing if user hasn't been active in this long
|
||||
const REFRESH_USER_ACTIVITY = 1000 * 60 * 10;
|
||||
|
||||
// Refresh dropdown periodically
|
||||
const REFRESH_PERIOD = 1000 * 60 * 1;
|
||||
|
||||
type Props = {
|
||||
workspace: Workspace,
|
||||
vcs: VCS,
|
||||
syncItems: Array<StatusCandidate>,
|
||||
|
||||
// Optional
|
||||
className?: string,
|
||||
};
|
||||
|
||||
type State = {
|
||||
loggedIn: boolean | null,
|
||||
loading: boolean,
|
||||
resourceGroupId: string | null,
|
||||
syncMode: string | null,
|
||||
syncPercent: number,
|
||||
workspaceName: string,
|
||||
currentBranch: string,
|
||||
localBranches: Array<string>,
|
||||
compare: {
|
||||
ahead: number,
|
||||
behind: number,
|
||||
},
|
||||
status: Status,
|
||||
initializing: boolean,
|
||||
historyCount: number,
|
||||
loadingPull: boolean,
|
||||
loadingPush: boolean,
|
||||
};
|
||||
|
||||
@autobind
|
||||
class SyncDropdown extends React.PureComponent<Props, State> {
|
||||
_hasPrompted: boolean;
|
||||
_isMounted: boolean;
|
||||
checkInterval: IntervalID;
|
||||
refreshOnNextSyncItems = false;
|
||||
lastUserActivity = Date.now();
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this._hasPrompted = false;
|
||||
this._isMounted = false;
|
||||
|
||||
this.state = {
|
||||
loggedIn: null,
|
||||
loading: false,
|
||||
resourceGroupId: null,
|
||||
syncMode: null,
|
||||
syncPercent: 0,
|
||||
workspaceName: '',
|
||||
localBranches: [],
|
||||
currentBranch: '',
|
||||
compare: {
|
||||
ahead: 0,
|
||||
behind: 0,
|
||||
},
|
||||
historyCount: 0,
|
||||
initializing: true,
|
||||
loadingPull: false,
|
||||
loadingPush: false,
|
||||
status: {
|
||||
key: 'n/a',
|
||||
stage: {},
|
||||
unstaged: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_handleShowShareSettings() {
|
||||
showModal(WorkspaceShareSettingsModal, { workspace: this.props.workspace });
|
||||
}
|
||||
async refreshMainAttributes(extraState?: Object = {}) {
|
||||
const { vcs, syncItems } = this.props;
|
||||
const localBranches = (await vcs.getBranches()).sort();
|
||||
const currentBranch = await vcs.getBranch();
|
||||
const historyCount = await vcs.getHistoryCount();
|
||||
const status = await vcs.status(syncItems, {});
|
||||
|
||||
async _handleSyncResourceGroupId() {
|
||||
const { resourceGroupId } = this.state;
|
||||
const newState = {
|
||||
status,
|
||||
historyCount,
|
||||
localBranches,
|
||||
currentBranch,
|
||||
...extraState,
|
||||
};
|
||||
|
||||
// Set loading state
|
||||
this.setState({ loading: true });
|
||||
|
||||
await sync.getOrCreateConfig(resourceGroupId);
|
||||
await sync.pull(resourceGroupId);
|
||||
await sync.push(resourceGroupId);
|
||||
|
||||
await this._reloadData();
|
||||
|
||||
// Unset loading state
|
||||
this.setState({ loading: false });
|
||||
}
|
||||
|
||||
async _reloadData() {
|
||||
const loggedIn = session.isLoggedIn();
|
||||
|
||||
if (loggedIn !== this.state.loggedIn) {
|
||||
this.setState({ loggedIn });
|
||||
// Do the remote stuff
|
||||
if (session.isLoggedIn()) {
|
||||
try {
|
||||
newState.compare = await vcs.compareRemoteBranch();
|
||||
} catch (err) {
|
||||
console.log('Failed to compare remote branches', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (!loggedIn) {
|
||||
return;
|
||||
}
|
||||
this.setState(newState);
|
||||
}
|
||||
|
||||
// Get or create any related sync data
|
||||
const { workspace } = this.props;
|
||||
const { resourceGroupId } = await sync.getOrCreateResourceForDoc(workspace);
|
||||
const config = await sync.getOrCreateConfig(resourceGroupId);
|
||||
componentDidMount() {
|
||||
this.setState({ initializing: true });
|
||||
this.refreshMainAttributes()
|
||||
.catch(err => console.log('[sync_menu] Error refreshing sync state', err))
|
||||
.finally(() => this.setState({ initializing: false }));
|
||||
|
||||
// Analyze it
|
||||
const dirty = await syncStorage.findDirtyResourcesForResourceGroup(resourceGroupId);
|
||||
const all = await syncStorage.findResourcesForResourceGroup(resourceGroupId);
|
||||
const numClean = all.length - dirty.length;
|
||||
const syncPercent = all.length === 0 ? 100 : parseInt((numClean / all.length) * 1000) / 10;
|
||||
// Refresh but only if the user has been active in the last n minutes
|
||||
this.checkInterval = setInterval(async () => {
|
||||
if (Date.now() - this.lastUserActivity < REFRESH_USER_ACTIVITY) {
|
||||
await this.refreshMainAttributes();
|
||||
}
|
||||
}, REFRESH_PERIOD);
|
||||
|
||||
if (this._isMounted) {
|
||||
this.setState({
|
||||
resourceGroupId,
|
||||
syncPercent,
|
||||
syncMode: config.syncMode,
|
||||
workspaceName: workspace.name,
|
||||
document.addEventListener('mousemove', this._handleUserActivity);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearInterval(this.checkInterval);
|
||||
document.removeEventListener('mousemove', this._handleUserActivity);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
const { vcs, syncItems } = this.props;
|
||||
|
||||
// Update if new sync items
|
||||
if (syncItems !== prevProps.syncItems) {
|
||||
vcs.status(syncItems, {}).then(status => {
|
||||
this.setState({ status });
|
||||
});
|
||||
|
||||
if (this.refreshOnNextSyncItems) {
|
||||
this.refreshMainAttributes();
|
||||
this.refreshOnNextSyncItems = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async _handleShowSyncModePrompt() {
|
||||
showModal(SetupSyncModal, {
|
||||
onSelectSyncMode: async syncMode => {
|
||||
await this._reloadData();
|
||||
_handleUserActivity() {
|
||||
this.lastUserActivity = Date.now();
|
||||
}
|
||||
|
||||
_handleShowBranchesModal() {
|
||||
showModal(SyncBranchesModal, {
|
||||
onHide: this.refreshMainAttributes,
|
||||
});
|
||||
}
|
||||
|
||||
_handleShowStagingModal() {
|
||||
showModal(SyncStagingModal, {
|
||||
onSnapshot: async () => {
|
||||
await this.refreshMainAttributes();
|
||||
},
|
||||
handlePush: async () => {
|
||||
await this._handlePushChanges();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
syncStorage.onChange(this._reloadData);
|
||||
this._reloadData();
|
||||
static _handleShowSharingModal() {
|
||||
showModal(SyncShareModal);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
syncStorage.offChange(this._reloadData);
|
||||
this._isMounted = false;
|
||||
static _handleShowLoginModal() {
|
||||
showModal(LoginModal);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const { resourceGroupId, syncMode } = this.state;
|
||||
async _handlePushChanges() {
|
||||
const { vcs } = this.props;
|
||||
this.setState({ loadingPush: true });
|
||||
|
||||
if (!resourceGroupId) {
|
||||
return;
|
||||
try {
|
||||
await vcs.push();
|
||||
} catch (err) {
|
||||
showModal(ErrorModal, {
|
||||
title: 'Push Error',
|
||||
error: err,
|
||||
message: 'Failed to push snapshots to remote',
|
||||
});
|
||||
}
|
||||
|
||||
// Sync has not yet been configured for this workspace, so prompt the user to do so
|
||||
const isModeUnset = !syncMode || syncMode === syncStorage.SYNC_MODE_UNSET;
|
||||
if (isModeUnset && !this._hasPrompted) {
|
||||
this._hasPrompted = true;
|
||||
this._handleShowSyncModePrompt();
|
||||
await this.refreshMainAttributes({ loadingPush: false });
|
||||
}
|
||||
|
||||
async _handlePullChanges() {
|
||||
const { vcs, syncItems } = this.props;
|
||||
|
||||
this.setState({ loadingPull: true });
|
||||
try {
|
||||
const delta = await vcs.pull(syncItems);
|
||||
await batchModifyDocs(delta);
|
||||
this.refreshOnNextSyncItems = true;
|
||||
} catch (err) {
|
||||
showModal(ErrorModal, {
|
||||
title: 'Pull Error',
|
||||
error: err,
|
||||
message: 'Failed to pull snapshots from remote',
|
||||
});
|
||||
}
|
||||
this.setState({ loadingPull: false });
|
||||
}
|
||||
|
||||
async _handleRollback(snapshot: Snapshot) {
|
||||
const { vcs, syncItems } = this.props;
|
||||
const delta = await vcs.rollback(snapshot.id, syncItems);
|
||||
await batchModifyDocs(delta);
|
||||
this.refreshOnNextSyncItems = true;
|
||||
}
|
||||
|
||||
async _handleRevert() {
|
||||
const { vcs, syncItems } = this.props;
|
||||
|
||||
try {
|
||||
const delta = await vcs.rollbackToLatest(syncItems);
|
||||
await batchModifyDocs(delta);
|
||||
} catch (err) {
|
||||
showModal(ErrorModal, {
|
||||
title: 'Revert Error',
|
||||
error: err,
|
||||
message: 'Failed to revert changes',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_getSyncDescription(syncMode: string | null, syncPercentage: number) {
|
||||
let el = null;
|
||||
if (syncMode === syncStorage.SYNC_MODE_NEVER) {
|
||||
el = <span>Sync Disabled</span>;
|
||||
} else if (syncPercentage === 100) {
|
||||
el = <span>Sync Up To Date</span>;
|
||||
} else if (syncMode === syncStorage.SYNC_MODE_OFF) {
|
||||
el = (
|
||||
<span>
|
||||
<i className="fa fa-pause-circle-o" /> Sync Required
|
||||
</span>
|
||||
);
|
||||
} else if (syncMode === syncStorage.SYNC_MODE_ON) {
|
||||
el = <span>Sync Pending</span>;
|
||||
} else if (!syncMode || syncMode === syncStorage.SYNC_MODE_UNSET) {
|
||||
el = (
|
||||
<span>
|
||||
<i className="fa fa-exclamation-circle" /> Configure Sync
|
||||
</span>
|
||||
_handleShowHistoryModal() {
|
||||
showModal(SyncHistoryModal, {
|
||||
handleRollback: this._handleRollback,
|
||||
});
|
||||
}
|
||||
|
||||
async _handleOpen() {
|
||||
await this.refreshMainAttributes();
|
||||
}
|
||||
|
||||
async _handleSwitchBranch(branch: string) {
|
||||
const { vcs, syncItems } = this.props;
|
||||
try {
|
||||
const delta = await vcs.checkout(syncItems, branch);
|
||||
await batchModifyDocs(delta);
|
||||
} catch (err) {
|
||||
showAlert({
|
||||
title: 'Branch Switch Error',
|
||||
message: err.message,
|
||||
});
|
||||
}
|
||||
|
||||
// We can't refresh now because we won't yet have the new syncItems
|
||||
this.refreshOnNextSyncItems = true;
|
||||
|
||||
// Still need to do this in case sync items don't change
|
||||
this.setState({ currentBranch: branch });
|
||||
}
|
||||
|
||||
renderBranch(branch: string) {
|
||||
const { currentBranch } = this.state;
|
||||
|
||||
const icon =
|
||||
branch === currentBranch ? <i className="fa fa-tag" /> : <i className="fa fa-empty" />;
|
||||
|
||||
const isCurrentBranch = branch === currentBranch;
|
||||
return (
|
||||
<DropdownItem
|
||||
key={branch}
|
||||
onClick={isCurrentBranch ? null : () => this._handleSwitchBranch(branch)}
|
||||
className={classnames({ bold: isCurrentBranch })}
|
||||
title={isCurrentBranch ? null : `Switch to "${branch}"`}>
|
||||
{icon}
|
||||
{branch}
|
||||
</DropdownItem>
|
||||
);
|
||||
}
|
||||
|
||||
renderButton() {
|
||||
const {
|
||||
currentBranch,
|
||||
compare: { ahead, behind },
|
||||
initializing,
|
||||
status,
|
||||
loadingPull,
|
||||
loadingPush,
|
||||
} = this.state;
|
||||
const canPush = ahead > 0;
|
||||
const canPull = behind > 0;
|
||||
const canCreateSnapshot =
|
||||
Object.keys(status.stage).length > 0 || Object.keys(status.unstaged).length > 0;
|
||||
|
||||
const loadIcon = <i className="fa fa-spin fa-refresh fa--fixed-width" />;
|
||||
|
||||
const pullToolTipMsg = canPull
|
||||
? `There ${behind === 1 ? 'is' : 'are'} ${behind} snapshot${behind === 1 ? '' : 's'} to pull`
|
||||
: 'No changes to pull';
|
||||
|
||||
const pushToolTipMsg = canPush
|
||||
? `There ${ahead === 1 ? 'is' : 'are'} ${ahead} snapshot${ahead === 1 ? '' : 's'} to push`
|
||||
: 'No changes to push';
|
||||
|
||||
const snapshotToolTipMsg = canCreateSnapshot ? 'Local changes made' : 'No local changes made';
|
||||
|
||||
if (currentBranch !== null) {
|
||||
return (
|
||||
<DropdownButton className="btn btn--compact wide text-left overflow-hidden row-spaced">
|
||||
<div className="ellipsis">
|
||||
<i className="fa fa-code-fork space-right" />{' '}
|
||||
{initializing ? 'Initializing...' : currentBranch}
|
||||
</div>
|
||||
<div className="space-left">
|
||||
<Tooltip message={snapshotToolTipMsg} delay={800}>
|
||||
<i
|
||||
className={classnames('icon fa fa-cube fa--fixed-width', {
|
||||
'super-duper-faint': !canCreateSnapshot,
|
||||
})}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
{/* Only show cloud icons if logged in */}
|
||||
{session.isLoggedIn() && (
|
||||
<React.Fragment>
|
||||
{loadingPull ? (
|
||||
loadIcon
|
||||
) : (
|
||||
<Tooltip message={pullToolTipMsg} delay={800}>
|
||||
<i
|
||||
className={classnames('fa fa-cloud-download fa--fixed-width', {
|
||||
'super-duper-faint': !canPull,
|
||||
})}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{loadingPush ? (
|
||||
loadIcon
|
||||
) : (
|
||||
<Tooltip message={pushToolTipMsg} delay={800}>
|
||||
<i
|
||||
className={classnames('fa fa-cloud-upload fa--fixed-width', {
|
||||
'super-duper-faint': !canPush,
|
||||
})}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
</DropdownButton>
|
||||
);
|
||||
} else {
|
||||
return <React.Fragment>Sync</React.Fragment>;
|
||||
}
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className } = this.props;
|
||||
const { resourceGroupId, loading, loggedIn } = this.state;
|
||||
|
||||
// Don't show the sync menu unless we're logged in
|
||||
if (!loggedIn) {
|
||||
if (!session.isLoggedIn()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!resourceGroupId) {
|
||||
return (
|
||||
<div className={className}>
|
||||
<button className="btn btn--compact wide" disabled>
|
||||
Initializing Sync...
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
const { syncMode, syncPercent } = this.state;
|
||||
return (
|
||||
<div className={className}>
|
||||
<Dropdown wide className="wide tall">
|
||||
<DropdownButton className="btn btn--compact wide">
|
||||
{this._getSyncDescription(syncMode, syncPercent)}
|
||||
</DropdownButton>
|
||||
<DropdownDivider>Workspace Synced {syncPercent}%</DropdownDivider>
|
||||
const { className } = this.props;
|
||||
const {
|
||||
localBranches,
|
||||
currentBranch,
|
||||
status,
|
||||
historyCount,
|
||||
loadingPull,
|
||||
loadingPush,
|
||||
compare: { ahead, behind },
|
||||
} = this.state;
|
||||
|
||||
<DropdownItem onClick={this._handleShowSyncModePrompt}>
|
||||
<i className="fa fa-wrench" />
|
||||
Change Sync Mode
|
||||
const canCreateSnapshot =
|
||||
Object.keys(status.stage).length > 0 || Object.keys(status.unstaged).length > 0;
|
||||
|
||||
const visibleBranches = localBranches.filter(b => !b.match(/\.hidden$/));
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Dropdown className="wide tall" onOpen={this._handleOpen}>
|
||||
{this.renderButton()}
|
||||
|
||||
<DropdownDivider>
|
||||
Insomnia Sync{' '}
|
||||
<HelpTooltip>
|
||||
Sync and collaborate on workspaces{' '}
|
||||
<Link href="https://support.insomnia.rest/article/67-version-control">
|
||||
<span className="no-wrap">
|
||||
<br />
|
||||
Documentation <i className="fa fa-external-link" />
|
||||
</span>
|
||||
</Link>
|
||||
</HelpTooltip>
|
||||
</DropdownDivider>
|
||||
|
||||
{!session.isLoggedIn() && (
|
||||
<DropdownItem onClick={SyncDropdown._handleShowLoginModal}>
|
||||
<i className="fa fa-sign-in" /> Log In
|
||||
</DropdownItem>
|
||||
)}
|
||||
|
||||
{/* SYNCED */}
|
||||
<DropdownItem onClick={SyncDropdown._handleShowSharingModal}>
|
||||
<i className="fa fa-users" />
|
||||
Share Settings
|
||||
</DropdownItem>
|
||||
|
||||
{syncMode !== syncStorage.SYNC_MODE_NEVER ? (
|
||||
<DropdownItem onClick={this._handleSyncResourceGroupId} stayOpenAfterClick>
|
||||
{loading ? (
|
||||
<i className="fa fa-refresh fa-spin" />
|
||||
) : (
|
||||
<i className="fa fa-cloud-upload" />
|
||||
)}
|
||||
Sync Now
|
||||
</DropdownItem>
|
||||
) : null}
|
||||
<DropdownItem onClick={this._handleShowBranchesModal}>
|
||||
<i className="fa fa-code-fork" />
|
||||
Branches
|
||||
</DropdownItem>
|
||||
|
||||
{syncMode !== syncStorage.SYNC_MODE_NEVER ? (
|
||||
<DropdownItem onClick={this._handleShowShareSettings}>
|
||||
<i className="fa fa-users" />
|
||||
Share Settings
|
||||
</DropdownItem>
|
||||
) : null}
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<DropdownDivider>Local Branches</DropdownDivider>
|
||||
{visibleBranches.map(this.renderBranch)}
|
||||
|
||||
<DropdownDivider>{currentBranch}</DropdownDivider>
|
||||
|
||||
<DropdownItem onClick={this._handleShowHistoryModal} disabled={historyCount === 0}>
|
||||
<i className="fa fa-clock-o" />
|
||||
History
|
||||
</DropdownItem>
|
||||
|
||||
<DropdownItem
|
||||
onClick={this._handleRevert}
|
||||
buttonClass={PromptButton}
|
||||
stayOpenAfterClick
|
||||
disabled={!canCreateSnapshot || historyCount === 0}>
|
||||
<i className="fa fa-undo" />
|
||||
Revert Changes
|
||||
</DropdownItem>
|
||||
|
||||
<DropdownItem onClick={this._handleShowStagingModal} disabled={!canCreateSnapshot}>
|
||||
<i className="fa fa-cube" />
|
||||
Create Snapshot
|
||||
</DropdownItem>
|
||||
|
||||
<DropdownItem onClick={this._handlePullChanges} disabled={behind === 0 || loadingPull}>
|
||||
{loadingPull ? (
|
||||
<React.Fragment>
|
||||
<i className="fa fa-spin fa-refresh" /> Pulling Snapshots...
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<i className="fa fa-cloud-upload" /> Pull {behind || ''} Snapshot
|
||||
{behind === 1 ? '' : 's'}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</DropdownItem>
|
||||
|
||||
<DropdownItem onClick={this._handlePushChanges} disabled={ahead === 0 || loadingPush}>
|
||||
{loadingPush ? (
|
||||
<React.Fragment>
|
||||
<i className="fa fa-spin fa-refresh" /> Pushing Snapshots...
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<i className="fa fa-cloud-upload" /> Push {ahead || ''} Snapshot
|
||||
{ahead === 1 ? '' : 's'}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import autobind from 'autobind-decorator';
|
||||
import { Dropdown, DropdownButton, DropdownDivider, DropdownItem } from '../base/dropdown';
|
||||
import { showModal } from '../modals';
|
||||
import * as syncStorage from '../../../sync-legacy/storage';
|
||||
import * as sync from '../../../sync-legacy';
|
||||
import WorkspaceShareSettingsModal from '../modals/workspace-share-settings-modal';
|
||||
import SetupSyncModal from '../modals/setup-sync-modal';
|
||||
import type { Workspace } from '../../../models/workspace';
|
||||
import * as session from '../../../account/session';
|
||||
|
||||
type Props = {
|
||||
workspace: Workspace,
|
||||
|
||||
// Optional
|
||||
className?: string,
|
||||
};
|
||||
|
||||
type State = {
|
||||
loggedIn: boolean | null,
|
||||
loading: boolean,
|
||||
resourceGroupId: string | null,
|
||||
syncMode: string | null,
|
||||
syncPercent: number,
|
||||
workspaceName: string,
|
||||
};
|
||||
|
||||
@autobind
|
||||
class SyncLegacyDropdown extends React.PureComponent<Props, State> {
|
||||
_hasPrompted: boolean;
|
||||
_isMounted: boolean;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this._hasPrompted = false;
|
||||
this._isMounted = false;
|
||||
|
||||
this.state = {
|
||||
loggedIn: null,
|
||||
loading: false,
|
||||
resourceGroupId: null,
|
||||
syncMode: null,
|
||||
syncPercent: 0,
|
||||
workspaceName: '',
|
||||
};
|
||||
}
|
||||
|
||||
_handleShowShareSettings() {
|
||||
showModal(WorkspaceShareSettingsModal, { workspace: this.props.workspace });
|
||||
}
|
||||
|
||||
async _handleSyncResourceGroupId() {
|
||||
const { resourceGroupId } = this.state;
|
||||
|
||||
// Set loading state
|
||||
this.setState({ loading: true });
|
||||
|
||||
await sync.getOrCreateConfig(resourceGroupId);
|
||||
await sync.pull(resourceGroupId);
|
||||
await sync.push(resourceGroupId);
|
||||
|
||||
await this._reloadData();
|
||||
|
||||
// Unset loading state
|
||||
this.setState({ loading: false });
|
||||
}
|
||||
|
||||
async _reloadData() {
|
||||
const loggedIn = session.isLoggedIn();
|
||||
|
||||
if (loggedIn !== this.state.loggedIn) {
|
||||
this.setState({ loggedIn });
|
||||
}
|
||||
|
||||
if (!loggedIn) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get or create any related sync data
|
||||
const { workspace } = this.props;
|
||||
const { resourceGroupId } = await sync.getOrCreateResourceForDoc(workspace);
|
||||
const config = await sync.getOrCreateConfig(resourceGroupId);
|
||||
|
||||
// Analyze it
|
||||
const dirty = await syncStorage.findDirtyResourcesForResourceGroup(resourceGroupId);
|
||||
const all = await syncStorage.findResourcesForResourceGroup(resourceGroupId);
|
||||
const numClean = all.length - dirty.length;
|
||||
const syncPercent = all.length === 0 ? 100 : parseInt((numClean / all.length) * 1000) / 10;
|
||||
|
||||
if (this._isMounted) {
|
||||
this.setState({
|
||||
resourceGroupId,
|
||||
syncPercent,
|
||||
syncMode: config.syncMode,
|
||||
workspaceName: workspace.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async _handleShowSyncModePrompt() {
|
||||
showModal(SetupSyncModal, {
|
||||
onSelectSyncMode: async syncMode => {
|
||||
await this._reloadData();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
syncStorage.onChange(this._reloadData);
|
||||
this._reloadData();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
syncStorage.offChange(this._reloadData);
|
||||
this._isMounted = false;
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const { resourceGroupId, syncMode } = this.state;
|
||||
|
||||
if (!resourceGroupId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sync has not yet been configured for this workspace, so prompt the user to do so
|
||||
const isModeUnset = !syncMode || syncMode === syncStorage.SYNC_MODE_UNSET;
|
||||
if (isModeUnset && !this._hasPrompted) {
|
||||
this._hasPrompted = true;
|
||||
this._handleShowSyncModePrompt();
|
||||
}
|
||||
}
|
||||
|
||||
_getSyncDescription(syncMode: string | null, syncPercentage: number) {
|
||||
let el = null;
|
||||
if (syncMode === syncStorage.SYNC_MODE_NEVER) {
|
||||
el = <span>Sync Disabled</span>;
|
||||
} else if (syncPercentage === 100) {
|
||||
el = <span>Sync Up To Date</span>;
|
||||
} else if (syncMode === syncStorage.SYNC_MODE_OFF) {
|
||||
el = (
|
||||
<span>
|
||||
<i className="fa fa-pause-circle-o" /> Sync Required
|
||||
</span>
|
||||
);
|
||||
} else if (syncMode === syncStorage.SYNC_MODE_ON) {
|
||||
el = <span>Sync Pending</span>;
|
||||
} else if (!syncMode || syncMode === syncStorage.SYNC_MODE_UNSET) {
|
||||
el = (
|
||||
<span>
|
||||
<i className="fa fa-exclamation-circle" /> Configure Sync
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className } = this.props;
|
||||
const { resourceGroupId, loading, loggedIn } = this.state;
|
||||
|
||||
// Don't show the sync menu unless we're logged in
|
||||
if (!loggedIn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!resourceGroupId) {
|
||||
return (
|
||||
<div className={className}>
|
||||
<button className="btn btn--compact wide" disabled>
|
||||
Initializing Sync...
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
const { syncMode, syncPercent } = this.state;
|
||||
return (
|
||||
<div className={className}>
|
||||
<Dropdown wide className="wide tall">
|
||||
<DropdownButton className="btn btn--compact wide">
|
||||
{this._getSyncDescription(syncMode, syncPercent)}
|
||||
</DropdownButton>
|
||||
<DropdownDivider>Workspace Synced {syncPercent}%</DropdownDivider>
|
||||
|
||||
<DropdownItem onClick={this._handleShowSyncModePrompt}>
|
||||
<i className="fa fa-wrench" />
|
||||
Change Sync Mode
|
||||
</DropdownItem>
|
||||
|
||||
{/* SYNCED */}
|
||||
|
||||
{syncMode !== syncStorage.SYNC_MODE_NEVER ? (
|
||||
<DropdownItem onClick={this._handleSyncResourceGroupId} stayOpenAfterClick>
|
||||
{loading ? (
|
||||
<i className="fa fa-refresh fa-spin" />
|
||||
) : (
|
||||
<i className="fa fa-cloud-upload" />
|
||||
)}
|
||||
Sync Now
|
||||
</DropdownItem>
|
||||
) : null}
|
||||
|
||||
{syncMode !== syncStorage.SYNC_MODE_NEVER ? (
|
||||
<DropdownItem onClick={this._handleShowShareSettings}>
|
||||
<i className="fa fa-users" />
|
||||
Share Settings
|
||||
</DropdownItem>
|
||||
) : null}
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SyncLegacyDropdown;
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import autobind from 'autobind-decorator';
|
||||
import * as classnames from 'classnames';
|
||||
import classnames from 'classnames';
|
||||
import Dropdown from '../base/dropdown/dropdown';
|
||||
import DropdownDivider from '../base/dropdown/dropdown-divider';
|
||||
import DropdownButton from '../base/dropdown/dropdown-button';
|
||||
@@ -10,32 +10,59 @@ import DropdownHint from '../base/dropdown/dropdown-hint';
|
||||
import SettingsModal, { TAB_INDEX_EXPORT } from '../modals/settings-modal';
|
||||
import * as models from '../../../models';
|
||||
import { getAppVersion } from '../../../common/constants';
|
||||
import { showModal, showPrompt } from '../modals/index';
|
||||
import { showAlert, showModal, showPrompt } from '../modals';
|
||||
import Link from '../base/link';
|
||||
import WorkspaceSettingsModal from '../modals/workspace-settings-modal';
|
||||
import WorkspaceShareSettingsModal from '../modals/workspace-share-settings-modal';
|
||||
import * as session from '../../../sync/session';
|
||||
import LoginModal from '../modals/login-modal';
|
||||
import Tooltip from '../tooltip';
|
||||
import KeydownBinder from '../keydown-binder';
|
||||
import { hotKeyRefs } from '../../../common/hotkeys';
|
||||
import { executeHotKey } from '../../../common/hotkeys-listener';
|
||||
import type { Workspace } from '../../../models/workspace';
|
||||
import type { HotKeyRegistry } from '../../../common/hotkeys';
|
||||
import SyncShareModal from '../modals/sync-share-modal';
|
||||
import * as db from '../../../common/database';
|
||||
import VCS from '../../../sync/vcs';
|
||||
import HelpTooltip from '../help-tooltip';
|
||||
import type { Project } from '../../../sync/types';
|
||||
import * as sync from '../../../sync-legacy/index';
|
||||
import PromptButton from '../base/prompt-button';
|
||||
import * as session from '../../../account/session';
|
||||
|
||||
type Props = {
|
||||
isLoading: boolean,
|
||||
handleSetActiveWorkspace: (id: string) => void,
|
||||
workspaces: Array<Workspace>,
|
||||
unseenWorkspaces: Array<Workspace>,
|
||||
activeWorkspace: Workspace,
|
||||
hotKeyRegistry: HotKeyRegistry,
|
||||
enableSyncBeta: boolean,
|
||||
vcs: VCS | null,
|
||||
|
||||
// Optional
|
||||
className?: string,
|
||||
};
|
||||
|
||||
type State = {
|
||||
remoteProjects: Array<Project>,
|
||||
localProjects: Array<Project>,
|
||||
pullingProjects: { [string]: boolean },
|
||||
};
|
||||
|
||||
@autobind
|
||||
class WorkspaceDropdown extends PureComponent {
|
||||
constructor(props) {
|
||||
class WorkspaceDropdown extends React.PureComponent<Props, State> {
|
||||
_dropdown: ?Dropdown;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
loggedIn: false,
|
||||
remoteProjects: [],
|
||||
localProjects: [],
|
||||
pullingProjects: {},
|
||||
};
|
||||
}
|
||||
|
||||
async _handleDropdownOpen() {
|
||||
if (this.state.loggedIn !== session.isLoggedIn()) {
|
||||
this.setState({ loggedIn: session.isLoggedIn() });
|
||||
}
|
||||
}
|
||||
|
||||
async _handleDropdownHide() {
|
||||
// Mark all unseen workspace as seen
|
||||
for (const workspace of this.props.unseenWorkspaces) {
|
||||
@@ -46,36 +73,95 @@ class WorkspaceDropdown extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
_setDropdownRef(n) {
|
||||
async _handleDropdownOpen() {
|
||||
this._refreshRemoteWorkspaces();
|
||||
}
|
||||
|
||||
async _refreshRemoteWorkspaces() {
|
||||
const { vcs } = this.props;
|
||||
if (!vcs) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!session.isLoggedIn()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const remoteProjects = await vcs.remoteProjects();
|
||||
const localProjects = await vcs.localProjects();
|
||||
this.setState({ remoteProjects, localProjects });
|
||||
}
|
||||
|
||||
async _handlePullRemoteWorkspace(project: Project) {
|
||||
const { vcs } = this.props;
|
||||
if (!vcs) {
|
||||
throw new Error('VCS is not defined');
|
||||
}
|
||||
|
||||
this.setState(state => ({
|
||||
pullingProjects: { ...state.pullingProjects, [project.id]: true },
|
||||
}));
|
||||
|
||||
try {
|
||||
// Clone old VCS so we don't mess anything up while working on other projects
|
||||
const newVCS = vcs.newInstance();
|
||||
|
||||
// Remove all projects for workspace first
|
||||
await newVCS.removeProjectsForRoot(project.rootDocumentId);
|
||||
|
||||
// Set project, checkout master, and pull
|
||||
await newVCS.setProject(project);
|
||||
await newVCS.checkout([], 'master');
|
||||
await newVCS.pull([]); // There won't be any existing docs since it's a new pull
|
||||
|
||||
const flushId = await db.bufferChanges();
|
||||
for (const doc of await newVCS.allDocuments()) {
|
||||
await db.upsert(doc);
|
||||
}
|
||||
await db.flushChanges(flushId);
|
||||
|
||||
// handleSetActiveWorkspace(project.rootDocumentId);
|
||||
vcs.setProject(project);
|
||||
await this._refreshRemoteWorkspaces();
|
||||
} catch (err) {
|
||||
this._dropdown && this._dropdown.hide();
|
||||
showAlert({
|
||||
title: 'Pull Error',
|
||||
message: `Failed to pull workspace. ${err.message}`,
|
||||
});
|
||||
}
|
||||
|
||||
this.setState(state => ({
|
||||
pullingProjects: { ...state.pullingProjects, [project.id]: false },
|
||||
}));
|
||||
}
|
||||
|
||||
_setDropdownRef(n: ?Dropdown) {
|
||||
this._dropdown = n;
|
||||
}
|
||||
|
||||
_handleShowLogin() {
|
||||
showModal(LoginModal);
|
||||
static async _handleLogout() {
|
||||
await sync.logout();
|
||||
}
|
||||
|
||||
_handleShowExport() {
|
||||
static _handleShowExport() {
|
||||
showModal(SettingsModal, TAB_INDEX_EXPORT);
|
||||
}
|
||||
|
||||
_handleShowSettings() {
|
||||
static _handleShowSettings() {
|
||||
showModal(SettingsModal);
|
||||
}
|
||||
|
||||
_handleShowWorkspaceSettings() {
|
||||
showModal(WorkspaceSettingsModal, {
|
||||
workspace: this.props.activeWorkspace,
|
||||
});
|
||||
static _handleShowWorkspaceSettings() {
|
||||
showModal(WorkspaceSettingsModal);
|
||||
}
|
||||
|
||||
_handleShowShareSettings() {
|
||||
showModal(WorkspaceShareSettingsModal, {
|
||||
workspace: this.props.activeWorkspace,
|
||||
});
|
||||
}
|
||||
|
||||
_handleSwitchWorkspace(workspaceId) {
|
||||
this.props.handleSetActiveWorkspace(workspaceId);
|
||||
if (this.props.enableSyncBeta) {
|
||||
showModal(SyncShareModal);
|
||||
} else {
|
||||
showModal(WorkspaceShareSettingsModal);
|
||||
}
|
||||
}
|
||||
|
||||
_handleWorkspaceCreate() {
|
||||
@@ -91,12 +177,23 @@ class WorkspaceDropdown extends PureComponent {
|
||||
});
|
||||
}
|
||||
|
||||
_handleKeydown(e) {
|
||||
_handleKeydown(e: KeyboardEvent) {
|
||||
executeHotKey(e, hotKeyRefs.TOGGLE_MAIN_MENU, () => {
|
||||
this._dropdown && this._dropdown.toggle(true);
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
// Reload workspaces if we just got a new VCS instance
|
||||
if (this.props.vcs && !prevProps.vcs) {
|
||||
this._refreshRemoteWorkspaces();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._refreshRemoteWorkspaces();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
@@ -105,9 +202,23 @@ class WorkspaceDropdown extends PureComponent {
|
||||
unseenWorkspaces,
|
||||
isLoading,
|
||||
hotKeyRegistry,
|
||||
handleSetActiveWorkspace,
|
||||
enableSyncBeta,
|
||||
...other
|
||||
} = this.props;
|
||||
|
||||
const { remoteProjects, localProjects, pullingProjects } = this.state;
|
||||
|
||||
const missingRemoteProjects = remoteProjects.filter(({ id, rootDocumentId }) => {
|
||||
const localProjectExists = localProjects.find(p => p.id === id);
|
||||
const workspaceExists = workspaces.find(w => w._id === rootDocumentId);
|
||||
|
||||
// Mark as missing if:
|
||||
// - the project doesn't yet exists locally
|
||||
// - the project exists locally but somehow the workspace doesn't anymore
|
||||
return !(workspaceExists && localProjectExists);
|
||||
});
|
||||
|
||||
const nonActiveWorkspaces = workspaces
|
||||
.filter(w => w._id !== activeWorkspace._id)
|
||||
.sort((w1, w2) => w1.name.localeCompare(w2.name));
|
||||
@@ -116,7 +227,8 @@ class WorkspaceDropdown extends PureComponent {
|
||||
|
||||
const unseenWorkspacesMessage = (
|
||||
<div>
|
||||
The following workspaces were added<br />
|
||||
The following workspaces were added
|
||||
<br />
|
||||
{addedWorkspaceNames}
|
||||
</div>
|
||||
);
|
||||
@@ -145,7 +257,7 @@ class WorkspaceDropdown extends PureComponent {
|
||||
</h1>
|
||||
</DropdownButton>
|
||||
<DropdownDivider>{activeWorkspace.name}</DropdownDivider>
|
||||
<DropdownItem onClick={this._handleShowWorkspaceSettings}>
|
||||
<DropdownItem onClick={WorkspaceDropdown._handleShowWorkspaceSettings}>
|
||||
<i className="fa fa-wrench" /> Workspace Settings
|
||||
<DropdownHint keyBindings={hotKeyRegistry[hotKeyRefs.WORKSPACE_SHOW_SETTINGS.id]} />
|
||||
</DropdownItem>
|
||||
@@ -159,10 +271,10 @@ class WorkspaceDropdown extends PureComponent {
|
||||
{nonActiveWorkspaces.map(w => {
|
||||
const isUnseen = !!unseenWorkspaces.find(v => v._id === w._id);
|
||||
return (
|
||||
<DropdownItem key={w._id} onClick={this._handleSwitchWorkspace} value={w._id}>
|
||||
<DropdownItem key={w._id} onClick={handleSetActiveWorkspace} value={w._id}>
|
||||
<i className="fa fa-random" /> To <strong>{w.name}</strong>
|
||||
{isUnseen && (
|
||||
<Tooltip message="This workspace is new">
|
||||
<Tooltip message="You haven't seen this workspace before" position="top">
|
||||
<i className="width-auto fa fa-asterisk surprise" />
|
||||
</Tooltip>
|
||||
)}
|
||||
@@ -171,28 +283,59 @@ class WorkspaceDropdown extends PureComponent {
|
||||
})}
|
||||
|
||||
<DropdownItem onClick={this._handleWorkspaceCreate}>
|
||||
<i className="fa fa-empty" /> New Workspace
|
||||
<i className="fa fa-empty" /> Create Workspace
|
||||
</DropdownItem>
|
||||
|
||||
{missingRemoteProjects.length > 0 && (
|
||||
<DropdownDivider>
|
||||
Remote Workspaces{' '}
|
||||
<HelpTooltip>
|
||||
These workspaces have been shared with you via Insomnia Sync and do not yet exist on
|
||||
your machine.
|
||||
</HelpTooltip>
|
||||
</DropdownDivider>
|
||||
)}
|
||||
|
||||
{missingRemoteProjects.map(p => (
|
||||
<DropdownItem
|
||||
key={p.id}
|
||||
stayOpenAfterClick
|
||||
onClick={() => this._handlePullRemoteWorkspace(p)}>
|
||||
{pullingProjects[p.id] ? (
|
||||
<i className="fa fa-refresh fa-spin" />
|
||||
) : (
|
||||
<i className="fa fa-cloud-download" />
|
||||
)}
|
||||
Pull <strong>{p.name}</strong>
|
||||
</DropdownItem>
|
||||
))}
|
||||
|
||||
<DropdownDivider>Insomnia Version {getAppVersion()}</DropdownDivider>
|
||||
|
||||
<DropdownItem onClick={this._handleShowSettings}>
|
||||
<DropdownItem onClick={WorkspaceDropdown._handleShowSettings}>
|
||||
<i className="fa fa-cog" /> Preferences
|
||||
<DropdownHint keyBindings={hotKeyRegistry[hotKeyRefs.PREFERENCES_SHOW_GENERAL.id]} />
|
||||
</DropdownItem>
|
||||
<DropdownItem onClick={this._handleShowExport}>
|
||||
<DropdownItem onClick={WorkspaceDropdown._handleShowExport}>
|
||||
<i className="fa fa-share" /> Import/Export
|
||||
</DropdownItem>
|
||||
|
||||
{/* Not Logged In */}
|
||||
|
||||
{!this.state.loggedIn && (
|
||||
<DropdownItem key="login" onClick={this._handleShowLogin}>
|
||||
{session.isLoggedIn() ? (
|
||||
<DropdownItem
|
||||
key="login"
|
||||
onClick={WorkspaceDropdown._handleLogout}
|
||||
buttonClass={PromptButton}>
|
||||
<i className="fa fa-sign-out" /> Logout
|
||||
</DropdownItem>
|
||||
) : (
|
||||
<DropdownItem key="login" onClick={() => showModal(LoginModal)}>
|
||||
<i className="fa fa-sign-in" /> Log In
|
||||
</DropdownItem>
|
||||
)}
|
||||
|
||||
{!this.state.loggedIn && (
|
||||
{!session.isLoggedIn() && (
|
||||
<DropdownItem
|
||||
key="invite"
|
||||
buttonClass={Link}
|
||||
@@ -208,19 +351,4 @@ class WorkspaceDropdown extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
WorkspaceDropdown.propTypes = {
|
||||
// Required
|
||||
isLoading: PropTypes.bool.isRequired,
|
||||
handleImportFile: PropTypes.func.isRequired,
|
||||
handleExportFile: PropTypes.func.isRequired,
|
||||
handleSetActiveWorkspace: PropTypes.func.isRequired,
|
||||
workspaces: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
unseenWorkspaces: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
activeWorkspace: PropTypes.object.isRequired,
|
||||
hotKeyRegistry: PropTypes.object.isRequired,
|
||||
|
||||
// Optional
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default WorkspaceDropdown;
|
||||
@@ -127,11 +127,7 @@ class AsapAuth extends React.PureComponent<Props> {
|
||||
}
|
||||
|
||||
_handleEditPrivateKey() {
|
||||
const {
|
||||
handleRender,
|
||||
handleGetRenderContext,
|
||||
request,
|
||||
} = this.props;
|
||||
const { handleRender, handleGetRenderContext, request } = this.props;
|
||||
|
||||
const { authentication } = request;
|
||||
|
||||
@@ -171,7 +167,7 @@ class AsapAuth extends React.PureComponent<Props> {
|
||||
},
|
||||
)}>
|
||||
<button className="btn btn--clicky wide" onClick={this._handleEditPrivateKey}>
|
||||
<i className="fa fa-edit space-right"/>
|
||||
<i className="fa fa-edit space-right" />
|
||||
{authentication.privateKey ? 'Click to Edit' : 'Click to Add'}
|
||||
</button>
|
||||
</div>
|
||||
@@ -188,30 +184,30 @@ class AsapAuth extends React.PureComponent<Props> {
|
||||
<div className="pad">
|
||||
<table>
|
||||
<tbody>
|
||||
{fields}
|
||||
<tr>
|
||||
<td className="pad-right no-wrap valign-middle">
|
||||
<label htmlFor="enabled" className="label--small no-pad">
|
||||
Enabled
|
||||
</label>
|
||||
</td>
|
||||
<td className="wide">
|
||||
<div className="form-control form-control--underlined">
|
||||
<Button
|
||||
className="btn btn--super-duper-compact"
|
||||
id="enabled"
|
||||
onClick={this._handleDisable}
|
||||
value={!authentication.disabled}
|
||||
title={authentication.disabled ? 'Enable item' : 'Disable item'}>
|
||||
{authentication.disabled ? (
|
||||
<i className="fa fa-square-o"/>
|
||||
) : (
|
||||
<i className="fa fa-check-square-o"/>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{fields}
|
||||
<tr>
|
||||
<td className="pad-right no-wrap valign-middle">
|
||||
<label htmlFor="enabled" className="label--small no-pad">
|
||||
Enabled
|
||||
</label>
|
||||
</td>
|
||||
<td className="wide">
|
||||
<div className="form-control form-control--underlined">
|
||||
<Button
|
||||
className="btn btn--super-duper-compact"
|
||||
id="enabled"
|
||||
onClick={this._handleDisable}
|
||||
value={!authentication.disabled}
|
||||
title={authentication.disabled ? 'Enable item' : 'Disable item'}>
|
||||
{authentication.disabled ? (
|
||||
<i className="fa fa-square-o" />
|
||||
) : (
|
||||
<i className="fa fa-check-square-o" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -160,7 +160,7 @@ class AuthWrapper extends React.PureComponent<Props> {
|
||||
/>
|
||||
);
|
||||
} else if (authentication.type === AUTH_NETRC) {
|
||||
return <NetrcAuth/>;
|
||||
return <NetrcAuth />;
|
||||
} else if (authentication.type === AUTH_ASAP) {
|
||||
return (
|
||||
<AsapAuth
|
||||
@@ -176,9 +176,9 @@ class AuthWrapper extends React.PureComponent<Props> {
|
||||
return (
|
||||
<div className="vertically-center text-center">
|
||||
<p className="pad super-faint text-sm text-center">
|
||||
<i className="fa fa-unlock-alt" style={{ fontSize: '8rem', opacity: 0.3 }}/>
|
||||
<br/>
|
||||
<br/>
|
||||
<i className="fa fa-unlock-alt" style={{ fontSize: '8rem', opacity: 0.3 }} />
|
||||
<br />
|
||||
<br />
|
||||
Select an auth type from above
|
||||
</p>
|
||||
</div>
|
||||
@@ -187,11 +187,7 @@ class AuthWrapper extends React.PureComponent<Props> {
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="tall">
|
||||
{this.renderEditor()}
|
||||
</div>
|
||||
);
|
||||
return <div className="tall">{this.renderEditor()}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -97,53 +97,53 @@ class AWSAuth extends React.PureComponent<Props> {
|
||||
<div className="pad">
|
||||
<table>
|
||||
<tbody>
|
||||
{this.renderRow('accessKeyId', 'Access Key ID', this._handleChangeAccessKeyId)}
|
||||
{this.renderRow(
|
||||
'secretAccessKey',
|
||||
'Secret Access Key',
|
||||
this._handleChangeSecretAccessKey,
|
||||
)}
|
||||
{this.renderRow(
|
||||
'region',
|
||||
'Region',
|
||||
this._handleChangeRegion,
|
||||
'will be calculated from hostname or host or use \'us-east-1\' if not given',
|
||||
)}
|
||||
{this.renderRow(
|
||||
'service',
|
||||
'Service',
|
||||
this._handleChangeService,
|
||||
'will be calculated from hostname or host if not given',
|
||||
)}
|
||||
{this.renderRow(
|
||||
'sessionToken',
|
||||
'Session Token',
|
||||
this._handleChangeSessionToken,
|
||||
'Optional token used for multi-factor authentication',
|
||||
)}
|
||||
<tr>
|
||||
<td className="pad-right no-wrap valign-middle">
|
||||
<label htmlFor="enabled" className="label--small no-pad">
|
||||
Enabled
|
||||
</label>
|
||||
</td>
|
||||
<td className="wide">
|
||||
<div className="form-control form-control--underlined">
|
||||
<Button
|
||||
className="btn btn--super-duper-compact"
|
||||
id="enabled"
|
||||
onClick={this._handleDisable}
|
||||
value={!authentication.disabled}
|
||||
title={authentication.disabled ? 'Enable item' : 'Disable item'}>
|
||||
{authentication.disabled ? (
|
||||
<i className="fa fa-square-o"/>
|
||||
) : (
|
||||
<i className="fa fa-check-square-o"/>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{this.renderRow('accessKeyId', 'Access Key ID', this._handleChangeAccessKeyId)}
|
||||
{this.renderRow(
|
||||
'secretAccessKey',
|
||||
'Secret Access Key',
|
||||
this._handleChangeSecretAccessKey,
|
||||
)}
|
||||
{this.renderRow(
|
||||
'region',
|
||||
'Region',
|
||||
this._handleChangeRegion,
|
||||
"will be calculated from hostname or host or use 'us-east-1' if not given",
|
||||
)}
|
||||
{this.renderRow(
|
||||
'service',
|
||||
'Service',
|
||||
this._handleChangeService,
|
||||
'will be calculated from hostname or host if not given',
|
||||
)}
|
||||
{this.renderRow(
|
||||
'sessionToken',
|
||||
'Session Token',
|
||||
this._handleChangeSessionToken,
|
||||
'Optional token used for multi-factor authentication',
|
||||
)}
|
||||
<tr>
|
||||
<td className="pad-right no-wrap valign-middle">
|
||||
<label htmlFor="enabled" className="label--small no-pad">
|
||||
Enabled
|
||||
</label>
|
||||
</td>
|
||||
<td className="wide">
|
||||
<div className="form-control form-control--underlined">
|
||||
<Button
|
||||
className="btn btn--super-duper-compact"
|
||||
id="enabled"
|
||||
onClick={this._handleDisable}
|
||||
value={!authentication.disabled}
|
||||
title={authentication.disabled ? 'Enable item' : 'Disable item'}>
|
||||
{authentication.disabled ? (
|
||||
<i className="fa fa-square-o" />
|
||||
) : (
|
||||
<i className="fa fa-check-square-o" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { Request, RequestAuthentication } from '../../../../models/request'
|
||||
type Props = {
|
||||
handleRender: Function,
|
||||
handleGetRenderContext: Function,
|
||||
handleUpdateSettingsShowPasswords: (boolean) => Promise<Settings>,
|
||||
handleUpdateSettingsShowPasswords: boolean => Promise<Settings>,
|
||||
nunjucksPowerUserMode: boolean,
|
||||
onChange: (Request, RequestAuthentication) => Promise<Request>,
|
||||
request: Request,
|
||||
@@ -54,78 +54,78 @@ class BasicAuth extends React.PureComponent<Props> {
|
||||
<div className="pad">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="pad-right no-wrap valign-middle">
|
||||
<label htmlFor="username" className="label--small no-pad">
|
||||
Username
|
||||
</label>
|
||||
</td>
|
||||
<td className="wide">
|
||||
<div
|
||||
className={classnames('form-control form-control--underlined no-margin', {
|
||||
'form-control--inactive': authentication.disabled,
|
||||
})}>
|
||||
<OneLineEditor
|
||||
type="text"
|
||||
id="username"
|
||||
disabled={authentication.disabled}
|
||||
onChange={this._handleChangeUsername}
|
||||
defaultValue={authentication.username || ''}
|
||||
nunjucksPowerUserMode={nunjucksPowerUserMode}
|
||||
render={handleRender}
|
||||
getRenderContext={handleGetRenderContext}
|
||||
isVariableUncovered={isVariableUncovered}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="pad-right no-wrap valign-middle">
|
||||
<label htmlFor="password" className="label--small no-pad">
|
||||
Password
|
||||
</label>
|
||||
</td>
|
||||
<td className="wide">
|
||||
<div
|
||||
className={classnames('form-control form-control--underlined no-margin', {
|
||||
'form-control--inactive': authentication.disabled,
|
||||
})}>
|
||||
<OneLineEditor
|
||||
type={showPasswords ? 'text' : 'password'}
|
||||
id="password"
|
||||
onChange={this._handleChangePassword}
|
||||
defaultValue={authentication.password || ''}
|
||||
nunjucksPowerUserMode={nunjucksPowerUserMode}
|
||||
render={handleRender}
|
||||
getRenderContext={handleGetRenderContext}
|
||||
isVariableUncovered={isVariableUncovered}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="pad-right no-wrap valign-middle">
|
||||
<label htmlFor="enabled" className="label--small no-pad">
|
||||
Enabled
|
||||
</label>
|
||||
</td>
|
||||
<td className="wide">
|
||||
<div className="form-control form-control--underlined">
|
||||
<Button
|
||||
className="btn btn--super-duper-compact"
|
||||
id="enabled"
|
||||
onClick={this._handleDisable}
|
||||
value={!authentication.disabled}
|
||||
title={authentication.disabled ? 'Enable item' : 'Disable item'}>
|
||||
{authentication.disabled ? (
|
||||
<i className="fa fa-square-o"/>
|
||||
) : (
|
||||
<i className="fa fa-check-square-o"/>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="pad-right no-wrap valign-middle">
|
||||
<label htmlFor="username" className="label--small no-pad">
|
||||
Username
|
||||
</label>
|
||||
</td>
|
||||
<td className="wide">
|
||||
<div
|
||||
className={classnames('form-control form-control--underlined no-margin', {
|
||||
'form-control--inactive': authentication.disabled,
|
||||
})}>
|
||||
<OneLineEditor
|
||||
type="text"
|
||||
id="username"
|
||||
disabled={authentication.disabled}
|
||||
onChange={this._handleChangeUsername}
|
||||
defaultValue={authentication.username || ''}
|
||||
nunjucksPowerUserMode={nunjucksPowerUserMode}
|
||||
render={handleRender}
|
||||
getRenderContext={handleGetRenderContext}
|
||||
isVariableUncovered={isVariableUncovered}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="pad-right no-wrap valign-middle">
|
||||
<label htmlFor="password" className="label--small no-pad">
|
||||
Password
|
||||
</label>
|
||||
</td>
|
||||
<td className="wide">
|
||||
<div
|
||||
className={classnames('form-control form-control--underlined no-margin', {
|
||||
'form-control--inactive': authentication.disabled,
|
||||
})}>
|
||||
<OneLineEditor
|
||||
type={showPasswords ? 'text' : 'password'}
|
||||
id="password"
|
||||
onChange={this._handleChangePassword}
|
||||
defaultValue={authentication.password || ''}
|
||||
nunjucksPowerUserMode={nunjucksPowerUserMode}
|
||||
render={handleRender}
|
||||
getRenderContext={handleGetRenderContext}
|
||||
isVariableUncovered={isVariableUncovered}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="pad-right no-wrap valign-middle">
|
||||
<label htmlFor="enabled" className="label--small no-pad">
|
||||
Enabled
|
||||
</label>
|
||||
</td>
|
||||
<td className="wide">
|
||||
<div className="form-control form-control--underlined">
|
||||
<Button
|
||||
className="btn btn--super-duper-compact"
|
||||
id="enabled"
|
||||
onClick={this._handleDisable}
|
||||
value={!authentication.disabled}
|
||||
title={authentication.disabled ? 'Enable item' : 'Disable item'}>
|
||||
{authentication.disabled ? (
|
||||
<i className="fa fa-square-o" />
|
||||
) : (
|
||||
<i className="fa fa-check-square-o" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -51,82 +51,82 @@ class BearerAuth extends React.PureComponent<Props> {
|
||||
<div className="pad">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="pad-right no-wrap valign-middle">
|
||||
<label htmlFor="token" className="label--small no-pad">
|
||||
Token
|
||||
</label>
|
||||
</td>
|
||||
<td className="wide">
|
||||
<div
|
||||
className={classnames('form-control form-control--underlined no-margin', {
|
||||
'form-control--inactive': authentication.disabled,
|
||||
})}>
|
||||
<OneLineEditor
|
||||
type="text"
|
||||
id="token"
|
||||
disabled={authentication.disabled}
|
||||
onChange={this._handleChangeToken}
|
||||
defaultValue={authentication.token || ''}
|
||||
nunjucksPowerUserMode={nunjucksPowerUserMode}
|
||||
render={handleRender}
|
||||
getRenderContext={handleGetRenderContext}
|
||||
isVariableUncovered={isVariableUncovered}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="pad-right no-wrap valign-middle">
|
||||
<label htmlFor="prefix" className="label--small no-pad">
|
||||
Prefix{' '}
|
||||
<HelpTooltip>
|
||||
Prefix to use when sending the Authorization header. Defaults to Bearer.
|
||||
</HelpTooltip>
|
||||
</label>
|
||||
</td>
|
||||
<td className="wide">
|
||||
<div
|
||||
className={classnames('form-control form-control--underlined no-margin', {
|
||||
'form-control--inactive': authentication.disabled,
|
||||
})}>
|
||||
<OneLineEditor
|
||||
type="text"
|
||||
id="prefix"
|
||||
disabled={authentication.disabled}
|
||||
onChange={this._handleChangePrefix}
|
||||
defaultValue={authentication.prefix || ''}
|
||||
nunjucksPowerUserMode={nunjucksPowerUserMode}
|
||||
render={handleRender}
|
||||
getRenderContext={handleGetRenderContext}
|
||||
isVariableUncovered={isVariableUncovered}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="pad-right no-wrap valign-middle">
|
||||
<label htmlFor="enabled" className="label--small no-pad">
|
||||
Enabled
|
||||
</label>
|
||||
</td>
|
||||
<td className="wide">
|
||||
<div className="form-control form-control--underlined">
|
||||
<Button
|
||||
className="btn btn--super-duper-compact"
|
||||
id="enabled"
|
||||
onClick={this._handleDisable}
|
||||
value={!authentication.disabled}
|
||||
title={authentication.disabled ? 'Enable item' : 'Disable item'}>
|
||||
{authentication.disabled ? (
|
||||
<i className="fa fa-square-o"/>
|
||||
) : (
|
||||
<i className="fa fa-check-square-o"/>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="pad-right no-wrap valign-middle">
|
||||
<label htmlFor="token" className="label--small no-pad">
|
||||
Token
|
||||
</label>
|
||||
</td>
|
||||
<td className="wide">
|
||||
<div
|
||||
className={classnames('form-control form-control--underlined no-margin', {
|
||||
'form-control--inactive': authentication.disabled,
|
||||
})}>
|
||||
<OneLineEditor
|
||||
type="text"
|
||||
id="token"
|
||||
disabled={authentication.disabled}
|
||||
onChange={this._handleChangeToken}
|
||||
defaultValue={authentication.token || ''}
|
||||
nunjucksPowerUserMode={nunjucksPowerUserMode}
|
||||
render={handleRender}
|
||||
getRenderContext={handleGetRenderContext}
|
||||
isVariableUncovered={isVariableUncovered}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="pad-right no-wrap valign-middle">
|
||||
<label htmlFor="prefix" className="label--small no-pad">
|
||||
Prefix{' '}
|
||||
<HelpTooltip>
|
||||
Prefix to use when sending the Authorization header. Defaults to Bearer.
|
||||
</HelpTooltip>
|
||||
</label>
|
||||
</td>
|
||||
<td className="wide">
|
||||
<div
|
||||
className={classnames('form-control form-control--underlined no-margin', {
|
||||
'form-control--inactive': authentication.disabled,
|
||||
})}>
|
||||
<OneLineEditor
|
||||
type="text"
|
||||
id="prefix"
|
||||
disabled={authentication.disabled}
|
||||
onChange={this._handleChangePrefix}
|
||||
defaultValue={authentication.prefix || ''}
|
||||
nunjucksPowerUserMode={nunjucksPowerUserMode}
|
||||
render={handleRender}
|
||||
getRenderContext={handleGetRenderContext}
|
||||
isVariableUncovered={isVariableUncovered}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="pad-right no-wrap valign-middle">
|
||||
<label htmlFor="enabled" className="label--small no-pad">
|
||||
Enabled
|
||||
</label>
|
||||
</td>
|
||||
<td className="wide">
|
||||
<div className="form-control form-control--underlined">
|
||||
<Button
|
||||
className="btn btn--super-duper-compact"
|
||||
id="enabled"
|
||||
onClick={this._handleDisable}
|
||||
value={!authentication.disabled}
|
||||
title={authentication.disabled ? 'Enable item' : 'Disable item'}>
|
||||
{authentication.disabled ? (
|
||||
<i className="fa fa-square-o" />
|
||||
) : (
|
||||
<i className="fa fa-check-square-o" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { Request, RequestAuthentication } from '../../../../models/request'
|
||||
type Props = {
|
||||
handleRender: Function,
|
||||
handleGetRenderContext: Function,
|
||||
handleUpdateSettingsShowPasswords: (boolean) => Promise<Settings>,
|
||||
handleUpdateSettingsShowPasswords: boolean => Promise<Settings>,
|
||||
nunjucksPowerUserMode: boolean,
|
||||
onChange: (Request, RequestAuthentication) => Promise<Request>,
|
||||
request: Request,
|
||||
@@ -55,78 +55,78 @@ class DigestAuth extends React.PureComponent<Props> {
|
||||
<div className="pad">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="pad-right no-wrap valign-middle">
|
||||
<label htmlFor="username" className="label--small no-pad">
|
||||
Username
|
||||
</label>
|
||||
</td>
|
||||
<td className="wide">
|
||||
<div
|
||||
className={classnames('form-control form-control--underlined no-margin', {
|
||||
'form-control--inactive': authentication.disabled,
|
||||
})}>
|
||||
<OneLineEditor
|
||||
type="text"
|
||||
id="username"
|
||||
disabled={authentication.disabled}
|
||||
onChange={this._handleChangeUsername}
|
||||
defaultValue={authentication.username || ''}
|
||||
nunjucksPowerUserMode={nunjucksPowerUserMode}
|
||||
render={handleRender}
|
||||
getRenderContext={handleGetRenderContext}
|
||||
isVariableUncovered={isVariableUncovered}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="pad-right no-wrap valign-middle">
|
||||
<label htmlFor="password" className="label--small no-pad">
|
||||
Password
|
||||
</label>
|
||||
</td>
|
||||
<td className="wide">
|
||||
<div
|
||||
className={classnames('form-control form-control--underlined no-margin', {
|
||||
'form-control--inactive': authentication.disabled,
|
||||
})}>
|
||||
<OneLineEditor
|
||||
type={showPasswords ? 'text' : 'password'}
|
||||
id="password"
|
||||
onChange={this._handleChangePassword}
|
||||
defaultValue={authentication.password || ''}
|
||||
nunjucksPowerUserMode={nunjucksPowerUserMode}
|
||||
render={handleRender}
|
||||
getRenderContext={handleGetRenderContext}
|
||||
isVariableUncovered={isVariableUncovered}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="pad-right no-wrap valign-middle">
|
||||
<label htmlFor="enabled" className="label--small no-pad">
|
||||
Enabled
|
||||
</label>
|
||||
</td>
|
||||
<td className="wide">
|
||||
<div className="form-control form-control--underlined">
|
||||
<Button
|
||||
className="btn btn--super-duper-compact"
|
||||
id="enabled"
|
||||
onClick={this._handleDisable}
|
||||
value={!authentication.disabled}
|
||||
title={authentication.disabled ? 'Enable item' : 'Disable item'}>
|
||||
{authentication.disabled ? (
|
||||
<i className="fa fa-square-o"/>
|
||||
) : (
|
||||
<i className="fa fa-check-square-o"/>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="pad-right no-wrap valign-middle">
|
||||
<label htmlFor="username" className="label--small no-pad">
|
||||
Username
|
||||
</label>
|
||||
</td>
|
||||
<td className="wide">
|
||||
<div
|
||||
className={classnames('form-control form-control--underlined no-margin', {
|
||||
'form-control--inactive': authentication.disabled,
|
||||
})}>
|
||||
<OneLineEditor
|
||||
type="text"
|
||||
id="username"
|
||||
disabled={authentication.disabled}
|
||||
onChange={this._handleChangeUsername}
|
||||
defaultValue={authentication.username || ''}
|
||||
nunjucksPowerUserMode={nunjucksPowerUserMode}
|
||||
render={handleRender}
|
||||
getRenderContext={handleGetRenderContext}
|
||||
isVariableUncovered={isVariableUncovered}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="pad-right no-wrap valign-middle">
|
||||
<label htmlFor="password" className="label--small no-pad">
|
||||
Password
|
||||
</label>
|
||||
</td>
|
||||
<td className="wide">
|
||||
<div
|
||||
className={classnames('form-control form-control--underlined no-margin', {
|
||||
'form-control--inactive': authentication.disabled,
|
||||
})}>
|
||||
<OneLineEditor
|
||||
type={showPasswords ? 'text' : 'password'}
|
||||
id="password"
|
||||
onChange={this._handleChangePassword}
|
||||
defaultValue={authentication.password || ''}
|
||||
nunjucksPowerUserMode={nunjucksPowerUserMode}
|
||||
render={handleRender}
|
||||
getRenderContext={handleGetRenderContext}
|
||||
isVariableUncovered={isVariableUncovered}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="pad-right no-wrap valign-middle">
|
||||
<label htmlFor="enabled" className="label--small no-pad">
|
||||
Enabled
|
||||
</label>
|
||||
</td>
|
||||
<td className="wide">
|
||||
<div className="form-control form-control--underlined">
|
||||
<Button
|
||||
className="btn btn--super-duper-compact"
|
||||
id="enabled"
|
||||
onClick={this._handleDisable}
|
||||
value={!authentication.disabled}
|
||||
title={authentication.disabled ? 'Enable item' : 'Disable item'}>
|
||||
{authentication.disabled ? (
|
||||
<i className="fa fa-square-o" />
|
||||
) : (
|
||||
<i className="fa fa-check-square-o" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { Settings } from '../../../../models/settings';
|
||||
type Props = {
|
||||
handleRender: Function,
|
||||
handleGetRenderContext: Function,
|
||||
handleUpdateSettingsShowPasswords: (boolean) => Promise<Settings>,
|
||||
handleUpdateSettingsShowPasswords: boolean => Promise<Settings>,
|
||||
nunjucksPowerUserMode: boolean,
|
||||
onChange: (Request, RequestAuthentication) => Promise<Request>,
|
||||
request: Request,
|
||||
@@ -54,78 +54,78 @@ class NTLMAuth extends React.PureComponent<Props> {
|
||||
<div className="pad">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="pad-right no-wrap valign-middle">
|
||||
<label htmlFor="username" className="label--small no-pad">
|
||||
Username
|
||||
</label>
|
||||
</td>
|
||||
<td className="wide">
|
||||
<div
|
||||
className={classnames('form-control form-control--underlined no-margin', {
|
||||
'form-control--inactive': authentication.disabled,
|
||||
})}>
|
||||
<OneLineEditor
|
||||
type="text"
|
||||
id="username"
|
||||
disabled={authentication.disabled}
|
||||
onChange={this._handleChangeUsername}
|
||||
defaultValue={authentication.username || ''}
|
||||
nunjucksPowerUserMode={nunjucksPowerUserMode}
|
||||
render={handleRender}
|
||||
getRenderContext={handleGetRenderContext}
|
||||
isVariableUncovered={isVariableUncovered}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="pad-right no-wrap valign-middle">
|
||||
<label htmlFor="password" className="label--small no-pad">
|
||||
Password
|
||||
</label>
|
||||
</td>
|
||||
<td className="wide">
|
||||
<div
|
||||
className={classnames('form-control form-control--underlined no-margin', {
|
||||
'form-control--inactive': authentication.disabled,
|
||||
})}>
|
||||
<OneLineEditor
|
||||
type={showPasswords ? 'text' : 'password'}
|
||||
id="password"
|
||||
onChange={this._handleChangePassword}
|
||||
defaultValue={authentication.password || ''}
|
||||
nunjucksPowerUserMode={nunjucksPowerUserMode}
|
||||
render={handleRender}
|
||||
getRenderContext={handleGetRenderContext}
|
||||
isVariableUncovered={isVariableUncovered}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="pad-right no-wrap valign-middle">
|
||||
<label htmlFor="enabled" className="label--small no-pad">
|
||||
Enabled
|
||||
</label>
|
||||
</td>
|
||||
<td className="wide">
|
||||
<div className="form-control form-control--underlined">
|
||||
<Button
|
||||
className="btn btn--super-duper-compact"
|
||||
id="enabled"
|
||||
onClick={this._handleDisable}
|
||||
value={!authentication.disabled}
|
||||
title={authentication.disabled ? 'Enable item' : 'Disable item'}>
|
||||
{authentication.disabled ? (
|
||||
<i className="fa fa-square-o"/>
|
||||
) : (
|
||||
<i className="fa fa-check-square-o"/>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="pad-right no-wrap valign-middle">
|
||||
<label htmlFor="username" className="label--small no-pad">
|
||||
Username
|
||||
</label>
|
||||
</td>
|
||||
<td className="wide">
|
||||
<div
|
||||
className={classnames('form-control form-control--underlined no-margin', {
|
||||
'form-control--inactive': authentication.disabled,
|
||||
})}>
|
||||
<OneLineEditor
|
||||
type="text"
|
||||
id="username"
|
||||
disabled={authentication.disabled}
|
||||
onChange={this._handleChangeUsername}
|
||||
defaultValue={authentication.username || ''}
|
||||
nunjucksPowerUserMode={nunjucksPowerUserMode}
|
||||
render={handleRender}
|
||||
getRenderContext={handleGetRenderContext}
|
||||
isVariableUncovered={isVariableUncovered}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="pad-right no-wrap valign-middle">
|
||||
<label htmlFor="password" className="label--small no-pad">
|
||||
Password
|
||||
</label>
|
||||
</td>
|
||||
<td className="wide">
|
||||
<div
|
||||
className={classnames('form-control form-control--underlined no-margin', {
|
||||
'form-control--inactive': authentication.disabled,
|
||||
})}>
|
||||
<OneLineEditor
|
||||
type={showPasswords ? 'text' : 'password'}
|
||||
id="password"
|
||||
onChange={this._handleChangePassword}
|
||||
defaultValue={authentication.password || ''}
|
||||
nunjucksPowerUserMode={nunjucksPowerUserMode}
|
||||
render={handleRender}
|
||||
getRenderContext={handleGetRenderContext}
|
||||
isVariableUncovered={isVariableUncovered}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="pad-right no-wrap valign-middle">
|
||||
<label htmlFor="enabled" className="label--small no-pad">
|
||||
Enabled
|
||||
</label>
|
||||
</td>
|
||||
<td className="wide">
|
||||
<div className="form-control form-control--underlined">
|
||||
<Button
|
||||
className="btn btn--super-duper-compact"
|
||||
id="enabled"
|
||||
onClick={this._handleDisable}
|
||||
value={!authentication.disabled}
|
||||
title={authentication.disabled ? 'Enable item' : 'Disable item'}>
|
||||
{authentication.disabled ? (
|
||||
<i className="fa fa-square-o" />
|
||||
) : (
|
||||
<i className="fa fa-check-square-o" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -31,7 +31,7 @@ import type { Settings } from '../../../../models/settings';
|
||||
type Props = {
|
||||
handleRender: Function,
|
||||
handleGetRenderContext: Function,
|
||||
handleUpdateSettingsShowPasswords: (boolean) => Promise<Settings>,
|
||||
handleUpdateSettingsShowPasswords: boolean => Promise<Settings>,
|
||||
nunjucksPowerUserMode: boolean,
|
||||
onChange: (Request, RequestAuthentication) => Promise<Request>,
|
||||
request: Request,
|
||||
@@ -456,7 +456,8 @@ class OAuth2Auth extends React.PureComponent<Props, State> {
|
||||
|
||||
return (
|
||||
<span>
|
||||
(expires <TimeFromNow timestamp={token.expiresAt} />)
|
||||
(expires <TimeFromNow timestamp={token.expiresAt} />
|
||||
)
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -586,8 +587,8 @@ class OAuth2Auth extends React.PureComponent<Props, State> {
|
||||
? 'Refreshing...'
|
||||
: 'Fetching...'
|
||||
: tok
|
||||
? 'Refresh Token'
|
||||
: 'Fetch Tokens'}
|
||||
? 'Refresh Token'
|
||||
: 'Fetch Tokens'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,12 @@ import {
|
||||
CONTENT_TYPE_GRAPHQL,
|
||||
getContentTypeFromHeaders,
|
||||
} from '../../../../common/constants';
|
||||
import type { Request, RequestBody, RequestBodyParameter, RequestHeader } from '../../../../models/request';
|
||||
import type {
|
||||
Request,
|
||||
RequestBody,
|
||||
RequestBodyParameter,
|
||||
RequestHeader,
|
||||
} from '../../../../models/request';
|
||||
import {
|
||||
newBodyFile,
|
||||
newBodyForm,
|
||||
|
||||
@@ -68,7 +68,6 @@ class GraphQLEditor extends React.PureComponent<Props, State> {
|
||||
_isMounted: boolean;
|
||||
_queryEditor: null | CodeMirror;
|
||||
_schemaFetchTimeout: TimeoutID;
|
||||
_highlightTimeout: TimeoutID;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
@@ -338,7 +337,7 @@ class GraphQLEditor extends React.PureComponent<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
_getOperations() {
|
||||
_getOperations(): Array<any> {
|
||||
if (!this._documentAST) {
|
||||
return [];
|
||||
}
|
||||
@@ -347,12 +346,6 @@ class GraphQLEditor extends React.PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
_handleBodyChange(query: string, variables: ?Object, operationName: ?string): void {
|
||||
try {
|
||||
this._documentAST = parse(query);
|
||||
} catch (e) {
|
||||
this._documentAST = null;
|
||||
}
|
||||
|
||||
const body: GraphQLBody = { query };
|
||||
|
||||
if (variables) {
|
||||
@@ -363,13 +356,27 @@ class GraphQLEditor extends React.PureComponent<Props, State> {
|
||||
body.operationName = operationName;
|
||||
}
|
||||
|
||||
const newContent = GraphQLEditor._graphQLToString(body);
|
||||
|
||||
// This method gets called a lot so make sure we only do something if the
|
||||
// new body has actually changed.
|
||||
if (this.props.content === newContent) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
variablesSyntaxError: '',
|
||||
body,
|
||||
});
|
||||
|
||||
this.props.onChange(GraphQLEditor._graphQLToString(body));
|
||||
this.props.onChange(newContent);
|
||||
this._highlightOperation(body.operationName || null);
|
||||
|
||||
try {
|
||||
this._documentAST = parse(query);
|
||||
} catch (e) {
|
||||
this._documentAST = null;
|
||||
}
|
||||
}
|
||||
|
||||
_handleQueryChange(query: string): void {
|
||||
@@ -551,23 +558,22 @@ class GraphQLEditor extends React.PureComponent<Props, State> {
|
||||
/>
|
||||
</div>
|
||||
<div className="graphql-editor__schema-error">
|
||||
{!hideSchemaFetchErrors &&
|
||||
schemaFetchError && (
|
||||
<div className="notice error margin no-margin-top margin-bottom-sm">
|
||||
<div className="pull-right">
|
||||
<Tooltip position="top" message="View introspection request/response timeline">
|
||||
<button className="icon icon--success" onClick={this._handleViewResponse}>
|
||||
<i className="fa fa-bug" />
|
||||
</button>
|
||||
</Tooltip>{' '}
|
||||
<button className="icon" onClick={this._hideSchemaFetchError}>
|
||||
<i className="fa fa-times" />
|
||||
{!hideSchemaFetchErrors && schemaFetchError && (
|
||||
<div className="notice error margin no-margin-top margin-bottom-sm">
|
||||
<div className="pull-right">
|
||||
<Tooltip position="top" message="View introspection request/response timeline">
|
||||
<button className="icon icon--success" onClick={this._handleViewResponse}>
|
||||
<i className="fa fa-bug" />
|
||||
</button>
|
||||
</div>
|
||||
{schemaFetchError.message}
|
||||
<br />
|
||||
</Tooltip>{' '}
|
||||
<button className="icon" onClick={this._hideSchemaFetchError}>
|
||||
<i className="fa fa-times" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{schemaFetchError.message}
|
||||
<br />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="graphql-editor__meta">
|
||||
{this.renderSchemaFetchMessage()}
|
||||
@@ -576,7 +582,8 @@ class GraphQLEditor extends React.PureComponent<Props, State> {
|
||||
<h2 className="no-margin pad-left-sm pad-top-sm pad-bottom-sm">
|
||||
Query Variables
|
||||
<HelpTooltip className="space-left">
|
||||
Variables to use in GraphQL query <br />(JSON format)
|
||||
Variables to use in GraphQL query <br />
|
||||
(JSON format)
|
||||
</HelpTooltip>
|
||||
{variablesSyntaxError && (
|
||||
<span className="text-danger italic pull-right">{variablesSyntaxError}</span>
|
||||
|
||||
@@ -68,7 +68,7 @@ class SingleErrorBoundary extends React.PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
class ErrorBoundary extends React.PureComponent<Props> {
|
||||
render() {
|
||||
render(): React.Node {
|
||||
const { children, ...extraProps } = this.props;
|
||||
|
||||
if (!children) {
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import autobind from 'autobind-decorator';
|
||||
import classnames from 'classnames';
|
||||
import type { RequestGroup } from '../../../models/request-group';
|
||||
|
||||
type Props = {
|
||||
// Required.
|
||||
handleSetRequestGroupCollapsed: Function,
|
||||
handleSetItemSelected: Function,
|
||||
isCollapsed: boolean,
|
||||
totalRequests: number,
|
||||
selectedRequests: number,
|
||||
requestGroup: RequestGroup,
|
||||
|
||||
// Optional.
|
||||
children?: React.Node,
|
||||
};
|
||||
|
||||
@autobind
|
||||
class RequestGroupRow extends React.PureComponent<Props> {
|
||||
checkbox: HTMLInputElement;
|
||||
|
||||
setCheckboxRef(checkbox: ?HTMLInputElement) {
|
||||
if (checkbox != null) {
|
||||
this.checkbox = checkbox;
|
||||
}
|
||||
}
|
||||
|
||||
handleCollapse() {
|
||||
const { requestGroup, handleSetRequestGroupCollapsed, isCollapsed } = this.props;
|
||||
handleSetRequestGroupCollapsed(requestGroup._id, !isCollapsed);
|
||||
}
|
||||
|
||||
handleSelect(e: SyntheticEvent<HTMLInputElement>) {
|
||||
const el = e.currentTarget;
|
||||
let value = el.checked;
|
||||
const { handleSetItemSelected, requestGroup } = this.props;
|
||||
return handleSetItemSelected(requestGroup._id, value);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { children, requestGroup, isCollapsed, totalRequests, selectedRequests } = this.props;
|
||||
|
||||
let folderIconClass = 'fa-folder';
|
||||
|
||||
folderIconClass += isCollapsed ? '' : '-open';
|
||||
|
||||
const isSelected = selectedRequests === totalRequests;
|
||||
if (this.checkbox != null) {
|
||||
// Partial or indeterminate checkbox.
|
||||
this.checkbox.indeterminate = selectedRequests > 0 && selectedRequests < totalRequests;
|
||||
}
|
||||
|
||||
return (
|
||||
<li key={requestGroup._id} className="tree__row">
|
||||
<div className="tree__item tree__item--big">
|
||||
<div className="tree__item__checkbox tree__indent">
|
||||
<input
|
||||
ref={this.setCheckboxRef}
|
||||
type="checkbox"
|
||||
checked={isSelected}
|
||||
onChange={this.handleSelect}
|
||||
/>
|
||||
</div>
|
||||
<button onClick={this.handleCollapse}>
|
||||
<i className={'tree__item__icon fa ' + folderIconClass} />
|
||||
{requestGroup.name}
|
||||
<span className="total-requests">{totalRequests} requests</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ul
|
||||
className={classnames('tree__list', {
|
||||
'tree__list--collapsed': isCollapsed,
|
||||
})}>
|
||||
{!isCollapsed ? children : null}
|
||||
</ul>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default RequestGroupRow;
|
||||
Loaded 100 of 259 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user