diff --git a/res/app/components/stf/device/device-service.js b/res/app/components/stf/device/device-service.js index 866b5326..3c97dcf2 100644 --- a/res/app/components/stf/device/device-service.js +++ b/res/app/components/stf/device/device-service.js @@ -1,5 +1,6 @@ var oboe = require('oboe') var _ = require('lodash') +var EventEmitter = require('eventemitter3') module.exports = function DeviceServiceFactory($http, socket, EnhanceDeviceService) { var deviceService = {} @@ -16,8 +17,6 @@ module.exports = function DeviceServiceFactory($http, socket, EnhanceDeviceServi }) function digest() { - $scope.$broadcast('devices.update', true) - // Not great. Consider something else if (!$scope.$$phase) { $scope.$digest() @@ -28,6 +27,10 @@ module.exports = function DeviceServiceFactory($http, socket, EnhanceDeviceServi } function notify(event) { + if (!options.digest) { + return + } + if (event.important) { // Handle important updates immediately. //digest() @@ -65,29 +68,31 @@ module.exports = function DeviceServiceFactory($http, socket, EnhanceDeviceServi EnhanceDeviceService.enhance(data) } - function get(data) { return devices[devicesBySerial[data.serial]] } - function insert(data) { + var insert = function insert(data) { devicesBySerial[data.serial] = devices.push(data) - 1 sync(data) - } + this.emit('add', data) + }.bind(this) - function modify(data, newData) { + var modify = function modify(data, newData) { _.merge(data, newData, function(a, b) { // New Arrays overwrite old Arrays return _.isArray(b) ? b : undefined }) sync(data) - } + this.emit('change', data) + }.bind(this) - function remove(data) { + var remove = function remove(data) { var index = devicesBySerial[data.serial] if (index >= 0) { devices.splice(index, 1) delete devicesBySerial[data.serial] + this.emit('remove', data) } } @@ -151,11 +156,14 @@ module.exports = function DeviceServiceFactory($http, socket, EnhanceDeviceServi this.devices = devices } + Tracker.prototype = new EventEmitter() + deviceService.trackAll = function ($scope) { var tracker = new Tracker($scope, { filter: function() { return true } + , digest: false }) oboe('/api/v1/devices') @@ -171,6 +179,7 @@ module.exports = function DeviceServiceFactory($http, socket, EnhanceDeviceServi filter: function(device) { return device.using } + , digest: true }) oboe('/api/v1/group') @@ -193,6 +202,7 @@ module.exports = function DeviceServiceFactory($http, socket, EnhanceDeviceServi filter: function(device) { return device.serial === serial } + , digest: true }) return deviceService.load(serial) diff --git a/res/app/device-list/device-list-details-directive.js b/res/app/device-list/device-list-details-directive.js new file mode 100644 index 00000000..9aceb5e8 --- /dev/null +++ b/res/app/device-list/device-list-details-directive.js @@ -0,0 +1,504 @@ +function zeroPadTwoDigit(digit) { + return digit < 10 ? '0' + digit : '' + digit +} + +function caseInsensitiveSort(prop) { + return function(a, b) { + return a[prop] === b[prop] + ? 0 + : (a[prop] || '').toLowerCase() < (b[prop] || '').toLowerCase() ? -1 : 1 + } +} + +function caseSensitiveSort(prop) { + return function(a, b) { + return a[prop] === b[prop] + ? 0 + : (a[prop] || '') < (b[prop] || '') ? -1 : 1 + } +} + +var stateSortOrder = { + using: 10 +, available: 20 +, busy: 30 +, ready: 40 +, preparing: 50 +, unauthorized: 60 +, offline: 70 +, present: 80 +, absent: 90 +} + +var columns = { + state: { + build: function() { + var td = document.createElement('td') + , a = document.createElement('a') + a.appendChild(document.createTextNode('')) + td.appendChild(a) + return td + } + , update: function(td, device) { + var a = td.firstChild + , t = a.firstChild + a.href = '#!/control/' + device.serial + t.nodeValue = device.state + return td + } + , compare: function(deviceA, deviceB) { + return stateSortOrder[deviceA.state] - stateSortOrder[deviceB.state] + } + } +, model: { + build: function() { + var td = document.createElement('td') + var image = document.createElement('img') + td.appendChild(image) + td.appendChild(document.createTextNode('')) + return td + } + , update: function(td, device) { + var image = td.firstChild + , t = image.nextSibling + image.src = '/static/devices/icon/x24/' + (device.image || '_default.jpg') + t.nodeValue = device.model || device.serial + } + , compare: caseSensitiveSort('model') + } +, name: { + build: function() { + var td = document.createElement('td') + td.appendChild(document.createTextNode('')) + return td + } + , update: function(td, device) { + var t = td.firstChild + t.nodeValue = device.name || '' + } + , compare: caseInsensitiveSort('name') + } +, operator: { + build: function() { + var td = document.createElement('td') + td.appendChild(document.createTextNode('')) + return td + } + , update: function(td, device) { + var t = td.firstChild + t.nodeValue = device.operator || '' + } + , compare: caseInsensitiveSort('operator') + } +, releasedAt: { + build: function() { + var td = document.createElement('td') + td.appendChild(document.createTextNode('')) + return td + } + , update: function(td, device) { + var t = td.firstChild + if (device.releasedAt) { + var date = new Date(device.releasedAt) + t.nodeValue = date.getFullYear() + + '-' + + zeroPadTwoDigit(date.getMonth() + 1) + + '-' + + zeroPadTwoDigit(date.getDate()) + } + else { + t.nodeValue = '' + } + } + , compare: caseSensitiveSort('releasedAt') + } +, version: { + build: function() { + var td = document.createElement('td') + td.appendChild(document.createTextNode('')) + return td + } + , update: function(td, device) { + var t = td.firstChild + t.nodeValue = device.version || '' + } + // @TODO semver + , compare: caseSensitiveSort('version') + } +, network: { + build: function() { + var td = document.createElement('td') + td.appendChild(document.createTextNode('')) + return td + } + , update: function(td, device) { + var t = td.firstChild + t.nodeValue = device.network + ? device.network.subtype || device.network.type || '' + : '' + } + } +, display: { + build: function() { + var td = document.createElement('td') + td.appendChild(document.createTextNode('')) + return td + } + , update: function(td, device) { + var t = td.firstChild + t.nodeValue = device.display + ? device.display.width + 'x' + device.display.height + : '' + } + } +, serial: { + build: function() { + var td = document.createElement('td') + td.appendChild(document.createTextNode('')) + return td + } + , update: function(td, device) { + var t = td.firstChild + t.nodeValue = device.serial + } + } +, manufacturer: { + build: function() { + var td = document.createElement('td') + td.appendChild(document.createTextNode('')) + return td + } + , update: function(td, device) { + var t = td.firstChild + t.nodeValue = device.manufacturer || '' + } + } +, sdk: { + build: function() { + var td = document.createElement('td') + td.appendChild(document.createTextNode('')) + return td + } + , update: function(td, device) { + var t = td.firstChild + t.nodeValue = device.sdk || '' + } + } +, abi: { + build: function() { + var td = document.createElement('td') + td.appendChild(document.createTextNode('')) + return td + } + , update: function(td, device) { + var t = td.firstChild + t.nodeValue = device.abi || '' + } + } +, phone: { + build: function() { + var td = document.createElement('td') + td.appendChild(document.createTextNode('')) + return td + } + , update: function(td, device) { + var t = td.firstChild + t.nodeValue = device.phone + ? device.phone.phoneNumber || '' + : '' + } + } +, imei: { + build: function() { + var td = document.createElement('td') + td.appendChild(document.createTextNode('')) + return td + } + , update: function(td, device) { + var t = td.firstChild + t.nodeValue = device.phone + ? device.phone.imei || '' + : '' + } + } +, iccid: { + build: function(device) { + var td = document.createElement('td') + if (device.phone) { + td.appendChild(document.createTextNode(device.phone.iccid)) + } + return td + } + } +, batteryHealth: { + build: function(device) { + var td = document.createElement('td') + if (device.battery) { + td.appendChild(document.createTextNode(device.battery.health)) + } + return td + } + } +, batterySource: { + build: function(device) { + var td = document.createElement('td') + if (device.battery) { + td.appendChild(document.createTextNode(device.battery.source)) + } + return td + } + } +, batteryStatus: { + build: function(device) { + var td = document.createElement('td') + if (device.battery) { + td.appendChild(document.createTextNode(device.battery.status)) + } + return td + } + } +, batteryLevel: { + build: function(device) { + var td = document.createElement('td') + if (device.battery) { + td.appendChild(document.createTextNode(device.battery.level)) + } + return td + } + } +, batteryTemp: { + build: function(device) { + var td = document.createElement('td') + if (device.battery) { + td.appendChild(document.createTextNode(device.battery.temp)) + } + return td + } + } +, provider: { + build: function() { + var td = document.createElement('td') + td.appendChild(document.createTextNode('')) + return td + } + , update: function(td, device) { + var t = td.firstChild + t.nodeValue = device.provider.name + } + } +, owner: { + build: function() { + var td = document.createElement('td') + td.appendChild(document.createTextNode('')) + return td + } + , update: function(td, device) { + var t = td.firstChild + t.nodeValue = device.owner + ? device.owner.name + : '' + } + } +} + +module.exports = function DeviceListDetailsDirective() { + return { + restrict: 'E' + , template: require('./device-list-details.jade') + , scope: { + tracker: '&tracker' + , columns: '&columns' + } + , link: function (scope, element) { + var tracker = scope.tracker() + , activeColumns = [ + 'state' + , 'model' + , 'serial' + , 'name' + , 'operator' + , 'releasedAt' + , 'version' + , 'network' + , 'provider' + , 'owner' + ] + , fixedSorting = [ + 'state' + ] + , activeSorting = fixedSorting.concat([ + 'name' + , 'model' + ]) + , table = element.find('table')[0] + , tbody = table.createTBody() + , rows = tbody.rows + , prefix = 'd' + Math.floor(Math.random() * 1000000) + '-' + , mapping = Object.create(null) + + table.className = 'table table-hover dataTable' + element.append(table) + + function calculateId(device) { + return prefix + device.serial + } + + function compare(deviceA, deviceB) { + var diff + + for (var i = 0, l = activeSorting.length; i < l; ++i) { + diff = columns[activeSorting[i]].compare(deviceA, deviceB) + if (diff !== 0) { + break + } + } + + return diff + } + + function createRow(device) { + var id = calculateId(device) + , tr = document.createElement('tr') + , td + + tr.id = id + + for (var i = 0, l = activeColumns.length; i < l; ++i) { + td = columns[activeColumns[i]].build() + columns[activeColumns[i]].update(td, device) + tr.appendChild(td) + } + + mapping[id] = device + + return tr + } + + function updateRow(tr, device) { + var id = calculateId(device) + + tr.id = id + + for (var i = 0, l = activeColumns.length; i < l; ++i) { + columns[activeColumns[i]].update(tr.cells[i], device) + } + + return tr + } + + function insertRow(tr, deviceA) { + return insertRowToSegment(tr, deviceA, 0, rows.length - 1) + } + + function insertRowToSegment(tr, deviceA, lo, hi) { + var pivot = 0 + , deviceB + , diff + , after = true + + if (hi < 0) { + tbody.appendChild(tr) + } + else { + while (lo <= hi) { + pivot = ~~((lo + hi) / 2) + deviceB = mapping[rows[pivot].id] + + diff = compare(deviceA, deviceB) + + if (diff === 0) { + after = true + break + } + + if (diff < 0) { + hi = pivot - 1 + after = false + } + else { + lo = pivot + 1 + after = true + } + } + + if (after) { + tbody.insertBefore(tr, rows[pivot].nextSibling) + } + else { + tbody.insertBefore(tr, rows[pivot]) + } + } + } + + function compareRow(tr, device) { + var prev = tr.previousSibling + , next = tr.nextSibling + , diff + + if (prev) { + diff = compare(device, mapping[prev.id]) + if (diff < 0) { + return diff + } + } + + if (next) { + diff = compare(device, mapping[next.id]) + if (diff > 0) { + return diff + } + } + + return 0 + } + + function addListener(device) { + insertRow(createRow(device), device) + } + + function changeListener(device) { + var id = calculateId(device) + , tr = document.getElementById(id) + , diff + + if (tr) { + // First, update columns + updateRow(tr, device) + + // Maybe the row is not sorted correctly anymore? + diff = compareRow(tr, device) + + if (diff < 0) { + // Should go higher in the list + insertRowToSegment(tr, device, 0, tr.rowIndex - 1) + } + else if (diff > 0) { + // Should go lower in the list + insertRowToSegment(tr, device, tr.rowIndex + 1, rows.length) + } + } + } + + function removeListener(device) { + var id = calculateId(device) + , tr = document.getElementById(id) + + if (tr) { + tbody.removeChild(tr) + } + + delete mapping[id] + } + + tracker.on('add', addListener) + tracker.on('change', changeListener) + tracker.on('remove', removeListener) + + scope.$on('$destroy', function() { + tracker.removeListener('add', addListener) + tracker.removeListener('change', changeListener) + tracker.removeListener('remove', removeListener) + }) + } + } +} diff --git a/res/app/device-list/device-list-details.jade b/res/app/device-list/device-list-details.jade new file mode 100644 index 00000000..ecbe2e6d --- /dev/null +++ b/res/app/device-list/device-list-details.jade @@ -0,0 +1 @@ +table diff --git a/res/app/device-list/device-list.jade b/res/app/device-list/device-list.jade index 4d7f4d41..fcc5130a 100644 --- a/res/app/device-list/device-list.jade +++ b/res/app/device-list/device-list.jade @@ -28,7 +28,11 @@ div.stf-device-list .col-md-12 .widget-container.fluid-height .widget-content.padded - tabset.overflow-auto(ng-if='activeTabs').device-list-active-tabs + label(ng-repeat='column in columns') + input(type='checkbox', ng-model='column.selected') + span {{ column.name }} + device-list-details(tracker='tracker', columns='columns') + //tabset.overflow-auto(ng-if='activeTabs').device-list-active-tabs tab(active='activeTabs.devices') tab-heading i.fa.fa-th-large diff --git a/res/app/device-list/index.js b/res/app/device-list/index.js index 363d1fbe..b7614f2f 100644 --- a/res/app/device-list/index.js +++ b/res/app/device-list/index.js @@ -22,5 +22,8 @@ module.exports = angular.module('device-list', [ $templateCache.put('device-list/details/user.jade', require('./details/user.jade')) }]) .controller('DeviceListCtrl', require('./device-list-controller')) - .controller('DeviceListDetailsCtrl', - require('./device-list-details-controller')) + .controller( + 'DeviceListDetailsCtrl' + , require('./device-list-details-controller') + ) + .directive('deviceListDetails', require('./device-list-details-directive'))