mirror of
https://github.com/openstf/stf
synced 2025-10-06 03:50:04 +02:00
505 lines
14 KiB
JavaScript
505 lines
14 KiB
JavaScript
var patchArray = require('./util/patch-array')
|
|
|
|
module.exports = function DeviceListDetailsDirective(
|
|
$filter
|
|
, gettext
|
|
, DeviceColumnService
|
|
, GroupService
|
|
, $rootScope
|
|
, LightboxImageService
|
|
) {
|
|
return {
|
|
restrict: 'E'
|
|
, template: require('./device-list-details.jade')
|
|
, scope: {
|
|
tracker: '&tracker'
|
|
, columns: '&columns'
|
|
, sort: '=sort'
|
|
, filter: '&filter'
|
|
}
|
|
, link: function (scope, element) {
|
|
var tracker = scope.tracker()
|
|
, activeColumns = []
|
|
, activeSorting = []
|
|
, activeFilters = []
|
|
, table = element.find('table')[0]
|
|
, tbody = table.createTBody()
|
|
, rows = tbody.rows
|
|
, prefix = 'd' + Math.floor(Math.random() * 1000000) + '-'
|
|
, mapping = Object.create(null)
|
|
|
|
|
|
function kickDevice(device, force) {
|
|
return GroupService.kick(device, force).catch(function (e) {
|
|
console.log(e)
|
|
alert($filter('translate')(gettext('Device cannot get kicked from the group')))
|
|
})
|
|
}
|
|
|
|
function inviteDevice(device) {
|
|
return GroupService.invite(device).then(function () {
|
|
scope.$digest()
|
|
})
|
|
}
|
|
|
|
function checkDeviceStatus (e) {
|
|
if (e.target.classList.contains('device-status')) {
|
|
var id = e.target.parentNode.parentNode.id
|
|
var device = mapping[id]
|
|
|
|
if (e.shiftKey && device.state === 'available') {
|
|
inviteDevice(device)
|
|
e.preventDefault()
|
|
}
|
|
|
|
if ($rootScope.adminMode && device.state === 'busy') {
|
|
kickDevice(device, true)
|
|
e.preventDefault()
|
|
}
|
|
else if (device.using) {
|
|
kickDevice(device)
|
|
e.preventDefault()
|
|
}
|
|
}
|
|
}
|
|
|
|
function checkDeviceSmallImage(e) {
|
|
if (e.target.classList.contains('device-small-image-img')) {
|
|
var id = e.target.parentNode.parentNode.parentNode.id
|
|
var device = mapping[id]
|
|
|
|
var title = device.name
|
|
var enhancedPhoto800 = '/static/app/devices/photo/x800/' + device.image
|
|
LightboxImageService.open(title, enhancedPhoto800)
|
|
}
|
|
}
|
|
|
|
element.on('click', function (e) {
|
|
checkDeviceStatus(e)
|
|
checkDeviceSmallImage(e)
|
|
})
|
|
|
|
// Import column definitions
|
|
scope.columnDefinitions = DeviceColumnService
|
|
|
|
// Sorting
|
|
scope.sortBy = function(column, multiple) {
|
|
function findInSorting(sorting) {
|
|
for (var i = 0, l = sorting.length; i < l; ++i) {
|
|
if (sorting[i].name === column.name) {
|
|
return sorting[i]
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
var swap = {
|
|
asc: 'desc'
|
|
, desc: 'asc'
|
|
}
|
|
|
|
var fixedMatch = findInSorting(scope.sort.fixed)
|
|
if (fixedMatch) {
|
|
fixedMatch.order = swap[fixedMatch.order]
|
|
return
|
|
}
|
|
|
|
var userMatch = findInSorting(scope.sort.user)
|
|
if (userMatch) {
|
|
userMatch.order = swap[userMatch.order]
|
|
if (!multiple) {
|
|
scope.sort.user = [userMatch]
|
|
}
|
|
}
|
|
else {
|
|
if (!multiple) {
|
|
scope.sort.user = []
|
|
}
|
|
scope.sort.user.push({
|
|
name: column.name
|
|
, order: scope.columnDefinitions[column.name].defaultOrder || 'asc'
|
|
})
|
|
}
|
|
}
|
|
|
|
// Watch for sorting changes
|
|
scope.$watch(
|
|
function() {
|
|
return scope.sort
|
|
}
|
|
, function(newValue) {
|
|
activeSorting = newValue.fixed.concat(newValue.user)
|
|
scope.sortedColumns = Object.create(null)
|
|
activeSorting.forEach(function(sort) {
|
|
scope.sortedColumns[sort.name] = sort
|
|
})
|
|
sortAll()
|
|
}
|
|
, true
|
|
)
|
|
|
|
// Watch for column updates
|
|
scope.$watch(
|
|
function() {
|
|
return scope.columns()
|
|
}
|
|
, function(newValue) {
|
|
updateColumns(newValue)
|
|
}
|
|
, true
|
|
)
|
|
|
|
// Update now so that we don't have to wait for the scope watcher to
|
|
// trigger.
|
|
updateColumns(scope.columns())
|
|
|
|
// Updates visible columns. This method doesn't necessarily have to be
|
|
// the fastest because it shouldn't get called all the time.
|
|
function updateColumns(columnSettings) {
|
|
var newActiveColumns = []
|
|
|
|
// Check what we're supposed to show now
|
|
columnSettings.forEach(function(column) {
|
|
|
|
var skipColumn = !$rootScope.adminMode && scope.columnDefinitions[column.name].admin
|
|
if (skipColumn) {
|
|
column.selected = false
|
|
return
|
|
}
|
|
|
|
if (column.selected) {
|
|
newActiveColumns.push(column.name)
|
|
}
|
|
})
|
|
|
|
// Figure out the patch
|
|
var patch = patchArray(activeColumns, newActiveColumns)
|
|
|
|
// Set up new active columns
|
|
activeColumns = newActiveColumns
|
|
|
|
return patchAll(patch)
|
|
}
|
|
|
|
// Updates filters on visible items.
|
|
function updateFilters(filters) {
|
|
activeFilters = filters
|
|
return filterAll()
|
|
}
|
|
|
|
// Applies filteRow() to all rows.
|
|
function filterAll() {
|
|
for (var i = 0, l = rows.length; i < l; ++i) {
|
|
filterRow(rows[i], mapping[rows[i].id])
|
|
}
|
|
}
|
|
|
|
// Filters a row, perhaps removing it from view.
|
|
function filterRow(row, device) {
|
|
if (match(device)) {
|
|
row.classList.remove('filter-out')
|
|
}
|
|
else {
|
|
row.classList.add('filter-out')
|
|
}
|
|
}
|
|
|
|
// Checks whether the device matches the currently active filters.
|
|
function match(device) {
|
|
for (var i = 0, l = activeFilters.length; i < l; ++i) {
|
|
var filter = activeFilters[i]
|
|
, column
|
|
if (filter.field) {
|
|
column = scope.columnDefinitions[filter.field]
|
|
if (column && !column.filter(device, filter)) {
|
|
return false
|
|
}
|
|
}
|
|
else {
|
|
var found = false
|
|
for (var j = 0, k = activeColumns.length; j < k; ++j) {
|
|
column = scope.columnDefinitions[activeColumns[j]]
|
|
if (column && column.filter(device, filter)) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if (!found) {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Update now so we're up to date.
|
|
updateFilters(scope.filter())
|
|
|
|
// Watch for filter updates.
|
|
scope.$watch(
|
|
function() {
|
|
return scope.filter()
|
|
}
|
|
, function(newValue) {
|
|
updateFilters(newValue)
|
|
}
|
|
, true
|
|
)
|
|
|
|
// Calculates a DOM ID for the device. Should be consistent for the
|
|
// same device within the same table, but unique among other tables.
|
|
function calculateId(device) {
|
|
return prefix + device.serial
|
|
}
|
|
|
|
// Compares two devices using the currently active sorting. Returns <0
|
|
// if deviceA is smaller, >0 if deviceA is bigger, or 0 if equal.
|
|
var compare = (function() {
|
|
var mapping = {
|
|
asc: 1
|
|
, desc: -1
|
|
}
|
|
return function(deviceA, deviceB) {
|
|
var diff
|
|
|
|
// Find the first difference
|
|
for (var i = 0, l = activeSorting.length; i < l; ++i) {
|
|
var sort = activeSorting[i]
|
|
diff = scope.columnDefinitions[sort.name].compare(deviceA, deviceB)
|
|
if (diff !== 0) {
|
|
diff *= mapping[sort.order]
|
|
break
|
|
}
|
|
}
|
|
|
|
return diff
|
|
}
|
|
})()
|
|
|
|
// Creates a completely new row for the device. Means that this is
|
|
// the first time we see the device.
|
|
function createRow(device) {
|
|
var id = calculateId(device)
|
|
, tr = document.createElement('tr')
|
|
, td
|
|
|
|
tr.id = id
|
|
|
|
if (!device.usable) {
|
|
tr.classList.add('device-not-usable')
|
|
}
|
|
|
|
for (var i = 0, l = activeColumns.length; i < l; ++i) {
|
|
td = scope.columnDefinitions[activeColumns[i]].build()
|
|
scope.columnDefinitions[activeColumns[i]].update(td, device)
|
|
tr.appendChild(td)
|
|
}
|
|
|
|
mapping[id] = device
|
|
|
|
return tr
|
|
}
|
|
|
|
// Patches all rows.
|
|
function patchAll(patch) {
|
|
for (var i = 0, l = rows.length; i < l; ++i) {
|
|
patchRow(rows[i], mapping[rows[i].id], patch)
|
|
}
|
|
}
|
|
|
|
// Patches the given row by running the given patch operations in
|
|
// order. The operations must take into account index changes caused
|
|
// by previous operations.
|
|
function patchRow(tr, device, patch) {
|
|
for (var i = 0, l = patch.length; i < l; ++i) {
|
|
var op = patch[i]
|
|
switch (op[0]) {
|
|
case 'insert':
|
|
var col = scope.columnDefinitions[op[2]]
|
|
tr.insertBefore(col.update(col.build(), device), tr.cells[op[1]])
|
|
break
|
|
case 'remove':
|
|
tr.deleteCell(op[1])
|
|
break
|
|
case 'swap':
|
|
tr.insertBefore(tr.cells[op[1]], tr.cells[op[2]])
|
|
tr.insertBefore(tr.cells[op[2]], tr.cells[op[1]])
|
|
break
|
|
}
|
|
}
|
|
|
|
return tr
|
|
}
|
|
|
|
// Updates all the columns in the row. Note that the row must be in
|
|
// the right format already (built with createRow() and patched with
|
|
// patchRow() if necessary).
|
|
function updateRow(tr, device) {
|
|
var id = calculateId(device)
|
|
|
|
tr.id = id
|
|
|
|
if (!device.usable) {
|
|
tr.classList.add('device-not-usable')
|
|
}
|
|
else {
|
|
tr.classList.remove('device-not-usable')
|
|
}
|
|
|
|
for (var i = 0, l = activeColumns.length; i < l; ++i) {
|
|
scope.columnDefinitions[activeColumns[i]].update(tr.cells[i], device)
|
|
}
|
|
|
|
return tr
|
|
}
|
|
|
|
// Inserts a row into the table into its correct position according to
|
|
// current sorting.
|
|
function insertRow(tr, deviceA) {
|
|
return insertRowToSegment(tr, deviceA, 0, rows.length - 1)
|
|
}
|
|
|
|
// Inserts a row into a segment of the table into its correct position
|
|
// according to current sorting. The value of `hi` is the index
|
|
// of the last item in the segment, or -1 if none. The value of `lo`
|
|
// is the index of the first item in the segment, or 0 if none.
|
|
function insertRowToSegment(tr, deviceA, lo, hi) {
|
|
var total = rows.length
|
|
|
|
if (lo > hi) {
|
|
// This means that `lo` refers to the first item of the next
|
|
// segment (which may or may not exist), and we should put the
|
|
// row before it.
|
|
tbody.insertBefore(tr, lo < total ? rows[lo] : null)
|
|
}
|
|
else {
|
|
var after = true
|
|
, pivot = 0
|
|
, deviceB
|
|
|
|
while (lo <= hi) {
|
|
pivot = ~~((lo + hi) / 2)
|
|
deviceB = mapping[rows[pivot].id]
|
|
|
|
var 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])
|
|
}
|
|
}
|
|
}
|
|
|
|
// Compares a row to its siblings to see if it's still in the correct
|
|
// position. Returns <0 if the device should actually go somewhere
|
|
// before the previous row, >0 if it should go somewhere after the next
|
|
// row, or 0 if the position is already correct.
|
|
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
|
|
}
|
|
|
|
// Sort all rows.
|
|
function sortAll() {
|
|
// This could be improved by getting rid of the array copying. The
|
|
// copy is made because rows can't be sorted directly.
|
|
var sorted = [].slice.call(rows).sort(function(rowA, rowB) {
|
|
return compare(mapping[rowA.id], mapping[rowB.id])
|
|
})
|
|
|
|
// Now, if we just append all the elements, they will be in the
|
|
// correct order in the table.
|
|
for (var i = 0, l = sorted.length; i < l; ++i) {
|
|
tbody.appendChild(sorted[i])
|
|
}
|
|
}
|
|
|
|
// Triggers when the tracker sees a device for the first time.
|
|
function addListener(device) {
|
|
insertRow(createRow(device), device)
|
|
}
|
|
|
|
// Triggers when the tracker notices that a device changed.
|
|
function changeListener(device) {
|
|
var id = calculateId(device)
|
|
, tr = tbody.children[id]
|
|
|
|
if (tr) {
|
|
// First, update columns
|
|
updateRow(tr, device)
|
|
|
|
// Maybe the row is not sorted correctly anymore?
|
|
var 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 - 1)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Triggers when a device is removed entirely from the tracker.
|
|
function removeListener(device) {
|
|
var id = calculateId(device)
|
|
, tr = tbody.children[id]
|
|
|
|
if (tr) {
|
|
tbody.removeChild(tr)
|
|
}
|
|
|
|
delete mapping[id]
|
|
}
|
|
|
|
tracker.on('add', addListener)
|
|
tracker.on('change', changeListener)
|
|
tracker.on('remove', removeListener)
|
|
|
|
// Maybe we're already late
|
|
tracker.devices.forEach(addListener)
|
|
|
|
scope.$on('$destroy', function() {
|
|
tracker.removeListener('add', addListener)
|
|
tracker.removeListener('change', changeListener)
|
|
tracker.removeListener('remove', removeListener)
|
|
})
|
|
}
|
|
}
|
|
}
|