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

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

Issue 2408743002: Move elements/ to ui/ (Closed)
Patch Set: rebase again 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_temp late)]]">
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 external_ip: function(bot) {
328 return bot.external_ip || "none";
329 },
330 first_seen: function(bot) {
331 return sk.human.localeTime(bot.first_seen_ts)
332 },
333 id: function(bot) {
334 return bot.bot_id;
335 },
336 last_seen: function(bot) {
337 if (this._verbose) {
338 return sk.human.localeTime(bot.last_seen_ts);
339 }
340 return this._timeDiffApprox(bot.last_seen_ts) + " ago";
341 },
342 mp_lease_id: function(bot) {
343 var id = bot.lease_id || "none";
344 if (this._verbose) {
345 return id;
346 }
347 return id.substring(0, 10);
348 },
349 mp_lease_expires: function(bot) {
350 if (!bot.lease_expiration_ts) {
351 return "N/A";
352 }
353 if (this._verbose) {
354 return sk.human.localeTime(bot.lease_expiration_ts);
355 }
356 if (bot.lease_expiration_ts < new Date()) {
357 return this._timeDiffApprox(bot.lease_expiration_ts) + " ago";
358 }
359 return "in " + this._timeDiffApprox(bot.lease_expiration_ts);
360 },
361 running_time: function(bot) {
362 var u = this._state(bot, "running_time");
363 if (!u) {
364 return "unknown";
365 }
366 return sk.human.strDuration(u);
367 },
368 status: function(bot) {
369 // If a bot is both dead and quarantined, show the deadness over the
370 // quarentinedness.
371 if (bot.is_dead) {
372 return "Dead. Last seen " + sk.human.diffDate(bot.last_seen_ts) +
373 " ago";
374 }
375 if (bot.quarantined) {
376 var msg = this._state(bot, "quarantined")[0];
377 // Sometimes, the quarantined message is actually in "error". This
378 // happens when the bot code has thrown an exception.
379 if (msg === UNKNOWN || msg === "true" || msg === true) {
380 msg = this._attribute(bot, "error");
381 }
382 return "Quarantined: " + msg;
383 }
384 return "Alive";
385 },
386 task: function(bot) {
387 return this._taskId(bot);
388 },
389 uptime: function(bot) {
390 var u = this._state(bot, "uptime");
391 if (!u) {
392 return "unknown";
393 }
394 return sk.human.strDuration(u);
395 },
396 version: function(bot) {
397 var v = bot.version || UNKNOWN
398 return v.substring(0, 10);
399 }
400 };
401
402 var deviceColumnMap = {
403 android_devices: function(device) {
404 var str = this._androidAliasDevice(device);
405 if (device.okay) {
406 str = swarming.alias.apply(this._deviceType(device), str);
407 }
408 str += " S/N:";
409 str += device.serial;
410 return str;
411 },
412 battery_health: function(device){
413 var h = (device.battery && device.battery.health) || UNKNOWN;
414 return swarming.alias.apply(h, "battery_health");
415 },
416 battery_level: function(device){
417 return (device.battery && device.battery.level) || UNKNOWN;
418 },
419 battery_status: function(device){
420 var s = (device.battery && device.battery.status) || UNKNOWN;
421 return swarming.alias.apply(s, "battery_status");
422 },
423 battery_temperature: function(device){
424 // Battery temps are in tenths of degrees C - convert to more human rang e.
425 return (device.battery && device.battery.temperature / 10) || UNKNOWN
426 },
427 battery_voltage: function(device){
428 return (device.battery && device.battery.voltage) || UNKNOWN;
429 },
430 device_temperature: function(device){
431 if (this._verbose) {
432 return device.temp.zones || UNKNOWN;
433 }
434 return device.temp.average || UNKNOWN;
435 },
436 device_os: function(device) {
437 if (device.build) {
438 return device.build["build.id"];
439 }
440 return UNKNOWN;
441 },
442 status: function(device) {
443 return device.state;
444 }
445 }
446
447
448 function deviceAverage(col) {
449 return function(dir, botA, botB) {
450 // sort by average of all devices or 0 if no devices.
451 var avgA = 0;
452 var avgB = 0;
453 var devsA = this._devices(botA);
454 devsA.forEach(function(device) {
455 var v = deviceColumnMap[col](device);
456 v = parseFloat(swarming.alias.unapply(v)) || 0;
457 avgA += v / devsA.length;
458 }.bind(this));
459 var devsB = this._devices(botB);
460 devsB.forEach(function(device) {
461 var v = deviceColumnMap[col](device);
462 v = parseFloat(swarming.alias.unapply(v)) || 0;
463 avgB += v / devsB.length;
464 }.bind(this));
465 return dir * swarming.naturalCompare(avgA, avgB);
466 };
467 }
468
469 var specialSort = {
470 android_devices: function(dir, botA, botB) {
471 // We sort on the number of attached devices. Note that this
472 // may not be the same as android_devices, because _devices().length
473 // counts all devices plugged into the bot, whereas android_devices
474 // counts just devices ready for work.
475 var botACol = this._devices(botA).length;
476 var botBCol = this._devices(botB).length;
477 return dir * swarming.naturalCompare(botACol, botBCol);
478 },
479
480 battery_health: deviceAverage("battery_health"),
481 battery_level: deviceAverage("battery_level"),
482 battery_status: deviceAverage("battery_status"),
483 battery_temperature: deviceAverage("battery_temperature"),
484 battery_voltage: deviceAverage("battery_voltage"),
485 device_temperature: deviceAverage("device_temperature"),
486
487 bot_temperature: function(dir, botA, botB) {
488 // Sort by average temperature.
489 var botACol = botA.state.temp.average || 0;
490 var botBCol = botB.state.temp.average || 0;
491 return dir * swarming.naturalCompare(botACol, botBCol);
492 },
493 disk_space: function(dir, botA, botB) {
494 // We sort based on the raw number of MB of the first disk.
495 var botACol = botA.disks[0].mb;
496 var botBCol = botB.disks[0].mb;
497 return dir * swarming.naturalCompare(botACol, botBCol);
498 },
499 first_seen: function(dir, botA, botB) {
500 var botACol = botA.first_seen_ts;
501 var botBCol = botB.first_seen_ts;
502 return dir * swarming.naturalCompare(botACol, botBCol)
503 },
504 last_seen: function(dir, botA, botB) {
505 var botACol = botA.last_seen_ts;
506 var botBCol = botB.last_seen_ts;
507 return dir * swarming.naturalCompare(botACol, botBCol)
508 },
509 running_time: function(dir, botA, botB) {
510 var botACol = this._state(botA, "running_time") || 0;
511 var botBCol = this._state(botB, "running_time") || 0;
512 return dir * swarming.naturalCompare(botACol, botBCol)
513 },
514 uptime: function(dir, botA, botB) {
515 var botACol = this._state(botA, "uptime") || 0;
516 var botBCol = this._state(botB, "uptime") || 0;
517 return dir * swarming.naturalCompare(botACol, botBCol)
518 },
519 };
520
521 Polymer({
522 is: 'bot-list',
523
524 // The order behaviors are applied in matters - later ones overwrite
525 // attributes of earlier ones
526 behaviors: [
527 SwarmingBehaviors.BotListBehavior,
528 SwarmingBehaviors.DynamicTableBehavior,
529 ],
530
531 properties: {
532 client_id: {
533 type: String,
534 },
535
536 _busy1: {
537 type: Boolean,
538 value: false
539 },
540 _busy2: {
541 type: Boolean,
542 value: false
543 },
544 _parseBots: {
545 type: Function,
546 value: function() {
547 return this.$.data.parseBots.bind(this);
548 }
549 },
550
551 // For dynamic table.
552 _columnMap: {
553 type: Object,
554 value: function() {
555 var base = this._commonColumns();
556 for (var attr in columnMap) {
557 base[attr] = columnMap[attr];
558 }
559 return base;
560 },
561 },
562 _headerMap: {
563 type: Object,
564 value: headerMap,
565 },
566 _specialColumns: {
567 type: Array,
568 value: specialColumns,
569 },
570 _specialSort: {
571 type: Object,
572 value: specialSort,
573 },
574 },
575
576 observers:["_reload(_query_params,_auth_headers)"],
577
578 _androidAliasDevice: function(device) {
579 if (device.notReady) {
580 return UNAUTHENTICATED.toUpperCase();
581 }
582 return swarming.alias.android(this._deviceType(device));
583 },
584
585 _botClass: function(bot) {
586 if (bot.is_dead) {
587 return "dead";
588 }
589 if (bot.quarantined) {
590 return "quarantined";
591 }
592 return "";
593 },
594
595 _ccLink: function(bot){
596 var z = this._attribute(bot, "zone")[0];
597 if (z === "unknown") {
598 return undefined;
599 }
600 return this._cloudConsoleLink(z, bot.bot_id);
601 },
602
603 _ccText: function(bot){
604 var z = this._attribute(bot, "zone")[0];
605 if (z === "unknown") {
606 return "Not on GCE";
607 }
608 return "View Bot";
609 },
610
611 _deviceColumn: function(col, device) {
612 var f = deviceColumnMap[col];
613 if (!f || !device) {
614 return "";
615 }
616 return f.bind(this)(device);
617 },
618
619 _deviceClass: function(device) {
620 if (!device.okay) {
621 return "bad-device";
622 }
623 return "";
624 },
625
626 _mpLink: function(bot, template) {
627 if (!bot || !bot.lease_id || !template) {
628 return false;
629 }
630 return template.replace("%s", bot.lease_id);
631 },
632
633 _reload: function() {
634 if (!this._auth_headers || !this._query_params) {
635 return;
636 }
637 var url = "/_ah/api/swarming/v1/bots/list?" + sk.query.fromParamSet(this ._query_params);
638 this.$.page_bots.load(url,this._auth_headers);
639 }
640
641 });
642 })();
643 </script>
644 </dom-module>
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698