var _ = require('lodash') var filterOps = { '<': function(a, filterValue) { return a < filterValue } , '<=': function(a, filterValue) { return a <= filterValue } , '>': function(a, filterValue) { return a > filterValue } , '>=': function(a, filterValue) { return a >= filterValue } , '=': function(a, filterValue) { return a === filterValue } } module.exports = function DeviceColumnService($filter, gettext) { // Definitions for all possible values. return { state: DeviceStatusCell({ title: gettext('Status') , value: function(device) { return $filter('translate')(device.enhancedStateAction) } }) , model: DeviceModelCell({ title: gettext('Model') , value: function(device) { return device.model || device.serial } }) , name: DeviceNameCell({ title: gettext('Product') , value: function(device) { return device.name || device.model || device.serial } }) , operator: TextCell({ title: gettext('Carrier') , value: function(device) { return device.operator || '' } }) , releasedAt: DateCell({ title: gettext('Released') , value: function(device) { return device.releasedAt ? new Date(device.releasedAt) : null } }) , version: TextCell({ title: gettext('OS') , value: function(device) { return device.version || '' } , compare: function(deviceA, deviceB) { var va = (deviceA.version || '0').split('.') , vb = (deviceB.version || '0').split('.') , la = va.length , lb = vb.length for (var i = 0, l = Math.max(la, lb); i < l; ++i) { var a = i < la ? parseInt(va[i], 10) : 0 , b = i < lb ? parseInt(vb[i], 10) : 0 , diff = a - b // One of the values might be something like 'M'. If so, do a string // comparison instead. if (isNaN(diff)) { diff = compareRespectCase(va[i], vb[i]) } if (diff !== 0) { return diff } } return 0 } , filter: function(device, filter) { var va = (device.version || '0').split('.') , vb = (filter.query || '0').split('.') , la = va.length , lb = vb.length , op = filterOps[filter.op || '='] // We have a single value and no operator or field. It matches // too easily, let's wait for a dot (e.g. '5.'). An example of a // bad match would be an unquoted query for 'Nexus 5', which targets // a very specific device but may easily match every Nexus device // as the two terms are handled separately. if (filter.op === null && filter.field === null && lb === 1) { return false } if (vb[lb - 1] === '') { // This means that the query is not complete yet, and we're // looking at something like "4.", which means that the last part // should be ignored. vb.pop() lb -= 1 } for (var i = 0, l = Math.min(la, lb); i < l; ++i) { var a = parseInt(va[i], 10) , b = parseInt(vb[i], 10) // One of the values might be non-numeric, e.g. 'M'. In that case // filter by string value instead. if (isNaN(a) || isNaN(b)) { if (!op(va[i], vb[i])) { return false } } else { if (!op(a, b)) { return false } } } return true } }) , network: TextCell({ title: gettext('Network') , value: function(device) { return device.phone ? device.phone.network : '' } }) , display: TextCell({ title: gettext('Screen') , defaultOrder: 'desc' , value: function(device) { return device.display && device.display.width ? device.display.width + 'x' + device.display.height : '' } , compare: function(deviceA, deviceB) { var va = deviceA.display && deviceA.display.width ? deviceA.display.width * deviceA.display.height : 0 var vb = deviceB.display && deviceB.display.width ? deviceB.display.width * deviceB.display.height : 0 return va - vb } }) , browser: DeviceBrowserCell({ title: gettext('Browser') , value: function(device) { return device.browser || {apps: []} } }) , serial: TextCell({ title: gettext('Serial') , value: function(device) { return device.serial || '' } }) , manufacturer: TextCell({ title: gettext('Manufacturer') , value: function(device) { return device.manufacturer || '' } }) , sdk: NumberCell({ title: gettext('SDK') , defaultOrder: 'desc' , value: function(device) { return device.sdk || '' } }) , abi: TextCell({ title: gettext('ABI') , value: function(device) { return device.abi || '' } }) , phone: TextCell({ title: gettext('Phone') , value: function(device) { return device.phone ? device.phone.phoneNumber : '' } }) , imei: TextCell({ title: gettext('Phone IMEI') , value: function(device) { return device.phone ? device.phone.imei : '' } }) , iccid: TextCell({ title: gettext('Phone ICCID') , value: function(device) { return device.phone ? device.phone.iccid : '' } }) , batteryHealth: TextCell({ title: gettext('Battery Health') , value: function(device) { return device.battery ? $filter('translate')(device.enhancedBatteryHealth) : '' } }) , batterySource: TextCell({ title: gettext('Battery Source') , value: function(device) { return device.battery ? $filter('translate')(device.enhancedBatterySource) : '' } }) , batteryStatus: TextCell({ title: gettext('Battery Status') , value: function(device) { return device.battery ? $filter('translate')(device.enhancedBatteryStatus) : '' } }) , batteryLevel: TextCell({ title: gettext('Battery Level') , value: function(device) { return device.battery ? Math.floor(device.battery.level / device.battery.scale * 100) + '%' : '' } , compare: function(deviceA, deviceB) { var va = deviceA.battery ? deviceA.battery.level : 0 , vb = deviceB.battery ? deviceB.battery.level : 0 return va - vb } }) , batteryTemp: TextCell({ title: gettext('Battery Temp') , value: function(device) { return device.battery ? device.battery.temp + '°C' : '' } , compare: function(deviceA, deviceB) { var va = deviceA.battery ? deviceA.battery.temp : 0 , vb = deviceB.battery ? deviceB.battery.temp : 0 return va - vb } }) , provider: TextCell({ title: gettext('Location') , value: function(device) { return device.provider ? device.provider.name : '' } }) , notes: DeviceNoteCell({ title: gettext('Notes') , value: function(device) { return device.notes || '' } }) , owner: LinkCell({ title: gettext('User') , target: '_blank' , value: function(device) { return device.owner ? device.owner.name : '' } , link: function(device) { return device.owner ? device.enhancedUserProfileUrl : '' } }) } } function zeroPadTwoDigit(digit) { return digit < 10 ? '0' + digit : '' + digit } function compareIgnoreCase(a, b) { var la = (a || '').toLowerCase() , lb = (b || '').toLowerCase() return la === lb ? 0 : (la < lb ? -1 : 1) } function filterIgnoreCase(a, filterValue) { var va = (a || '').toLowerCase() , vb = filterValue.toLowerCase() return va.indexOf(vb) !== -1 } function compareRespectCase(a, b) { return a === b ? 0 : (a < b ? -1 : 1) } function TextCell(options) { return _.defaults(options, { title: options.title , defaultOrder: 'asc' , build: function () { var td = document.createElement('td') td.appendChild(document.createTextNode('')) return td } , update: function(td, item) { var t = td.firstChild t.nodeValue = options.value(item) return td } , compare: function(a, b) { return compareIgnoreCase(options.value(a), options.value(b)) } , filter: function(item, filter) { return filterIgnoreCase(options.value(item), filter.query) } }) } function NumberCell(options) { return _.defaults(options, { title: options.title , defaultOrder: 'asc' , build: function () { var td = document.createElement('td') td.appendChild(document.createTextNode('')) return td } , update: function(td, item) { var t = td.firstChild t.nodeValue = options.value(item) return td } , compare: function(a, b) { return options.value(a) - options.value(b) } , filter: (function() { return function(item, filter) { return filterOps[filter.op || '=']( options.value(item) , +filter.query ) } })() }) } function DateCell(options) { return _.defaults(options, { title: options.title , defaultOrder: 'desc' , build: function () { var td = document.createElement('td') td.appendChild(document.createTextNode('')) return td } , update: function(td, item) { var t = td.firstChild , date = options.value(item) if (date) { t.nodeValue = date.getFullYear() + '-' + zeroPadTwoDigit(date.getMonth() + 1) + '-' + zeroPadTwoDigit(date.getDate()) } else { t.nodeValue = '' } return td } , compare: function(a, b) { var va = options.value(a) || 0 , vb = options.value(b) || 0 return va - vb } , filter: (function() { function dateNumber(d) { return d ? d.getFullYear() * 10000 + d.getMonth() * 100 + d.getDate() : 0 } return function(item, filter) { var filterDate = new Date(filter.query) , va = dateNumber(options.value(item)) , vb = dateNumber(filterDate) return filterOps[filter.op || '='](va, vb) } })() }) } function LinkCell(options) { return _.defaults(options, { title: options.title , defaultOrder: 'asc' , build: function () { var td = document.createElement('td') , a = document.createElement('a') a.appendChild(document.createTextNode('')) td.appendChild(a) return td } , update: function(td, item) { var a = td.firstChild , t = a.firstChild , href = options.link(item) if (href) { a.setAttribute('href', href) } else { a.removeAttribute('href') } a.target = options.target || '' t.nodeValue = options.value(item) return td } , compare: function(a, b) { return compareIgnoreCase(options.value(a), options.value(b)) } , filter: function(item, filter) { return filterIgnoreCase(options.value(item), filter.query) } }) } function DeviceBrowserCell(options) { return _.defaults(options, { title: options.title , defaultOrder: 'asc' , build: function() { var td = document.createElement('td') , span = document.createElement('span') span.className = 'device-browser-list' td.appendChild(span) return td } , update: function(td, device) { var span = td.firstChild , browser = options.value(device) , apps = browser.apps.slice().sort(function(appA, appB) { return compareIgnoreCase(appA.name, appB.name) }) for (var i = 0, l = apps.length; i < l; ++i) { var app = apps[i] , img = span.childNodes[i] || span.appendChild(document.createElement('img')) , src = '/static/app/browsers/icon/36x36/' + (app.type || '_default') + '.png' // Only change if necessary so that we don't trigger a download if (img.getAttribute('src') !== src) { img.setAttribute('src', src) } img.title = app.name + ' (' + app.developer + ')' } while (span.childNodes.length > browser.apps.length) { span.removeChild(span.lastChild) } return td } , compare: function(a, b) { return options.value(a).apps.length - options.value(b).apps.length } , filter: function(device, filter) { return options.value(device).apps.some(function(app) { return filterIgnoreCase(app.type, filter.query) }) } }) } function DeviceModelCell(options) { return _.defaults(options, { title: options.title , defaultOrder: 'asc' , build: function() { var td = document.createElement('td') , span = document.createElement('span') , image = document.createElement('img') span.className = 'device-small-image' image.className = 'device-small-image-img pointer' span.appendChild(image) td.appendChild(span) td.appendChild(document.createTextNode('')) return td } , update: function(td, device) { var span = td.firstChild , image = span.firstChild , t = span.nextSibling , src = '/static/app/devices/icon/x24/' + (device.image || '_default.jpg') // Only change if necessary so that we don't trigger a download if (image.getAttribute('src') !== src) { image.setAttribute('src', src) } t.nodeValue = options.value(device) return td } , compare: function(a, b) { return compareRespectCase(options.value(a), options.value(b)) } , filter: function(device, filter) { return filterIgnoreCase(options.value(device), filter.query) } }) } function DeviceNameCell(options) { return _.defaults(options, { title: options.title , defaultOrder: 'asc' , 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 if (device.using) { a.className = 'device-product-name-using' a.href = '#!/control/' + device.serial } else if (device.usable) { a.className = 'device-product-name-usable' a.href = '#!/control/' + device.serial } else { a.className = 'device-product-name-unusable' a.removeAttribute('href') } t.nodeValue = options.value(device) return td } , compare: function(a, b) { return compareIgnoreCase(options.value(a), options.value(b)) } , filter: function(device, filter) { return filterIgnoreCase(options.value(device), filter.query) } }) } function DeviceStatusCell(options) { var stateClasses = { using: 'state-using btn-primary' , busy: 'state-busy btn-warning' , available: 'state-available btn-primary-outline' , ready: 'state-ready btn-primary-outline' , present: 'state-present btn-primary-outline' , preparing: 'state-preparing btn-primary-outline btn-success-outline' , unauthorized: 'state-unauthorized btn-danger-outline' , offline: 'state-offline btn-warning-outline' } return _.defaults(options, { title: options.title , defaultOrder: 'asc' , 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.className = 'btn btn-xs device-status ' + (stateClasses[device.state] || 'btn-default-outline') if (device.usable && !device.using) { a.href = '#!/control/' + device.serial } else { a.removeAttribute('href') } t.nodeValue = options.value(device) return td } , compare: (function() { var order = { using: 10 , available: 20 , busy: 30 , ready: 40 , preparing: 50 , unauthorized: 60 , offline: 70 , present: 80 , absent: 90 } return function(deviceA, deviceB) { return order[deviceA.state] - order[deviceB.state] } })() , filter: function(device, filter) { return device.state === filter.query } }) } function DeviceNoteCell(options) { return _.defaults(options, { title: options.title , defaultOrder: 'asc' , build: function () { var td = document.createElement('td') , span = document.createElement('span') , i = document.createElement('i') td.className = 'device-note' span.className = 'xeditable-wrapper' span.appendChild(document.createTextNode('')) i.className = 'device-note-edit fa fa-pencil pointer' td.appendChild(span) td.appendChild(i) return td } , update: function(td, item) { var span = td.firstChild , t = span.firstChild t.nodeValue = options.value(item) return td } , compare: function(a, b) { return compareIgnoreCase(options.value(a), options.value(b)) } , filter: function(item, filter) { return filterIgnoreCase(options.value(item), filter.query) } }) }