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

Unified Diff: appengine/swarming/elements/build/elements.html

Issue 2266133002: Add filter to task-list (Closed) Base URL: https://chromium.googlesource.com/external/github.com/luci/luci-py@extract-filters
Patch Set: tidy up and add task cancelling 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 side-by-side diff with in-line comments
Download patch
Index: appengine/swarming/elements/build/elements.html
diff --git a/appengine/swarming/elements/build/elements.html b/appengine/swarming/elements/build/elements.html
index 533eeef9ba18d9304b001e0fd142a901dc21fb30..0f13028f01c202587226417e0cd6ac0a777e2c3b 100644
--- a/appengine/swarming/elements/build/elements.html
+++ b/appengine/swarming/elements/build/elements.html
@@ -14610,6 +14610,16 @@ You can bind to `isAuthorized` property to monitor authorization state.
value: false,
notify:true,
},
+ permissions: {
+ type: Object,
+ value: function() {
+ // TODO(kjlubick): Make this call the whoami endpoint after signing in.
+ return {
+ can_cancel_task: true,
+ }
+ },
+ notify: true,
+ },
busy: {
type: Boolean,
@@ -14683,6 +14693,37 @@ You can bind to `isAuthorized` property to monitor authorization state.
// This behavior wraps up all the shared swarming functionality.
SwarmingBehaviors.CommonBehavior = {
+ // _getJsonAsync makes an XHR to a url, parses the response as JSON
+ // and sticks the resulting object into the property with the name given
+ // by "bindTo". If busy is defined, the property with that name will be
+ // set to true while the request is in flight and false afterwards.
+ // request headers (e.g. authentication) and query params will be used if
+ // provided. On error, bindTo will be set to false. It is not set to
+ // undefined because computed values in Polymer don't fire if a property
+ // is undefined. Clients should check that bindTo is not falsey.
+ _getJsonAsync: function(bindTo, url, busy, headers, params) {
+ if (!bindTo || !url) {
+ console.log("Need at least a polymer element to bind to and a url");
+ return;
+ }
+ if (busy) {
+ this.set(busy, true);
+ }
+ url = url + "?" + sk.query.fromParamSet(params);
+ sk.request("GET", url, "", headers).then(JSON.parse).then(function(json){
+ this.set(bindTo, json);
+ if (busy) {
+ this.set(busy, false);
+ }
+ }.bind(this)).catch(function(reason){
+ console.log("Reason for failure of request to " + url, reason);
+ this.set(bindTo, false);
+ if (busy) {
+ this.set(busy, false);
+ }
+ }.bind(this));
+ },
+
_not: function(a) {
return !a;
},
@@ -14857,14 +14898,6 @@ You can bind to `isAuthorized` property to monitor authorization state.
_commonColumns: function() {
// return a fresh object so all elements have their own copy
return {
- android_devices: function(bot) {
- var devs = this._attribute(bot, "android_devices", "0");
- if (this._verbose) {
- return devs.join(" | ") + " devices available";
- }
- // max() works on strings as long as they can be coerced to Number.
- return Math.max(...devs) + " devices available";
- },
device_type: function(bot) {
var dt = this._attribute(bot, "device_type", "none");
dt = dt[0];
@@ -24133,6 +24166,14 @@ the fleet.">
};
var columnMap = {
+ android_devices: function(bot) {
+ var devs = this._attribute(bot, "android_devices", "0");
+ if (this._verbose) {
+ return devs.join(" | ") + " devices available";
+ }
+ // max() works on strings as long as they can be coerced to Number.
+ return Math.max(...devs) + " devices available";
+ },
disk_space: function(bot) {
var aliased = [];
bot.disks.forEach(function(disk){
@@ -24298,64 +24339,498 @@ the fleet.">
});
})();
</script>
-</dom-module><dom-module id="task-filters" assetpath="/res/imp/tasklist/">
+</dom-module><script>
+ /** @polymerBehavior Polymer.PaperButtonBehavior */
+ Polymer.PaperButtonBehaviorImpl = {
+ properties: {
+ /**
+ * The z-depth of this element, from 0-5. Setting to 0 will remove the
+ * shadow, and each increasing number greater than 0 will be "deeper"
+ * than the last.
+ *
+ * @attribute elevation
+ * @type number
+ * @default 1
+ */
+ elevation: {
+ type: Number,
+ reflectToAttribute: true,
+ readOnly: true
+ }
+ },
+
+ observers: [
+ '_calculateElevation(focused, disabled, active, pressed, receivedFocusFromKeyboard)',
+ '_computeKeyboardClass(receivedFocusFromKeyboard)'
+ ],
+
+ hostAttributes: {
+ role: 'button',
+ tabindex: '0',
+ animated: true
+ },
+
+ _calculateElevation: function() {
+ var e = 1;
+ if (this.disabled) {
+ e = 0;
+ } else if (this.active || this.pressed) {
+ e = 4;
+ } else if (this.receivedFocusFromKeyboard) {
+ e = 3;
+ }
+ this._setElevation(e);
+ },
+
+ _computeKeyboardClass: function(receivedFocusFromKeyboard) {
+ this.toggleClass('keyboard-focus', receivedFocusFromKeyboard);
+ },
+
+ /**
+ * In addition to `IronButtonState` behavior, when space key goes down,
+ * create a ripple down effect.
+ *
+ * @param {!KeyboardEvent} event .
+ */
+ _spaceKeyDownHandler: function(event) {
+ Polymer.IronButtonStateImpl._spaceKeyDownHandler.call(this, event);
+ // Ensure that there is at most one ripple when the space key is held down.
+ if (this.hasRipple() && this.getRipple().ripples.length < 1) {
+ this._ripple.uiDownAction();
+ }
+ },
+
+ /**
+ * In addition to `IronButtonState` behavior, when space key goes up,
+ * create a ripple up effect.
+ *
+ * @param {!KeyboardEvent} event .
+ */
+ _spaceKeyUpHandler: function(event) {
+ Polymer.IronButtonStateImpl._spaceKeyUpHandler.call(this, event);
+ if (this.hasRipple()) {
+ this._ripple.uiUpAction();
+ }
+ }
+ };
+
+ /** @polymerBehavior */
+ Polymer.PaperButtonBehavior = [
+ Polymer.IronButtonState,
+ Polymer.IronControlState,
+ Polymer.PaperRippleBehavior,
+ Polymer.PaperButtonBehaviorImpl
+ ];
+</script>
+<style is="custom-style">
+
+ :root {
+
+ --shadow-transition: {
+ transition: box-shadow 0.28s cubic-bezier(0.4, 0, 0.2, 1);
+ };
+
+ --shadow-none: {
+ box-shadow: none;
+ };
+
+ /* from http://codepen.io/shyndman/pen/c5394ddf2e8b2a5c9185904b57421cdb */
+
+ --shadow-elevation-2dp: {
+ box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14),
+ 0 1px 5px 0 rgba(0, 0, 0, 0.12),
+ 0 3px 1px -2px rgba(0, 0, 0, 0.2);
+ };
+
+ --shadow-elevation-3dp: {
+ box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.14),
+ 0 1px 8px 0 rgba(0, 0, 0, 0.12),
+ 0 3px 3px -2px rgba(0, 0, 0, 0.4);
+ };
+
+ --shadow-elevation-4dp: {
+ box-shadow: 0 4px 5px 0 rgba(0, 0, 0, 0.14),
+ 0 1px 10px 0 rgba(0, 0, 0, 0.12),
+ 0 2px 4px -1px rgba(0, 0, 0, 0.4);
+ };
+
+ --shadow-elevation-6dp: {
+ box-shadow: 0 6px 10px 0 rgba(0, 0, 0, 0.14),
+ 0 1px 18px 0 rgba(0, 0, 0, 0.12),
+ 0 3px 5px -1px rgba(0, 0, 0, 0.4);
+ };
+
+ --shadow-elevation-8dp: {
+ box-shadow: 0 8px 10px 1px rgba(0, 0, 0, 0.14),
+ 0 3px 14px 2px rgba(0, 0, 0, 0.12),
+ 0 5px 5px -3px rgba(0, 0, 0, 0.4);
+ };
+
+ --shadow-elevation-12dp: {
+ box-shadow: 0 12px 16px 1px rgba(0, 0, 0, 0.14),
+ 0 4px 22px 3px rgba(0, 0, 0, 0.12),
+ 0 6px 7px -4px rgba(0, 0, 0, 0.4);
+ };
+
+ --shadow-elevation-16dp: {
+ box-shadow: 0 16px 24px 2px rgba(0, 0, 0, 0.14),
+ 0 6px 30px 5px rgba(0, 0, 0, 0.12),
+ 0 8px 10px -5px rgba(0, 0, 0, 0.4);
+ };
+
+ }
+
+</style>
+<dom-module id="paper-material-shared-styles" assetpath="/res/imp/bower_components/paper-material/">
<template>
<style>
:host {
display: block;
+ position: relative;
}
+
+ :host([elevation="1"]) {
+ @apply(--shadow-elevation-2dp);
+ }
+
+ :host([elevation="2"]) {
+ @apply(--shadow-elevation-4dp);
+ }
+
+ :host([elevation="3"]) {
+ @apply(--shadow-elevation-6dp);
+ }
+
+ :host([elevation="4"]) {
+ @apply(--shadow-elevation-8dp);
+ }
+
+ :host([elevation="5"]) {
+ @apply(--shadow-elevation-16dp);
+ }
+ </style>
+ </template>
+</dom-module>
+
+
+<dom-module id="paper-material" assetpath="/res/imp/bower_components/paper-material/">
+ <template>
+ <style include="paper-material-shared-styles"></style>
+ <style>
+ :host([animated]) {
+ @apply(--shadow-transition);
+ }
+ </style>
+
+ <content></content>
+ </template>
+</dom-module>
+<script>
+ Polymer({
+ is: 'paper-material',
+
+ properties: {
+ /**
+ * The z-depth of this element, from 0-5. Setting to 0 will remove the
+ * shadow, and each increasing number greater than 0 will be "deeper"
+ * than the last.
+ *
+ * @attribute elevation
+ * @type number
+ * @default 1
+ */
+ elevation: {
+ type: Number,
+ reflectToAttribute: true,
+ value: 1
+ },
+
+ /**
+ * Set this to true to animate the shadow when setting a new
+ * `elevation` value.
+ *
+ * @attribute animated
+ * @type boolean
+ * @default false
+ */
+ animated: {
+ type: Boolean,
+ reflectToAttribute: true,
+ value: false
+ }
+ }
+ });
+</script>
+
+
+<dom-module id="paper-button" assetpath="/res/imp/bower_components/paper-button/">
+ <template strip-whitespace="">
+ <style include="paper-material">
+ :host {
+ @apply(--layout-inline);
+ @apply(--layout-center-center);
+ position: relative;
+ box-sizing: border-box;
+ min-width: 5.14em;
+ margin: 0 0.29em;
+ background: transparent;
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+ -webkit-tap-highlight-color: transparent;
+ font: inherit;
+ text-transform: uppercase;
+ outline-width: 0;
+ border-radius: 3px;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ -webkit-user-select: none;
+ user-select: none;
+ cursor: pointer;
+ z-index: 0;
+ padding: 0.7em 0.57em;
+
+ @apply(--paper-font-common-base);
+ @apply(--paper-button);
+ }
+
+ :host([raised].keyboard-focus) {
+ font-weight: bold;
+ @apply(--paper-button-raised-keyboard-focus);
+ }
+
+ :host(:not([raised]).keyboard-focus) {
+ font-weight: bold;
+ @apply(--paper-button-flat-keyboard-focus);
+ }
+
+ :host([disabled]) {
+ background: #eaeaea;
+ color: #a8a8a8;
+ cursor: auto;
+ pointer-events: none;
+
+ @apply(--paper-button-disabled);
+ }
+
+ paper-ripple {
+ color: var(--paper-button-ink-color);
+ }
+ </style>
+
+ <content></content>
+ </template>
+
+ <script>
+ Polymer({
+ is: 'paper-button',
+
+ behaviors: [
+ Polymer.PaperButtonBehavior
+ ],
+
+ properties: {
+ /**
+ * If true, the button should be styled with a shadow.
+ */
+ raised: {
+ type: Boolean,
+ reflectToAttribute: true,
+ value: false,
+ observer: '_calculateElevation'
+ }
+ },
+
+ _calculateElevation: function() {
+ if (!this.raised) {
+ this._setElevation(0);
+ } else {
+ Polymer.PaperButtonBehaviorImpl._calculateElevation.apply(this);
+ }
+ }
+
+ /**
+ Fired when the animation finishes.
+ This is useful if you want to wait until
+ the ripple animation finishes to perform some action.
+
+ @event transitionend
+ Event param: {{node: Object}} detail Contains the animated node.
+ */
+ });
+ </script>
+</dom-module>
+<dom-module id="task-filters" assetpath="/res/imp/tasklist/">
+ <template>
+ <style is="custom-style" include="iron-flex iron-flex-alignment iron-positioning query-column-filter-style">
+
</style>
+ <url-param name="filters" value="{{_filters}}" default_values="[]" multi="">
+ </url-param>
+ <url-param name="columns" value="{{columns}}" default_values="[&quot;name&quot;,&quot;state&quot;,&quot;created_ts&quot;,&quot;user&quot;]" multi="">
+ </url-param>
+ <url-param name="query" value="{{_query}}" default_value="">
+ </url-param>
+ <url-param name="limit" default_value="200" value="{{limit}}">
+ </url-param>
+
+ <div class="container horizontal layout">
+
+
+ <div class="narrow-down-selector">
+ <div>
+ <paper-input id="filter" label="Search columns and filters" placeholder="gpu nvidia" value="{{_query}}">
+ </paper-input>
+ </div>
+
+ <div class="selector side-by-side" title="This shows all task tags and other interesting task properties. Mark the check box to add as a column. Select the row to see filter options.">
+ <iron-selector attr-for-selected="label" selected="{{_primarySelected}}">
+ <template is="dom-repeat" items="[[_primaryItems]]" as="item">
+ <div class="selectable item horizontal layout" label="[[item]]">
+
+ <span>[[_beforeBold(item,_query)]]<span class="bold">[[_bold(item,_query)]]</span>[[_afterBold(item,_query)]]</span>
+ <span class="flex"></span>
+ <paper-checkbox noink="" disabled$="[[_cantToggleColumn(item)]]" checked="[[_columnState(item,columns.*)]]" on-change="_toggleColumn">
+ </paper-checkbox>
+ </div>
+ </template>
+ </iron-selector>
+ </div>
+
+ <div class="selector side-by-side" title="These are all options (if any) that the task list can be filtered on.">
+ <template is="dom-repeat" id="secondaryList" items="[[_secondaryItems]]" as="item">
+ <div class="item horizontal layout" label="[[item]]">
+
+ <span>[[_beforeBold(item,_query)]]<span class="bold">[[_bold(item,_query)]]</span>[[_afterBold(item,_query)]]</span>
+ <span class="flex"></span>
+ <iron-icon class="icons" icon="icons:arrow-forward" hidden="[[_cantAddFilter(_primarySelected,item,_filters.*)]]" on-tap="_addFilter">
+ </iron-icon>
+ </div>
+ </template>
+ </div>
+
+ <div class="selector side-by-side" title="These tag filters are AND'd together and applied to all tasks.">
+ <template is="dom-repeat" items="[[_filters]]" as="fil">
+ <div class="item horizontal layout" label="[[fil]]">
+ <span>[[fil]]</span>
+ <span class="flex"></span>
+ <iron-icon class="icons" icon="icons:remove-circle-outline" hidden="[[_cantRemoveFilter(fil,_filters.*)]]" on-tap="_removeFilter">
+ </iron-icon>
+ </div>
+ </template>
+ </div>
+
+ <div class="side-by-side">
+ <paper-input id="limit" label="Limit Results" auto-validate="" min="0" max="1000" pattern="[0-9]+" value="{{limit}}">
+ </paper-input>
+ </div>
+ </div>
+
+ </div>
+
</template>
<script>
(function(){
+ // see query-column-filter for more documentation on these properties.
+ var filterMap = {
+ state: function(task, s) {
+ var state = this._attribute(task, "state")[0];
+ if (s === state) {
+ return true;
+ }
+ if (s === "PENDING_RUNNING") {
+ return state === "PENDING" || state === "RUNNING";
+ }
+ var failure = this._attribute(task, "failure", false)[0];
+ if (s === "COMPLETED_SUCCESS") {
+ return state === "COMPLETED" && !failure;
+ }
+ if (s === "COMPLETED_FAILURE") {
+ return state === "COMPLETED" && failure;
+ }
+ var tryNum = this._attribute(task, "try_number", "-1")[0];
+ if (s === "DEDUPED") {
+ return state === "COMPLETED" && tryNum === "0";
+ }
+
+ },
+
+ };
Polymer({
is: 'task-filters',
+ behaviors: [SwarmingBehaviors.QueryColumnFilter],
+
properties: {
// output
columns: {
type: Array,
- value: function() {
- return ["name", "state", "user"];
- },
- notify: true,
- },
- filter: {
- type: Function,
- value: function() {
- return function(){
- return true;
- };
- },
notify: true,
},
query_params: {
type: Object,
+ computed: "_extractQueryParams(_filters.*, limit)",
notify: true,
},
- verbose: {
- type: Boolean,
- value: true,
- notify: true,
- },
+
+ // for QueryColumnFilter
+ _filterMap: {
+ type: Object,
+ value: function() {
+ var base = this._commonFilters();
+ for (var attr in filterMap) {
+ base[attr] = filterMap[attr];
+ }
+ return base;
+ },
+ }
+ },
+
+ _cantToggleColumn: function(col) {
+ // Don't allow the name column to be removed, as the task list is
+ // basically meaningless without it.
+ return !col || col === "name" ;
+ },
+
+ _extractQueryParams: function() {
+ var params = {};
+ var tags = [];
+ this._filters.forEach(function(f) {
+ var split = f.split(this.FILTER_SEP, 1)
+ var col = split[0];
+ var rest = f.substring(col.length + this.FILTER_SEP.length);
+ if (col === "state") {
+ params["state"] = [rest];
+ } else {
+ tags.push(col + this.FILTER_SEP + swarming.alias.unapply(rest))
+ }
+ }.bind(this));
+ params["tags"] = tags;
+ var lim = Math.floor(this.limit)
+ if (Number.isInteger(lim)) {
+ // Clamp the limit
+ lim = Math.max(lim, 1);
+ lim = Math.min(1000, lim);
+ params["limit"] = [lim];
+ // not !== because limit could be the string "900"
+ if (this.limit != lim) {
+ this.set("limit", lim);
+ }
+ }
+ return params;
}
+
});
})();
</script>
</dom-module><dom-module id="task-list-data" assetpath="/res/imp/tasklist/">
- <template>
- <iron-ajax id="tasklist" url="/_ah/api/swarming/v1/tasks/list" headers="[[auth_headers]]" params="[[query_params]]" handle-as="json" last-response="{{_list}}" loading="{{_busy1}}">
- </iron-ajax>
-
-
- </template>
<script>
(function(){
Polymer({
is: 'task-list-data',
- behaviors: [SwarmingBehaviors.CommonBehavior],
+ behaviors: [
+ SwarmingBehaviors.CommonBehavior,
+ ],
properties: {
// inputs
@@ -24365,26 +24840,175 @@ the fleet.">
},
query_params: {
type: Object,
+ observer: "_request",
},
- //outputs
+ // outputs
busy: {
type: Boolean,
- computed: "_or(_busy1)",
+ computed: "_or(_busy1,_busy2,_busy3)",
+ notify: true,
+ },
+ primary_map: {
+ type: Object,
+ computed: "_primaryMap(_tags,_dimensions,tasks)",
+ notify: true,
+ },
+ primary_arr: {
+ type: Array,
+ computed: "_primaryArr(primary_map)",
notify: true,
},
tasks: {
type: Array,
computed: "_tasks(_list)",
notify: true,
- }
+ },
+
+ // private
+ _busy1: {
+ type: Boolean,
+ value: false
+ },
+ _busy2: {
+ type: Boolean,
+ value: false
+ },
+ _busy3: {
+ type: Boolean,
+ value: false
+ },
+ _dimensions: {
+ type: Object,
+ },
+ _list: {
+ type: Object,
+ },
+ _tags: {
+ type: Object,
+ },
},
+
signIn: function(){
- // Auto on iron-ajax means to automatically re-make the request if
- // the url or the query params change. Auto does not trigger if the
- // [auth] headers change, so we wait until the user is signed in
- // before making any requests.
- this.$.tasklist.auto = true;
+ this._getJsonAsync("_tags", "/_ah/api/swarming/v1/tasks/tags",
+ "_busy2", this.auth_headers);
+ this._getJsonAsync("_dimensions","/_ah/api/swarming/v1/bots/dimensions",
+ "_busy3", this.auth_headers);
+
+ this._request();
+ },
+
+ _primaryArr: function(map) {
+ var arr = Object.keys(map);
+ arr.sort();
+ return arr;
+ },
+
+ _primaryMap: function(tags, dims, tasks) {
+ tags = (tags && tags.tasks_tags) || [];
+ dims = (dims && dims.bots_dimensions) || [];
+ tasks = tasks || [];
+ var map = {};
+ // We combine all the tags reported by the tags endpoint, all known
+ // dimensions from the dimensions endpoint, and the tags seen in the
+ // returned tasks, just in case they didn't show up in the first two.
+ // This way a user can filter by what the data actually has and can
+ // discover new tags to filter by.
+ tags.forEach(function(t) {
+ if (!map[t.key]) {
+ map[t.key] = {};
+ }
+ var values = t.value || [];
+ values.forEach(function(v) {
+ map[t.key][v] = true;
+ })
+ });
+
+ dims.forEach(function(d) {
+ var vals = d.value;
+ if (!map[d.key]) {
+ map[d.key] = {};
+ }
+ vals.forEach(function(v) {
+ map[d.key][v] = true;
+ })
+ });
+
+ tasks.forEach(function(t) {
+ Object.keys(t.tagMap).forEach(function(k) {
+ var v = t.tagMap[k];
+ if (!map[k]) {
+ map[k] = {};
+ }
+ map[k][v] = true;
+ });
+ });
+
+ // Turn the Map<Object,Map<Boolean>> into a Map<Object,Array<String>>
+ // with all of the aliases applied.
+ var pMap = {};
+ for (key in map) {
+ var values = Object.keys(map[key]);
+ if (swarming.alias.DIMENSIONS_WITH_ALIASES.indexOf(key) === -1) {
+ pMap[key] = values;
+ } else if (key === "gpu") {
+ var gpus = [];
+ values.forEach(function(g){
+ var alias = swarming.alias.gpu(g);
+ if (alias !== "unknown") {
+ gpus.push(swarming.alias.apply(g, alias));
+ } else {
+ gpus.push(g);
+ }
+ }.bind(this));
+ pMap["gpu"] = gpus;
+ } else if (key === "device_type") {
+ var devs = [];
+ values.forEach(function(dt){
+ var alias = swarming.alias.android(dt);
+ if (alias !== "unknown") {
+ devs.push(swarming.alias.apply(dt, alias));
+ } else {
+ devs.push(dt);
+ }
+ }.bind(this));
+ pMap["device_type"] = devs;
+ } else {
+ console.log("Unknown alias type: ", d);
+ }
+ }
+
+ // Add some options that might not show up.
+ pMap["android_devices"].push("0");
+ pMap["device_os"].push("none");
+ pMap["device_type"].push("none");
+ pMap["user"].push("none");
+
+ // Custom filter options
+ pMap["name"] = [];
+ pMap["state"] = ["PENDING", "RUNNING", "PENDING_RUNNING", "COMPLETED",
+ "COMPLETED_SUCCESS","COMPLETED_FAILURE", "EXPIRED", "TIMED_OUT",
+ "BOT_DIED", "CANCELED", "ALL", "DEDUPED"];
+ pMap["abandoned_ts"] = [];
+ pMap["completed_ts"] = [];
+ pMap["costs_usd"] = [];
+ pMap["created_ts"] = [];
+ pMap["deduped_from"] = [];
+ pMap["duration"] = [];
+ pMap["modified_ts"] = [];
+ pMap["server_versions"] = [];
+ pMap["started_ts"] = [];
+
+ return pMap;
+ },
+
+ _request: function() {
+ // wait until the user has logged in before requesting this.
+ if (!this.auth_headers) {
+ return;
+ }
+ this._getJsonAsync("_list", "/_ah/api/swarming/v1/tasks/list",
+ "_busy1", this.auth_headers, this.query_params);
},
_tasks: function() {
@@ -24392,6 +25016,16 @@ the fleet.">
return [];
}
// Do any preprocessing here
+ this._list.items.forEach(function(t) {
+ var tagMap = {};
+ t.tags.forEach(function(tag) {
+ var split = tag.split(":", 1)
+ var key = split[0];
+ var rest = tag.substring(key.length + 1);
+ tagMap[key] = rest;
+ });
+ t.tagMap = tagMap;
+ });
return this._list.items;
}
});
@@ -24401,27 +25035,46 @@ the fleet.">
<dom-module id="task-list" assetpath="/res/imp/tasklist/">
<template>
<style include="iron-flex iron-flex-alignment iron-positioning swarming-app-style dynamic-table-style">
-
+ task-filters {
+ margin-bottom: 8px;
+ margin-right: 10px;
+ }
.task-list th > span {
/* Leave space for sort-toggle*/
padding-right: 30px;
}
+
+ /* These colors are from buildbot */
+ .failed {
+ background-color: #ffdddd;
+ }
+ .died {
+ background-color: #cccccc;
+ }
+ .exception {
+ background-color: #edd2ff;
+ }
+ .pending {
+ background-color: #fffc6c;
+ }
</style>
<url-param name="sort" value="{{_sortstr}}" default_value="id:asc">
</url-param>
- <swarming-app client_id="[[client_id]]" auth_headers="{{_auth_headers}}" signed_in="{{_signed_in}}" busy="[[_busy]]" name="Swarming Task List">
+ <swarming-app client_id="[[client_id]]" auth_headers="{{_auth_headers}}" permissions="{{_permissions}}" signed_in="{{_signed_in}}" busy="[[_busy]]" name="Swarming Task List">
<h2 hidden$="[[_signed_in]]">You must sign in to see anything useful.</h2>
<div hidden$="[[_not(_signed_in)]]">
- <task-list-data auth_headers="[[_auth_headers]]" query_params="[[_query_params]]" tasks="{{_items}}" busy="{{_busy}}">
+ <task-list-data auth_headers="[[_auth_headers]]" query_params="[[_query_params]]" tasks="{{_items}}" busy="{{_busy}}" primary_map="{{_primary_map}}" primary_arr="{{_primary_arr}}">
</task-list-data>
+ <paper-toast id="toast"></paper-toast>
+
<div class="horizontal layout">
- <task-filters columns="{{_columns}}" query_params="{{_query_params}}" filter="{{_filter}}" verbose="{{_verbose}}">
+ <task-filters primary_map="[[_primary_map]]" primary_arr="[[_primary_arr]]" columns="{{_columns}}" query_params="{{_query_params}}" filter="{{_filter}}">
</task-filters>
</div>
@@ -24437,11 +25090,17 @@ the fleet.">
</th>
<th hidden$="[[_hide('state', _columns.*)]]">
- <span>Status</span>
+ <span>State</span>
<sort-toggle name="state" current="[[_sort]]">
</sort-toggle>
</th>
+ <th hidden$="[[_hide('deduped_from', _columns.*)]]">
+ <span>Deduped from</span>
+ <sort-toggle name="deduped_from" current="[[_sort]]">
+ </sort-toggle>
+ </th>
+
<template is="dom-repeat" items="[[_plainColumns]]" as="c">
<th hidden$="[[_hide(c)]]">
<span>[[_header(c)]]</span>
@@ -24456,18 +25115,25 @@ the fleet.">
<tr class$="[[_taskClass(task)]]">
<td>
- <a class="center" href$="[[_taskLink(task)]]" target="_blank">
+ <a class="center" href$="[[_taskLink(task.task_id)]]" target="_blank">
[[task.name]]
</a>
</td>
<td hidden$="[[_hide('state', _columns.*)]]">
- [[_column('state', task, _verbose)]]
-
+ [[_column('state', task)]]
+ <paper-button raised="" hidden$="[[_cannotCancel(task,_permissions)]]" on-tap="_cancelTask">
+ Cancel
+ </paper-button>
+ </td>
+ <td hidden$="[[_hide('deduped_from', _columns.*)]]">
+ <a class="center" href$="[[_taskLink(task.deduped_from)]]" target="_blank">
+ [[_column('deduped_from',task)]]
+ </a>
</td>
<template is="dom-repeat" items="[[_plainColumns]]" as="c">
<td hidden$="[[_hide(c)]]">
- [[_column(c, task, _verbose)]]
+ [[_column(c, task)]]
</td>
</template>
@@ -24482,8 +25148,27 @@ the fleet.">
</template>
<script>
(function(){
- var specialColumns = ["name", "state"];
- var columnMap = {};
+ var specialColumns = ["deduped_from", "name", "state"];
+ var columnMap = {
+ costs_usd: function(task) {
+ return this._attribute(task, "costs_usd", 0)[0];
+ },
+ state: function(task) {
+ var state = this._attribute(task, "state")[0];
+ if (state === "COMPLETED") {
+
+ if (this._attribute(task, "failure", false)[0]) {
+ return "COMPLETED (FAILURE)";
+ }
+ var tryNum = this._attribute(task, "try_number", "-1")[0];
+ if (tryNum === "0") {
+ return "COMPLETED (DEDUPED)";
+ }
+ return "COMPLETED (SUCCESS)";
+ }
+ return state;
+ },
+ };
var headerMap = {
"user": "Requesting User",
};
@@ -24503,7 +25188,13 @@ the fleet.">
// For dynamic table.
_columnMap: {
type: Object,
- value: columnMap,
+ value: function() {
+ var base = this._commonColumns();
+ for (var attr in columnMap) {
+ base[attr] = columnMap[attr];
+ }
+ return base;
+ },
},
_headerMap: {
type: Object,
@@ -24520,20 +25211,79 @@ the fleet.">
},
_attribute: function(task, col, def) {
- var retVal = task[col] || [def];
+ if (def === undefined) {
+ def = "none";
+ }
+ var retVal = this._tag(task, col) || task[col] || [def];
if (!Array.isArray(retVal)) {
return [retVal];
}
return retVal;
},
- _taskLink: function(task) {
+ _cannotCancel: function(task, permissions) {
+ return !(permissions && permissions.can_cancel_task &&
+ this._column("state", task) === "PENDING");
+ },
+
+ _cancelTask: function(e) {
+ var task = e.model.task;
+ if (!task || !task.task_id) {
+ console.log("Missing task info", task);
+ return
+ }
+ var id = task.task_id
+
+ // Keep toast displayed until we hear back from the cancel.
+ this.$.toast.duration = 0;
+ this.$.toast.text="Canceling task " + id;
+ this.$.toast.open();
+ var url = "/_ah/api/swarming/v1/task/" + id +"/cancel";
+ sk.request("POST", url, undefined, this._auth_headers).then(function(response) {
+ this.$.toast.close();
+ this.$.toast.show({
+ text: "Request sent. Response: "+response,
+ duration: 3000,
+ });
+ }.bind(this)).catch(function(reason) {
+ console.log("Cancellation failed", reason);
+ this.$.toast.close();
+ this.$.toast.show({
+ text: "Request failed. Reason: "+reason,
+ duration: 3000,
+ });
+ }.bind(this));
+ },
+
+ _tag: function(task, col) {
+ if (!task || !task.tagMap) {
+ return undefined;
+ }
+ return task.tagMap[col];
+ },
+
+ _taskLink: function(taskId) {
+ if (!taskId) {
+ return undefined;
+ }
// TODO(kjlubick) Make this point to /newui/ when appropriate.
- return "/restricted/task/"+task.task_id;
+ return "/restricted/task/"+taskId;
},
_taskClass: function(task) {
- // TODO(kjlubick): Color tasks?
+ var state = this._column("state", task);
+ if (state === "CANCELED" ||state === "TIMED_OUT" || state === "EXPIRED") {
+ return "exception";
+ }
+ if (state === "BOT_DIED") {
+ return "died";
+ }
+ if (state === "COMPLETED (FAILURE)") {
+ return "failed";
+ }
+ if (state === "RUNNING" || state === "PENDING") {
+ return "pending";
+ }
return "";
}

Powered by Google App Engine
This is Rietveld 408576698