| OLD | NEW | 
|---|
| (Empty) |  | 
|  | 1 <!-- | 
|  | 2   Copyright 2016 The LUCI Authors. All rights reserved. | 
|  | 3   Use of this source code is governed under the Apache License, Version 2.0 | 
|  | 4   that can be found in the LICENSE file. | 
|  | 5 | 
|  | 6   This in an HTML Import-able file that contains the definition | 
|  | 7   of the following elements: | 
|  | 8 | 
|  | 9     <bot-list> | 
|  | 10 | 
|  | 11   bot-list creats a dynamic table for viewing swarming bots.  Columns can be | 
|  | 12   dynamically filtered and it supports client-side filtering. | 
|  | 13 | 
|  | 14   Properties: | 
|  | 15     None.  This is a top-level element. | 
|  | 16 | 
|  | 17   Methods: | 
|  | 18     None. | 
|  | 19 | 
|  | 20   Events: | 
|  | 21     None. | 
|  | 22 --> | 
|  | 23 | 
|  | 24 <link rel="import" href="/res/imp/bower_components/iron-flex-layout/iron-flex-la
     yout-classes.html"> | 
|  | 25 <link rel="import" href="/res/imp/bower_components/polymer/polymer.html"> | 
|  | 26 | 
|  | 27 <link rel="import" href="/res/imp/common/sort-toggle.html"> | 
|  | 28 <link rel="import" href="/res/imp/common/swarming-app.html"> | 
|  | 29 | 
|  | 30 <link rel="import" href="bot-filters.html"> | 
|  | 31 <link rel="import" href="bot-list-data.html"> | 
|  | 32 <link rel="import" href="bot-list-shared.html"> | 
|  | 33 | 
|  | 34 | 
|  | 35 <dom-module id="bot-list"> | 
|  | 36   <template> | 
|  | 37     <style include="iron-flex iron-flex-alignment iron-positioning"> | 
|  | 38       bot-filters { | 
|  | 39         margin-bottom:2px; | 
|  | 40       } | 
|  | 41       .bot { | 
|  | 42         margin:5px; | 
|  | 43         max-width:400px; | 
|  | 44         min-height:100px; | 
|  | 45         min-width:300px; | 
|  | 46       } | 
|  | 47       table { | 
|  | 48         border-collapse: collapse; | 
|  | 49         border: 1px solid black; | 
|  | 50       } | 
|  | 51       td, th { | 
|  | 52         border: 1px solid black; | 
|  | 53         padding: 5px; | 
|  | 54       } | 
|  | 55 | 
|  | 56       .bot-list th > span { | 
|  | 57         display:inline-block; | 
|  | 58       } | 
|  | 59 | 
|  | 60       .quarantined, .bad-device { | 
|  | 61         background-color: #ffdddd; | 
|  | 62         border: 2px solid black; | 
|  | 63       } | 
|  | 64       .dead { | 
|  | 65         background-color: #cccccc; | 
|  | 66         border: 2px solid black; | 
|  | 67       } | 
|  | 68     </style> | 
|  | 69 | 
|  | 70     <swarming-app | 
|  | 71       auth_headers="{{auth_headers}}" | 
|  | 72       busy="[[busy]]" | 
|  | 73       name="Swarming Bot List"> | 
|  | 74 | 
|  | 75       <bot-filters | 
|  | 76         primary_map="[[primary_map]]" | 
|  | 77         primary_arr="[[primary_arr]]" | 
|  | 78 | 
|  | 79         columns="{{columns}}" | 
|  | 80         filter="{{filter}}" | 
|  | 81         verbose="{{verbose}}"> | 
|  | 82       </bot-filters> | 
|  | 83 | 
|  | 84       <bot-list-data | 
|  | 85         auth_headers="[[auth_headers]]" | 
|  | 86 | 
|  | 87         bots="{{bots}}" | 
|  | 88         busy="{{busy}}" | 
|  | 89         primary_map="{{primary_map}}" | 
|  | 90         primary_arr="{{primary_arr}}"> | 
|  | 91       </bot-list-data> | 
|  | 92 | 
|  | 93       <table class="bot-list"> | 
|  | 94         <thead on-sort_change="sortChange"> | 
|  | 95         <!-- To allow for dynamic columns without having a lot of copy-pasted | 
|  | 96         code, we break columns up into "special" and "plain" columns. Special | 
|  | 97         columns require some sort of HTML output (e.g. anchor tags) and plain | 
|  | 98         columns just output text.  The plain columns use Polymer functions to | 
|  | 99         insert their text [_header(), _column(), _deviceColumn()].  Polymer | 
|  | 100         functions do not allow HTML (to avoid XSS), so special columns, like id | 
|  | 101         and task are inserted in a fixed order. | 
|  | 102         --> | 
|  | 103           <th> | 
|  | 104             <span>Bot Id</span> | 
|  | 105             <sort-toggle name="id"></sort-toggle> | 
|  | 106           </th> | 
|  | 107           <!-- This wonky syntax is the proper way to listen to changes on an | 
|  | 108           array (we are listening to all subproperties). The element returned is | 
|  | 109           not of much use, so we'll ignore it in _hide() and use this.columns. | 
|  | 110           --> | 
|  | 111           <th hidden$="[[_hide('task', columns.*)]]"> | 
|  | 112             <span>Current Task</span> | 
|  | 113             <sort-toggle name="task"></sort-toggle> | 
|  | 114           </th> | 
|  | 115 | 
|  | 116           <template is="dom-repeat" | 
|  | 117                 items="[[plain_columns]]" | 
|  | 118                 as="c"> | 
|  | 119             <th hidden$="[[_hide(c)]]"> | 
|  | 120               <span>[[_header(c)]]</span> | 
|  | 121               <sort-toggle name="[[c]]"></sort-toggle> | 
|  | 122             </th> | 
|  | 123           </template> | 
|  | 124         </thead> | 
|  | 125         <tbody> | 
|  | 126           <template id="bot_table" is="dom-repeat" | 
|  | 127                 items="[[bots]]" | 
|  | 128                 as="bot" | 
|  | 129                 initial-count=50 | 
|  | 130                 sort="_sortBotTable" | 
|  | 131                 filter="_filterBotTable"> | 
|  | 132 | 
|  | 133             <tr class$="[[_botClass(bot)]]"> | 
|  | 134               <td> | 
|  | 135                 <a class="center" | 
|  | 136                    href$="[[_botLink(bot.bot_id)]]" | 
|  | 137                    target="_blank"> | 
|  | 138                    [[bot.bot_id]] | 
|  | 139                 </a> | 
|  | 140               </td> | 
|  | 141               <td hidden$="[[_hide('task', columns.*)]]"> | 
|  | 142                 Current Task: | 
|  | 143                 <a href$="[[_taskLink(bot)]]">[[_taskId(bot)]]</a> | 
|  | 144               </td> | 
|  | 145 | 
|  | 146               <template is="dom-repeat" | 
|  | 147                     items="[[plain_columns]]" | 
|  | 148                     as="c"> | 
|  | 149                 <td hidden$="[[_hide(c)]]"> | 
|  | 150                   [[_column(c, bot, verbose)]] | 
|  | 151                 </td> | 
|  | 152               </template> | 
|  | 153 | 
|  | 154             </tr> | 
|  | 155             <template is="dom-repeat" | 
|  | 156                   items="[[_devices(bot)]]" | 
|  | 157                   as="device"> | 
|  | 158               <tr hidden$="[[_hide('devices', columns.*)]]" | 
|  | 159                   class$="[[_deviceClass(device)]]"> | 
|  | 160                 <td></td> | 
|  | 161                 <td hidden$="[[_hide('task', columns.*)]]"></td> | 
|  | 162                 <template is="dom-repeat" | 
|  | 163                       items="[[plain_columns]]" | 
|  | 164                       as="c"> | 
|  | 165                   <td hidden$="[[_hide(c)]]"> | 
|  | 166                     [[_deviceColumn(c, device, verbose)]] | 
|  | 167                   </td> | 
|  | 168                 </template> | 
|  | 169               </tr> | 
|  | 170             </template> <!--devices repeat--> | 
|  | 171           </template> <!--bot-table repeat--> | 
|  | 172         </tbody> | 
|  | 173       </table> | 
|  | 174 | 
|  | 175     </swarming-app> | 
|  | 176 | 
|  | 177   </template> | 
|  | 178   <script> | 
|  | 179   (function(){ | 
|  | 180     var special_columns = ["id", "task"]; | 
|  | 181 | 
|  | 182     var headerMap = { | 
|  | 183       // "id" and "task" are special, so they don't go here and have their | 
|  | 184       // headers hard-coded below. | 
|  | 185       "cores": "Cores", | 
|  | 186       "cpu": "CPU", | 
|  | 187       "devices": "Devices", | 
|  | 188       "gpu": "GPU", | 
|  | 189       "os": "OS", | 
|  | 190       "pool": "Pool", | 
|  | 191       "status": "Status", | 
|  | 192     }; | 
|  | 193 | 
|  | 194     // This maps column name to a function that will return the content for a | 
|  | 195     // given bot. These functions are bound to this element, and have access | 
|  | 196     // to all functions defined here and in bot-list-shared. | 
|  | 197     var columnMap = { | 
|  | 198       cores: function(bot){ | 
|  | 199         var cores =  this._cores(bot); | 
|  | 200         if (this.verbose){ | 
|  | 201           return cores.join(" | "); | 
|  | 202         } | 
|  | 203         return cores[0]; | 
|  | 204       }, | 
|  | 205       cpu: function(bot){ | 
|  | 206         var cpus = this._dimension(bot, 'cpu') || ['Unknown']; | 
|  | 207         if (this.verbose){ | 
|  | 208           return cpus.join(" | "); | 
|  | 209         } | 
|  | 210         return cpus[0]; | 
|  | 211       }, | 
|  | 212       devices: function(bot){ | 
|  | 213         return this._devices(bot).length + " devices attached"; | 
|  | 214       }, | 
|  | 215       gpu: function(bot){ | 
|  | 216         var gpus = this._dimension(bot, 'gpu') | 
|  | 217         if (!gpus) { | 
|  | 218           return "none"; | 
|  | 219         } | 
|  | 220         var verbose = [] | 
|  | 221         var named = []; | 
|  | 222         // non-verbose mode has only the top level GPU info "e.g. NVidia" | 
|  | 223         // which is found by looking for gpu ids w/o a colon. | 
|  | 224         gpus.forEach(function(g){ | 
|  | 225           var alias = this._gpuAlias(g); | 
|  | 226           if (alias === "UNKNOWN") { | 
|  | 227             verbose.push(g); | 
|  | 228             if (g.indexOf(":") === -1) { | 
|  | 229               named.push(g); | 
|  | 230             } | 
|  | 231             return; | 
|  | 232           } | 
|  | 233           verbose.push(this._applyAlias(g, alias)); | 
|  | 234           if (g.indexOf(":") === -1) { | 
|  | 235             named.push(this._applyAlias(g, alias)); | 
|  | 236           } | 
|  | 237         }.bind(this)) | 
|  | 238         if (this.verbose) { | 
|  | 239           return verbose.join(" | "); | 
|  | 240         } | 
|  | 241         return named.join(" | "); | 
|  | 242       }, | 
|  | 243       id: function(bot) { | 
|  | 244         return bot.bot_id; | 
|  | 245       }, | 
|  | 246       os: function(bot) { | 
|  | 247         var os = this._dimension(bot, 'os') || ['Unknown']; | 
|  | 248         if (this.verbose){ | 
|  | 249           return os.join(" | "); | 
|  | 250         } | 
|  | 251         return os[0]; | 
|  | 252       }, | 
|  | 253       pool: function(bot) { | 
|  | 254         var pool = this._dimension(bot, 'pool') || ['Unknown']; | 
|  | 255         return pool.join(" | "); | 
|  | 256       }, | 
|  | 257       status: function(bot) { | 
|  | 258         // If a bot is both dead and quarantined, show the deadness over the | 
|  | 259         // quarentinedness. | 
|  | 260         if (bot.is_dead) { | 
|  | 261           return "Dead: " + bot.is_dead; | 
|  | 262         } | 
|  | 263         if (bot.quarantined) { | 
|  | 264           return "Quarantined: " + bot.quarantined; | 
|  | 265         } | 
|  | 266         return "Alive"; | 
|  | 267       }, | 
|  | 268       task: function(bot){ | 
|  | 269         return this._taskId(bot); | 
|  | 270       }, | 
|  | 271     }; | 
|  | 272 | 
|  | 273     Polymer({ | 
|  | 274       is: 'bot-list', | 
|  | 275       behaviors: [SwarmingBehaviors.BotListBehavior], | 
|  | 276 | 
|  | 277       properties: { | 
|  | 278 | 
|  | 279         columns: { | 
|  | 280           type: Array, | 
|  | 281         }, | 
|  | 282         // Should have a property "filter" which is a function. | 
|  | 283         filter: { | 
|  | 284           type: Object, | 
|  | 285         }, | 
|  | 286 | 
|  | 287         plain_columns: { | 
|  | 288           type: Array, | 
|  | 289           computed: "_stripSpecial(columns.*)", | 
|  | 290         }, | 
|  | 291 | 
|  | 292         // sorts is an array of objects showing how to sort the table. | 
|  | 293         sorts: { | 
|  | 294           type: Array, | 
|  | 295         }, | 
|  | 296 | 
|  | 297         verbose: { | 
|  | 298           type: Boolean, | 
|  | 299         } | 
|  | 300       }, | 
|  | 301 | 
|  | 302       observers: [ | 
|  | 303         '_reRender(filter.*)', | 
|  | 304         '_checkSorts(columns.*)' | 
|  | 305       ], | 
|  | 306 | 
|  | 307       _botClass: function(bot) { | 
|  | 308         if (bot.is_dead) { | 
|  | 309           return "dead"; | 
|  | 310         } | 
|  | 311         if (bot.quarantined) { | 
|  | 312           return "quarantined"; | 
|  | 313         } | 
|  | 314         return ""; | 
|  | 315       }, | 
|  | 316 | 
|  | 317       _botLink: function(id) { | 
|  | 318         // TODO(kjlubick) Make this point to /newui/ when appropriate. | 
|  | 319         return "/restricted/bot/"+id; | 
|  | 320       }, | 
|  | 321 | 
|  | 322       // _checkSorts makes sure that if a column has been removed, the related | 
|  | 323       // sort is also removed. | 
|  | 324       _checkSorts: function() { | 
|  | 325         if (!this.sorts) { | 
|  | 326           return; | 
|  | 327         } | 
|  | 328         this.sorts = this.sorts.filter(function(s){ | 
|  | 329           return this.columns.indexOf(s) !== -1; | 
|  | 330         }.bind(this)); | 
|  | 331         this._reRender(); | 
|  | 332       }, | 
|  | 333 | 
|  | 334       _column: function(col, bot) { | 
|  | 335         return columnMap[col].bind(this)(bot); | 
|  | 336       }, | 
|  | 337 | 
|  | 338       _deviceColumn: function(col, device) { | 
|  | 339         if (col === "devices") { | 
|  | 340           var str = this._androidAlias(device); | 
|  | 341           if (device.okay) { | 
|  | 342             str = this._applyAlias(this._deviceType(device), str); | 
|  | 343           } | 
|  | 344           str += " S/N:"; | 
|  | 345           str += device.serial; | 
|  | 346           return str; | 
|  | 347         } | 
|  | 348         if (col === "status") { | 
|  | 349           return device.state; | 
|  | 350         } | 
|  | 351         return ""; | 
|  | 352       }, | 
|  | 353 | 
|  | 354       _deviceClass: function(device) { | 
|  | 355         if (!device.okay) { | 
|  | 356           return "bad-device"; | 
|  | 357         } | 
|  | 358         return ""; | 
|  | 359       }, | 
|  | 360 | 
|  | 361       _filterBotTable: function(bot) { | 
|  | 362         if (!this.filter || !this.filter.filter) { | 
|  | 363           return true; | 
|  | 364         } | 
|  | 365         return this.filter.filter.bind(this)(bot); | 
|  | 366       }, | 
|  | 367 | 
|  | 368       _header: function(col){ | 
|  | 369         return headerMap[col]; | 
|  | 370       }, | 
|  | 371 | 
|  | 372       _hide: function(col) { | 
|  | 373         return this.columns.indexOf(col) === -1; | 
|  | 374       }, | 
|  | 375 | 
|  | 376       _reRender: function(filter, sort) { | 
|  | 377         this.$.bot_table.render(); | 
|  | 378       }, | 
|  | 379 | 
|  | 380       _sortBotTable: function(botA, botB) { | 
|  | 381         if (!this.sorts) { | 
|  | 382           return 0; | 
|  | 383         } | 
|  | 384         for (var i = 0; i < this.sorts.length; i++) { | 
|  | 385           var s = this.sorts[i]; | 
|  | 386           var botACol = this._column(s.name, botA); | 
|  | 387           var botBCol = this._column(s.name, botB); | 
|  | 388           // s.sort is either -1 or 1 (if it is 0, it should have been removed f
     rom this list). | 
|  | 389           var sort = s.sort * botACol.localeCompare(botBCol); | 
|  | 390           // Try numeric, aka "natural" sort and use it if ns is not NaN. | 
|  | 391           // Javascript will try to corece these to numbers or return NaN. | 
|  | 392           var ns = botACol - botBCol; | 
|  | 393           if (ns) { | 
|  | 394             sort = s.sort * ns; | 
|  | 395           } | 
|  | 396           // If sort is non-zero, we are done sorting.  Otherwise, we'll have to | 
|  | 397           // got to the next sorting critera. | 
|  | 398           if (sort) { | 
|  | 399             return sort; | 
|  | 400           } | 
|  | 401         } | 
|  | 402         return 0; | 
|  | 403       }, | 
|  | 404 | 
|  | 405       sortChange: function(e) { | 
|  | 406         // The event we get from sort-toggle tells us the name of what needs | 
|  | 407         // to be sorting and how to sort it. | 
|  | 408         if (!(e && e.detail && e.detail.name)) { | 
|  | 409           return; | 
|  | 410         } | 
|  | 411         var sorts = this.sorts || []; | 
|  | 412         var found = false; | 
|  | 413         for (var i = 0; i < sorts.length; i++) { | 
|  | 414           if (sorts[i].name === e.detail.name) { | 
|  | 415             // e.detail.sort is 1, 0, -1 for ascending/nothing/descending. | 
|  | 416             sorts[i].sort = e.detail.sort; | 
|  | 417             found = true; | 
|  | 418             break; | 
|  | 419           } | 
|  | 420         } | 
|  | 421         if (!found) { | 
|  | 422           sorts.push(e.detail); | 
|  | 423         } | 
|  | 424 | 
|  | 425         this.sorts = sorts; | 
|  | 426         this._reRender(); | 
|  | 427       }, | 
|  | 428 | 
|  | 429       // _stripSpecial removes the special columns and sorts the remaining | 
|  | 430       // columns so they always appear in the same order, regardless of | 
|  | 431       // the order they are added. | 
|  | 432       _stripSpecial: function(){ | 
|  | 433         return this.columns.filter(function(c){ | 
|  | 434           return special_columns.indexOf(c) === -1; | 
|  | 435         }).sort(); | 
|  | 436       }, | 
|  | 437 | 
|  | 438       _taskLink: function(data) { | 
|  | 439         if (data && data.task_id) { | 
|  | 440           return "/user/task/" + data.task_id; | 
|  | 441         } | 
|  | 442         return undefined; | 
|  | 443       } | 
|  | 444 | 
|  | 445     }); | 
|  | 446   })(); | 
|  | 447   </script> | 
|  | 448 </dom-module> | 
| OLD | NEW | 
|---|