Skip to content
This repository has been archived by the owner on Apr 4, 2024. It is now read-only.

Commit

Permalink
Merge pull request #301 from complexdatacollective/feature/revamp-ser…
Browse files Browse the repository at this point in the history
…ver-panel

Revamp server panel
  • Loading branch information
jthrilly authored Nov 24, 2020
2 parents 6f05ff2 + 7991741 commit 9421c03
Show file tree
Hide file tree
Showing 140 changed files with 8,745 additions and 2,254 deletions.
3 changes: 2 additions & 1 deletion config/jest/setupTestEnv.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,8 @@ const mockProtocol = {
name: 'MyProtocol',
createdAt: new Date(),
updatedAt: new Date(),
schemaVersion: '1',
lastModified: new Date(),
schemaVersion: 1,
version: '2.0',
};

Expand Down
2 changes: 1 addition & 1 deletion jsdoc.conf.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"source": {
"includePattern": ".+\\.js$",
"excludePattern": "__tests__|utils\/network-query"
"excludePattern": "__tests__|utils\/network-query|utils\/network-exporters"
},
"plugins": ["plugins/markdown"]
}
411 changes: 290 additions & 121 deletions package-lock.json

Large diffs are not rendered by default.

13 changes: 8 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@
"@babel/plugin-transform-runtime": "^7.11.0",
"@babel/preset-env": "^7.11.0",
"@babel/preset-react": "^7.10.4",
"@codaco/ui": "^3.8.2",
"autoprefixer": "^9.8.6",
"babel-eslint": "^9.0.0",
"babel-jest": "^25.5.1",
Expand Down Expand Up @@ -76,16 +75,16 @@
"eventemitter3": "^4.0.7",
"faker": "^4.1.0",
"file-loader": "^1.1.11",
"framer-motion": "^2.7.7",
"framer-motion": "^2.9.1",
"fs-extra": "^6.0.1",
"html-webpack-plugin": "^4.3.0",
"icon-gen": "^1.2.1",
"istanbul-lib-coverage": "^1.2.0",
"jssha": "^3.1.0",
"jest": "^25.5.4",
"jest-fetch-mock": "^1.6.5",
"jsdoc": "^3.6.5",
"json-loader": "^0.5.7",
"jssha": "^3.1.0",
"mini-css-extract-plugin": "^0.8.0",
"node-sass": "^4.14.1",
"postcss-loader": "^2.1.6",
Expand Down Expand Up @@ -113,13 +112,14 @@
},
"dependencies": {
"@babel/runtime": "^7.11.2",
"@codaco/ui": "^3.9.1",
"animejs": "^2.2.0",
"archiver": "^4.0.1",
"async": "^3.2.0",
"big-integer": "^1.6.43",
"classnames": "^2.2.6",
"compare-versions": "^3.6.0",
"detect-port": "^1.2.3",
"framer-motion": "^2.7.7",
"jszip": "^3.4.0",
"libsodium-wrappers": "^0.7.8",
"lodash": "^4.17.20",
Expand All @@ -130,9 +130,11 @@
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-error-overlay": "^5.0.0-next.3e165448",
"react-identicons": "^1.2.4",
"react-router-dom": "^4.3.1",
"react-sortable-hoc": "^0.6.8",
"react-test-renderer": "^16.13.1",
"react-tooltip": "^4.2.10",
"react-transition-group": "^2.5.0",
"recharts": "^1.0.1",
"recompose": "^0.26.0",
Expand Down Expand Up @@ -248,7 +250,8 @@
],
"moduleNameMapper": {
"^react-native$": "react-native-web"
}
},
"timers": "fake"
},
"eslintConfig": {
"extends": "react-app"
Expand Down
14 changes: 6 additions & 8 deletions scripts/db-size.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
const fs = require('fs');
const uuidv4 = require('uuid/v4');
const SessionDB = require('../src/main/data-managers/SessionDB');
const { caseProperty, remoteProtocolProperty, entityPrimaryKeyProperty, entityAttributesProperty } = require('../src/main/utils/network-exporters/src/utils/reservedAttributes');

const resetDb = !process.env.SKIP_RESET;

Expand All @@ -31,9 +32,6 @@ const printResult = (taskDesc, result, hrtimeDiff) => {
console.log(`[${(ns / nsPerSec).toLocaleString()}s]`, taskDesc, result);
}

const nodePrimaryKeyProperty = '_uid';
const nodeAttributesProperty = 'attributes';

const dbFile = 'perf-test-sessions.db';
let db;

Expand Down Expand Up @@ -74,10 +72,10 @@ function buildMockData({ sessionCount = SessionCount, edgesPerSession = EdgesPer
* Note that variable IDs are already transposed to names.
*/
const mockNode = {
[nodePrimaryKeyProperty]: 'person_3',
[entityPrimaryKeyProperty]: 'person_3',
type: 'person',
itemType: 'NEW_NODE',
[nodeAttributesProperty]: {
[entityAttributesProperty]: {
name: 'Carlito',
nickname: 'Carl',
age: '25',
Expand All @@ -101,8 +99,8 @@ function buildMockData({ sessionCount = SessionCount, edgesPerSession = EdgesPer
if (edges.length) { edges[edges.length - 1].from = 13; }

if (useRealIds) {
const pickNodeUid = () => nodes[~~(Math.random() * nodes.length)][nodePrimaryKeyProperty];
nodes = nodes.map(node => ({ ...node, [nodePrimaryKeyProperty]: uuidv4(), age: ~~(Math.random() * 80) + 20 }))
const pickNodeUid = () => nodes[~~(Math.random() * nodes.length)][entityPrimaryKeyProperty];
nodes = nodes.map(node => ({ ...node, [entityPrimaryKeyProperty]: uuidv4(), age: ~~(Math.random() * 80) + 20 }))
edges = edges.map(edge => ({ ...edge, from: pickNodeUid(), to: pickNodeUid() }))

if (includeEgo) {
Expand All @@ -113,7 +111,7 @@ function buildMockData({ sessionCount = SessionCount, edgesPerSession = EdgesPer
return {
nodes,
edges,
sessionVariables: { _caseID: `a_${index++}`, _remoteProtocolID: "629aa7b8a90c8ca577ae8c6b3e245ba1e0f1fad99035d6cddd19265186e375cf" },
sessionVariables: { [caseProperty]: `a_${index++}`, [remoteProtocolProperty]: "629aa7b8a90c8ca577ae8c6b3e245ba1e0f1fad99035d6cddd19265186e375cf" },
};
}

Expand Down
2 changes: 1 addition & 1 deletion src/main/MainApp.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const createApp = () => {
guiProxy.setMainWindow(mainWindow);

const openMainWindow = () => mainWindow.open();
const reloadHomeScreen = () => mainWindow.open('/overview');
const reloadHomeScreen = () => mainWindow.open('/');

const regenerateCertificates = () => {
dialog.showMessageBox(mainWindow.window, {
Expand Down
10 changes: 6 additions & 4 deletions src/main/components/mainWindow.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ const { BrowserWindow } = require('electron');

const loadDevToolsExtensions = require('./loadDevToolsExtensions');

const DefaultHomeRoute = '/overview';
const DefaultHomeRoute = '/';

/**
* Convert an app route to a full URL using hash-based routing.
* If the route is already a URL, return it unchanged
* @private
* @param {string} path or URL (Example: '/overview')
* @return {string} full app URL (Example: 'file:///www/index.html#/overview')
* @param {string} path or URL (Example: '/')
* @return {string} full app URL (Example: 'file:///www/index.html#/')
*/
const getAppUrl = (route) => {
if (url.parse(route).protocol) {
Expand Down Expand Up @@ -45,6 +45,8 @@ class MainWindow {
height: 900,
title: 'Network Canvas Server',
width: 1440,
minWidth: 1280,
minHeight: 800,
webPreferences: {
nodeIntegration: true,
},
Expand Down Expand Up @@ -86,7 +88,7 @@ class MainWindow {
* Open the window with the provided route.
* If no route is given and a URL is already loaded, then maintain the current URL
* without reloading.
* @param {string?} route A route ('/overview') or URL to open
* @param {string?} route A route ('/') or URL to open
*/
open(route) {
this.create();
Expand Down
3 changes: 2 additions & 1 deletion src/main/data-managers/PairingRequestDB.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ const dbConfig = {
inMemoryOnly: true,
};

const DeviceRequestTTLSeconds = 5 * 60;
// Used for various API request timeouts.
const DeviceRequestTTLSeconds = 5 * 60; // 5 minutes.

class PairingRequestDB extends DatabaseAdapter {
constructor(dbName, additionalConfig = {}) {
Expand Down
21 changes: 11 additions & 10 deletions src/main/data-managers/ProtocolManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const SessionDB = require('./SessionDB');
const { ErrorMessages, RequestError } = require('../errors/RequestError');
const { readFile, rename, tryUnlink } = require('../utils/promised-fs');
const { validateGraphML, convertGraphML } = require('../utils/importGraphML');
const { caseProperty, protocolName: protocolNameProperty, remoteProtocolProperty, codebookHashProperty } = require('../utils/network-exporters/src/utils/reservedAttributes');
const { hexDigest } = require('../utils/sha256');
const { sendToGui } = require('../guiProxy');
const { get, debounce } = require('lodash');
Expand Down Expand Up @@ -637,10 +638,10 @@ class ProtocolManager {
// to all be based on the same protocol
const graphml = xmlDoc.getElementsByTagName('graphml');
const graphs = graphml[0].getElementsByTagName('graph');
const protocolId = graphs && graphs[0].getAttribute('nc:remoteProtocolID');
const protocolName = graphs && graphs[0].getAttribute('nc:protocolName');
const caseId = graphs && graphs[0].getAttribute('nc:caseId');
const codebookHash = graphs && graphs[0].getAttribute('nc:codebookHash');
const protocolId = graphs && graphs[0].getAttribute(`nc:${remoteProtocolProperty}`);
const protocolName = graphs && graphs[0].getAttribute(`nc:${protocolNameProperty}`);
const caseId = graphs && graphs[0].getAttribute(`nc:${caseProperty}`);
const codebookHash = graphs && graphs[0].getAttribute(`nc:${codebookHashProperty}`);

return this.getProtocol(protocolId)
.then((protocol) => {
Expand Down Expand Up @@ -692,18 +693,18 @@ class ProtocolManager {
return this.getProtocol(protocolId)
.then((protocol) => {
if (!protocol) {
const protocolName = get(session, 'data.sessionVariables.protocolName', 'Unknown Protocol');
const caseID = get(session, 'data.sessionVariables.caseId', null);
const protocolName = get(session, `data.sessionVariables.${protocolNameProperty}`, 'Unknown Protocol');
const caseID = get(session, `data.sessionVariables.${caseProperty}`, null);

return Promise.reject(constructErrorObject(`The protocol ("${protocolName}") used by this session has not been imported into Server. Import it first, and try again.`, caseID));
}

const ourCodebookHash = objectHash(protocol.codebook);
const incomingCodebookHash = get(session, 'data.sessionVariables.codebookHash', null);
const incomingCodebookHash = get(session, `data.sessionVariables.${codebookHashProperty}`, null);

if (ourCodebookHash !== incomingCodebookHash) {
const protocolName = get(session, 'data.sessionVariables.protocolName', 'Unknown Protocol');
const caseID = get(session, 'data.sessionVariables.caseId', null);
const protocolName = get(session, `data.sessionVariables.${protocolNameProperty}`, 'Unknown Protocol');
const caseID = get(session, `data.sessionVariables.${caseProperty}`, null);

return Promise.reject(constructErrorObject(`The version of the protocol ("${protocolName}") used to create this session does not match the version installed in Server. Ensure you have matching versions of your protocols installed in Interviewer and Server, and try again.`, caseID));
}
Expand All @@ -721,7 +722,7 @@ class ProtocolManager {
if (insertErr.errorType === 'uniqueViolated') {
return Promise.reject(constructErrorObject(
ErrorMessages.SessionAlreadyExists,
get(session, 'data.sessionVariables.caseId', null),
get(session, `data.sessionVariables.${caseProperty}`, null),
));
}

Expand Down
6 changes: 3 additions & 3 deletions src/main/data-managers/Reportable.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable no-param-reassign */ // disabled for reducers
const attributesProperty = require('../utils/network-query/nodeAttributesProperty');
const { leastRecentlyCreated, resolveOrReject } = require('../utils/db');
const { entityAttributesProperty } = require('../utils/network-exporters/src/utils/reservedAttributes');

// This is a dummy field used for projections when counting network members.
// When we don't need to know anything about members other than size, returning
Expand Down Expand Up @@ -217,7 +217,7 @@ const Reportable = Super => class extends Super {
acc[variableName] = acc[variableName] || {};

const optionValue =
entity[attributesProperty] && entity[attributesProperty][variableName];
entity[entityAttributesProperty] && entity[entityAttributesProperty][variableName];

if (optionValue !== undefined) {
// Categorical values are expressed as arrays of multiple options
Expand All @@ -235,7 +235,7 @@ const Reportable = Super => class extends Super {
acc[entity.type][variableName] = acc[entity.type][variableName] || {};

const optionValue =
entity[attributesProperty] && entity[attributesProperty][variableName];
entity[entityAttributesProperty] && entity[entityAttributesProperty][variableName];

if (optionValue !== undefined) {
// Categorical values are expressed as arrays of multiple options
Expand Down
3 changes: 2 additions & 1 deletion src/main/data-managers/SessionDB.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const DatabaseAdapter = require('./DatabaseAdapter');
const Reportable = require('./Reportable');
const { ErrorMessages, RequestError } = require('../errors/RequestError');
const { mostRecentlyCreated, resolveOrReject } = require('../utils/db');
const { caseProperty } = require('../utils/network-exporters/src/utils/reservedAttributes');

// The ID & data properties documented for the API request
const sessionUidField = 'uuid';
Expand Down Expand Up @@ -60,7 +61,7 @@ class SessionDB extends Reportable(DatabaseAdapter) {
findAll(protocolId, limit = null, projection = { updatedAt: 1, createdAt: 1, 'data.sessionVariables': 1 }, sort = mostRecentlyCreated, filterValue = '') {
return new Promise((resolve, reject) => {
const exp = new RegExp(filterValue);
let cursor = this.db.find({ protocolId, $or: [{ 'data.sessionVariables._caseID': exp }, { _id: exp }] }, projection || undefined);
let cursor = this.db.find({ protocolId, $or: [{ [`data.sessionVariables.${caseProperty}`]: exp }, { _id: exp }] }, projection || undefined);
cursor = cursor.sort(sort);
if (limit) { cursor = cursor.limit(limit); }
cursor.exec((err, docs) => {
Expand Down
44 changes: 21 additions & 23 deletions src/main/server/AdminService.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
const restify = require('restify');
const logger = require('electron-log');
const uuid = require('uuid');
const { BrowserWindow, ipcMain } = require('electron');
const corsMiddleware = require('restify-cors-middleware');
const detectPort = require('detect-port');
Expand Down Expand Up @@ -319,54 +318,53 @@ class AdminService {
req.setTimeout(0);
res.setTimeout(0);

const id = uuid();
const sender = BrowserWindow.getFocusedWindow();
const sender = BrowserWindow.getAllWindows()[0];

this.protocolManager.getProtocol(req.params.protocolId)
.then(protocol =>
this.exportManager.exportSessions(protocol, req.body),
)
.then(({
exportSessions,
fileExportManager,
exportSessions, // Export promise decorated with abord method
fileExportManager, // Instance of FileExportManager for event binding
}) => {
const reportUpdate = throttle((data) => {
// Don't send updates to the log, there are too many of them
logger.debug('update', data);
sender.webContents.send('EXPORT/UPDATE', { ...data, id });
}, 50);
sender.webContents.send('EXPORT/UPDATE', { ...data, id: req.id() });
}, 1000);

fileExportManager.on('begin', (data) => {
logger.log('begin', { data });
sender.webContents.send('EXPORT/BEGIN', { ...data, id });
sender.webContents.send('EXPORT/BEGIN', { ...data, id: req.id() });
});

fileExportManager.on('update', reportUpdate);

fileExportManager.on('error', (err) => {
logger.error('non-fatal error in export', err);
sender.webContents.send('EXPORT/ERROR', { error: err, id });
sender.webContents.send('EXPORT/ERROR', { error: err, id: req.id() });
});

fileExportManager.on('finished', (data) => {
logger.log('finished', data);
sender.webContents.send('EXPORT/FINISHED', { ...data, id });
sender.webContents.send('EXPORT/FINISHED', { ...data, id: req.id() });
});

fileExportManager.on('cancelled', (data) => {
logger.log('cancelled', data);
sender.webContents.send('EXPORT/CANCELLED', { ...data, id });
sender.webContents.send('EXPORT/CANCELLED', { ...data, id: req.id() });
});

const exportRequest = exportSessions();

abortRequest = exportRequest.abort;

ipcMain.on('EXPORT/ABORT', (_, abortId) => {
logger.warn('abort export');
if (abortId !== id) { return; }
abortRequest();
});
const exportRequest = exportSessions()
.then(({ run, abort }) => {
ipcMain.on('EXPORT/ABORT', (_, abortId) => {
if (abortId !== req.id()) {
logger.warn('Attempted to abort exportSessions() but abort ID was incorrect! Ignoring. Looking for', req.id(), 'was sent', abortId);
return;
}
logger.log('Aborting exportSessions().');
abort();
});
return run();
});

return exportRequest;
})
Expand Down
1 change: 1 addition & 0 deletions src/main/server/__tests__/AdminService-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ jest.mock('../devices/PairingRequestService');
jest.mock('../../data-managers/ExportManager', () => class {
exportSessions = jest.fn().mockResolvedValue({
exportSessions: jest.fn(() => Promise.resolve({
run: jest.fn(() => Promise.resolve()),
abort: jest.fn(),
})),
fileExportManager: {
Expand Down
Loading

0 comments on commit 9421c03

Please sign in to comment.