Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(100)

Side by Side Diff: appengine/swarming/elements/res/imp/botlist/bot-list.html

Issue 2204483002: Add UI to new botlist to show summary (Closed) Base URL: https://chromium.googlesource.com/external/github.com/luci/luci-py@bot-summary-api
Patch Set: Add docs Created 4 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 <!-- 1 <!--
2 Copyright 2016 The LUCI Authors. All rights reserved. 2 Copyright 2016 The LUCI Authors. All rights reserved.
3 Use of this source code is governed under the Apache License, Version 2.0 3 Use of this source code is governed under the Apache License, Version 2.0
4 that can be found in the LICENSE file. 4 that can be found in the LICENSE file.
5 5
6 This in an HTML Import-able file that contains the definition 6 This in an HTML Import-able file that contains the definition
7 of the following elements: 7 of the following elements:
8 8
9 <bot-list> 9 <bot-list>
10 10
(...skipping 12 matching lines...) Expand all
23 23
24 <link rel="import" href="/res/imp/bower_components/iron-flex-layout/iron-flex-la yout-classes.html"> 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"> 25 <link rel="import" href="/res/imp/bower_components/polymer/polymer.html">
26 26
27 <link rel="import" href="/res/imp/common/sort-toggle.html"> 27 <link rel="import" href="/res/imp/common/sort-toggle.html">
28 <link rel="import" href="/res/imp/common/swarming-app.html"> 28 <link rel="import" href="/res/imp/common/swarming-app.html">
29 29
30 <link rel="import" href="bot-filters.html"> 30 <link rel="import" href="bot-filters.html">
31 <link rel="import" href="bot-list-data.html"> 31 <link rel="import" href="bot-list-data.html">
32 <link rel="import" href="bot-list-shared.html"> 32 <link rel="import" href="bot-list-shared.html">
33 33 <link rel="import" href="bot-list-summary.html">
34 34
35 <dom-module id="bot-list"> 35 <dom-module id="bot-list">
36 <template> 36 <template>
37 <style include="iron-flex iron-flex-alignment iron-positioning swarming-app- style"> 37 <style include="iron-flex iron-flex-alignment iron-positioning swarming-app- style">
38 bot-filters { 38 bot-filters, bot-list-summary {
39 margin-bottom: 5px; 39 margin-bottom: 8px;
40 margin-right: 10px;
40 } 41 }
41 .bot { 42 .bot {
42 margin:5px; 43 margin:5px;
43 max-width:400px; 44 max-width:400px;
44 min-height:100px; 45 min-height:100px;
45 min-width:300px; 46 min-width:300px;
46 } 47 }
47 table { 48 table {
48 border-collapse: collapse; 49 border-collapse: collapse;
49 margin-left: 5px; 50 margin-left: 5px;
(...skipping 18 matching lines...) Expand all
68 right: 0; 69 right: 0;
69 top: 0.4em; 70 top: 0.4em;
70 } 71 }
71 .bot-list th > span { 72 .bot-list th > span {
72 /* Leave space for sort-toggle*/ 73 /* Leave space for sort-toggle*/
73 padding-right: 30px; 74 padding-right: 30px;
74 } 75 }
75 </style> 76 </style>
76 77
77 <swarming-app 78 <swarming-app
78 auth_headers="{{auth_headers}}" 79 auth_headers="{{_auth_headers}}"
79 busy="[[busy]]" 80 signed_in="{{_signed_in}}"
81
82 busy="[[_busy]]"
80 name="Swarming Bot List"> 83 name="Swarming Bot List">
81 84
82 <bot-filters 85 <h2 hidden$="[[_signed_in]]">You must sign in to see anything useful.</h2>
83 primary_map="[[primary_map]]"
84 primary_arr="[[primary_arr]]"
85 86
86 columns="{{columns}}" 87 <div hidden$="[[_not(_signed_in)]]">
87 filter="{{filter}}"
88 verbose="{{verbose}}">
89 </bot-filters>
90 88
91 <bot-list-data 89 <div class="horizontal layout">
92 auth_headers="[[auth_headers]]"
93 90
94 bots="{{bots}}" 91 <bot-filters
95 busy="{{busy}}" 92 primary_map="[[_primary_map]]"
96 primary_map="{{primary_map}}" 93 primary_arr="[[_primary_arr]]"
97 primary_arr="{{primary_arr}}">
98 </bot-list-data>
99 94
100 <table class="bot-list"> 95 columns="{{_columns}}"
101 <thead on-sort_change="sortChange"> 96 filter="{{_filter}}"
102 <!-- To allow for dynamic columns without having a lot of copy-pasted 97 verbose="{{_verbose}}">
103 code, we break columns up into "special" and "plain" columns. Special 98 </bot-filters>
104 columns require some sort of HTML output (e.g. anchor tags) and plain 99
105 columns just output text. The plain columns use Polymer functions to 100 <bot-list-summary
106 insert their text [_header(), _column(), _deviceColumn()]. Polymer 101 fleet="[[_fleet]]"
107 functions do not allow HTML (to avoid XSS), so special columns, like id 102 filtered_bots="[[_filteredSortedBots]]">
108 and task are inserted in a fixed order. 103 </bot-list-summary>
109 --> 104
110 <th> 105 </div>
111 <span>Bot Id</span> 106
112 <sort-toggle 107 <bot-list-data
113 name="id" 108 auth_headers="[[_auth_headers]]"
114 current="[[sort]]"> 109
115 </sort-toggle> 110 bots="{{_bots}}"
116 </th> 111 busy="{{_busy}}"
117 <!-- This wonky syntax is the proper way to listen to changes on an 112 fleet="{{_fleet}}"
118 array (we are listening to all subproperties). The element returned is 113 primary_map="{{_primary_map}}"
119 not of much use, so we'll ignore it in _hide() and use this.columns. 114 primary_arr="{{_primary_arr}}">
115 </bot-list-data>
116
117 <table class="bot-list">
118 <thead on-sort_change="_sortChange">
119 <!-- To allow for dynamic columns without having a lot of copy-pasted
120 code, we break columns up into "special" and "plain" columns. Special
121 columns require some sort of HTML output (e.g. anchor tags) and plain
122 columns just output text. The plain columns use Polymer functions to
123 insert their text [_header(), _column(), _deviceColumn()]. Polymer
124 functions do not allow HTML (to avoid XSS), so special columns, like i d
125 and task are inserted in a fixed order.
120 --> 126 -->
121 <th hidden$="[[_hide('task', columns.*)]]"> 127 <tr>
122 <span>Current Task</span> 128 <th>
123 <sort-toggle 129 <span>Bot Id</span>
124 name="task" 130 <sort-toggle
125 current="[[sort]]"> 131 name="id"
126 </sort-toggle> 132 current="[[_sort]]">
127 </th> 133 </sort-toggle>
128 134 </th>
129 <template is="dom-repeat" 135 <!-- This wonky syntax is the proper way to listen to changes on a n
130 items="[[plain_columns]]" 136 array (we are listening to all subproperties). The element returne d is
131 as="c"> 137 not of much use, so we'll ignore it in _hide() and use this._colum ns.
132 <th hidden$="[[_hide(c)]]"> 138 -->
133 <span>[[_header(c)]]</span> 139 <th hidden$="[[_hide('task', _columns.*)]]">
134 <sort-toggle 140 <span>Current Task</span>
135 name="[[c]]" 141 <sort-toggle
136 current="[[sort]]"> 142 name="task"
137 </sort-toggle> 143 current="[[_sort]]">
138 </th> 144 </sort-toggle>
139 </template> 145 </th>
140 </thead>
141 <tbody>
142 <template id="bot_table" is="dom-repeat"
143 items="[[bots]]"
144 as="bot"
145 initial-count=50
146 filter="_filterBotTable">
147
148 <tr class$="[[_botClass(bot)]]">
149 <td>
150 <a class="center"
151 href$="[[_botLink(bot.bot_id)]]"
152 target="_blank">
153 [[bot.bot_id]]
154 </a>
155 </td>
156 <td hidden$="[[_hide('task', columns.*)]]">
157 <a href$="[[_taskLink(bot)]]">[[_taskId(bot)]]</a>
158 </td>
159 146
160 <template is="dom-repeat" 147 <template is="dom-repeat"
161 items="[[plain_columns]]" 148 items="[[_plain_columns]]"
162 as="c"> 149 as="c">
163 <td hidden$="[[_hide(c)]]"> 150 <th hidden$="[[_hide(c)]]">
164 [[_column(c, bot, verbose)]] 151 <span>[[_header(c)]]</span>
152 <sort-toggle
153 name="[[c]]"
154 current="[[_sort]]">
155 </sort-toggle>
156 </th>
157 </template>
158 </tr>
159 </thead>
160 <tbody>
161 <template id="bot_table" is="dom-repeat"
162 items="[[_filteredSortedBots]]"
163 as="bot"
164 initial-count=50>
165
166 <tr class$="[[_botClass(bot)]]">
167 <td>
168 <a class="center"
169 href$="[[_botLink(bot.bot_id)]]"
170 target="_blank">
171 [[bot.bot_id]]
172 </a>
165 </td> 173 </td>
166 </template> 174 <td hidden$="[[_hide('task', _columns.*)]]">
175 <a href$="[[_taskLink(bot)]]">[[_taskId(bot)]]</a>
176 </td>
167 177
168 </tr>
169 <template is="dom-repeat"
170 items="[[_devices(bot)]]"
171 as="device">
172 <tr hidden$="[[_hide('devices', columns.*)]]"
173 class$="[[_deviceClass(device)]]">
174 <td></td>
175 <td hidden$="[[_hide('task', columns.*)]]"></td>
176 <template is="dom-repeat" 178 <template is="dom-repeat"
177 items="[[plain_columns]]" 179 items="[[_plain_columns]]"
178 as="c"> 180 as="c">
179 <td hidden$="[[_hide(c)]]"> 181 <td hidden$="[[_hide(c)]]">
180 [[_deviceColumn(c, device, verbose)]] 182 [[_column(c, bot, _verbose)]]
181 </td> 183 </td>
182 </template> 184 </template>
185
183 </tr> 186 </tr>
184 </template> <!--devices repeat--> 187 <template is="dom-repeat"
185 </template> <!--bot-table repeat--> 188 items="[[_devices(bot)]]"
186 </tbody> 189 as="device">
187 </table> 190 <tr hidden$="[[_hide('devices', _columns.*)]]"
191 class$="[[_deviceClass(device)]]">
192 <td></td>
193 <td hidden$="[[_hide('task', _columns.*)]]"></td>
194 <template is="dom-repeat"
195 items="[[_plain_columns]]"
196 as="c">
197 <td hidden$="[[_hide(c)]]">
198 [[_deviceColumn(c, device, _verbose)]]
199 </td>
200 </template>
201 </tr>
202 </template> <!--devices repeat-->
203 </template> <!--bot-table repeat-->
204 </tbody>
205 </table>
206 </div>
188 207
189 </swarming-app> 208 </swarming-app>
190 209
191 </template> 210 </template>
192 <script> 211 <script>
193 (function(){ 212 (function(){
194 var special_columns = ["id", "task"]; 213 var special_columns = ["id", "task"];
195 214
196 var headerMap = { 215 var headerMap = {
197 // "id" and "task" are special, so they don't go here and have their 216 // "id" and "task" are special, so they don't go here and have their
198 // headers hard-coded below. 217 // headers hard-coded below.
199 "cores": "Cores", 218 "cores": "Cores",
200 "cpu": "CPU", 219 "cpu": "CPU",
201 "devices": "Devices", 220 "devices": "Devices",
202 "gpu": "GPU", 221 "gpu": "GPU",
203 "os": "OS", 222 "os": "OS",
204 "pool": "Pool", 223 "pool": "Pool",
205 "status": "Status", 224 "status": "Status",
206 }; 225 };
207 226
208 // This maps column name to a function that will return the content for a 227 // This maps column name to a function that will return the content for a
209 // given bot. These functions are bound to this element, and have access 228 // given bot. These functions are bound to this element, and have access
210 // to all functions defined here and in bot-list-shared. 229 // to all functions defined here and in bot-list-shared.
211 var columnMap = { 230 var columnMap = {
212 cores: function(bot){ 231 cores: function(bot){
213 var cores = this._cores(bot); 232 var cores = this._cores(bot);
214 if (this.verbose){ 233 if (this._verbose){
215 return cores.join(" | "); 234 return cores.join(" | ");
216 } 235 }
217 return cores[0]; 236 return cores[0];
218 }, 237 },
219 cpu: function(bot){ 238 cpu: function(bot){
220 var cpus = this._dimension(bot, 'cpu') || ['Unknown']; 239 var cpus = this._dimension(bot, 'cpu') || ['Unknown'];
221 if (this.verbose){ 240 if (this._verbose){
222 return cpus.join(" | "); 241 return cpus.join(" | ");
223 } 242 }
224 return cpus[0]; 243 return cpus[0];
225 }, 244 },
226 devices: function(bot){ 245 devices: function(bot){
227 return this._devices(bot).length + " devices attached"; 246 return this._devices(bot).length + " devices attached";
228 }, 247 },
229 gpu: function(bot){ 248 gpu: function(bot){
230 var gpus = this._dimension(bot, 'gpu') 249 var gpus = this._dimension(bot, 'gpu')
231 if (!gpus) { 250 if (!gpus) {
(...skipping 10 matching lines...) Expand all
242 if (g.indexOf(":") === -1) { 261 if (g.indexOf(":") === -1) {
243 named.push(g); 262 named.push(g);
244 } 263 }
245 return; 264 return;
246 } 265 }
247 verbose.push(this._applyAlias(g, alias)); 266 verbose.push(this._applyAlias(g, alias));
248 if (g.indexOf(":") === -1) { 267 if (g.indexOf(":") === -1) {
249 named.push(this._applyAlias(g, alias)); 268 named.push(this._applyAlias(g, alias));
250 } 269 }
251 }.bind(this)) 270 }.bind(this))
252 if (this.verbose) { 271 if (this._verbose) {
253 return verbose.join(" | "); 272 return verbose.join(" | ");
254 } 273 }
255 return named.join(" | "); 274 return named.join(" | ");
256 }, 275 },
257 id: function(bot) { 276 id: function(bot) {
258 return bot.bot_id; 277 return bot.bot_id;
259 }, 278 },
260 os: function(bot) { 279 os: function(bot) {
261 var os = this._dimension(bot, 'os') || ['Unknown']; 280 var os = this._dimension(bot, 'os') || ['Unknown'];
262 if (this.verbose){ 281 if (this._verbose){
263 return os.join(" | "); 282 return os.join(" | ");
264 } 283 }
265 return os[0]; 284 return os[0];
266 }, 285 },
267 pool: function(bot) { 286 pool: function(bot) {
268 var pool = this._dimension(bot, 'pool') || ['Unknown']; 287 var pool = this._dimension(bot, 'pool') || ['Unknown'];
269 return pool.join(" | "); 288 return pool.join(" | ");
270 }, 289 },
271 status: function(bot) { 290 status: function(bot) {
272 // If a bot is both dead and quarantined, show the deadness over the 291 // If a bot is both dead and quarantined, show the deadness over the
(...skipping 10 matching lines...) Expand all
283 return this._taskId(bot); 302 return this._taskId(bot);
284 }, 303 },
285 }; 304 };
286 305
287 Polymer({ 306 Polymer({
288 is: 'bot-list', 307 is: 'bot-list',
289 behaviors: [SwarmingBehaviors.BotListBehavior], 308 behaviors: [SwarmingBehaviors.BotListBehavior],
290 309
291 properties: { 310 properties: {
292 311
293 columns: { 312 _bots: {
294 type: Array, 313 type: Array,
295 }, 314 },
296 // Should have a property "filter" which is a function. 315
297 filter: { 316 _columns: {
298 type: Object, 317 type: Array,
299 }, 318 },
300 319
301 plain_columns: { 320 _filter: {
302 type: Array, 321 type: Function,
303 computed: "_stripSpecial(columns.*)", 322 value: function() {
323 return true;
324 },
304 }, 325 },
305 326
306 // sort is an Object {name:String, direction:String}. 327 _filteredSortedBots: {
307 sort: { 328 type: Array,
308 type: Object, 329 computed: "_filterAndSort(_bots,_filter.*,_sort.*)"
309 }, 330 },
310 331
311 verbose: { 332 _plain_columns: {
333 type: Array,
334 computed: "_stripSpecial(_columns.*)",
335 },
336
337 // _sort is an Object {name:String, direction:String}.
338 _sort: {
339 type: Object,
340 value: function() {
341 return {
342 name: "id",
343 direction: "asc",
344 };
345 }
346 },
347
348 _verbose: {
312 type: Boolean, 349 type: Boolean,
313 } 350 }
314 }, 351 },
315 352
316 observers: [
317 '_reRender(filter.*)',
318 '_checkSorts(columns.*)'
319 ],
320
321 _botClass: function(bot) { 353 _botClass: function(bot) {
322 if (bot.is_dead) { 354 if (bot.is_dead) {
323 return "dead"; 355 return "dead";
324 } 356 }
325 if (bot.quarantined) { 357 if (bot.quarantined) {
326 return "quarantined"; 358 return "quarantined";
327 } 359 }
328 return ""; 360 return "";
329 }, 361 },
330 362
331 _botLink: function(id) { 363 _botLink: function(id) {
332 // TODO(kjlubick) Make this point to /newui/ when appropriate. 364 // TODO(kjlubick) Make this point to /newui/ when appropriate.
333 return "/restricted/bot/"+id; 365 return "/restricted/bot/"+id;
334 }, 366 },
335 367
336 // _checkSorts makes sure that if a column has been removed, the related
337 // sort is also removed.
338 _checkSorts: function() {
339 if (!this.sort) {
340 return;
341 }
342 this._reRender();
343 },
344 368
345 _column: function(col, bot) { 369 _column: function(col, bot) {
346 return columnMap[col].bind(this)(bot); 370 return columnMap[col].bind(this)(bot);
347 }, 371 },
348 372
349 _deviceColumn: function(col, device) { 373 _deviceColumn: function(col, device) {
350 if (col === "devices") { 374 if (col === "devices") {
351 var str = this._androidAlias(device); 375 var str = this._androidAlias(device);
352 if (device.okay) { 376 if (device.okay) {
353 str = this._applyAlias(this._deviceType(device), str); 377 str = this._applyAlias(this._deviceType(device), str);
354 } 378 }
355 str += " S/N:"; 379 str += " S/N:";
356 str += device.serial; 380 str += device.serial;
357 return str; 381 return str;
358 } 382 }
359 if (col === "status") { 383 if (col === "status") {
360 return device.state; 384 return device.state;
361 } 385 }
362 return ""; 386 return "";
363 }, 387 },
364 388
365 _deviceClass: function(device) { 389 _deviceClass: function(device) {
366 if (!device.okay) { 390 if (!device.okay) {
367 return "bad-device"; 391 return "bad-device";
368 } 392 }
369 return ""; 393 return "";
370 }, 394 },
371 395
372 _filterBotTable: function(bot) { 396 _filterAndSort: function(a,b,c) {
373 if (!this.filter || !this.filter.filter) { 397 // We intentionally sort this._bots (and not a copy) to allow users to
374 return true; 398 // "chain" sorts, that is, sort by one thing and then another, and
399 // have both orderings properly impact the list.
400 swarming.stableSort(this._bots, this._sortBotTable.bind(this));
401 var bots = this._bots;
402 if (this._filter) {
403 bots = bots.filter(this._filter.bind(this));
375 } 404 }
376 return this.filter.filter.bind(this)(bot); 405
406 return bots;
377 }, 407 },
378 408
379 _header: function(col){ 409 _header: function(col){
380 return headerMap[col]; 410 return headerMap[col];
381 }, 411 },
382 412
383 _hide: function(col) { 413 _hide: function(col) {
384 return this.columns.indexOf(col) === -1; 414 return this._columns.indexOf(col) === -1;
385 }, 415 },
386 416
387 _reRender: function(filter, sort) { 417 _reRender: function(filter, sort) {
388 this.$.bot_table.render(); 418 this.$.bot_table.render();
389 }, 419 },
390 420
391 _sortBotTable: function(botA, botB) { 421 _sortBotTable: function(botA, botB) {
392 if (!this.sort) { 422 if (!this._sort) {
393 return 0; 423 return 0;
394 } 424 }
395 var dir = 1; 425 var dir = 1;
396 if (this.sort.direction === "desc") { 426 if (this._sort.direction === "desc") {
397 dir = -1; 427 dir = -1;
398 } 428 }
399 var botACol = this._column(this.sort.name, botA); 429 var botACol = this._column(this._sort.name, botA);
400 var botBCol = this._column(this.sort.name, botB); 430 var botBCol = this._column(this._sort.name, botB);
401 431
402 return dir * swarming.naturalCompare(botACol, botBCol); 432 return dir * swarming.naturalCompare(botACol, botBCol);
403 }, 433 },
404 434
405 sortChange: function(e) { 435 _sortChange: function(e) {
406 // The event we get from sort-toggle tells us the name of what needs 436 // The event we get from sort-toggle tells us the name of what needs
407 // to be sorting and how to sort it. 437 // to be sorting and how to sort it.
408 if (!(e && e.detail && e.detail.name)) { 438 if (!(e && e.detail && e.detail.name)) {
409 return; 439 return;
410 } 440 }
411 this.set("sort", e.detail); 441 // should trigger __filterAndSort
412 swarming.stableSort(this.bots, this._sortBotTable.bind(this)); 442 this.set("_sort", e.detail);
413 this._reRender();
414 }, 443 },
415 444
416 // _stripSpecial removes the special columns and sorts the remaining 445 // _stripSpecial removes the special columns and sorts the remaining
417 // columns so they always appear in the same order, regardless of 446 // columns so they always appear in the same order, regardless of
418 // the order they are added. 447 // the order they are added.
419 _stripSpecial: function(){ 448 _stripSpecial: function(){
420 return this.columns.filter(function(c){ 449 return this._columns.filter(function(c){
421 return special_columns.indexOf(c) === -1; 450 return special_columns.indexOf(c) === -1;
422 }).sort(); 451 }).sort();
423 }, 452 },
424 453
425 _taskLink: function(data) { 454 _taskLink: function(data) {
426 if (data && data.task_id) { 455 if (data && data.task_id) {
427 return "/user/task/" + data.task_id; 456 return "/user/task/" + data.task_id;
428 } 457 }
429 return undefined; 458 return undefined;
430 } 459 }
431 460
432 }); 461 });
433 })(); 462 })();
434 </script> 463 </script>
435 </dom-module> 464 </dom-module>
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698