mirror of
https://github.com/Kong/insomnia.git
synced 2026-05-24 08:37:35 -04:00
Remove legacy sync (#3028)
This commit is contained in:
@@ -55,9 +55,6 @@ type BaseSettings = {
|
||||
useBulkHeaderEditor: boolean,
|
||||
useBulkParametersEditor: boolean,
|
||||
validateSSL: boolean,
|
||||
|
||||
// Feature flags
|
||||
enableSyncBeta: boolean,
|
||||
};
|
||||
|
||||
export type Settings = BaseModel & BaseSettings;
|
||||
@@ -111,9 +108,6 @@ export function init(): BaseSettings {
|
||||
useBulkHeaderEditor: false,
|
||||
useBulkParametersEditor: false,
|
||||
validateSSL: true,
|
||||
|
||||
// Feature flags
|
||||
enableSyncBeta: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,557 +0,0 @@
|
||||
import * as sync from '../index';
|
||||
import * as models from '../../models';
|
||||
import * as db from '../../common/database';
|
||||
import * as syncStorage from '../storage';
|
||||
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 () => {
|
||||
await globalBeforeEach();
|
||||
|
||||
// Reset some things
|
||||
sync._testReset();
|
||||
await _setSessionData();
|
||||
await _setupSessionMocks();
|
||||
|
||||
// Init sync and storage
|
||||
const config = { inMemoryOnly: true, autoload: false, filename: null };
|
||||
await syncStorage.initDB(config, true);
|
||||
|
||||
// Add some data
|
||||
await models.workspace.create({ _id: 'wrk_1', name: 'Workspace 1' });
|
||||
await models.workspace.create({ _id: 'wrk_2', name: 'Workspace 2' });
|
||||
await models.request.create({
|
||||
_id: 'req_1',
|
||||
name: 'Request 1',
|
||||
parentId: 'wrk_1',
|
||||
});
|
||||
await models.request.create({
|
||||
_id: 'req_2',
|
||||
name: 'Request 2',
|
||||
parentId: 'wrk_2',
|
||||
});
|
||||
|
||||
// Create resources, resource groups, and configs
|
||||
const workspaces = await models.workspace.all();
|
||||
const requests = await models.request.all();
|
||||
for (const d of [...workspaces, ...requests]) {
|
||||
await sync.getOrCreateResourceForDoc(d);
|
||||
}
|
||||
});
|
||||
|
||||
it('Pushes sync mode with and without resource group id', async () => {
|
||||
const request = await models.request.getById('req_1');
|
||||
const request2 = await models.request.getById('req_2');
|
||||
const resourceRequest = await syncStorage.getResourceByDocId(request._id);
|
||||
const resourceRequest2 = await syncStorage.getResourceByDocId(request2._id);
|
||||
|
||||
// Set up sync modes
|
||||
await sync.createOrUpdateConfig(resourceRequest.resourceGroupId, {
|
||||
syncMode: syncStorage.SYNC_MODE_ON,
|
||||
});
|
||||
await sync.createOrUpdateConfig(resourceRequest2.resourceGroupId, {
|
||||
syncMode: syncStorage.SYNC_MODE_UNSET,
|
||||
});
|
||||
|
||||
await sync.push(); // Push only active configs
|
||||
await sync.push(resourceRequest.resourceGroupId); // Force push rg_1
|
||||
await sync.push(resourceRequest2.resourceGroupId); // Force push rg_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 () => {
|
||||
const request = await models.request.getById('req_1');
|
||||
const resourceRequest = await syncStorage.getResourceByDocId(request._id);
|
||||
await sync.createOrUpdateConfig(resourceRequest.resourceGroupId, {
|
||||
syncMode: syncStorage.SYNC_MODE_ON,
|
||||
});
|
||||
|
||||
network.syncPush.mockReturnValueOnce({
|
||||
updated: [],
|
||||
created: [{ id: request._id, version: 'new-version' }],
|
||||
removed: [],
|
||||
conflicts: [],
|
||||
});
|
||||
|
||||
const resourceBefore = await syncStorage.getResourceByDocId(request._id);
|
||||
await sync.push(resourceRequest.resourceGroupId);
|
||||
const resourceAfter = await syncStorage.getResourceByDocId(request._id);
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
it('Updates resources for pull response', async () => {
|
||||
const request = await models.request.getById('req_1');
|
||||
const request2 = await models.request.getById('req_2');
|
||||
const requestNew = Object.assign({}, request, {
|
||||
_id: 'req_new',
|
||||
name: 'New Request',
|
||||
});
|
||||
const resourceBefore = await syncStorage.getResourceByDocId(request._id);
|
||||
const resource2Before = await syncStorage.getResourceByDocId(requestNew._id);
|
||||
await sync.createOrUpdateConfig(resourceBefore.resourceGroupId, {
|
||||
syncMode: syncStorage.SYNC_MODE_ON,
|
||||
});
|
||||
const updatedRequest = Object.assign({}, request, {
|
||||
name: 'Request Updated',
|
||||
});
|
||||
const updatedResource = Object.assign({}, resourceBefore, {
|
||||
version: 'ver1',
|
||||
encContent: await sync.encryptDoc(resourceBefore.resourceGroupId, updatedRequest),
|
||||
});
|
||||
const createdResourceNew = Object.assign({}, resourceBefore, {
|
||||
id: requestNew._id,
|
||||
resourceGroupId: 'rg_1',
|
||||
encContent: await sync.encryptDoc(resourceBefore.resourceGroupId, requestNew),
|
||||
});
|
||||
|
||||
network.syncPull.mockReturnValueOnce({
|
||||
updatedResources: [updatedResource],
|
||||
createdResources: [createdResourceNew],
|
||||
idsToPush: [],
|
||||
idsToRemove: ['req_2'],
|
||||
});
|
||||
|
||||
// Pull and get docs/resources
|
||||
await sync.pull(resourceBefore.resourceGroupId);
|
||||
const requestAfter = await models.request.getById(request._id);
|
||||
const request2After = await models.request.getById(request2._id);
|
||||
const requestNewAfter = await models.request.getById('req_new');
|
||||
const resourceAfter = await syncStorage.getResourceByDocId(
|
||||
request._id,
|
||||
resourceBefore.resourceGroupId,
|
||||
);
|
||||
const resource2After = await syncStorage.getResourceByDocId(request2._id);
|
||||
const resourceNewAfter = await syncStorage.getResourceByDocId(requestNewAfter._id);
|
||||
|
||||
// Assert
|
||||
expect(resourceBefore.version).toBe('__NO_VERSION__');
|
||||
expect(resourceAfter.version).toBe(updatedResource.version);
|
||||
expect(resourceBefore.dirty).toBe(true);
|
||||
expect(resource2Before).toBe(null);
|
||||
expect(resourceAfter.dirty).toBe(false);
|
||||
expect(resource2After.removed).toBe(true);
|
||||
expect(requestAfter.name).toBe('Request Updated');
|
||||
expect(request2After).toBe(null);
|
||||
expect(resourceNewAfter).not.toBe(null);
|
||||
});
|
||||
|
||||
it('Conflict: local version wins on modified before', async () => {
|
||||
const requestClient = await models.request.getById('req_1');
|
||||
const requestServer = Object.assign({}, requestClient, {
|
||||
name: 'Server Request',
|
||||
});
|
||||
const resourceRequest = await syncStorage.getResourceByDocId(requestClient._id);
|
||||
const resourceConflict = Object.assign({}, resourceRequest, {
|
||||
version: 'ver-2',
|
||||
encContent: await sync.encryptDoc(resourceRequest.resourceGroupId, requestServer),
|
||||
lastEdited: resourceRequest.lastEdited - 1000, // Same edited time
|
||||
});
|
||||
|
||||
network.syncPush.mockReturnValueOnce({
|
||||
updated: [],
|
||||
created: [],
|
||||
removed: [],
|
||||
conflicts: [resourceConflict],
|
||||
});
|
||||
|
||||
await sync.push(resourceRequest.resourceGroupId);
|
||||
const resourceAfter = await syncStorage.getResourceByDocId(
|
||||
requestClient._id,
|
||||
resourceRequest.resourceGroupId,
|
||||
);
|
||||
const requestAfter = await models.request.getById(requestClient._id);
|
||||
|
||||
// Assert
|
||||
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
|
||||
expect(resourceAfter.dirty).toBe(true);
|
||||
// Local db should not be changed since the local won
|
||||
expect(requestAfter).toEqual(requestClient);
|
||||
});
|
||||
|
||||
it('Conflict: local version wins on modified tie', async () => {
|
||||
const requestClient = await models.request.getById('req_1');
|
||||
const requestServer = Object.assign({}, requestClient, {
|
||||
name: 'Server Request',
|
||||
});
|
||||
const resourceRequest = await syncStorage.getResourceByDocId(requestClient._id);
|
||||
const resourceConflict = Object.assign({}, resourceRequest, {
|
||||
version: 'ver-2',
|
||||
encContent: await sync.encryptDoc(resourceRequest.resourceGroupId, requestServer),
|
||||
lastEdited: resourceRequest.lastEdited, // Same edited time
|
||||
});
|
||||
|
||||
network.syncPush.mockReturnValueOnce({
|
||||
updated: [],
|
||||
created: [],
|
||||
removed: [],
|
||||
conflicts: [resourceConflict],
|
||||
});
|
||||
|
||||
await sync.push(resourceRequest.resourceGroupId);
|
||||
const resourceAfter = await syncStorage.getResourceByDocId(
|
||||
requestClient._id,
|
||||
resourceRequest.resourceGroupId,
|
||||
);
|
||||
const requestAfter = await models.request.getById(requestClient._id);
|
||||
|
||||
// Assert
|
||||
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
|
||||
expect(resourceAfter.dirty).toBe(true);
|
||||
// Local db should not be changed since the local won
|
||||
expect(requestAfter).toEqual(requestClient);
|
||||
});
|
||||
|
||||
it('Conflict: server version wins if modified after', async () => {
|
||||
const requestClient = await models.request.getById('req_1');
|
||||
const requestServer = Object.assign({}, requestClient, {
|
||||
name: 'Server Request',
|
||||
});
|
||||
const resourceRequest = await syncStorage.getResourceByDocId(requestClient._id);
|
||||
const resourceConflict = Object.assign({}, resourceRequest, {
|
||||
version: 'ver-2',
|
||||
encContent: await sync.encryptDoc(resourceRequest.resourceGroupId, requestServer),
|
||||
lastEdited: resourceRequest.lastEdited + 1000,
|
||||
});
|
||||
|
||||
network.syncPush.mockReturnValueOnce({
|
||||
updated: [],
|
||||
created: [],
|
||||
removed: [],
|
||||
conflicts: [resourceConflict],
|
||||
});
|
||||
|
||||
await sync.push(resourceRequest.resourceGroupId);
|
||||
const resourceAfter = await syncStorage.getResourceByDocId(
|
||||
requestClient._id,
|
||||
resourceRequest.resourceGroupId,
|
||||
);
|
||||
const requestAfter = await models.request.getById(requestClient._id);
|
||||
|
||||
// Assert
|
||||
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
|
||||
expect(resourceAfter.dirty).toBe(false);
|
||||
expect(requestAfter.name).toBe('Server Request');
|
||||
expect(requestAfter.modified).toBe(requestServer.modified);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Integration tests for creating Resources and pushing', () => {
|
||||
beforeEach(async () => {
|
||||
await globalBeforeEach();
|
||||
|
||||
// Reset some things
|
||||
await _setSessionData();
|
||||
sync._testReset();
|
||||
|
||||
// Mock some things
|
||||
await _setupSessionMocks();
|
||||
jest.useFakeTimers();
|
||||
|
||||
// Init storage
|
||||
const config = { inMemoryOnly: true, autoload: false, filename: null };
|
||||
await syncStorage.initDB(config, true);
|
||||
|
||||
// Add some data
|
||||
await models.workspace.create({
|
||||
_id: 'wrk_empty',
|
||||
name: 'Workspace Empty',
|
||||
});
|
||||
await models.workspace.create({ _id: 'wrk_1', name: 'Workspace 1' });
|
||||
await models.request.create({
|
||||
_id: 'req_1',
|
||||
name: 'Request 1',
|
||||
parentId: 'wrk_1',
|
||||
});
|
||||
await models.request.create({
|
||||
_id: 'req_2',
|
||||
name: 'Request 2',
|
||||
parentId: 'wrk_1',
|
||||
});
|
||||
await models.request.create({
|
||||
_id: 'req_3',
|
||||
name: 'Request 3',
|
||||
parentId: 'wrk_1',
|
||||
});
|
||||
await models.environment.create({
|
||||
_id: 'env_2',
|
||||
name: 'Env Prv',
|
||||
parentId: 'wrk_1',
|
||||
isPrivate: true,
|
||||
});
|
||||
|
||||
// Flush changes just to be sure they won't affect our tests
|
||||
await db.flushChanges();
|
||||
await sync.writePendingChanges();
|
||||
|
||||
// Assert that all our new models were created
|
||||
expect((await models.workspace.all()).length).toBe(2);
|
||||
expect((await models.request.all()).length).toBe(3);
|
||||
expect((await models.environment.all()).length).toBe(1);
|
||||
expect((await models.cookieJar.all()).length).toBe(0);
|
||||
|
||||
// Assert that initializing sync will create the initial resources
|
||||
expect((await syncStorage.allConfigs()).length).toBe(0);
|
||||
expect((await syncStorage.allResources()).length).toBe(0);
|
||||
const promise = sync.init();
|
||||
jest.runOnlyPendingTimers();
|
||||
await promise;
|
||||
expect((await syncStorage.allConfigs()).length).toBe(2);
|
||||
expect((await syncStorage.allResources()).length).toBe(5);
|
||||
|
||||
// Mark all configs as auto sync
|
||||
const configs = await syncStorage.allConfigs();
|
||||
for (const config of configs) {
|
||||
await syncStorage.updateConfig(config, {
|
||||
syncMode: syncStorage.SYNC_MODE_ON,
|
||||
});
|
||||
}
|
||||
|
||||
// Do initial push
|
||||
await sync.push();
|
||||
|
||||
// Reset mocks once again before tests
|
||||
await _setupSessionMocks();
|
||||
});
|
||||
|
||||
it('Resources created on DB change', async () => {
|
||||
// Fetch the workspace and create a new request
|
||||
await db.bufferChanges();
|
||||
await models.request.create({
|
||||
_id: 'req_t',
|
||||
url: 'https://google.com',
|
||||
parentId: 'wrk_1',
|
||||
});
|
||||
|
||||
await db.flushChanges();
|
||||
await sync.writePendingChanges();
|
||||
await sync.push();
|
||||
|
||||
// Push changes and get resource
|
||||
const resource = await syncStorage.getResourceByDocId('req_t');
|
||||
|
||||
// Assert
|
||||
expect((await syncStorage.allConfigs()).length).toBe(2);
|
||||
expect((await syncStorage.allResources()).length).toBe(6);
|
||||
expect(_decryptResource(resource).url).toBe('https://google.com');
|
||||
expect(resource.removed).toBe(false);
|
||||
|
||||
expect(network.syncPush.mock.calls.length).toBe(1);
|
||||
expect(network.syncPush.mock.calls[0][0].length).toBe(6);
|
||||
|
||||
expect(network.syncPull.mock.calls).toEqual([]);
|
||||
});
|
||||
|
||||
it('Resources revived on DB change', async () => {
|
||||
// Fetch the workspace and create a new request
|
||||
await db.bufferChanges();
|
||||
const request = await models.request.create({
|
||||
_id: 'req_t',
|
||||
name: 'Original Request',
|
||||
parentId: 'wrk_1',
|
||||
});
|
||||
await db.flushChanges();
|
||||
await sync.writePendingChanges();
|
||||
await sync.push();
|
||||
|
||||
// Mark resource as removed
|
||||
const originalResource = await syncStorage.getResourceByDocId('req_t');
|
||||
const updatedResource = await syncStorage.updateResource(originalResource, {
|
||||
removed: true,
|
||||
});
|
||||
|
||||
// Update it and push it again
|
||||
await db.bufferChanges();
|
||||
await models.request.update(request, { name: 'New Name' });
|
||||
await db.flushChanges();
|
||||
await sync.writePendingChanges();
|
||||
await sync.push();
|
||||
const finalResource = await syncStorage.getResourceByDocId('req_t');
|
||||
|
||||
// Assert
|
||||
expect(originalResource.removed).toBe(false);
|
||||
expect(updatedResource.removed).toBe(true);
|
||||
expect(finalResource.removed).toBe(false);
|
||||
});
|
||||
|
||||
it('Resources update on DB change', async () => {
|
||||
// Create, update a request, and fetch it's resource
|
||||
const request = await models.request.getById('req_1');
|
||||
const resource = await syncStorage.getResourceByDocId(request._id);
|
||||
await db.bufferChanges();
|
||||
const updatedRequest = await models.request.update(request, {
|
||||
name: 'New Name',
|
||||
});
|
||||
|
||||
// Drain and fetch new resource
|
||||
await db.flushChanges();
|
||||
await sync.writePendingChanges();
|
||||
await sync.push();
|
||||
const updatedResource = await syncStorage.getResourceByDocId(request._id);
|
||||
|
||||
// Assert
|
||||
expect(request.name).toBe('Request 1');
|
||||
expect(_decryptResource(resource).name).toBe('Request 1');
|
||||
expect(updatedRequest.name).toBe('New Name');
|
||||
expect(_decryptResource(updatedResource).name).toBe('New Name');
|
||||
expect(resource.removed).toBe(false);
|
||||
|
||||
expect(network.syncPush.mock.calls.length).toBe(1);
|
||||
expect(network.syncPush.mock.calls[0][0].length).toBe(5);
|
||||
|
||||
expect(network.syncPull.mock.calls).toEqual([]);
|
||||
});
|
||||
|
||||
it('Resources removed on DB change', async () => {
|
||||
// Create, update a request, and fetch it's resource
|
||||
const request = await models.request.getById('req_1');
|
||||
const resource = await syncStorage.getResourceByDocId(request._id);
|
||||
await db.bufferChanges();
|
||||
await models.request.remove(request);
|
||||
|
||||
// Drain and fetch new resource
|
||||
await db.flushChanges();
|
||||
await sync.writePendingChanges();
|
||||
await sync.push();
|
||||
const updatedResource = await syncStorage.getResourceByDocId(request._id);
|
||||
|
||||
// Assert
|
||||
expect(resource.removed).toBe(false);
|
||||
expect(updatedResource.removed).toBe(true);
|
||||
|
||||
expect(network.syncPush.mock.calls.length).toBe(1);
|
||||
expect(network.syncPush.mock.calls[0][0].length).toBe(5);
|
||||
|
||||
expect(network.syncPull.mock.calls).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// ~~~~~~~ //
|
||||
// Helpers //
|
||||
// ~~~~~~~ //
|
||||
|
||||
function _decryptResource(resource) {
|
||||
const message = JSON.parse(resource.encContent);
|
||||
const fakeKey = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
|
||||
const docJSON = crypt.decryptAES(fakeKey, message);
|
||||
return JSON.parse(docJSON);
|
||||
}
|
||||
|
||||
async function _setSessionData() {
|
||||
const symmetricKey = {
|
||||
alg: 'A256GCM',
|
||||
ext: true,
|
||||
k: '3-QU2OcQcpSyFIoL8idgclbImP3M8Y2d0oVAca3Vl4g',
|
||||
key_ops: ['encrypt', 'decrypt'],
|
||||
kty: 'oct',
|
||||
};
|
||||
|
||||
const publicKey = {
|
||||
alg: 'RSA-OAEP-256',
|
||||
e: 'AQAB',
|
||||
ext: true,
|
||||
key_ops: ['encrypt'],
|
||||
kty: 'RSA',
|
||||
n: 'aaaa',
|
||||
};
|
||||
|
||||
const { privateKey } = await crypt.generateKeyPairJWK();
|
||||
const encPrivateKey = {
|
||||
ad: '',
|
||||
d: Buffer.from(JSON.stringify(privateKey)).toString('hex'),
|
||||
iv: '968f1d810efdaec58f9e313e',
|
||||
t: '0e87a2e57a198ca79cb99585fe9c244a',
|
||||
};
|
||||
|
||||
// Setup mocks and stuff
|
||||
session.setSessionData(
|
||||
'ses_123',
|
||||
'acct_123',
|
||||
'Tammy',
|
||||
'Tester',
|
||||
'greg.schier@konghq.com',
|
||||
symmetricKey,
|
||||
publicKey,
|
||||
encPrivateKey,
|
||||
);
|
||||
}
|
||||
|
||||
async function _setupSessionMocks() {
|
||||
const resourceGroups = {};
|
||||
|
||||
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
|
||||
// convenience
|
||||
const publicKey = session.getPublicKey();
|
||||
const symmetricKeyStr = JSON.stringify({ k: id });
|
||||
const encSymmetricKey = crypt.encryptRSAWithJWK(publicKey, symmetricKeyStr);
|
||||
|
||||
// Store the resource group and return it
|
||||
resourceGroups[id] = Object.assign(
|
||||
{},
|
||||
{ id, encSymmetricKey },
|
||||
{
|
||||
parentResourceId: parentId,
|
||||
name: name,
|
||||
encSymmetricKey: encSymmetricKey,
|
||||
},
|
||||
);
|
||||
return resourceGroups[id];
|
||||
});
|
||||
|
||||
network.syncGetResourceGroup = jest.fn(id => {
|
||||
if (resourceGroups[id]) {
|
||||
return resourceGroups[id];
|
||||
}
|
||||
|
||||
const err = new Error(`Not Found for ${id}`);
|
||||
err.statusCode = 404;
|
||||
throw err;
|
||||
});
|
||||
|
||||
network.syncPull = jest.fn(body => ({
|
||||
updatedResources: [],
|
||||
createdResources: [],
|
||||
idsToPush: [],
|
||||
idsToRemove: [],
|
||||
}));
|
||||
|
||||
network.syncPush = jest.fn(body => ({
|
||||
conflicts: [],
|
||||
updated: [],
|
||||
created: [],
|
||||
removed: [],
|
||||
}));
|
||||
}
|
||||
@@ -1,870 +0,0 @@
|
||||
import * as db from '../common/database';
|
||||
import * as models from '../models';
|
||||
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;
|
||||
export const WRITE_PERIOD = 1e3;
|
||||
|
||||
const WHITE_LIST = {
|
||||
[models.workspace.type]: true,
|
||||
[models.request.type]: true,
|
||||
[models.requestGroup.type]: true,
|
||||
[models.environment.type]: true,
|
||||
[models.unitTest.type]: true,
|
||||
[models.unitTestSuite.type]: true,
|
||||
|
||||
// These can be overridden in sync config
|
||||
[models.cookieJar.type]: true,
|
||||
[models.clientCertificate.type]: true,
|
||||
};
|
||||
|
||||
export const logger = new Logger();
|
||||
|
||||
// TODO: Move this stuff somewhere else
|
||||
const NO_VERSION = '__NO_VERSION__';
|
||||
const resourceGroupSymmetricKeysCache = {};
|
||||
let _pullChangesInterval = null;
|
||||
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;
|
||||
}
|
||||
|
||||
// NOTE: This is at the top to prevent race conditions
|
||||
_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,
|
||||
);
|
||||
|
||||
for (const [event, doc, fromSync] of sortedChanges) {
|
||||
const notOnWhitelist = !WHITE_LIST[doc.type];
|
||||
const notLoggedIn = !session.isLoggedIn();
|
||||
|
||||
if (doc.isPrivate) {
|
||||
logger.debug(`Skip private doc change ${doc._id}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (notLoggedIn || notOnWhitelist || fromSync) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const key = `${event}:${doc._id}`;
|
||||
_pendingDBChanges[key] = [event, doc, Date.now()];
|
||||
}
|
||||
});
|
||||
|
||||
await misc.delay(START_DELAY);
|
||||
|
||||
await push();
|
||||
await pull();
|
||||
|
||||
let nextSyncTime = 0;
|
||||
let isSyncing = false;
|
||||
_pullChangesInterval = setInterval(async () => {
|
||||
if (isSyncing) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Date.now() < nextSyncTime) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark that we are currently executing a sync op
|
||||
isSyncing = true;
|
||||
|
||||
const syncStartTime = Date.now();
|
||||
|
||||
let extraDelay = 0;
|
||||
try {
|
||||
await push();
|
||||
await pull();
|
||||
} catch (err) {
|
||||
logger.error('Sync failed with', err);
|
||||
extraDelay += PULL_PERIOD;
|
||||
}
|
||||
|
||||
const totalSyncTime = Date.now() - syncStartTime;
|
||||
|
||||
// Add sync duration to give the server some room if it's being slow.
|
||||
// Also, multiply it by a random value so everyone doesn't sync up
|
||||
extraDelay += totalSyncTime * (Math.random() * 2);
|
||||
|
||||
nextSyncTime = Date.now() + PULL_PERIOD + extraDelay;
|
||||
isSyncing = false;
|
||||
}, PULL_PERIOD / 5);
|
||||
|
||||
_writeChangesInterval = setInterval(writePendingChanges, WRITE_PERIOD);
|
||||
|
||||
logger.debug('Initialized');
|
||||
}
|
||||
|
||||
// Used only during tests!
|
||||
export function _testReset() {
|
||||
_isInitialized = false;
|
||||
clearInterval(_pullChangesInterval);
|
||||
clearInterval(_writeChangesInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Non-blocking function to perform initial sync for an account. This will pull
|
||||
* all remote resources (if they exist) before initializing sync.
|
||||
*/
|
||||
export function doInitialSync() {
|
||||
process.nextTick(async () => {
|
||||
// First, pull down all remote resources, without first creating new ones.
|
||||
// This makes sure that the first sync won't create resources locally, when
|
||||
// they already exist on the server.
|
||||
await pull(null, false);
|
||||
|
||||
// Make sure sync is on (start the timers)
|
||||
await init();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a function to clean up Workspaces that might have had more than one
|
||||
* ResourceGroup created for them. This function should be called on init (or maybe
|
||||
* even periodically) and can be removed once the bug stops persisting.
|
||||
*/
|
||||
export async function fixDuplicateResourceGroups() {
|
||||
if (!session.isLoggedIn()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let duplicateCount = 0;
|
||||
const workspaces = await models.workspace.all();
|
||||
for (const workspace of workspaces) {
|
||||
const resources = await store.findResourcesByDocId(workspace._id);
|
||||
|
||||
// No duplicates found
|
||||
if (resources.length <= 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fix duplicates
|
||||
const ids = resources.map(r => r.resourceGroupId);
|
||||
const { deleteResourceGroupIds } = await syncFixDupes(ids);
|
||||
|
||||
for (const idToDelete of deleteResourceGroupIds) {
|
||||
await store.removeResourceGroup(idToDelete);
|
||||
}
|
||||
|
||||
duplicateCount++;
|
||||
}
|
||||
|
||||
if (duplicateCount) {
|
||||
logger.debug(`Fixed ${duplicateCount}/${workspaces.length} duplicate synced Workspaces`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function writePendingChanges() {
|
||||
// First make a copy and clear pending changes
|
||||
const changes = Object.assign({}, _pendingDBChanges);
|
||||
_pendingDBChanges = {};
|
||||
|
||||
const keys = Object.keys(changes);
|
||||
|
||||
if (keys.length === 0) {
|
||||
// No changes, just return
|
||||
return;
|
||||
}
|
||||
|
||||
for (const key of Object.keys(changes)) {
|
||||
const [event, doc, timestamp] = changes[key];
|
||||
await _handleChangeAndPush(event, doc, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
export async function push(resourceGroupId = null) {
|
||||
if (_disabledForSession) {
|
||||
logger.debug('Legacy sync is disabled for current session');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!session.isLoggedIn()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let allDirtyResources = [];
|
||||
if (resourceGroupId) {
|
||||
allDirtyResources = await store.findActiveDirtyResourcesForResourceGroup(resourceGroupId);
|
||||
} else {
|
||||
allDirtyResources = await store.findActiveDirtyResources();
|
||||
}
|
||||
|
||||
if (!allDirtyResources.length) {
|
||||
// No changes to push
|
||||
return;
|
||||
}
|
||||
|
||||
const dirtyResources = [];
|
||||
for (const r of allDirtyResources) {
|
||||
// Check if resource type is blacklisted by user
|
||||
const config = await store.getConfig(r.resourceGroupId);
|
||||
|
||||
if (r.type === models.clientCertificate.type && config.syncDisableClientCertificates) {
|
||||
logger.debug(`Skipping pushing blacklisted client certificate ${r.id}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (r.type === models.cookieJar.type && config.syncDisableCookieJars) {
|
||||
logger.debug(`Skipping pushing blacklisted cookie jar ${r.id}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
dirtyResources.push(r);
|
||||
}
|
||||
|
||||
let responseBody;
|
||||
try {
|
||||
responseBody = await syncPush(dirtyResources);
|
||||
} catch (e) {
|
||||
logger.error('Failed to push changes', e);
|
||||
return;
|
||||
}
|
||||
|
||||
const { updated, created, removed, conflicts } = responseBody;
|
||||
|
||||
// Update all resource versions with the ones that were returned
|
||||
for (const { id, version } of updated) {
|
||||
const resource = await store.getResourceByDocId(id);
|
||||
await store.updateResource(resource, { version, dirty: false });
|
||||
}
|
||||
if (updated.length) {
|
||||
logger.debug(`Push updated ${updated.length} resources`);
|
||||
}
|
||||
|
||||
// Update all resource versions with the ones that were returned
|
||||
for (const { id, version } of created) {
|
||||
const resource = await store.getResourceByDocId(id);
|
||||
await store.updateResource(resource, { version, dirty: false });
|
||||
}
|
||||
if (created.length) {
|
||||
logger.debug(`Push created ${created.length} resources`);
|
||||
}
|
||||
|
||||
// Update all resource versions with the ones that were returned
|
||||
for (const { id, version } of removed) {
|
||||
const resource = await store.getResourceByDocId(id);
|
||||
await store.updateResource(resource, { version, dirty: false });
|
||||
}
|
||||
if (removed.length) {
|
||||
logger.debug(`Push removed ${removed.length} resources`);
|
||||
}
|
||||
|
||||
// Resolve conflicts
|
||||
await db.bufferChanges();
|
||||
for (const serverResource of conflicts) {
|
||||
const localResource = await store.getResourceByDocId(
|
||||
serverResource.id,
|
||||
serverResource.resourceGroupId,
|
||||
);
|
||||
|
||||
// On conflict, choose last edited one
|
||||
const serverIsNewer = serverResource.lastEdited > localResource.lastEdited;
|
||||
const winner = serverIsNewer ? serverResource : localResource;
|
||||
|
||||
// Update local resource
|
||||
// NOTE: using localResource as the base to make sure we have _id
|
||||
await store.updateResource(localResource, winner, {
|
||||
version: serverResource.version, // Act as the server resource no matter what
|
||||
dirty: !serverIsNewer, // It's dirty if we chose the local doc
|
||||
});
|
||||
|
||||
// Decrypt the docs from the resources. Don't fetch the local doc from the
|
||||
// app database, because it might have been deleted.
|
||||
const winnerName = serverIsNewer ? 'Server' : 'Local';
|
||||
logger.debug(`Resolved conflict for ${serverResource.id} (${winnerName})`);
|
||||
|
||||
// If the server won, update ourselves. If we won, we already have the
|
||||
// latest version, so do nothing.
|
||||
if (serverIsNewer) {
|
||||
const doc = await decryptDoc(winner.resourceGroupId, winner.encContent);
|
||||
if (winner.removed) {
|
||||
await db.remove(doc, true);
|
||||
} else {
|
||||
await db.update(doc, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db.flushChangesAsync();
|
||||
}
|
||||
|
||||
export async function pull(resourceGroupId = null, createMissingResources = true) {
|
||||
if (_disabledForSession) {
|
||||
logger.debug('Legacy sync is disabled for current session');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!session.isLoggedIn()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to fix duplicates first. Don't worry if this is called a lot since if there
|
||||
// are no duplicates found it doesn't contact the network.
|
||||
await fixDuplicateResourceGroups();
|
||||
|
||||
let allResources;
|
||||
if (createMissingResources) {
|
||||
allResources = await getOrCreateAllActiveResources(resourceGroupId);
|
||||
} else {
|
||||
allResources = await store.allActiveResources(resourceGroupId);
|
||||
}
|
||||
|
||||
let blacklistedConfigs;
|
||||
if (resourceGroupId) {
|
||||
// When doing specific sync, blacklist all configs except the one we're trying to sync.
|
||||
const allConfigs = await store.allConfigs();
|
||||
blacklistedConfigs = allConfigs.filter(c => c.resourceGroupId !== resourceGroupId);
|
||||
} else {
|
||||
// When doing a full sync, blacklist the inactive configs
|
||||
blacklistedConfigs = await store.findInactiveConfigs(resourceGroupId);
|
||||
}
|
||||
|
||||
const resources = allResources.map(r => ({
|
||||
id: r.id,
|
||||
resourceGroupId: r.resourceGroupId,
|
||||
version: r.version,
|
||||
removed: r.removed,
|
||||
}));
|
||||
|
||||
const blacklistedResourceGroupIds = blacklistedConfigs.map(c => c.resourceGroupId);
|
||||
|
||||
const body = {
|
||||
resources,
|
||||
blacklist: blacklistedResourceGroupIds,
|
||||
};
|
||||
|
||||
if (resources.length) {
|
||||
logger.debug(`Pulling with ${resources.length} resources`);
|
||||
}
|
||||
|
||||
let responseBody;
|
||||
try {
|
||||
responseBody = await syncPull(body);
|
||||
} catch (e) {
|
||||
logger.error('Failed to sync changes', e, body);
|
||||
return;
|
||||
}
|
||||
|
||||
const { updatedResources, createdResources, idsToPush, idsToRemove } = responseBody;
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
|
||||
// Insert all the created docs to the DB //
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
|
||||
|
||||
await db.bufferChanges();
|
||||
for (const serverResource of createdResources) {
|
||||
let doc;
|
||||
|
||||
try {
|
||||
const { resourceGroupId, encContent } = serverResource;
|
||||
doc = await decryptDoc(resourceGroupId, encContent);
|
||||
} catch (e) {
|
||||
logger.warn('Failed to decode created resource', e, serverResource);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if resource type is blacklisted by user
|
||||
const config = await store.getConfig(serverResource.resourceGroupId);
|
||||
|
||||
if (
|
||||
serverResource.type === models.clientCertificate.type &&
|
||||
config.syncDisableClientCertificates
|
||||
) {
|
||||
logger.debug(`[sync] Skipping pulling blacklisted client certificate ${serverResource.id}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (serverResource.type === models.cookieJar.type && config.syncDisableCookieJars) {
|
||||
logger.debug(`[sync] Skipping pulling blacklisted cookie jar ${serverResource.id}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Update local Resource
|
||||
try {
|
||||
await store.insertResource(serverResource, { dirty: false });
|
||||
} catch (e) {
|
||||
// This probably means we already have it. This should never happen, but
|
||||
// might due to a rare race condition.
|
||||
logger.error('Failed to insert resource', e, serverResource);
|
||||
return;
|
||||
}
|
||||
|
||||
// NOTE: If the above Resource insert succeeded, that means we have safely
|
||||
// insert the document. However, we're using an upsert here instead because
|
||||
// it's very possible that the client already had that document locally.
|
||||
// This might happen, for example, if the user logs out and back in again.
|
||||
const existingDoc = await db.get(doc.type, doc._id);
|
||||
if (existingDoc) {
|
||||
await db.update(doc, true);
|
||||
} else {
|
||||
// Mark as not seen if we created a new workspace from sync
|
||||
if (doc.type === models.workspace.type) {
|
||||
const workspaceMeta = await models.workspaceMeta.getOrCreateByParentId(doc._id);
|
||||
await models.workspaceMeta.update(workspaceMeta, { hasSeen: false });
|
||||
}
|
||||
await db.insert(doc, true);
|
||||
}
|
||||
}
|
||||
|
||||
if (createdResources.length) {
|
||||
logger.debug(`Pull created ${createdResources.length} resources`);
|
||||
}
|
||||
|
||||
db.flushChangesAsync();
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
|
||||
// Save all the updated docs to the DB //
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
|
||||
|
||||
await db.bufferChanges();
|
||||
for (const serverResource of updatedResources) {
|
||||
try {
|
||||
const { resourceGroupId, encContent } = serverResource;
|
||||
const doc = await decryptDoc(resourceGroupId, encContent);
|
||||
|
||||
// Update app database
|
||||
// Needs to be upsert because we could be "undeleting" something
|
||||
await db.upsert(doc, true);
|
||||
|
||||
// Update local resource
|
||||
const resource = await store.getResourceByDocId(
|
||||
serverResource.id,
|
||||
serverResource.resourceGroupId,
|
||||
);
|
||||
await store.updateResource(resource, serverResource, { dirty: false });
|
||||
} catch (e) {
|
||||
logger.warn('Failed to decode updated resource', e, serverResource);
|
||||
}
|
||||
}
|
||||
db.flushChangesAsync();
|
||||
|
||||
if (updatedResources.length) {
|
||||
logger.debug(`Pull updated ${updatedResources.length} resources`);
|
||||
}
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
|
||||
// Remove all the docs that need removing //
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
|
||||
|
||||
await db.bufferChanges();
|
||||
for (const id of idsToRemove) {
|
||||
const resource = await store.getResourceByDocId(id);
|
||||
if (!resource) {
|
||||
throw new Error(`Could not find Resource to remove for ${id}`);
|
||||
}
|
||||
|
||||
const doc = await decryptDoc(resource.resourceGroupId, resource.encContent);
|
||||
if (!doc) {
|
||||
throw new Error(`Could not find doc to remove ${id}`);
|
||||
}
|
||||
|
||||
// Mark resource as deleted
|
||||
await store.updateResource(resource, { dirty: false, removed: true });
|
||||
|
||||
// Remove from DB
|
||||
await db.remove(doc, true);
|
||||
}
|
||||
db.flushChangesAsync();
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
|
||||
// Push all the docs that need pushing //
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
|
||||
|
||||
for (const id of idsToPush) {
|
||||
const resource = await store.getResourceByDocId(id);
|
||||
if (!resource) {
|
||||
throw new Error(`Could not find Resource to push for id ${id}`);
|
||||
}
|
||||
|
||||
// Mark all resources to push as dirty for the next push
|
||||
await store.updateResource(resource, { dirty: true });
|
||||
}
|
||||
|
||||
return updatedResources.length + createdResources.length;
|
||||
}
|
||||
|
||||
export async function getOrCreateConfig(resourceGroupId) {
|
||||
const config = await store.getConfig(resourceGroupId);
|
||||
|
||||
if (!config) {
|
||||
return store.insertConfig({ resourceGroupId });
|
||||
} else {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureConfigExists(resourceGroupId, syncMode) {
|
||||
const config = await store.getConfig(resourceGroupId);
|
||||
if (!config) {
|
||||
await store.insertConfig({ resourceGroupId, syncMode });
|
||||
}
|
||||
}
|
||||
|
||||
export async function createOrUpdateConfig(resourceGroupId, patch) {
|
||||
const config = await store.getConfig(resourceGroupId);
|
||||
const finalPatch = { resourceGroupId, ...patch };
|
||||
|
||||
if (config) {
|
||||
return store.updateConfig(config, finalPatch);
|
||||
} else {
|
||||
return store.insertConfig(finalPatch);
|
||||
}
|
||||
}
|
||||
|
||||
export async function logout() {
|
||||
await session.logout();
|
||||
await resetLocalData();
|
||||
}
|
||||
|
||||
export async function cancelTrial() {
|
||||
await session.endTrial();
|
||||
await session.logout();
|
||||
await resetLocalData();
|
||||
}
|
||||
|
||||
export async function resetLocalData() {
|
||||
for (const c of await store.allConfigs()) {
|
||||
await store.removeConfig(c);
|
||||
}
|
||||
|
||||
for (const r of await store.allResources()) {
|
||||
await store.removeResource(r);
|
||||
}
|
||||
}
|
||||
|
||||
export async function resetRemoteData() {
|
||||
await syncResetData();
|
||||
}
|
||||
|
||||
// ~~~~~~~ //
|
||||
// HELPERS //
|
||||
// ~~~~~~~ //
|
||||
|
||||
async function _handleChangeAndPush(event, doc, timestamp) {
|
||||
// Update the resource content and set dirty
|
||||
// TODO: Remove one of these steps since it does encryption twice
|
||||
// in the case where the resource does not exist yet
|
||||
const resource = await getOrCreateResourceForDoc(doc);
|
||||
|
||||
const updatedResource = await store.updateResource(resource, {
|
||||
name: doc.name || 'n/a',
|
||||
lastEdited: timestamp,
|
||||
lastEditedBy: session.getAccountId(),
|
||||
encContent: await encryptDoc(resource.resourceGroupId, doc),
|
||||
removed: event === db.CHANGE_REMOVE,
|
||||
dirty: true,
|
||||
});
|
||||
|
||||
// Debounce pushing of dirty resources
|
||||
logger.debug(`Queue ${event} ${updatedResource.id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a ResourceGroup. If it has been fetched before, lookup from memory
|
||||
*
|
||||
* @param resourceGroupId
|
||||
* @returns {*}
|
||||
*/
|
||||
const _fetchResourceGroupPromises = {};
|
||||
const _resourceGroupCache = {};
|
||||
|
||||
export async function fetchResourceGroup(resourceGroupId, invalidateCache = false) {
|
||||
if (invalidateCache) {
|
||||
delete _resourceGroupCache[resourceGroupId];
|
||||
delete _fetchResourceGroupPromises[resourceGroupId];
|
||||
}
|
||||
|
||||
// PERF: If we're currently fetching, return stored promise
|
||||
// TODO: Maybe move parallel fetch caching into the fetch helper
|
||||
if (_fetchResourceGroupPromises[resourceGroupId]) {
|
||||
return _fetchResourceGroupPromises[resourceGroupId];
|
||||
}
|
||||
|
||||
const promise = new Promise(async (resolve, reject) => {
|
||||
let resourceGroup = _resourceGroupCache[resourceGroupId];
|
||||
|
||||
if (!resourceGroup) {
|
||||
try {
|
||||
resourceGroup = await syncGetResourceGroup(resourceGroupId);
|
||||
} catch (e) {
|
||||
if (e.statusCode === 404) {
|
||||
await store.removeResourceGroup(resourceGroupId);
|
||||
logger.debug('ResourceGroup not found. Deleting...');
|
||||
reject(new Error('ResourceGroup was not found'));
|
||||
return;
|
||||
} else {
|
||||
logger.error(`Failed to get ResourceGroup ${resourceGroupId}: ${e}`);
|
||||
reject(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (resourceGroup.isDisabled) {
|
||||
await store.removeResourceGroup(resourceGroup.id);
|
||||
logger.debug('ResourceGroup was disabled. Deleting...');
|
||||
reject(new Error('ResourceGroup was disabled'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Also make sure a config exists when we first fetch it.
|
||||
// (This may not be needed but we'll do it just in case)
|
||||
await ensureConfigExists(resourceGroupId);
|
||||
}
|
||||
|
||||
// Bust cached promise because we're done with it.
|
||||
_fetchResourceGroupPromises[resourceGroupId] = null;
|
||||
|
||||
// Cache the ResourceGroup for next time (they never change)
|
||||
_resourceGroupCache[resourceGroupId] = resourceGroup;
|
||||
|
||||
// Return the ResourceGroup
|
||||
resolve(resourceGroup);
|
||||
});
|
||||
|
||||
// Cache the Promise in case we get asked for the same thing before done
|
||||
_fetchResourceGroupPromises[resourceGroupId] = promise;
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a ResourceGroup's symmetric encryption key
|
||||
*
|
||||
* @param resourceGroupId
|
||||
* @private
|
||||
*/
|
||||
async function _getResourceGroupSymmetricKey(resourceGroupId) {
|
||||
let key = resourceGroupSymmetricKeysCache[resourceGroupId];
|
||||
|
||||
if (!key) {
|
||||
const resourceGroup = await fetchResourceGroup(resourceGroupId);
|
||||
const accountPrivateKey = await session.getPrivateKey();
|
||||
|
||||
const symmetricKeyStr = crypt.decryptRSAWithJWK(
|
||||
accountPrivateKey,
|
||||
resourceGroup.encSymmetricKey,
|
||||
);
|
||||
|
||||
key = JSON.parse(symmetricKeyStr);
|
||||
|
||||
// Update cache
|
||||
resourceGroupSymmetricKeysCache[resourceGroupId] = key;
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
export async function encryptDoc(resourceGroupId, doc) {
|
||||
try {
|
||||
const symmetricKey = await _getResourceGroupSymmetricKey(resourceGroupId);
|
||||
|
||||
// TODO: Turn on compression once enough users are on version >= 5.7.0
|
||||
// const jsonStr = JSON.stringify(doc);
|
||||
// const docStr = zlib.gzipSync(jsonStr);
|
||||
|
||||
// Don't use compression for now
|
||||
const docStr = JSON.stringify(doc);
|
||||
|
||||
const message = crypt.encryptAES(symmetricKey, docStr);
|
||||
return JSON.stringify(message);
|
||||
} catch (e) {
|
||||
logger.error(`Failed to encrypt for ${resourceGroupId}: ${e}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export async function decryptDoc(resourceGroupId, messageJSON) {
|
||||
let decrypted;
|
||||
try {
|
||||
const symmetricKey = await _getResourceGroupSymmetricKey(resourceGroupId);
|
||||
const message = JSON.parse(messageJSON);
|
||||
decrypted = crypt.decryptAES(symmetricKey, message);
|
||||
} catch (e) {
|
||||
logger.error(`Failed to decrypt from ${resourceGroupId}: ${e}`, messageJSON);
|
||||
throw e;
|
||||
}
|
||||
|
||||
try {
|
||||
decrypted = zlib.gunzipSync(decrypted);
|
||||
} catch (err) {
|
||||
// It's not compressed (legacy), which is okay for now
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(decrypted);
|
||||
} catch (e) {
|
||||
logger.error(`Failed to parse after decrypt from ${resourceGroupId}: ${e}`, decrypted);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async function _getWorkspaceForDoc(doc) {
|
||||
const ancestors = await db.withAncestors(doc);
|
||||
return ancestors.find(d => d.type === models.workspace.type);
|
||||
}
|
||||
|
||||
export async function createResourceGroup(parentId, name) {
|
||||
// Generate symmetric key for ResourceGroup
|
||||
const rgSymmetricJWK = await crypt.generateAES256Key();
|
||||
const rgSymmetricJWKStr = JSON.stringify(rgSymmetricJWK);
|
||||
|
||||
// Encrypt the symmetric key with Account public key
|
||||
const publicJWK = session.getPublicKey();
|
||||
const encRGSymmetricJWK = crypt.encryptRSAWithJWK(publicJWK, rgSymmetricJWKStr);
|
||||
|
||||
// Create the new ResourceGroup
|
||||
let resourceGroup;
|
||||
try {
|
||||
resourceGroup = await syncCreateResourceGroup(parentId, name, encRGSymmetricJWK);
|
||||
} catch (e) {
|
||||
logger.error(`Failed to create ResourceGroup: ${e}`);
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Create a config for it
|
||||
await ensureConfigExists(resourceGroup.id, store.SYNC_MODE_UNSET);
|
||||
|
||||
logger.debug(`Created ResourceGroup ${resourceGroup.id}`);
|
||||
return resourceGroup;
|
||||
}
|
||||
|
||||
export async function createResource(doc, resourceGroupId) {
|
||||
return store.insertResource({
|
||||
id: doc._id,
|
||||
name: doc.name || 'n/a', // Set name to the doc name if it has one
|
||||
resourceGroupId: resourceGroupId,
|
||||
version: NO_VERSION,
|
||||
createdBy: session.getAccountId(),
|
||||
lastEdited: doc.modified,
|
||||
lastEditedBy: session.getAccountId(),
|
||||
removed: false,
|
||||
type: doc.type,
|
||||
encContent: await encryptDoc(resourceGroupId, doc),
|
||||
dirty: true,
|
||||
});
|
||||
}
|
||||
|
||||
export async function createResourceForDoc(doc) {
|
||||
// No resource yet, so create one
|
||||
const workspace = await _getWorkspaceForDoc(doc);
|
||||
|
||||
if (!workspace) {
|
||||
// Workspace was probably deleted before it's children could be synced.
|
||||
// TODO: Handle this case better
|
||||
throw new Error(`Could not find workspace for doc ${doc._id}`);
|
||||
}
|
||||
|
||||
let workspaceResource = await store.getResourceByDocId(workspace._id);
|
||||
|
||||
if (!workspaceResource) {
|
||||
const workspaceResourceGroup = await createResourceGroup(workspace._id, workspace.name);
|
||||
workspaceResource = await createResource(workspace, workspaceResourceGroup.id);
|
||||
}
|
||||
|
||||
if (workspace === doc) {
|
||||
// If the current doc IS a Workspace, just return it
|
||||
return workspaceResource;
|
||||
} else {
|
||||
return createResource(doc, workspaceResource.resourceGroupId);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getOrCreateResourceForDoc(doc) {
|
||||
const [resource, ...extras] = await store.findResourcesByDocId(doc._id);
|
||||
|
||||
// Sometimes there may be multiple resources created by accident for
|
||||
// the same doc. Let's delete the extras here if there are any.
|
||||
for (const resource of extras) {
|
||||
await store.removeResource(resource);
|
||||
}
|
||||
|
||||
if (resource) {
|
||||
return resource;
|
||||
} else {
|
||||
return createResourceForDoc(doc);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getOrCreateAllActiveResources(resourceGroupId = null) {
|
||||
const startTime = Date.now();
|
||||
const activeResourceMap = {};
|
||||
|
||||
let activeResources;
|
||||
if (resourceGroupId) {
|
||||
activeResources = await store.activeResourcesForResourceGroup(resourceGroupId);
|
||||
} else {
|
||||
activeResources = await store.allActiveResources();
|
||||
}
|
||||
|
||||
for (const r of activeResources) {
|
||||
activeResourceMap[r.id] = r;
|
||||
}
|
||||
|
||||
// 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,
|
||||
);
|
||||
|
||||
let created = 0;
|
||||
for (const type of modelTypes) {
|
||||
for (const doc of await db.all(type)) {
|
||||
if (doc.isPrivate) {
|
||||
// logger.debug(`Skip private doc ${doc._id}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const resource = await store.getResourceByDocId(doc._id);
|
||||
if (!resource) {
|
||||
try {
|
||||
activeResourceMap[doc._id] = await createResourceForDoc(doc);
|
||||
created++;
|
||||
} catch (e) {
|
||||
// logger.warn(`Failed to create resource for ${doc._id} ${e}`, {doc});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const resources = Object.keys(activeResourceMap).map(k => activeResourceMap[k]);
|
||||
|
||||
const time = (Date.now() - startTime) / 1000;
|
||||
if (created > 0) {
|
||||
logger.debug(`Created ${created}/${resources.length} Resources (${time.toFixed(2)}s)`);
|
||||
}
|
||||
return resources;
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
export default class Logger {
|
||||
constructor() {
|
||||
this._logs = [];
|
||||
}
|
||||
|
||||
debug(message, ...args) {
|
||||
this._log('debug', message, ...args);
|
||||
}
|
||||
|
||||
warn(message, ...args) {
|
||||
this._log('warn', message, ...args);
|
||||
}
|
||||
|
||||
error(message, ...args) {
|
||||
this._log('error', message, ...args);
|
||||
}
|
||||
|
||||
tail() {
|
||||
return this._logs;
|
||||
}
|
||||
|
||||
/** @private */
|
||||
_log(type, message, ...args) {
|
||||
let fn;
|
||||
if (type === 'debug') {
|
||||
fn = 'log';
|
||||
} else if (type === 'warn') {
|
||||
fn = 'warn';
|
||||
} else {
|
||||
fn = 'error';
|
||||
}
|
||||
|
||||
console[fn](`[sync] ${message}`, ...args);
|
||||
const date = new Date();
|
||||
this._logs.push({ type, date, message });
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
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', null, 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`,
|
||||
null,
|
||||
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(),
|
||||
);
|
||||
}
|
||||
@@ -1,240 +0,0 @@
|
||||
import NeDB from 'nedb';
|
||||
import fsPath from 'path';
|
||||
import crypto from 'crypto';
|
||||
import * as util from '../common/misc';
|
||||
import { DB_PERSIST_INTERVAL } from '../common/constants';
|
||||
|
||||
const TYPE_RESOURCE = 'Resource';
|
||||
const TYPE_CONFIG = 'Config';
|
||||
|
||||
export const SYNC_MODE_OFF = 'paused';
|
||||
export const SYNC_MODE_ON = 'active';
|
||||
export const SYNC_MODE_NEVER = 'never';
|
||||
export const SYNC_MODE_UNSET = 'unset';
|
||||
let changeListeners = [];
|
||||
|
||||
export function onChange(callback) {
|
||||
changeListeners.push(callback);
|
||||
}
|
||||
|
||||
export function offChange(callback) {
|
||||
changeListeners = changeListeners.filter(l => l !== callback);
|
||||
}
|
||||
|
||||
let _changeTimeout = null;
|
||||
function _notifyChange() {
|
||||
clearTimeout(_changeTimeout);
|
||||
_changeTimeout = setTimeout(() => {
|
||||
for (const fn of changeListeners) {
|
||||
fn();
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
|
||||
export function allActiveResources(resourceGroupId = null) {
|
||||
if (resourceGroupId) {
|
||||
return findActiveResources({ resourceGroupId });
|
||||
} else {
|
||||
return findActiveResources({});
|
||||
}
|
||||
}
|
||||
|
||||
export function activeResourcesForResourceGroup(resourceGroupId) {
|
||||
return findActiveResources({ resourceGroupId });
|
||||
}
|
||||
|
||||
export function allResources() {
|
||||
return findResources({});
|
||||
}
|
||||
|
||||
export async function findResources(query = {}) {
|
||||
return _execDB(TYPE_RESOURCE, 'find', query);
|
||||
}
|
||||
|
||||
export async function findActiveResources(query) {
|
||||
const configs = await findActiveConfigs();
|
||||
const resourceGroupIds = configs.map(c => c.resourceGroupId);
|
||||
return findResources(Object.assign({ resourceGroupId: { $in: resourceGroupIds } }, query));
|
||||
}
|
||||
|
||||
export async function findActiveDirtyResources() {
|
||||
return findActiveResources({ dirty: true });
|
||||
}
|
||||
|
||||
export async function findActiveDirtyResourcesForResourceGroup(resourceGroupId) {
|
||||
return findActiveResources({ dirty: true, resourceGroupId });
|
||||
}
|
||||
|
||||
export async function findDirtyResourcesForResourceGroup(resourceGroupId) {
|
||||
return findResources({ dirty: true, resourceGroupId });
|
||||
}
|
||||
|
||||
export async function findResourcesForResourceGroup(resourceGroupId) {
|
||||
return findResources({ resourceGroupId });
|
||||
}
|
||||
|
||||
export async function getResourceByDocId(id, resourceGroupId = null) {
|
||||
let query;
|
||||
if (resourceGroupId) {
|
||||
query = { id, resourceGroupId };
|
||||
} else {
|
||||
query = { id };
|
||||
}
|
||||
|
||||
const rawDocs = await _execDB(TYPE_RESOURCE, 'find', query);
|
||||
return rawDocs.length >= 1 ? rawDocs[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is temporary and should only be called when cleaning
|
||||
* up duplicate ResourceGroups
|
||||
* @param id
|
||||
* @returns {*}
|
||||
*/
|
||||
export function findResourcesByDocId(id) {
|
||||
return _execDB(TYPE_RESOURCE, 'find', { id });
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is temporary and should only be called when cleaning
|
||||
* up duplicate ResourceGroups
|
||||
* @param resourceGroupId
|
||||
* @returns {*}
|
||||
*/
|
||||
export async function removeResourceGroup(resourceGroupId) {
|
||||
await _execDB(TYPE_RESOURCE, 'remove', { resourceGroupId }, { multi: true });
|
||||
await _execDB(TYPE_CONFIG, 'remove', { resourceGroupId }, { multi: true });
|
||||
_notifyChange();
|
||||
}
|
||||
|
||||
export async function insertResource(resource) {
|
||||
const h = crypto.createHash('md5');
|
||||
h.update(resource.resourceGroupId);
|
||||
h.update(resource.id);
|
||||
const newResource = Object.assign({}, resource, {
|
||||
_id: `rs_${h.digest('hex')}`,
|
||||
});
|
||||
await _execDB(TYPE_RESOURCE, 'insert', newResource);
|
||||
_notifyChange();
|
||||
return newResource;
|
||||
}
|
||||
|
||||
export async function updateResource(resource, ...patches) {
|
||||
const newDoc = Object.assign({}, resource, ...patches);
|
||||
await _execDB(TYPE_RESOURCE, 'update', { _id: resource._id }, newDoc, {
|
||||
multi: true,
|
||||
});
|
||||
_notifyChange();
|
||||
return newDoc;
|
||||
}
|
||||
|
||||
export async function removeResource(resource) {
|
||||
await _execDB(TYPE_RESOURCE, 'remove', { _id: resource._id }, { multi: true });
|
||||
_notifyChange();
|
||||
}
|
||||
|
||||
// ~~~~~~ //
|
||||
// Config //
|
||||
// ~~~~~~ //
|
||||
|
||||
export function findConfigs(query) {
|
||||
return _execDB(TYPE_CONFIG, 'find', query);
|
||||
}
|
||||
|
||||
export function allConfigs() {
|
||||
return findConfigs({});
|
||||
}
|
||||
|
||||
export function findInactiveConfigs(excludedResourceGroupId = null) {
|
||||
if (excludedResourceGroupId) {
|
||||
return findConfigs({
|
||||
$not: { syncMode: SYNC_MODE_ON, excludedResourceGroupId },
|
||||
});
|
||||
} else {
|
||||
return findConfigs({ $not: { syncMode: SYNC_MODE_ON } });
|
||||
}
|
||||
}
|
||||
|
||||
export function findActiveConfigs(resourceGroupId = null) {
|
||||
if (resourceGroupId) {
|
||||
return findConfigs({ syncMode: SYNC_MODE_ON, resourceGroupId });
|
||||
} else {
|
||||
return findConfigs({ syncMode: SYNC_MODE_ON });
|
||||
}
|
||||
}
|
||||
|
||||
export async function getConfig(resourceGroupId) {
|
||||
const rawDocs = await _execDB(TYPE_CONFIG, 'find', { resourceGroupId });
|
||||
return rawDocs.length >= 1 ? _initConfig(rawDocs[0]) : null;
|
||||
}
|
||||
|
||||
export async function updateConfig(config, ...patches) {
|
||||
const doc = _initConfig(Object.assign(config, ...patches));
|
||||
await _execDB(TYPE_CONFIG, 'update', { _id: doc._id }, doc);
|
||||
return doc;
|
||||
}
|
||||
|
||||
export function removeConfig(config) {
|
||||
return _execDB(TYPE_CONFIG, 'remove', { _id: config._id });
|
||||
}
|
||||
|
||||
export async function insertConfig(config) {
|
||||
const doc = _initConfig(config);
|
||||
await _execDB(TYPE_CONFIG, 'insert', doc);
|
||||
return doc;
|
||||
}
|
||||
|
||||
function _initConfig(data) {
|
||||
return Object.assign(
|
||||
{
|
||||
_id: util.generateId('scf'),
|
||||
syncMode: SYNC_MODE_UNSET,
|
||||
resourceGroupId: null,
|
||||
},
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
export function initDB(config, forceReset) {
|
||||
if (!_database || forceReset) {
|
||||
const basePath = util.getDataDirectory();
|
||||
_database = {};
|
||||
|
||||
// NOTE: Do not EVER change this. EVER!
|
||||
const resourcePath = fsPath.join(basePath, 'sync/Resource.db');
|
||||
const configPath = fsPath.join(basePath, 'sync/Config.db');
|
||||
|
||||
// Fill in the defaults
|
||||
_database.Resource = new NeDB(
|
||||
Object.assign({ filename: resourcePath, autoload: true }, config),
|
||||
);
|
||||
|
||||
_database.Config = new NeDB(Object.assign({ filename: configPath, autoload: true }, config));
|
||||
|
||||
for (const key of Object.keys(_database)) {
|
||||
_database[key].persistence.setAutocompactionInterval(DB_PERSIST_INTERVAL);
|
||||
}
|
||||
|
||||
// Done
|
||||
console.log(`[sync] Initialize Sync DB at ${basePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ~~~~~~~ //
|
||||
// Helpers //
|
||||
// ~~~~~~~ //
|
||||
|
||||
let _database = null;
|
||||
|
||||
function _getDB(type, config = {}) {
|
||||
initDB(config);
|
||||
return _database[type];
|
||||
}
|
||||
|
||||
function _execDB(type, fnName, ...args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
_getDB(type)[fnName](...args, (err, data) => {
|
||||
err ? reject(err) : resolve(data);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,234 +0,0 @@
|
||||
// @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';
|
||||
import { clickLink } from '../../../common/misc';
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static _handleShowSyncBetaPrompt() {
|
||||
clickLink('https://support.insomnia.rest/article/67-version-control');
|
||||
}
|
||||
|
||||
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}
|
||||
|
||||
{syncMode === syncStorage.SYNC_MODE_OFF && [
|
||||
// NOTE: We can't use <React.Fragment> here because the nesting breaks
|
||||
// the <Dropdown> component's child detection
|
||||
<DropdownDivider key="divider" />,
|
||||
<DropdownItem key="beta" onClick={SyncLegacyDropdown._handleShowSyncBetaPrompt}>
|
||||
<i className="fa fa-star" />
|
||||
Try New Sync Beta
|
||||
</DropdownItem>,
|
||||
]}
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SyncLegacyDropdown;
|
||||
@@ -13,7 +13,6 @@ import { getAppName, getAppVersion } from '../../../common/constants';
|
||||
import { showAlert, showError, 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 LoginModal from '../modals/login-modal';
|
||||
import Tooltip from '../tooltip';
|
||||
import KeydownBinder from '../keydown-binder';
|
||||
@@ -26,7 +25,6 @@ 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';
|
||||
import type { WorkspaceAction } from '../../../plugins';
|
||||
@@ -38,7 +36,6 @@ import type { Environment } from '../../../models/environment';
|
||||
type Props = {
|
||||
activeEnvironment: Environment | null,
|
||||
activeWorkspace: Workspace,
|
||||
enableSyncBeta: boolean,
|
||||
handleSetActiveWorkspace: (id: string) => void,
|
||||
hotKeyRegistry: HotKeyRegistry,
|
||||
isLoading: boolean,
|
||||
@@ -197,7 +194,7 @@ class WorkspaceDropdown extends React.PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
static async _handleLogout() {
|
||||
await sync.logout();
|
||||
await session.logout();
|
||||
}
|
||||
|
||||
static _handleShowExport() {
|
||||
@@ -213,11 +210,7 @@ class WorkspaceDropdown extends React.PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
_handleShowShareSettings() {
|
||||
if (this.props.enableSyncBeta) {
|
||||
showModal(SyncShareModal);
|
||||
} else {
|
||||
showModal(WorkspaceShareSettingsModal);
|
||||
}
|
||||
showModal(SyncShareModal);
|
||||
}
|
||||
|
||||
_handleWorkspaceCreate() {
|
||||
@@ -259,7 +252,6 @@ class WorkspaceDropdown extends React.PureComponent<Props, State> {
|
||||
isLoading,
|
||||
hotKeyRegistry,
|
||||
handleSetActiveWorkspace,
|
||||
enableSyncBeta,
|
||||
...other
|
||||
} = this.props;
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import Modal from '../base/modal';
|
||||
import ModalBody from '../base/modal-body';
|
||||
import ModalHeader from '../base/modal-header';
|
||||
import ModalFooter from '../base/modal-footer';
|
||||
import * as sync from '../../../sync-legacy';
|
||||
import * as session from '../../../account/session';
|
||||
|
||||
@autobind
|
||||
@@ -42,10 +41,6 @@ class LoginModal extends PureComponent {
|
||||
|
||||
try {
|
||||
await session.login(email, password);
|
||||
|
||||
// Clear all existing sync data that might be there and enable sync
|
||||
await sync.resetLocalData();
|
||||
await sync.doInitialSync();
|
||||
this.hide();
|
||||
} catch (e) {
|
||||
this.setState({ error: e.message, loading: false });
|
||||
|
||||
@@ -5,8 +5,7 @@ import Link from '../base/link';
|
||||
import Modal from '../base/modal';
|
||||
import ModalBody from '../base/modal-body';
|
||||
import ModalHeader from '../base/modal-header';
|
||||
import * as sync from '../../../sync-legacy/index';
|
||||
import { getFirstName } from '../../../account/session';
|
||||
import { getFirstName, endTrial, logout } from '../../../account/session';
|
||||
|
||||
let hidePaymentNotificationUntilNextLaunch = false;
|
||||
|
||||
@@ -14,7 +13,8 @@ let hidePaymentNotificationUntilNextLaunch = false;
|
||||
class PaymentNotificationModal extends PureComponent {
|
||||
async _handleCancel() {
|
||||
try {
|
||||
await sync.cancelTrial();
|
||||
await endTrial();
|
||||
await logout();
|
||||
} catch (err) {
|
||||
// That's okay
|
||||
}
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import autobind from 'autobind-decorator';
|
||||
import Modal from '../base/modal';
|
||||
import ModalBody from '../base/modal-body';
|
||||
import ModalHeader from '../base/modal-header';
|
||||
import ModalFooter from '../base/modal-footer';
|
||||
import * as sync from '../../../sync-legacy';
|
||||
import {
|
||||
SYNC_MODE_OFF,
|
||||
SYNC_MODE_ON,
|
||||
SYNC_MODE_NEVER,
|
||||
SYNC_MODE_UNSET,
|
||||
} from '../../../sync-legacy/storage';
|
||||
import type { Workspace } from '../../../models/workspace';
|
||||
import HelpTooltip from '../help-tooltip';
|
||||
|
||||
type Props = {
|
||||
workspace: Workspace,
|
||||
};
|
||||
|
||||
type State = {
|
||||
syncMode: string,
|
||||
selectedSyncMode: string,
|
||||
syncDisableCookieJars: boolean,
|
||||
syncDisableClientCertificates: boolean,
|
||||
};
|
||||
|
||||
@autobind
|
||||
class SetupSyncModal extends React.PureComponent<Props, State> {
|
||||
modal: ?Modal;
|
||||
_onSelectSyncMode: ?(selectedSyncMode: string) => void;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
syncMode: SYNC_MODE_UNSET,
|
||||
selectedSyncMode: SYNC_MODE_ON,
|
||||
syncDisableCookieJars: false,
|
||||
syncDisableClientCertificates: false,
|
||||
};
|
||||
}
|
||||
|
||||
_setModalRef(n: ?Modal) {
|
||||
this.modal = n;
|
||||
}
|
||||
|
||||
_handleToggleSyncCertificates(e: SyntheticEvent<HTMLInputElement>) {
|
||||
this.setState({ syncDisableClientCertificates: !e.currentTarget.checked });
|
||||
}
|
||||
|
||||
_handleToggleSyncCookieJars(e: SyntheticEvent<HTMLInputElement>) {
|
||||
this.setState({ syncDisableCookieJars: !e.currentTarget.checked });
|
||||
}
|
||||
|
||||
async _handleDone() {
|
||||
const { workspace } = this.props;
|
||||
const { selectedSyncMode, syncDisableClientCertificates, syncDisableCookieJars } = this.state;
|
||||
|
||||
const resource = await sync.getOrCreateResourceForDoc(workspace);
|
||||
await sync.createOrUpdateConfig(resource.resourceGroupId, {
|
||||
syncMode: selectedSyncMode,
|
||||
syncDisableClientCertificates: !!syncDisableClientCertificates,
|
||||
syncDisableCookieJars: !!syncDisableCookieJars,
|
||||
});
|
||||
|
||||
this.hide();
|
||||
|
||||
this._onSelectSyncMode && this._onSelectSyncMode(selectedSyncMode);
|
||||
}
|
||||
|
||||
_handleSyncModeChange(e: SyntheticEvent<HTMLSelectElement>) {
|
||||
const selectedSyncMode = e.currentTarget.value;
|
||||
|
||||
this.setState({
|
||||
selectedSyncMode,
|
||||
});
|
||||
}
|
||||
|
||||
async show(options: { onSelectSyncMode: (syncMode: string) => void }) {
|
||||
const { workspace } = this.props;
|
||||
|
||||
const resource = await sync.getOrCreateResourceForDoc(workspace);
|
||||
const config = await sync.getOrCreateConfig(resource.resourceGroupId);
|
||||
const { syncMode, syncDisableCookieJars, syncDisableClientCertificates } = config;
|
||||
|
||||
// Set selected sync mode. If it's unset, default it to ON
|
||||
const selectedSyncMode = syncMode !== SYNC_MODE_UNSET ? syncMode : SYNC_MODE_ON;
|
||||
|
||||
this.setState({
|
||||
syncMode,
|
||||
selectedSyncMode,
|
||||
syncDisableCookieJars,
|
||||
syncDisableClientCertificates,
|
||||
});
|
||||
|
||||
this._onSelectSyncMode = options.onSelectSyncMode;
|
||||
|
||||
this.modal && this.modal.show();
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.modal && this.modal.hide();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { workspace } = this.props;
|
||||
const {
|
||||
syncMode,
|
||||
selectedSyncMode,
|
||||
syncDisableClientCertificates,
|
||||
syncDisableCookieJars,
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<Modal ref={this._setModalRef} noEscape>
|
||||
<ModalHeader>Workspace Sync Setup</ModalHeader>
|
||||
<ModalBody className="wide pad-left pad-right">
|
||||
{syncMode === SYNC_MODE_UNSET ? (
|
||||
<p className="notice info">
|
||||
You have not yet configured sync for your <strong>{workspace.name}</strong> workspace.
|
||||
</p>
|
||||
) : null}
|
||||
<br />
|
||||
<div className="form-control form-control--outlined">
|
||||
<label>
|
||||
Sync mode
|
||||
<HelpTooltip className="space-left">
|
||||
Control how and when data for this workspace is synced with the server
|
||||
</HelpTooltip>
|
||||
<select onChange={this._handleSyncModeChange} value={selectedSyncMode}>
|
||||
<option value={SYNC_MODE_ON}>Automatically sync changes</option>
|
||||
<option value={SYNC_MODE_OFF}>Manually sync changes</option>
|
||||
<option value={SYNC_MODE_NEVER}>Disable sync for this workspace</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<br />
|
||||
<label className="bold">
|
||||
Advanced Rules
|
||||
<HelpTooltip className="space-left">
|
||||
Customize sync for you or your team's needs by choosing which resources are synced for
|
||||
this workspace
|
||||
</HelpTooltip>
|
||||
</label>
|
||||
<div className="form-control form-control--thin">
|
||||
<label>
|
||||
Sync Cookie Jars
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!syncDisableCookieJars}
|
||||
onChange={this._handleToggleSyncCookieJars}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-control form-control--thin">
|
||||
<label>
|
||||
Sync SSL Client Certificates
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!syncDisableClientCertificates}
|
||||
onChange={this._handleToggleSyncCertificates}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<br />
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<div className="margin-left faint italic txt-sm tall">
|
||||
* This can be changed at any time
|
||||
</div>
|
||||
<button className="btn" onClick={this._handleDone}>
|
||||
Continue
|
||||
</button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SetupSyncModal;
|
||||
@@ -1,203 +0,0 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import autobind from 'autobind-decorator';
|
||||
import { Dropdown, DropdownButton, DropdownDivider, DropdownItem } from '../base/dropdown';
|
||||
import Link from '../base/link';
|
||||
import Modal from '../base/modal';
|
||||
import ModalBody from '../base/modal-body';
|
||||
import ModalHeader from '../base/modal-header';
|
||||
import ModalFooter from '../base/modal-footer';
|
||||
import * as sync from '../../../sync-legacy/index';
|
||||
import PromptButton from '../base/prompt-button';
|
||||
import { shareWithTeam, unshareWithAllTeams } from '../../../sync-legacy/network';
|
||||
import * as session from '../../../account/session';
|
||||
|
||||
@autobind
|
||||
class WorkspaceShareSettingsModal extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
static _handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
_handleClose() {
|
||||
this.hide();
|
||||
}
|
||||
|
||||
_setModalRef(n) {
|
||||
this.modal = n;
|
||||
}
|
||||
|
||||
async _handleUnshare() {
|
||||
if (!session.isLoggedIn()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { resourceGroup } = this.state;
|
||||
|
||||
this._resetState({ loading: true });
|
||||
|
||||
try {
|
||||
await unshareWithAllTeams(resourceGroup.id);
|
||||
await this._load();
|
||||
} catch (err) {
|
||||
console.warn('Failed to unshare workspace', err);
|
||||
this._resetState({ error: err.message, loading: false });
|
||||
}
|
||||
}
|
||||
|
||||
async _handleShareWithTeam(team) {
|
||||
const { resourceGroup } = this.state;
|
||||
this._resetState({ loading: true });
|
||||
|
||||
try {
|
||||
await shareWithTeam(resourceGroup.id, team.id);
|
||||
await this._load();
|
||||
} catch (err) {
|
||||
this._resetState({ error: err.message, loading: false });
|
||||
}
|
||||
}
|
||||
|
||||
async _load() {
|
||||
if (!session.isLoggedIn()) {
|
||||
this._resetState({});
|
||||
return;
|
||||
}
|
||||
|
||||
const { workspace } = this.props;
|
||||
const resource = await sync.getOrCreateResourceForDoc(workspace);
|
||||
|
||||
const teams = await session.listTeams();
|
||||
|
||||
try {
|
||||
const resourceGroup = await sync.fetchResourceGroup(resource.resourceGroupId, true);
|
||||
this.setState({ teams, resourceGroup, loading: false, error: '' });
|
||||
} catch (err) {
|
||||
console.warn('Failed to fetch ResourceGroup', err);
|
||||
this.setState({
|
||||
error: 'No sync info found. Please try again.',
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_resetState(patch = {}) {
|
||||
this.setState(
|
||||
Object.assign(
|
||||
{
|
||||
teams: [],
|
||||
resourceGroup: null,
|
||||
error: '',
|
||||
loading: false,
|
||||
},
|
||||
patch,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async show() {
|
||||
this._resetState();
|
||||
this.modal.show();
|
||||
|
||||
// This takes a while, so do it after show()
|
||||
await this._load();
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.modal.hide();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
UNSAFE_componentWillMount() {
|
||||
this._resetState();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { teams, resourceGroup, error, loading } = this.state;
|
||||
const { workspace } = this.props;
|
||||
return (
|
||||
<form onSubmit={WorkspaceShareSettingsModal._handleSubmit}>
|
||||
<Modal ref={this._setModalRef}>
|
||||
<ModalHeader key="header">Share Workspace</ModalHeader>
|
||||
<ModalBody key="body" className="pad text-center" noScroll>
|
||||
<p>
|
||||
Share <strong>{workspace.name}</strong> to automatically sync your API workspace with
|
||||
your team members.
|
||||
</p>
|
||||
<div className="form-control pad">
|
||||
{error ? <div className="danger">Oops: {error}</div> : null}
|
||||
<Dropdown outline>
|
||||
<DropdownDivider>Teams</DropdownDivider>
|
||||
{!loading ? (
|
||||
resourceGroup && resourceGroup.teamId ? (
|
||||
<DropdownButton className="btn btn--clicky">
|
||||
<i className="fa fa-users" /> Shared with{' '}
|
||||
<strong>{resourceGroup.teamName}</strong> <i className="fa fa-caret-down" />
|
||||
</DropdownButton>
|
||||
) : (
|
||||
<DropdownButton className="btn btn--clicky">
|
||||
<i className="fa fa-lock" /> Private <i className="fa fa-caret-down" />
|
||||
</DropdownButton>
|
||||
)
|
||||
) : (
|
||||
<DropdownButton className="btn btn--clicky">
|
||||
<i className="fa fa-spin fa-refresh" /> Loading...{' '}
|
||||
<i className="fa fa-caret-down" />
|
||||
</DropdownButton>
|
||||
)}
|
||||
{teams.map(team => (
|
||||
<DropdownItem key={team.id} value={team} onClick={this._handleShareWithTeam}>
|
||||
<i className="fa fa-users" /> Share with <strong>{team.name}</strong>
|
||||
</DropdownItem>
|
||||
))}
|
||||
{teams.length === 0 && (
|
||||
<DropdownItem disabled onClick={this._handleShareWithTeam}>
|
||||
<i className="fa fa-warning" /> You have no teams
|
||||
</DropdownItem>
|
||||
)}
|
||||
<DropdownDivider>Other</DropdownDivider>
|
||||
<DropdownItem
|
||||
addIcon
|
||||
buttonClass={PromptButton}
|
||||
confirmMessage="Really make private?"
|
||||
onClick={this._handleUnshare}>
|
||||
<i className="fa fa-lock" /> Private
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
|
||||
{session.isLoggedIn() ? (
|
||||
<Link
|
||||
button
|
||||
className="btn btn--super-compact inline-block"
|
||||
href="https://insomnia.rest/app/teams/">
|
||||
Manage Teams
|
||||
</Link>
|
||||
) : (
|
||||
<Link
|
||||
button
|
||||
className="btn btn--super-compact inline-block"
|
||||
href="https://insomnia.rest/teams/">
|
||||
Manage Teams
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter key="footer">
|
||||
<button type="button" className="btn" onClick={this._handleClose}>
|
||||
Done
|
||||
</button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
WorkspaceShareSettingsModal.propTypes = {
|
||||
workspace: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default WorkspaceShareSettingsModal;
|
||||
@@ -123,7 +123,6 @@ class PageLayout extends React.PureComponent<Props, State> {
|
||||
ref={handleSetSidebarRef}
|
||||
activeEnvironment={activeEnvironment}
|
||||
activeGitRepository={activeGitRepository}
|
||||
enableSyncBeta={settings.enableSyncBeta}
|
||||
environmentHighlightColorStyle={settings.environmentHighlightColorStyle}
|
||||
handleInitializeEntities={handleInitializeEntities}
|
||||
handleSetActiveEnvironment={handleSetActiveEnvironment}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import autobind from 'autobind-decorator';
|
||||
import * as sync from '../../../sync-legacy/index';
|
||||
import Link from '../base/link';
|
||||
import LoginModal from '../modals/login-modal';
|
||||
import { hideAllModals, showModal } from '../modals/index';
|
||||
@@ -81,7 +80,7 @@ class Account extends React.PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
async _handleLogout() {
|
||||
await sync.logout();
|
||||
await session.logout();
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
} from '../../../common/constants';
|
||||
import type { Settings } from '../../../models/settings';
|
||||
import { setFont } from '../../../plugins/misc';
|
||||
import * as session from '../../../account/session';
|
||||
import Tooltip from '../tooltip';
|
||||
import CheckForUpdatesButton from '../check-for-updates-button';
|
||||
import { initNewOAuthSession } from '../../../network/o-auth-2/misc';
|
||||
@@ -519,13 +518,6 @@ class General extends React.PureComponent<Props, State> {
|
||||
as request data, names, etc.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{session.isLoggedIn() && (
|
||||
<React.Fragment>
|
||||
<hr />
|
||||
{this.renderBooleanSetting('Enable version control beta', 'enableSyncBeta', '', true)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,14 +7,12 @@ import type { Environment } from '../../../models/environment';
|
||||
import classnames from 'classnames';
|
||||
import { COLLAPSE_SIDEBAR_REMS, SIDEBAR_SKINNY_REMS } from '../../../common/constants';
|
||||
import SyncDropdown from '../dropdowns/sync-dropdown';
|
||||
import SyncLegacyDropdown from '../dropdowns/sync-legacy-dropdown';
|
||||
import type { StatusCandidate } from '../../../sync/types';
|
||||
import { isLoggedIn } from '../../../account/session';
|
||||
|
||||
type Props = {|
|
||||
activeEnvironment: Environment | null,
|
||||
children: React.Node,
|
||||
enableSyncBeta: boolean,
|
||||
environmentHighlightColorStyle: string,
|
||||
handleSetActiveEnvironment: Function,
|
||||
handleSetActiveWorkspace: Function,
|
||||
@@ -35,7 +33,6 @@ class Sidebar extends React.PureComponent<Props> {
|
||||
const {
|
||||
activeEnvironment,
|
||||
children,
|
||||
enableSyncBeta,
|
||||
environmentHighlightColorStyle,
|
||||
hidden,
|
||||
syncItems,
|
||||
@@ -61,7 +58,7 @@ class Sidebar extends React.PureComponent<Props> {
|
||||
}}>
|
||||
{children}
|
||||
|
||||
{enableSyncBeta && vcs && isLoggedIn() && (
|
||||
{vcs && isLoggedIn() && (
|
||||
<SyncDropdown
|
||||
className="sidebar__footer"
|
||||
workspace={workspace}
|
||||
@@ -69,14 +66,6 @@ class Sidebar extends React.PureComponent<Props> {
|
||||
syncItems={syncItems}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!enableSyncBeta && (
|
||||
<SyncLegacyDropdown
|
||||
className="sidebar__footer"
|
||||
key={workspace._id}
|
||||
workspace={workspace}
|
||||
/>
|
||||
)}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -142,7 +142,6 @@ class WrapperDebug extends React.PureComponent<Props> {
|
||||
unseenWorkspaces={unseenWorkspaces}
|
||||
hotKeyRegistry={settings.hotKeyRegistry}
|
||||
handleSetActiveWorkspace={handleSetActiveWorkspace}
|
||||
enableSyncBeta={settings.enableSyncBeta}
|
||||
isLoading={isLoading}
|
||||
vcs={vcs}
|
||||
/>
|
||||
|
||||
@@ -35,7 +35,6 @@ import RequestSwitcherModal from './modals/request-switcher-modal';
|
||||
import SettingsModal from './modals/settings-modal';
|
||||
import FilterHelpModal from './modals/filter-help-modal';
|
||||
import RequestSettingsModal from './modals/request-settings-modal';
|
||||
import SetupSyncModal from './modals/setup-sync-modal';
|
||||
import SyncStagingModal from './modals/sync-staging-modal';
|
||||
import GitRepositorySettingsModal from './modals/git-repository-settings-modal';
|
||||
import GitStagingModal from './modals/git-staging-modal';
|
||||
@@ -49,7 +48,6 @@ import SyncDeleteModal from './modals/sync-delete-modal';
|
||||
import RequestRenderErrorModal from './modals/request-render-error-modal';
|
||||
import WorkspaceEnvironmentsEditModal from './modals/workspace-environments-edit-modal';
|
||||
import WorkspaceSettingsModal from './modals/workspace-settings-modal';
|
||||
import WorkspaceShareSettingsModal from './modals/workspace-share-settings-modal';
|
||||
import CodePromptModal from './modals/code-prompt-modal';
|
||||
import * as db from '../../common/database';
|
||||
import * as models from '../../models/index';
|
||||
@@ -672,8 +670,6 @@ class Wrapper extends React.PureComponent<WrapperProps, State> {
|
||||
isVariableUncovered={isVariableUncovered}
|
||||
/>
|
||||
|
||||
<WorkspaceShareSettingsModal ref={registerModal} workspace={activeWorkspace} />
|
||||
|
||||
<GenerateCodeModal
|
||||
ref={registerModal}
|
||||
environmentId={activeEnvironment ? activeEnvironment._id : 'n/a'}
|
||||
@@ -720,8 +716,6 @@ class Wrapper extends React.PureComponent<WrapperProps, State> {
|
||||
isVariableUncovered={isVariableUncovered}
|
||||
/>
|
||||
|
||||
<SetupSyncModal ref={registerModal} workspace={activeWorkspace} />
|
||||
|
||||
{gitVCS && (
|
||||
<React.Fragment>
|
||||
<GitStagingModal ref={registerModal} workspace={activeWorkspace} vcs={gitVCS} />
|
||||
|
||||
@@ -5,16 +5,14 @@ import App from './containers/app';
|
||||
import * as models from '../models';
|
||||
import * as db from '../common/database';
|
||||
import { init as initStore } from './redux/modules';
|
||||
import * as legacySync from '../sync-legacy';
|
||||
import { init as initPlugins } from '../plugins';
|
||||
import './css/index.less';
|
||||
import { getAppId, getAppLongName, isDevelopment } from '../common/constants';
|
||||
import { getAppLongName, isDevelopment } from '../common/constants';
|
||||
import { setFont, setTheme } from '../plugins/misc';
|
||||
import { AppContainer } from 'react-hot-loader';
|
||||
import { DragDropContext } from 'react-dnd';
|
||||
import DNDBackend from './dnd-backend';
|
||||
import { trackEvent } from '../common/analytics';
|
||||
import { APP_ID_DESIGNER, APP_ID_INSOMNIA } from '../../config';
|
||||
import * as styledComponents from 'styled-components';
|
||||
import { initNewOAuthSession } from '../network/o-auth-2/misc';
|
||||
import { initializeLogging } from '../common/log';
|
||||
@@ -60,22 +58,6 @@ document.title = getAppLongName();
|
||||
// render(App);
|
||||
// });
|
||||
}
|
||||
|
||||
const appId = getAppId();
|
||||
|
||||
// Legacy sync not part of Designer
|
||||
if (appId === APP_ID_DESIGNER) {
|
||||
legacySync.disableForSession();
|
||||
} else if (appId === APP_ID_INSOMNIA) {
|
||||
// Do things that can wait
|
||||
const { enableSyncBeta } = await models.settings.getOrCreate();
|
||||
if (enableSyncBeta) {
|
||||
console.log('[app] Enabling sync beta');
|
||||
legacySync.disableForSession();
|
||||
} else {
|
||||
process.nextTick(legacySync.init);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
// Export some useful things for dev
|
||||
|
||||
Reference in New Issue
Block a user