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

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

Issue 2408743002: Move elements/ to ui/ (Closed)
Patch Set: Created 4 years, 2 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
(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 This is a top-level element.
15
16 Properties:
17 client_id: String, Oauth 2.0 client id. It will be set by server-side
18 template evaluation.
19
20 Methods:
21 None.
22
23 Events:
24 None.
25 -->
26
27 <link rel="import" href="/res/imp/bower_components/iron-flex-layout/iron-flex-la yout-classes.html">
28 <link rel="import" href="/res/imp/bower_components/polymer/polymer.html">
29
30 <link rel="import" href="/res/imp/common/dynamic-table-behavior.html">
31 <link rel="import" href="/res/imp/common/error-toast.html">
32 <link rel="import" href="/res/imp/common/sort-toggle.html">
33 <link rel="import" href="/res/imp/common/swarming-app.html">
34 <link rel="import" href="/res/imp/common/url-param.html">
35 <link rel="import" href="/res/imp/common/pageable-data.html">
36
37 <link rel="import" href="bot-filters.html">
38 <link rel="import" href="bot-list-data.html">
39 <link rel="import" href="bot-list-shared-behavior.html">
40 <link rel="import" href="bot-list-summary.html">
41
42 <dom-module id="bot-list">
43 <template>
44 <style include="iron-flex iron-flex-alignment iron-positioning swarming-app- style dynamic-table-style">
45 bot-filters, bot-list-summary {
46 margin-bottom: 8px;
47 margin-right: 10px;
48 }
49 .quarantined, .bad-device {
50 background-color: #ffdddd;
51 }
52 .dead {
53 background-color: #cccccc;
54 }
55 .bot-list th > span {
56 /* Leave space for sort-toggle*/
57 padding-right: 30px;
58 }
59 </style>
60
61 <url-param name="s"
62 value="{{_sortstr}}"
63 default_value="id:asc">
64 </url-param>
65
66 <swarming-app
67 client_id="[[client_id]]"
68 auth_headers="{{_auth_headers}}"
69 signed_in="{{_signed_in}}"
70 server_details="{{_server_details}}"
71
72 busy="[[_or(_busy1,_busy2)]]"
73 name="Swarming Bot List">
74
75 <h2 hidden$="[[_signed_in]]">You must sign in to see anything useful.</h2>
76
77 <div hidden$="[[_not(_signed_in)]]">
78
79 <div class="horizontal layout">
80
81 <bot-filters
82 dimensions="[[_dimensions]]"
83 primary_map="[[_primary_map]]"
84 primary_arr="[[_primary_arr]]"
85
86 columns="{{_columns}}"
87 query_params="{{_query_params}}"
88 filter="{{_filter}}"
89 verbose="{{_verbose}}">
90 </bot-filters>
91
92 <bot-list-summary
93 columns="[[_columns]]"
94 fleet="[[_fleet]]"
95 filtered_bots="[[_filteredSortedItems]]"
96 sort="[[_sortstr]]"
97 verbose="[[_verbose]]">
98 </bot-list-summary>
99
100 </div>
101
102 <bot-list-data
103 id="data"
104 auth_headers="[[_auth_headers]]"
105 query_params="[[_query_params]]"
106
107 busy="{{_busy1}}"
108 dimensions="{{_dimensions}}"
109 fleet="{{_fleet}}"
110 primary_map="{{_primary_map}}"
111 primary_arr="{{_primary_arr}}">
112 </bot-list-data>
113
114 <table class="bot-list">
115 <thead on-sort_change="_sortChange">
116 <!-- To allow for dynamic columns without having a lot of copy-pasted
117 code, we break columns up into "special" and "plain" columns. Special
118 columns require some sort of HTML output (e.g. anchor tags) and plain
119 columns just output text. The plain columns use Polymer functions to
120 insert their text [_header(), _column(), _deviceColumn()]. Polymer
121 functions do not allow HTML (to avoid XSS), so special columns, like i d
122 and task are inserted in a fixed order.
123 -->
124 <tr>
125 <th>
126 <span>Bot Id</span>
127 <sort-toggle
128 name="id"
129 current="[[_sort]]">
130 </sort-toggle>
131 </th>
132 <!-- This wonky syntax is the proper way to listen to changes on a n
133 array (we are listening to all subproperties). The element returne d is
134 not of much use, so we'll ignore it in _hide() and use this._colum ns.
135 -->
136 <th hidden$="[[_hide('cloud_console_link', _columns.*)]]">
137 <span>Bot in Cloud Console</span>
138 <sort-toggle
139 name="cloud_console_link"
140 current="[[_sort]]">
141 </sort-toggle>
142 </th>
143 <th hidden$="[[_hide('mp_lease_id', _columns.*)]]">
144 <span>Machine Provider Lease Id</span>
145 <sort-toggle
146 name="mp_lease_id"
147 current="[[_sort]]">
148 </sort-toggle>
149 </th>
150 <th hidden$="[[_hide('task', _columns.*)]]">
151 <span>Current Task</span>
152 <sort-toggle
153 name="task"
154 current="[[_sort]]">
155 </sort-toggle>
156 </th>
157
158 <template
159 is="dom-repeat"
160 items="[[_plainColumns]]"
161 as="c">
162 <th hidden$="[[_hide(c)]]">
163 <span>[[_header(c)]]</span>
164 <sort-toggle
165 name="[[c]]"
166 current="[[_sort]]">
167 </sort-toggle>
168 </th>
169 </template>
170 </tr>
171 </thead>
172 <tbody>
173 <template
174 id="bot_table"
175 is="dom-repeat"
176 items="[[_filteredSortedItems]]"
177 as="bot"
178 initial-count=50>
179
180 <tr class$="[[_botClass(bot)]]">
181 <td>
182 <a
183 class="center"
184 href$="[[_botLink(bot.bot_id)]]"
185 target="_blank"
186 rel="noopener">
187 [[bot.bot_id]]
188 </a>
189 </td>
190 <td hidden$="[[_hide('cloud_console_link', _columns.*)]]">
191 <a href$="[[_ccLink(bot)]]">[[_ccText(bot)]]</a>
192 </td>
193 <td hidden$="[[_hide('mp_lease_id', _columns.*)]]">
194 <a href$="[[_mpLink(bot,_server_details.machine_provider_prefi x)]]">
195 [[_column('mp_lease_id',bot,_verbose)]]
196 </a>
197 </td>
198 <td hidden$="[[_hide('task', _columns.*)]]">
199 <a href$="[[_taskLink(bot.task_id)]]">[[_taskId(bot)]]</a>
200 </td>
201
202 <template
203 is="dom-repeat"
204 items="[[_plainColumns]]"
205 as="c">
206 <td hidden$="[[_hide(c)]]">
207 [[_column(c, bot, _verbose)]]
208 </td>
209 </template>
210
211 </tr>
212 <template
213 is="dom-repeat"
214 items="[[_devices(bot)]]"
215 as="device">
216 <tr
217 hidden$="[[_hide('android_devices', _columns.*)]]"
218 class$="[[_deviceClass(device)]]">
219 <td></td>
220 <td hidden$="[[_hide('task', _columns.*)]]"></td>
221 <template
222 is="dom-repeat"
223 items="[[_plainColumns]]"
224 as="c">
225 <td hidden$="[[_hide(c)]]">
226 [[_deviceColumn(c, device, _verbose)]]
227 </td>
228 </template>
229 </tr>
230 </template> <!--devices repeat-->
231 </template> <!--bot-table repeat-->
232 </tbody>
233 </table>
234 <pageable-data
235 id="page_bots"
236 busy="{{_busy2}}"
237 label="Show more bots"
238 output="{{_items}}"
239 parse="[[_parseBots]]">
240 </pageable-data>
241 </div>
242
243 <error-toast></error-toast>
244 </swarming-app>
245
246 </template>
247 <script>
248 (function(){
249 var UNKNOWN = "unknown";
250 // see dynamic-table for more information on specialColumns, headerMap,
251 // columnMap, and specialSort
252 var specialColumns = ["id", "task", "cloud_console_link", "mp_lease_id"];
253
254 var headerMap = {
255 // "id" and "task" are special, so they don't go here. They have their
256 // headers hard-coded above.
257 "android_devices": "Android Devices",
258 "battery_health": "Battery Health",
259 "battery_level": "Battery Level (%)",
260 "battery_status": "Battery Status",
261 "battery_temperature": "Battery Temp (°C)",
262 "battery_voltage": "Battery Voltage (mV)",
263 "bot_temperature": "Bot Temp (°C)",
264 "cores": "Cores",
265 "cpu": "CPU",
266 "device": "Non-android Device",
267 "device_os": "Device OS",
268 "device_temperature": "Device Temp (°C)",
269 "device_type": "Device Type",
270 "disk_space": "Free Space (MB)",
271 "first_seen": "First Seen",
272 "gpu": "GPU",
273 "last_seen": "Last Seen",
274 "mp_lease_expires": "Machine Provider Lease Expires",
275 "os": "OS",
276 "pool": "Pool",
277 "running_time": "Swarming Uptime",
278 "status": "Status",
279 "uptime": "Bot Uptime",
280 "xcode_version": "XCode Version",
281 };
282
283 var columnMap = {
284 android_devices: function(bot) {
285 var devs = this._attribute(bot, "android_devices", "0");
286 if (this._verbose) {
287 return devs.join(" | ") + " devices available";
288 }
289 // max() works on strings as long as they can be coerced to Number.
290 return Math.max(...devs) + " devices available";
291 },
292 battery_health: function(){
293 return "";
294 },
295 battery_level: function(){
296 return "";
297 },
298 battery_status: function(){
299 return "";
300 },
301 battery_temperature: function(){
302 return "";
303 },
304 battery_voltage: function(){
305 return "";
306 },
307 bot_temperature: function(bot){
308 if (this._verbose) {
309 return bot.state.temp.zones || UNKNOWN;
310 }
311 return bot.state.temp.average || UNKNOWN;
312 },
313 device_temperature: function(){
314 return "";
315 },
316 disk_space: function(bot) {
317 var aliased = [];
318 bot.disks.forEach(function(disk){
319 var alias = sk.human.bytes(disk.mb, sk.MB);
320 aliased.push(swarming.alias.apply(disk.mb, disk.id + " "+ alias));
321 }.bind(this));
322 if (this._verbose) {
323 return aliased.join(" | ");
324 }
325 return aliased[0];
326 },
327
328 external_ip: function(bot) {
329 return bot.external_ip || "none";
330 },
331 first_seen: function(bot) {
332 return sk.human.localeTime(bot.first_seen_ts)
333 },
334 id: function(bot) {
335 return bot.bot_id;
336 },
337 last_seen: function(bot) {
338 if (this._verbose) {
339 return sk.human.localeTime(bot.last_seen_ts);
340 }
341 return this._timeDiffApprox(bot.last_seen_ts) + " ago";
342 },
343 mp_lease_id: function(bot) {
344 var id = bot.lease_id || "none";
345 if (this._verbose) {
346 return id;
347 }
348 return id.substring(0, 10);
349 },
350 mp_lease_expires: function(bot) {
351 if (!bot.lease_expiration_ts) {
352 return "N/A";
353 }
354 if (this._verbose) {
355 return sk.human.localeTime(bot.lease_expiration_ts);
356 }
357 if (bot.lease_expiration_ts < new Date()) {
358 return this._timeDiffApprox(bot.lease_expiration_ts) + " ago";
359 }
360 return "in " + this._timeDiffApprox(bot.lease_expiration_ts);
361 },
362 running_time: function(bot) {
363 var u = this._state(bot, "running_time");
364 if (!u) {
365 return "unknown";
366 }
367 return sk.human.strDuration(u);
368 },
369 status: function(bot) {
370 // If a bot is both dead and quarantined, show the deadness over the
371 // quarentinedness.
372 if (bot.is_dead) {
373 return "Dead. Last seen " + sk.human.diffDate(bot.last_seen_ts) +
374 " ago";
375 }
376 if (bot.quarantined) {
377 var msg = this._state(bot, "quarantined")[0];
378 // Sometimes, the quarantined message is actually in "error". This
379 // happens when the bot code has thrown an exception.
380 if (msg === UNKNOWN || msg === "true" || msg === true) {
381 msg = this._attribute(bot, "error");
382 }
383 return "Quarantined: " + msg;
384 }
385 return "Alive";
386 },
387 task: function(bot) {
388 return this._taskId(bot);
389 },
390 uptime: function(bot) {
391 var u = this._state(bot, "uptime");
392 if (!u) {
393 return "unknown";
394 }
395 return sk.human.strDuration(u);
396 },
397 version: function(bot) {
398 var v = bot.version || UNKNOWN
399 return v.substring(0, 10);
400 }
401 };
402
403 var deviceColumnMap = {
404 android_devices: function(device) {
405 var str = this._androidAliasDevice(device);
406 if (device.okay) {
407 str = swarming.alias.apply(this._deviceType(device), str);
408 }
409 str += " S/N:";
410 str += device.serial;
411 return str;
412 },
413 battery_health: function(device){
414 var h = (device.battery && device.battery.health) || UNKNOWN;
415 return swarming.alias.apply(h, "battery_health");
416 },
417 battery_level: function(device){
418 return (device.battery && device.battery.level) || UNKNOWN;
419 },
420 battery_status: function(device){
421 var s = (device.battery && device.battery.status) || UNKNOWN;
422 return swarming.alias.apply(s, "battery_status");
423 },
424 battery_temperature: function(device){
425 // Battery temps are in tenths of degrees C - convert to more human rang e.
426 return (device.battery && device.battery.temperature / 10) || UNKNOWN
427 },
428 battery_voltage: function(device){
429 return (device.battery && device.battery.voltage) || UNKNOWN;
430 },
431 device_temperature: function(device){
432 if (this._verbose) {
433 return device.temp.zones || UNKNOWN;
434 }
435 return device.temp.average || UNKNOWN;
436 },
437 device_os: function(device) {
438 if (device.build) {
439 return device.build["build.id"];
440 }
441 return UNKNOWN;
442 },
443 status: function(device) {
444 return device.state;
445 }
446 }
447
448
449 function deviceAverage(col) {
450 return function(dir, botA, botB) {
451 // sort by average of all devices or 0 if no devices.
452 var avgA = 0;
453 var avgB = 0;
454 var devsA = this._devices(botA);
455 devsA.forEach(function(device) {
456 var v = deviceColumnMap[col](device);
457 v = parseFloat(swarming.alias.unapply(v)) || 0;
458 avgA += v / devsA.length;
459 }.bind(this));
460 var devsB = this._devices(botB);
461 devsB.forEach(function(device) {
462 var v = deviceColumnMap[col](device);
463 v = parseFloat(swarming.alias.unapply(v)) || 0;
464 avgB += v / devsB.length;
465 }.bind(this));
466 return dir * swarming.naturalCompare(avgA, avgB);
467 };
468 }
469
470 var specialSort = {
471 android_devices: function(dir, botA, botB) {
472 // We sort on the number of attached devices. Note that this
473 // may not be the same as android_devices, because _devices().length
474 // counts all devices plugged into the bot, whereas android_devices
475 // counts just devices ready for work.
476 var botACol = this._devices(botA).length;
477 var botBCol = this._devices(botB).length;
478 return dir * swarming.naturalCompare(botACol, botBCol);
479 },
480
481 battery_health: deviceAverage("battery_health"),
482 battery_level: deviceAverage("battery_level"),
483 battery_status: deviceAverage("battery_status"),
484 battery_temperature: deviceAverage("battery_temperature"),
485 battery_voltage: deviceAverage("battery_voltage"),
486 device_temperature: deviceAverage("device_temperature"),
487
488 bot_temperature: function(dir, botA, botB) {
489 // Sort by average temperature.
490 var botACol = botA.state.temp.average || 0;
491 var botBCol = botB.state.temp.average || 0;
492 return dir * swarming.naturalCompare(botACol, botBCol);
493 },
494 disk_space: function(dir, botA, botB) {
495 // We sort based on the raw number of MB of the first disk.
496 var botACol = botA.disks[0].mb;
497 var botBCol = botB.disks[0].mb;
498 return dir * swarming.naturalCompare(botACol, botBCol);
499 },
500 first_seen: function(dir, botA, botB) {
501 var botACol = botA.first_seen_ts;
502 var botBCol = botB.first_seen_ts;
503 return dir * swarming.naturalCompare(botACol, botBCol)
504 },
505 last_seen: function(dir, botA, botB) {
506 var botACol = botA.last_seen_ts;
507 var botBCol = botB.last_seen_ts;
508 return dir * swarming.naturalCompare(botACol, botBCol)
509 },
510 running_time: function(dir, botA, botB) {
511 var botACol = this._state(botA, "running_time") || 0;
512 var botBCol = this._state(botB, "running_time") || 0;
513 return dir * swarming.naturalCompare(botACol, botBCol)
514 },
515 uptime: function(dir, botA, botB) {
516 var botACol = this._state(botA, "uptime") || 0;
517 var botBCol = this._state(botB, "uptime") || 0;
518 return dir * swarming.naturalCompare(botACol, botBCol)
519 },
520 };
521
522 Polymer({
523 is: 'bot-list',
524
525 // The order behaviors are applied in matters - later ones overwrite
526 // attributes of earlier ones
527 behaviors: [
528 SwarmingBehaviors.BotListBehavior,
529 SwarmingBehaviors.DynamicTableBehavior,
530 ],
531
532 properties: {
533 client_id: {
534 type: String,
535 },
536
537 _busy1: {
538 type: Boolean,
539 value: false
540 },
541 _busy2: {
542 type: Boolean,
543 value: false
544 },
545 _parseBots: {
546 type: Function,
547 value: function() {
548 return this.$.data.parseBots.bind(this);
549 }
550 },
551
552 // For dynamic table.
553 _columnMap: {
554 type: Object,
555 value: function() {
556 var base = this._commonColumns();
557 for (var attr in columnMap) {
558 base[attr] = columnMap[attr];
559 }
560 return base;
561 },
562 },
563 _headerMap: {
564 type: Object,
565 value: headerMap,
566 },
567 _specialColumns: {
568 type: Array,
569 value: specialColumns,
570 },
571 _specialSort: {
572 type: Object,
573 value: specialSort,
574 },
575 },
576
577 observers:["_reload(_query_params,_auth_headers)"],
578
579 _androidAliasDevice: function(device) {
580 if (device.notReady) {
581 return UNAUTHENTICATED.toUpperCase();
582 }
583 return swarming.alias.android(this._deviceType(device));
584 },
585
586 _botClass: function(bot) {
587 if (bot.is_dead) {
588 return "dead";
589 }
590 if (bot.quarantined) {
591 return "quarantined";
592 }
593 return "";
594 },
595
596 _ccLink: function(bot){
597 var z = this._attribute(bot, "zone")[0];
598 if (z === "unknown") {
599 return undefined;
600 }
601 return this._cloudConsoleLink(z, bot.bot_id);
602 },
603
604 _ccText: function(bot){
605 var z = this._attribute(bot, "zone")[0];
606 if (z === "unknown") {
607 return "Not on GCE";
608 }
609 return "View Bot";
610 },
611
612 _deviceColumn: function(col, device) {
613 var f = deviceColumnMap[col];
614 if (!f || !device) {
615 return "";
616 }
617 return f.bind(this)(device);
618 },
619
620 _deviceClass: function(device) {
621 if (!device.okay) {
622 return "bad-device";
623 }
624 return "";
625 },
626
627 _mpLink: function(bot, prefix) {
628 if (!bot || !bot.lease_id || !prefix) {
629 return false;
630 }
631 return prefix + bot.lease_id
632 },
633
634 _reload: function() {
635 if (!this._auth_headers || !this._query_params) {
636 return;
637 }
638 var url = "/_ah/api/swarming/v1/bots/list?" + sk.query.fromParamSet(this ._query_params);
639 this.$.page_bots.load(url,this._auth_headers);
640 }
641
642 });
643 })();
644 </script>
645 </dom-module>
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698