diff --git a/dashboard/.gitignore b/dashboard/.gitignore index 640dc4b..881a2c3 100644 --- a/dashboard/.gitignore +++ b/dashboard/.gitignore @@ -3,3 +3,5 @@ node_modules/ # Prevent test coverage reports from being added .coverage .nyc_output + +/lib/data diff --git a/dashboard/bin/index.js b/dashboard/bin/index.js index b0839ac..131842f 100644 --- a/dashboard/bin/index.js +++ b/dashboard/bin/index.js @@ -19,7 +19,7 @@ program ) .option('-d, --daemonize', 'Run the server as a daemon.') .option('-t, --test', 'Run the CLI without any actions for unit tests.') - .action(() => { + .action(async () => { if (program.opts().test) { return; } @@ -31,7 +31,7 @@ program return; } - Server.init(); + await Server.init(); }); // Run this script if this is a direct stdin. diff --git a/dashboard/lib/controllers/batchQueue/index.js b/dashboard/lib/controllers/batchQueue/index.js new file mode 100644 index 0000000..e099357 --- /dev/null +++ b/dashboard/lib/controllers/batchQueue/index.js @@ -0,0 +1,38 @@ +class BatchQueue { + constructor(queueProcessor) { + this.queue = []; + this.processing = false; + this.queueProcessor = queueProcessor; + } + + add(data) { + this.queue.push(data); + this.processQueue(); + } + + async processQueue() { + if (this.queue.length == 0) { + this.processing = false; + return; + } + + if (this.processing) return; + + this.processing = true; + const currentQueue = this.queue; + this.queue = []; + + await currentQueue.reduce(async (prevPromise, item, index) => { + await prevPromise; + await this.queueProcessor(item); + + // at last, mark processing as complete so that new batch is saved + if (index == currentQueue.length - 1) { + this.processing = false; + this.processQueue(); + } + }, Promise.resolve()); + } +} + +module.exports = BatchQueue; diff --git a/dashboard/lib/controllers/run.js b/dashboard/lib/controllers/run.js index 7e81bc2..677d221 100644 --- a/dashboard/lib/controllers/run.js +++ b/dashboard/lib/controllers/run.js @@ -1,83 +1,101 @@ const Run = require('../models/run'); -const db = require('../store'); +const Event = require('../models/event'); +const RunStat = require('../models/runStat'); +const BatchQueue = require('./batchQueue'); + +// queues for batching stats and events +let statsBatch, eventsBatch; const api = { insert: async (data) => { - const runs = await db.getTable('runs'); - await runs.insert(data.id, new Run(data)); - }, - - findOne: async (id) => { - const runs = await db.getTable('runs'); - - if (!id) throw new TypeError('Invalid id'); - return runs.findOne(id); - }, + if (!data.id) throw new TypeError('Invalid run data'); - find: async () => { - const runs = await db.getTable('runs'); - return runs.find(); - }, + const runDocument = Run.create(data); + const run = await runDocument.save(); - clear: async () => { - const runs = await db.getTable('runs'); - return runs.clear(); + if (!run) + throw new Error('Unable to save run data. Incoherent schema.'); }, pause: async (id) => { if (!id) throw new TypeError('Invalid id'); - const run = await api.findOne(id); + const run = await Run.findOne({ _id: id }); if (!run) throw new Error('Run not found.'); - run.setPaused(); + + run.status = 'paused'; + await run.save(); }, resume: async (id) => { if (!id) throw new TypeError('Invalid id'); - const run = await api.findOne(id); + const run = await Run.findOne({ _id: id }); if (!run) throw new Error('Run not found.'); - run.setActive(); + + run.status = 'active'; + await run.save(); }, abort: async (id) => { if (!id) throw new TypeError('Invalid id'); - const run = await api.findOne(id); + const run = await Run.findOne({ _id: id }); if (!run) throw new Error('Run not found.'); - run.setAborted(); + + run.status = 'aborted'; + run.endTime = Date.now(); + await run.save(); }, done: async (id) => { if (!id) throw new TypeError('Invalid id'); - const run = await api.findOne(id); + const run = await Run.findOne({ _id: id }); if (!run) throw new Error('Run not found.'); - run.setFinished(); + + if (run.status === 'aborted') return; + + run.status = 'finished'; + run.endTime = Date.now(); + await run.save(); }, interrupt: async (id) => { if (!id) throw new TypeError('Invalid id'); - const run = await api.findOne(id); + const run = await Run.findOne({ _id: id }); if (!run) throw new Error('Run not found.'); - run.setInterrupted(); + + run.status = 'interrupted'; + await run.save(); }, - addEvent: async (data) => { - if (!data.id) throw new TypeError('Invalid id'); - const run = await api.findOne(data.id); + saveEvent: async (data) => { + if (!data.runId) throw new TypeError('Invalid parent id'); - if (!run) throw new Error('Run not found.'); - run.addEvent(data); + const event = Event.create(data); + if (!event) throw new Error('Invalid event type'); + await event.save(); }, - addStats: async (data) => { - if (!data.id) throw new TypeError('Invalid id'); - const run = await api.findOne(data.id); + saveStats: async (data) => { + if (!data.runId) throw new TypeError('Invalid parent id'); - if (!run) throw new Error('Run not found.'); - run.addRunStats(data); + const stat = RunStat.create(data); + if (!stat) throw new Error('Invalid stat type'); + + await stat.save(); + }, + + addEvent: (data) => { + eventsBatch = eventsBatch || new BatchQueue(api.saveEvent); + eventsBatch.add(data); + }, + + addStats: (data) => { + statsBatch = statsBatch || new BatchQueue(api.saveStats); + statsBatch.add(data); }, }; diff --git a/dashboard/lib/models/event.js b/dashboard/lib/models/event.js index 07a5461..cb913bc 100644 --- a/dashboard/lib/models/event.js +++ b/dashboard/lib/models/event.js @@ -1,14 +1,15 @@ -class Event { - constructor(data) { - this._init(data); - } +const Document = require('camo').Document; + +class Event extends Document { + constructor() { + super(); - _init(data) { - this.type = data.type; - this.parentId = data.id; - this.time = Date.now(); - this.args = data.args; - this.err = data.err; + this.type = String; + this.parentId = String; + this.time = Number; + this.args = [String]; + this.err = String; + this.runId = String; } } diff --git a/dashboard/lib/models/run.js b/dashboard/lib/models/run.js index 496ab4f..eba91eb 100644 --- a/dashboard/lib/models/run.js +++ b/dashboard/lib/models/run.js @@ -1,86 +1,28 @@ +const Document = require('camo').Document; const Event = require('./event'); +const RunStat = require('./runStat'); -const RUN_STATUS = { - ACTIVE: 'active', - PAUSED: 'paused', - FINISHED: 'finished', - ABORTED: 'aborted', - INTERRUPTED: 'interrupted', -}; +class Run extends Document { + constructor() { + super(); -class Run { - constructor(data) { - this._init(data); + this.command = String; + this._id = String; + this.startTime = Date; + this.endTime = { + type: Date, + default: 0, + }; + this.status = String; } - _init(data) { - this.command = data.command; - this.id = data.id; - - this.startTime = data.startTime; - this.endTime = 0; - - this.events = []; - this.cpuUsage = []; - this.memoryUsage = []; - } - - // setters - setPaused() { - this.status = RUN_STATUS.PAUSED; - } - - setFinished() { - if (this.isAborted()) return; - - this.status = RUN_STATUS.FINISHED; - this.endTime = Date.now(); - } - - setActive() { - this.status = RUN_STATUS.ACTIVE; - } - - setAborted() { - this.status = RUN_STATUS.ABORTED; - this.endTime = Date.now(); - } - - setInterrupted() { - this.status = RUN_STATUS.INTERRUPTED; - } - - addEvent(data) { - if (data.err) this.setInterrupted(); - this.events.push(new Event(data)); - } - - addRunStats(data) { - data.cpu && - data.memory && - this.cpuUsage.push(data.cpu) && - this.memoryUsage.push(data.memory); - } - - // getters - isPaused() { - return this.status === RUN_STATUS.PAUSED; - } - - isFinished() { - return this.status === RUN_STATUS.FINISHED; - } - - isActive() { - return this.status === RUN_STATUS.ACTIVE; - } - - isAborted() { - return this.status === RUN_STATUS.ABORTED; + async populate() { + this.stats = await RunStat.find({ runId: this._id }); + this.events = await Event.find({ runId: this._id }, { sort: 'time' }); } - isInterrupted() { - return this.status === RUN_STATUS.INTERRUPTED; + static collectionName() { + return 'runs'; } } diff --git a/dashboard/lib/models/runStat.js b/dashboard/lib/models/runStat.js new file mode 100644 index 0000000..a80bf0c --- /dev/null +++ b/dashboard/lib/models/runStat.js @@ -0,0 +1,13 @@ +const Document = require('camo').Document; + +class RunStat extends Document { + constructor() { + super(); + + this.cpu = Number; + this.memory = Number; + this.runId = String; + } +} + +module.exports = RunStat; diff --git a/dashboard/lib/store/index.js b/dashboard/lib/store/index.js index e7fc0bd..328ee13 100644 --- a/dashboard/lib/store/index.js +++ b/dashboard/lib/store/index.js @@ -1,17 +1,16 @@ -const Table = require('./table'); +const connect = require('camo').connect; +const paths = require('env-paths')('newman-dashboard'); +const uri = `nedb://${paths.data}`; -const cache = {}; +const init = async () => { + try { + await connect(uri); -const api = { - _createTable: (tableName) => { - cache[tableName] = new Table({}); - return; - }, - - getTable: async (tableName) => { - if (!cache.hasOwnProperty(tableName)) api._createTable(tableName); - return cache[tableName]; - }, + // cleanup function to terminate db connection + return () => process.exit(0); + } catch (e) { + console.log('Error in connecting to database.'); + } }; -module.exports = api; +module.exports = { init }; diff --git a/dashboard/lib/store/table/index.js b/dashboard/lib/store/table/index.js deleted file mode 100644 index 728aa26..0000000 --- a/dashboard/lib/store/table/index.js +++ /dev/null @@ -1,28 +0,0 @@ -class Table { - constructor(initialData = {}) { - this.cache = initialData; - } - - async insert(id, data) { - if (!id || !data) return; - this.cache[id] = data; - } - - async find() { - return this.cache; - } - - async findOne(id) { - return id && this.cache[id]; - } - - async clear() { - this.cache = {}; - } - - async remove(id) { - id && delete this.cache[id]; - } -} - -module.exports = Table; diff --git a/dashboard/lib/utils/index.js b/dashboard/lib/utils/index.js index 2a0cfd8..58219cd 100644 --- a/dashboard/lib/utils/index.js +++ b/dashboard/lib/utils/index.js @@ -13,4 +13,8 @@ module.exports = { next(new Error('Unauthorized access.')); }, + + handleDBError: (err) => { + console.log('Error loading data files. Indicates corrupted data.'); + }, }; diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index 62b68d0..65f5357 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -9,9 +9,12 @@ "version": "0.0.1", "license": "Apache-2.0", "dependencies": { + "camo": "^0.12.4", "commander": "7.2.0", "cors": "^2.8.5", + "env-paths": "^2.2.1", "express": "4.17.1", + "nedb": "^1.8.0", "socket.io": "4.1.2", "socket.io-client": "4.1.2" }, @@ -1026,6 +1029,11 @@ "node": ">=8" } }, + "node_modules/async": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=" + }, "node_modules/backo2": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", @@ -1062,6 +1070,14 @@ "node": ">=8" } }, + "node_modules/binary-search-tree": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/binary-search-tree/-/binary-search-tree-0.2.5.tgz", + "integrity": "sha1-fbs7IQ/coIJFDa0jNMMErzm9x4Q=", + "dependencies": { + "underscore": "~1.4.4" + } + }, "node_modules/body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", @@ -1230,6 +1246,16 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bson": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/bson/-/bson-0.4.23.tgz", + "integrity": "sha1-5louPHUH/63kEJvHV1p25Q+NqRU=", + "deprecated": "Fixed a critical issue with BSON serialization documented in CVE-2019-2391, see https://bit.ly/2KcpXdo for more details", + "optional": true, + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", @@ -1320,6 +1346,30 @@ "node": ">=10" } }, + "node_modules/camo": { + "version": "0.12.4", + "resolved": "https://registry.npmjs.org/camo/-/camo-0.12.4.tgz", + "integrity": "sha512-zN9l2009K92DqTKUwwl9rL07y6EAQveJWVmSubuntFkN8oPXEBu4Jk8HEoJ9gSxA34QGIFy7pyh0TjJY5RHNwg==", + "dependencies": { + "depd": "1.1.0", + "lodash": "4.17.21" + }, + "engines": { + "node": ">=2.0.0" + }, + "optionalDependencies": { + "mongodb": "2.0.42", + "nedb": "1.8.0" + } + }, + "node_modules/camo/node_modules/depd": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz", + "integrity": "sha1-4b2Cxqq2ztlluXuIsX7T5SjKGMM=", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001249", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001249.tgz", @@ -1604,6 +1654,12 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "optional": true + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -1916,6 +1972,14 @@ "node": ">=8.6" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "engines": { + "node": ">=6" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -1973,6 +2037,12 @@ "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", "dev": true }, + "node_modules/es6-promise": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-2.1.1.tgz", + "integrity": "sha1-A+jzxyl5KOVHjWqx0GQyUVB73t0=", + "optional": true + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -3064,6 +3134,11 @@ "minimatch": "^3.0.4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=" + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -3406,7 +3481,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true + "devOptional": true }, "node_modules/isexe": { "version": "2.0.0", @@ -3651,6 +3726,16 @@ "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", "dev": true }, + "node_modules/kerberos": { + "version": "0.0.24", + "resolved": "https://registry.npmjs.org/kerberos/-/kerberos-0.0.24.tgz", + "integrity": "sha512-QO6bFq9eETHB5zcA0OJiQtw137TH45OuUcGtI+QGg2ZJQIPCvwXL2kjCqZZMColcIdbPhj4X40EY5f3oOiBfiw==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "nan": "~2.10.0" + } + }, "node_modules/keyv": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", @@ -3685,6 +3770,14 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", @@ -3700,6 +3793,14 @@ "node": ">=4" } }, + "node_modules/localforage": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", + "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", + "dependencies": { + "lie": "3.1.1" + } + }, "node_modules/locate-path": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", @@ -3713,6 +3814,11 @@ "node": ">=4" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "node_modules/lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", @@ -3872,8 +3978,18 @@ "node_modules/minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "node_modules/mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } }, "node_modules/mocha": { "version": "8.4.0", @@ -4084,11 +4200,41 @@ "node": ">=0.10.0" } }, + "node_modules/mongodb": { + "version": "2.0.42", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.0.42.tgz", + "integrity": "sha1-G614E9ByXOLjvVQYDzH67dJNnlM=", + "deprecated": "Please upgrade to 2.2.19 or higher", + "optional": true, + "dependencies": { + "es6-promise": "2.1.1", + "mongodb-core": "1.2.10", + "readable-stream": "1.0.31" + } + }, + "node_modules/mongodb-core": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-1.2.10.tgz", + "integrity": "sha1-7OFyAb05WmR/uY9Ivo+64Vvgr9k=", + "optional": true, + "dependencies": { + "bson": "~0.4" + }, + "optionalDependencies": { + "kerberos": "~0.0" + } + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "node_modules/nan": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", + "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", + "optional": true + }, "node_modules/nanoid": { "version": "3.1.20", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz", @@ -4107,6 +4253,18 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "node_modules/nedb": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/nedb/-/nedb-1.8.0.tgz", + "integrity": "sha1-DjUCzYLABNU1WkPJ5VV3vXvZHYg=", + "dependencies": { + "async": "0.2.10", + "binary-search-tree": "0.2.5", + "localforage": "^1.3.0", + "mkdirp": "~0.5.1", + "underscore": "~1.4.4" + } + }, "node_modules/negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", @@ -5019,6 +5177,18 @@ "node": ">=4" } }, + "node_modules/readable-stream": { + "version": "1.0.31", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.31.tgz", + "integrity": "sha1-jyUC4LyeOw2huUUgqrtOJgPsr64=", + "optional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, "node_modules/readdirp": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", @@ -5554,6 +5724,12 @@ "stubs": "^3.0.0" } }, + "node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "optional": true + }, "node_modules/string-width": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", @@ -5849,6 +6025,11 @@ "debug": "^2.2.0" } }, + "node_modules/underscore": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.4.4.tgz", + "integrity": "sha1-YaajIBBiKvoHljvzJSA88SI51gQ=" + }, "node_modules/unique-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", @@ -7103,6 +7284,11 @@ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true }, + "async": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=" + }, "backo2": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", @@ -7130,6 +7316,14 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, + "binary-search-tree": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/binary-search-tree/-/binary-search-tree-0.2.5.tgz", + "integrity": "sha1-fbs7IQ/coIJFDa0jNMMErzm9x4Q=", + "requires": { + "underscore": "~1.4.4" + } + }, "body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", @@ -7264,6 +7458,12 @@ "node-releases": "^1.1.73" } }, + "bson": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/bson/-/bson-0.4.23.tgz", + "integrity": "sha1-5louPHUH/63kEJvHV1p25Q+NqRU=", + "optional": true + }, "bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", @@ -7335,6 +7535,24 @@ "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", "dev": true }, + "camo": { + "version": "0.12.4", + "resolved": "https://registry.npmjs.org/camo/-/camo-0.12.4.tgz", + "integrity": "sha512-zN9l2009K92DqTKUwwl9rL07y6EAQveJWVmSubuntFkN8oPXEBu4Jk8HEoJ9gSxA34QGIFy7pyh0TjJY5RHNwg==", + "requires": { + "depd": "1.1.0", + "lodash": "4.17.21", + "mongodb": "2.0.42", + "nedb": "1.8.0" + }, + "dependencies": { + "depd": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz", + "integrity": "sha1-4b2Cxqq2ztlluXuIsX7T5SjKGMM=" + } + } + }, "caniuse-lite": { "version": "1.0.30001249", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001249.tgz", @@ -7568,6 +7786,12 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "optional": true + }, "cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -7823,6 +8047,11 @@ "ansi-colors": "^4.1.1" } }, + "env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==" + }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -7874,6 +8103,12 @@ "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", "dev": true }, + "es6-promise": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-2.1.1.tgz", + "integrity": "sha1-A+jzxyl5KOVHjWqx0GQyUVB73t0=", + "optional": true + }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -8757,6 +8992,11 @@ "minimatch": "^3.0.4" } }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=" + }, "import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -9018,7 +9258,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true + "devOptional": true }, "isexe": { "version": "2.0.0", @@ -9217,6 +9457,15 @@ "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", "dev": true }, + "kerberos": { + "version": "0.0.24", + "resolved": "https://registry.npmjs.org/kerberos/-/kerberos-0.0.24.tgz", + "integrity": "sha512-QO6bFq9eETHB5zcA0OJiQtw137TH45OuUcGtI+QGg2ZJQIPCvwXL2kjCqZZMColcIdbPhj4X40EY5f3oOiBfiw==", + "optional": true, + "requires": { + "nan": "~2.10.0" + } + }, "keyv": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", @@ -9245,6 +9494,14 @@ "type-check": "~0.4.0" } }, + "lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=", + "requires": { + "immediate": "~3.0.5" + } + }, "load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", @@ -9257,6 +9514,14 @@ "strip-bom": "^3.0.0" } }, + "localforage": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", + "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", + "requires": { + "lie": "3.1.1" + } + }, "locate-path": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", @@ -9267,6 +9532,11 @@ "path-exists": "^3.0.0" } }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", @@ -9389,8 +9659,15 @@ "minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "requires": { + "minimist": "^1.2.5" + } }, "mocha": { "version": "8.4.0", @@ -9564,11 +9841,38 @@ } } }, + "mongodb": { + "version": "2.0.42", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.0.42.tgz", + "integrity": "sha1-G614E9ByXOLjvVQYDzH67dJNnlM=", + "optional": true, + "requires": { + "es6-promise": "2.1.1", + "mongodb-core": "1.2.10", + "readable-stream": "1.0.31" + } + }, + "mongodb-core": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-1.2.10.tgz", + "integrity": "sha1-7OFyAb05WmR/uY9Ivo+64Vvgr9k=", + "optional": true, + "requires": { + "bson": "~0.4", + "kerberos": "~0.0" + } + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "nan": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", + "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", + "optional": true + }, "nanoid": { "version": "3.1.20", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz", @@ -9581,6 +9885,18 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "nedb": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/nedb/-/nedb-1.8.0.tgz", + "integrity": "sha1-DjUCzYLABNU1WkPJ5VV3vXvZHYg=", + "requires": { + "async": "0.2.10", + "binary-search-tree": "0.2.5", + "localforage": "^1.3.0", + "mkdirp": "~0.5.1", + "underscore": "~1.4.4" + } + }, "negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", @@ -10301,6 +10617,18 @@ "read-pkg": "^3.0.0" } }, + "readable-stream": { + "version": "1.0.31", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.31.tgz", + "integrity": "sha1-jyUC4LyeOw2huUUgqrtOJgPsr64=", + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, "readdirp": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", @@ -10757,6 +11085,12 @@ "stubs": "^3.0.0" } }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "optional": true + }, "string-width": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", @@ -11000,6 +11334,11 @@ "debug": "^2.2.0" } }, + "underscore": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.4.4.tgz", + "integrity": "sha1-YaajIBBiKvoHljvzJSA88SI51gQ=" + }, "unique-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", diff --git a/dashboard/package.json b/dashboard/package.json index 43fe7c3..0af51e8 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -23,9 +23,12 @@ "node": ">= 10" }, "dependencies": { + "camo": "^0.12.4", "commander": "7.2.0", "cors": "^2.8.5", + "env-paths": "^2.2.1", "express": "4.17.1", + "nedb": "^1.8.0", "socket.io": "4.1.2", "socket.io-client": "4.1.2" }, diff --git a/dashboard/server/api/server.js b/dashboard/server/api/server.js index e799dd0..aaaf6af 100644 --- a/dashboard/server/api/server.js +++ b/dashboard/server/api/server.js @@ -1,8 +1,15 @@ -const runController = require('../../lib/controllers/run'); +const Run = require('../../lib/models/run'); module.exports = { getAllRuns: async (req, res) => { - const runs = await runController.find(); + const limit = req.query.limit || 10; + const page = req.query.page || 0; + const skip = Number(page) * Number(limit); + + const runs = await Run.find( + {}, + { populate: true, sort: '-endTime', limit, skip } + ); return res.status(200).json({ store: runs, @@ -11,8 +18,15 @@ module.exports = { getRun: async (req, res) => { const id = req.params.id; - const run = await runController.findOne(id); + const run = await Run.findOne({ _id: id }, { populate: true }); + + if (!run) { + return res.status(404).json({ + run: {}, + }); + } + await run.populate(); return res.status(200).json({ run, }); diff --git a/dashboard/server/index.js b/dashboard/server/index.js index a782835..9ded43f 100644 --- a/dashboard/server/index.js +++ b/dashboard/server/index.js @@ -5,6 +5,7 @@ const Server = require('./server'); const runHandlers = require('./api/runApi'); const frontendHandlers = require('./api/frontendApi'); const utils = require('../lib/utils'); +const db = require('../lib/store'); const { START_RUN, @@ -27,9 +28,11 @@ const { FRONTEND_REQUEST_TERMINATE, } = require('../lib/constants/frontend-events'); -const init = () => { +const init = async () => { // setup socket.io server const server = Server.init(); + const dbCleanup = await db.init(); + const io = socket(server, { cors: { origin: '*', @@ -76,7 +79,14 @@ const init = () => { } }); - return { server, io }; + // clean close the server + const close = () => { + server.close(); + io.close(); + dbCleanup(); + }; + + return { close }; }; // Run this script if this is a direct stdin. diff --git a/dashboard/test/unit/lib/models/event.test.js b/dashboard/test/unit/lib/models/event.test.js deleted file mode 100644 index dc13b5c..0000000 --- a/dashboard/test/unit/lib/models/event.test.js +++ /dev/null @@ -1,26 +0,0 @@ -const expect = require('chai').expect; - -// unit under test -const Event = require('../../../../lib/models/event'); - -describe('EventModel', () => { - const mockEventData = { - type: 'abc', - id: 'id', - args: 'sample-args', - err: 'sample-error', - }; - let event; - - before(() => { - event = new Event(mockEventData); - }); - - it('should create the instance correctly', () => { - expect(event.type).to.equal('abc'); - expect(event.parentId).to.equal('id'); - expect(event.args).to.equal('sample-args'); - expect(event.err).to.equal('sample-error'); - expect(event).to.haveOwnProperty('time'); - }); -}); diff --git a/dashboard/test/unit/lib/models/run.test.js b/dashboard/test/unit/lib/models/run.test.js deleted file mode 100644 index 10fd34b..0000000 --- a/dashboard/test/unit/lib/models/run.test.js +++ /dev/null @@ -1,130 +0,0 @@ -const expect = require('chai').expect; - -// unit under test -const Run = require('../../../../lib/models/run'); - -describe('RunModel', () => { - let currTime = Date.now(); - - const mockRunData = { - command: 'abc', - id: 'id', - startTime: currTime, - }; - let run; - - before(() => { - run = new Run(mockRunData); - }); - - it('should create the instance correctly', () => { - expect(run.command).to.equal('abc'); - expect(run.id).to.equal('id'); - expect(run.startTime).to.equal(currTime); - - expect(run.endTime).to.equal(0); - expect(run.events.length).to.equal(0); - expect(run.cpuUsage.length).to.equal(0); - expect(run.memoryUsage.length).to.equal(0); - }); - - describe('Setters and getters', () => { - describe('setPaused', () => { - before(() => { - run.setPaused(); - }); - - it('should set the status as paused', () => { - expect(run.isPaused()).to.be.true; - }); - }); - - describe('setActive', () => { - before(() => { - run.setActive(); - }); - - it('should set the status as active', () => { - expect(run.isActive()).to.be.true; - }); - }); - - describe('setInterrupted', () => { - before(() => { - run.setInterrupted(); - }); - - it('should set the status as interrupted', () => { - expect(run.isInterrupted()).to.be.true; - }); - }); - - describe('setAborted', () => { - before(() => { - run.setAborted(); - }); - - it('should set the status as aborted', () => { - expect(run.isAborted()).to.be.true; - }); - }); - - describe('setFinished', () => { - before(() => { - run.setFinished(); - }); - - it('should keep the status as aborted if run was aborted', () => { - expect(run.isAborted()).to.be.true; - }); - - it('should set the status as done if run is active', () => { - run.setActive(); - run.setFinished(); - expect(run.isFinished()).to.be.true; - }); - }); - - describe('addEvent', () => { - before(() => { - run.addEvent({ - type: 'abc', - id: 'id', - args: 'sample-args', - }); - }); - - it('should add a new run event', () => { - expect(run.events.length).to.equal(1); - }); - - it('should mark a run as errored if event has error', () => { - run.addEvent({ - type: 'abc', - id: 'id', - args: 'sample-args', - err: 'error', - }); - expect(run.events.length).to.equal(2); - expect(run.isInterrupted()).to.be.true; - }); - }); - - describe('addRunStats', () => { - before(() => { - run.addRunStats({ - cpu: 20, - memory: 30, - }); - }); - - it('should add a new run stats', () => { - expect(run.cpuUsage.length).to.equal(1); - expect(run.memoryUsage.length).to.equal(1); - - expect(run.memoryUsage[0]).to.equal(30); - expect(run.cpuUsage[0]).to.equal(20); - }); - }); - }); -}); diff --git a/dashboard/test/unit/lib/store/table.test.js b/dashboard/test/unit/lib/store/table.test.js deleted file mode 100644 index c17a006..0000000 --- a/dashboard/test/unit/lib/store/table.test.js +++ /dev/null @@ -1,52 +0,0 @@ -const expect = require('chai').expect; - -// unit under test -const Table = require('../../../../lib/store/table'); - -describe('StoreTable', () => { - let table, mockData; - - before('setup table mock', () => { - table = new Table({}); - mockData = { - id: 'abc', - arg: 'arg', - }; - }); - - it('should initialize the cache correctly', async () => { - const cache = await table.find(); - expect(cache).to.deep.equal({}); - }); - - it('should insert new item to cache', async () => { - const item = await table.insert(mockData.id, mockData); - expect(item).to.be.undefined; - }); - - it('should fetch item from cache', async () => { - const item = await table.findOne(mockData.id); - expect(item).to.deep.equal(mockData); - }); - - it('should not fetch item if not present in cache', async () => { - const item = await table.findOne('xyz'); - expect(item).to.be.undefined; - }); - - it('should remove item from cache', async () => { - const removeItem = await table.remove(mockData.id); - const item = await table.findOne(mockData.id); - - expect(removeItem).to.be.undefined; - expect(item).to.be.undefined; - }); - - it('should remove clear the cache', async () => { - const clear = await table.clear(); - const cache = await table.find(); - - expect(clear).to.be.undefined; - expect(cache).to.deep.equal({}); - }); -}); diff --git a/dashboard/test/unit/server/api/server.test.js b/dashboard/test/unit/server/api/server.test.js index 4c07c00..aeea42c 100644 --- a/dashboard/test/unit/server/api/server.test.js +++ b/dashboard/test/unit/server/api/server.test.js @@ -1,5 +1,6 @@ const expect = require('chai').expect; const sinon = require('sinon'); +const Run = require('../../../../lib/models/run'); // unit to test const handlers = require('../../../../../dashboard/server/api/server'); @@ -9,7 +10,12 @@ describe('Server API Endpoints', () => { let req, res, jsonSpy; beforeEach('setup mocks and spies', () => { - req = sinon.spy(); + req = { + query: { + limit: 1, + }, + }; + Run.find = sinon.spy(); jsonSpy = sinon.spy(); res = { @@ -22,7 +28,20 @@ describe('Server API Endpoints', () => { it('should return the response correctly', async () => { await handlers.getAllRuns(req, res); - expect(req.callCount).to.equal(0); + expect(Run.find.callCount).to.equal(1); + expect(Run.find.firstCall.args).to.have.lengthOf(2); + + expect(Run.find.firstCall.args[0]).to.deep.equal({}); + + expect(Run.find.firstCall.args[1]).to.haveOwnProperty('populate'); + expect(Run.find.firstCall.args[1]).to.haveOwnProperty('sort'); + expect(Run.find.firstCall.args[1]).to.haveOwnProperty('limit'); + expect(Run.find.firstCall.args[1]).to.haveOwnProperty('skip'); + + expect(Run.find.firstCall.args[1].populate).to.be.true; + expect(Run.find.firstCall.args[1].sort).to.equal('-endTime'); + expect(Run.find.firstCall.args[1].limit).to.equal(1); + expect(Run.find.firstCall.args[1].skip).to.equal(0); expect(res.status.callCount).to.equal(1); expect(res.status().json.callCount).to.equal(1); @@ -47,6 +66,8 @@ describe('Server API Endpoints', () => { }, }; + Run.findOne = sinon.spy(); + jsonSpy = sinon.spy(); res = { @@ -59,11 +80,22 @@ describe('Server API Endpoints', () => { it('should return the response correctly', async () => { await handlers.getRun(req, res); + expect(Run.findOne.callCount).to.equal(1); + expect(Run.findOne.firstCall.args).to.have.lengthOf(2); + + expect(Run.findOne.firstCall.args[0]).to.deep.equal({ _id: 1 }); + + expect(Run.findOne.firstCall.args[1]).to.haveOwnProperty( + 'populate' + ); + + expect(Run.findOne.firstCall.args[1].populate).to.be.true; + expect(res.status.callCount).to.equal(1); expect(res.status().json.callCount).to.equal(1); expect(res.status.firstCall.args).to.have.lengthOf(1); - expect(res.status.firstCall.args[0]).to.equal(200); + expect(res.status.firstCall.args[0]).to.equal(404); expect(res.status().json.firstCall.args).to.have.lengthOf(1); expect(res.status().json.firstCall.args[0]).to.haveOwnProperty( diff --git a/dashboard/test/unit/server/index.test.js b/dashboard/test/unit/server/index.test.js index 70b5d75..2c78373 100644 --- a/dashboard/test/unit/server/index.test.js +++ b/dashboard/test/unit/server/index.test.js @@ -10,13 +10,12 @@ const Server = require('../../../../dashboard/server'); describe('Server', () => { let launchedServer; - before(() => { - launchedServer = Server.init(); + before(async () => { + launchedServer = await Server.init(); }); - after(() => { - launchedServer.io.close(); - launchedServer.server.close(); + after(async () => { + launchedServer.close(); }); describe('Run events', () => { diff --git a/frontend/actions/index.js b/frontend/actions/index.js index 528201d..14195f8 100644 --- a/frontend/actions/index.js +++ b/frontend/actions/index.js @@ -73,7 +73,7 @@ const mountSockets = (store) => { }); socket.on('run-event', (data) => { - const run = store.find(data.id); + const run = store.find(data.runId); run.addEvent(data); if(data.err) { @@ -83,7 +83,7 @@ const mountSockets = (store) => { }); socket.on('run-stats', (data) => { - const run = store.find(data.id); + const run = store.find(data.runId); run.addRunStats(data); }); diff --git a/frontend/components/RunData.js b/frontend/components/RunData.js index eef10dc..9c03a58 100644 --- a/frontend/components/RunData.js +++ b/frontend/components/RunData.js @@ -72,7 +72,6 @@ const RunData = observer(({ run }) => { run.events.map((event) => ( ))} diff --git a/frontend/pages/run/[id].js b/frontend/pages/run/[id].js index 76466b0..8fac5e6 100644 --- a/frontend/pages/run/[id].js +++ b/frontend/pages/run/[id].js @@ -20,7 +20,7 @@ const RunDetails = observer(() => { useEffect(() => { const execService = async () => { - if (!run) { + if (!run || !run.events.length) { run = await RunService.fetchOne(id); } setIsLoading(false); diff --git a/frontend/services/run.js b/frontend/services/run.js index 87c9e51..1f99646 100644 --- a/frontend/services/run.js +++ b/frontend/services/run.js @@ -1,4 +1,4 @@ -import store from '../state/stores/runStore'; +import store from "../state/stores/runStore"; class RunService { async fetch() { @@ -10,9 +10,10 @@ class RunService { async fetchOne(id) { const res = await fetch(`http://localhost:5001/api/run/${id}`); const data = await res.json(); + data.run && store.add(data.run); return data.run; } -}; +} -module.exports = new RunService(); \ No newline at end of file +module.exports = new RunService(); diff --git a/frontend/state/models/event.js b/frontend/state/models/event.js index e2eb2cf..3458c65 100644 --- a/frontend/state/models/event.js +++ b/frontend/state/models/event.js @@ -18,7 +18,7 @@ export default class EventModel { _init(data) { this.type = data.type; this.parentId = data.id; - this.time = Date.now(); + this.time = data.time; this.args = data.args; this.err = data.err; } diff --git a/frontend/state/models/run.js b/frontend/state/models/run.js index a325890..f2a5df8 100644 --- a/frontend/state/models/run.js +++ b/frontend/state/models/run.js @@ -66,8 +66,7 @@ export default class RunModel { this.endTime = 0; this.status = data.status || RUN_STATUS.ACTIVE; - this.cpuUsage = data.cpuUsage || []; - this.memoryUsage = data.memoryUsage || []; + this.stats = data.stats || []; this.events = data.events || []; } @@ -114,16 +113,13 @@ export default class RunModel { @action addRunStats(data) { - data.cpu && - data.memory && - this.cpuUsage.push(data.cpu) && - this.memoryUsage.push(data.memory); + this.stats.push(data); } @computed sortEvents() { - return this.events.sort( - (firstEvent, secondEvent) => firstEvent.time < secondEvent.time + return this.events.slice().sort( + (firstEvent, secondEvent) => Number(firstEvent.time) > Number(secondEvent.time) ); } @@ -131,7 +127,7 @@ export default class RunModel { getMemoryUsage() { // show live memory stats while run is active if (this.isActive() || this.isPaused()) { - let currMemory = this.memoryUsage[this.memoryUsage.length - 1] / 1e6; + let currMemory = this.stats[this.stats.length - 1].memory / 1e6; let roundedMemory = Math.round((currMemory + Number.EPSILON) * 100) / 100; return roundedMemory; @@ -139,11 +135,11 @@ export default class RunModel { // show average memory stats if run is complete let totalMemory = 0; - this.memoryUsage.forEach((memory) => { - totalMemory += memory; + this.stats.forEach((stat) => { + totalMemory += stat.memory; }); - let avgMemoryB = totalMemory / this.memoryUsage.length; + let avgMemoryB = totalMemory / this.stats.length; let avgMemoryMB = avgMemoryB / 1e6; let roundedMemory = Math.round((avgMemoryMB + Number.EPSILON) * 100) / 100; @@ -155,7 +151,7 @@ export default class RunModel { getCpuUsage() { // show live CPU stats while run is active if(this.isActive() || this.isPaused()) { - let currCpu = this.cpuUsage[this.cpuUsage.length - 1]; + let currCpu = this.stats[this.stats.length - 1].cpu; let roundedCpu = Math.round((currCpu + Number.EPSILON) * 100) / 100; return roundedCpu; @@ -163,11 +159,11 @@ export default class RunModel { // show average CPU stats if run is complete let totalCpu = 0; - this.cpuUsage.forEach((cpu) => { - totalCpu += cpu; + this.stats.forEach((data) => { + totalCpu += data.cpu; }); - let avgCpu = totalCpu / this.cpuUsage.length; + let avgCpu = totalCpu / this.stats.length; let roundedCpu = Math.round((avgCpu + Number.EPSILON) * 100) / 100; diff --git a/frontend/state/stores/runStore.js b/frontend/state/stores/runStore.js index cb6f85e..c2bde25 100644 --- a/frontend/state/stores/runStore.js +++ b/frontend/state/stores/runStore.js @@ -14,7 +14,7 @@ class RunStore { this.clear(); try { - Object.values(runs).forEach(run => { + runs.forEach(run => { this.add(run); }); @@ -30,7 +30,9 @@ class RunStore { @action add(run) { - if(!run.hasOwnProperty('id')) return; + if (run.hasOwnProperty('_id')) run.id = run._id; + + if (!run.hasOwnProperty('id')) return; this.runs[run.id] = new Run(run, socket); } @@ -40,10 +42,9 @@ class RunStore { return this.runs[id]; } + @computed getSortedRuns() { - return Object.values(this.runs).sort((firstRun, secondRun) => { - firstRun.endTime < secondRun.endTime; - }); + return Object.values(this.runs).sort((firstRun, secondRun) => secondRun.endTime - firstRun.endTime); } }; diff --git a/reporter/lib/handlers/index.js b/reporter/lib/handlers/index.js index d45559c..66f6316 100644 --- a/reporter/lib/handlers/index.js +++ b/reporter/lib/handlers/index.js @@ -52,7 +52,7 @@ module.exports = (socket, id) => { handleRunStats: (stats) => { socket.emit('run-stats', { - id, + runId: id, cpu: stats.cpu, memory: stats.memory, }); @@ -62,7 +62,8 @@ module.exports = (socket, id) => { return (err, args) => { socket.emit('run-event', { type: event, - id, + runId: id, + time: Date.now(), args, err, }); diff --git a/reporter/test/unit/lib/handlers/index.test.js b/reporter/test/unit/lib/handlers/index.test.js index 75eb798..12c1d6f 100644 --- a/reporter/test/unit/lib/handlers/index.test.js +++ b/reporter/test/unit/lib/handlers/index.test.js @@ -185,8 +185,8 @@ describe('Reporter events', () => { expect(socket.emit.calledOnce).to.be.true; expect(socket.emit.firstCall.args).to.have.lengthOf(2); expect(socket.emit.firstCall.args[0]).to.equal('run-event'); - expect(socket.emit.firstCall.args[1]).to.haveOwnProperty('id'); - expect(socket.emit.firstCall.args[1].id).to.equal('abc'); + expect(socket.emit.firstCall.args[1]).to.haveOwnProperty('runId'); + expect(socket.emit.firstCall.args[1].runId).to.equal('abc'); expect(socket.emit.firstCall.args[1]).to.haveOwnProperty('type'); expect(socket.emit.firstCall.args[1].type).to.equal('event'); });