| Index: appengine/swarming/elements/build/elements.html
|
| diff --git a/appengine/swarming/elements/build/elements.html b/appengine/swarming/elements/build/elements.html
|
| index 6e78d4ab734422b01902a968d9d8214283d5e8fd..54dcf0ef3fabedb130f98832e9e96ccb78e7b01d 100644
|
| --- a/appengine/swarming/elements/build/elements.html
|
| +++ b/appengine/swarming/elements/build/elements.html
|
| @@ -23611,7 +23611,7 @@ the fleet.">
|
| notify: true,
|
| },
|
| primary_map: {
|
| - type:Object,
|
| + type: Object,
|
| computed: "_primaryMap(_dimensions)",
|
| notify: true,
|
| },
|
| @@ -24296,21 +24296,182 @@ the fleet.">
|
| });
|
| })();
|
| </script>
|
| -</dom-module><dom-module id="task-filters" assetpath="/res/imp/tasklist/">
|
| +</dom-module><dom-module id="query-column-filter-style" assetpath="/res/imp/common/">
|
| <template>
|
| <style>
|
| :host {
|
| display: block;
|
| + font-family: sans-serif;
|
| }
|
| + #filter {
|
| + margin:0 5px;
|
| + }
|
| +
|
| + .container {
|
| + min-height: 120px;
|
| + width: 100%;
|
| + }
|
| +
|
| + .item {
|
| + border-bottom: 1px solid #EEE;
|
| + max-width: 250px;
|
| + min-height: 1.0em;
|
| + min-width: 100px;
|
| + padding: 0.1em 0.2em;
|
| + line-height: 1.5em;
|
| + }
|
| +
|
| + .header {
|
| + height: 2em;
|
| + padding: .25em;
|
| + line-height: 2em;
|
| + }
|
| +
|
| + .selector {
|
| + border: 1px solid black;
|
| + margin: 0 5px;
|
| + max-height: 200px;
|
| + min-height: 130px;
|
| + min-width: 200px;
|
| + overflow-y: auto;
|
| + overflow-x: hidden;
|
| + }
|
| +
|
| + .selectable {
|
| + cursor: pointer;
|
| + }
|
| +
|
| + .selectable:hover {
|
| + /* See https://sites.google.com/a/google.com/skia-infrastructure/design-docs/general-design-guidance */
|
| + background-color: #A6CEE3;
|
| + }
|
| +
|
| + .iron-selected {
|
| + /* See https://sites.google.com/a/google.com/skia-infrastructure/design-docs/general-design-guidance */
|
| + background-color: #1F78B4;
|
| + color: white;
|
| + }
|
| +
|
| + .icons {
|
| + cursor:pointer;
|
| + height:20px;
|
| + margin:2px;
|
| + width:20px;
|
| + flex-shrink: 0;
|
| + }
|
| +
|
| + .side-by-side {
|
| + display: inline-block;
|
| + vertical-align: top;
|
| + }
|
| +
|
| + .bold {
|
| + font-weight: bold;
|
| + }
|
| +
|
| + paper-checkbox {
|
| + max-height: 2em;
|
| + margin: 2px;
|
| + /* See https://sites.google.com/a/google.com/skia-infrastructure/design-docs/general-design-guidance */
|
| + --paper-checkbox-checked-color: black;
|
| + --paper-checkbox-checked-ink-color: black;
|
| + --paper-checkbox-unchecked-color: black;
|
| + --paper-checkbox-unchecked-ink-color: black;
|
| + --paper-checkbox-label-color: black;
|
| + }
|
| + </style>
|
| +
|
| + </template>
|
| +</dom-module>
|
| +
|
| +<script>
|
| + window.SwarmingBehaviors = window.SwarmingBehaviors || {};
|
| + (function(){
|
| + // This behavior wraps up all the shared swarming functionality.
|
| + SwarmingBehaviors.QueryColumnFilter = {
|
| +
|
| + };
|
| + })();
|
| +</script>
|
| +<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>
|
|
|
| + <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 bot dimension names and other interesting bot 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 bot 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 filters are AND'd together and applied to all bots in
|
| +the fleet.">
|
| + <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-checkbox checked="{{verbose}}">Verbose Entries</paper-checkbox>
|
| + <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(){
|
| Polymer({
|
| is: 'task-filters',
|
|
|
| + behaviors: [SwarmingBehaviors.QueryColumnFilter],
|
| +
|
| properties: {
|
| + // input
|
| + primary_map: {
|
| + type: Object,
|
| + },
|
| + primary_arr: {
|
| + type: Array,
|
| + },
|
| +
|
| // output
|
| columns: {
|
| type: Array,
|
| @@ -24337,7 +24498,91 @@ the fleet.">
|
| value: true,
|
| notify: true,
|
| },
|
| - }
|
| +
|
| + // private
|
| + _filters: {
|
| + type:Array,
|
| + },
|
| + _limit: {
|
| + type: Number,
|
| + },
|
| + _primaryItems: {
|
| + type: Array,
|
| + computed: "_primary(_query, primary_map, primary_arr, columns.*)",
|
| + },
|
| + _primarySelected: {
|
| + type: String,
|
| + value: "",
|
| + },
|
| + // query is treated as a space separated list.
|
| + _query: {
|
| + type:String,
|
| + },
|
| + _secondaryItems: {
|
| + type: Array,
|
| + computed: "_secondary(_primarySelected, _query, primary_map)",
|
| + },
|
| + },
|
| +
|
| +
|
| + _primary: function(query, primary_map, primary_arr) {
|
| + // If the user has typed in a query, only show those primary keys that
|
| + // partially match the query or that have secondary values which
|
| + // partially match.
|
| + var arr = this.primary_arr.filter(function(s){
|
| + if (matchPartCaseInsensitive(s, query).idx !== -1) {
|
| + return true;
|
| + }
|
| + var opts = primary_map[s];
|
| + for (var i = 0; i < opts.length; i++) {
|
| + if (matchPartCaseInsensitive(opts[i], query).idx !== -1) {
|
| + return true;
|
| + }
|
| + }
|
| + return false;
|
| + });
|
| + // Update the selected to be the current one (if it is still with being
|
| + // shown) or the first match. This saves the user from having to click
|
| + // the first result before seeing results.
|
| + if (query && arr.length > 0 &&
|
| + arr.indexOf(this._primarySelected) === -1) {
|
| + this.set("_primarySelected", arr[0]);
|
| + }
|
| + return arr;
|
| + },
|
| +
|
| + _secondary: function(primarySelected, query, primary_map) {
|
| + // Changing the secondary list doesn't always trigger a reorder of the
|
| + // secondary elements. So, we request it be done asynchronously.
|
| + requestAnimationFrame(function(){
|
| + this.$.secondaryList.render();
|
| + }.bind(this));
|
| +
|
| + // Only show secondary options when a primary option has been selected.
|
| + // If the user has typed in a query, show all secondary elements if
|
| + // their primary element matches. If it doesn't match the primary
|
| + // element, only show those secondary elements that do.
|
| + if (!primarySelected) {
|
| + return [];
|
| + }
|
| + if (matchPartCaseInsensitive(primarySelected, query).idx !== -1) {
|
| + // Sort the secondaries alphabetically, but prioritize query matches.
|
| + return primary_map[primarySelected].sort(function(a, b){
|
| + var aMatch = matchPartCaseInsensitive(a, query).idx !== -1;
|
| + var bMatch = matchPartCaseInsensitive(b, query).idx !== -1;
|
| + if (aMatch === bMatch) {
|
| + return swarming.naturalCompare(a,b);
|
| + }
|
| + // true == 1 and false == 0. So, put the one that matches first.
|
| + return bMatch - aMatch;
|
| + });
|
| + }
|
| + // Otherwise, filter out those that do not match.
|
| + return primary_map[primarySelected].filter(function(s) {
|
| + return matchPartCaseInsensitive(s, query).idx !== -1;
|
| + });
|
| + },
|
| +
|
| });
|
| })();
|
| </script>
|
| @@ -24345,7 +24590,9 @@ the fleet.">
|
| <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>
|
| -
|
| +
|
| + <iron-ajax id="tags" url="/_ah/api/swarming/v1/tasks/tags" headers="[[auth_headers]]" handle-as="json" last-response="{{_tags}}" loading="{{_busy2}}">
|
| + </iron-ajax>
|
|
|
| </template>
|
| <script>
|
| @@ -24365,10 +24612,20 @@ the fleet.">
|
| type: Object,
|
| },
|
|
|
| - //outputs
|
| + // outputs
|
| busy: {
|
| type: Boolean,
|
| - computed: "_or(_busy1)",
|
| + computed: "_or(_busy1,_busy2)",
|
| + notify: true,
|
| + },
|
| + primary_map: {
|
| + type: Object,
|
| + computed: "_primaryMap(_tags)",
|
| + notify: true,
|
| + },
|
| + primary_arr: {
|
| + type: Array,
|
| + computed: "_primaryArr(_tags)",
|
| notify: true,
|
| },
|
| tasks: {
|
| @@ -24377,12 +24634,38 @@ the fleet.">
|
| notify: true,
|
| }
|
| },
|
| +
|
| 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.$.tags.auto = true;
|
| + },
|
| +
|
| + _primaryArr: function(tags) {
|
| + tags = tags.tasks_tags;
|
| +
|
| + var arr = [];
|
| + tags.forEach(function(t) {
|
| + arr.push(t.key)
|
| + });
|
| +
|
| + arr.sort();
|
| +
|
| + return arr;
|
| + },
|
| +
|
| + _primaryMap: function(tags) {
|
| + tags = tags.tasks_tags;
|
| +
|
| + var pMap = {};
|
| + tags.forEach(function(t) {
|
| + pMap[t.key] = t.value;
|
| + });
|
| +
|
| + return pMap;
|
| },
|
|
|
| _tasks: function() {
|
| @@ -24414,12 +24697,12 @@ the fleet.">
|
| <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>
|
|
|
| <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}}" verbose="{{_verbose}}">
|
| </task-filters>
|
|
|
| </div>
|
|
|