From 6311e8b8b35feee0f78da04e839c62323c07f7f0 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Mon, 28 Mar 2016 11:39:37 -0400 Subject: [PATCH 1/4] initial commit of tag search ui code --- awx/ui/client/src/app.js | 2 + awx/ui/client/src/helpers/search.js | 3 - awx/ui/client/src/lists/Projects.js | 1 + .../src/search/getSearchHtml.service.js | 31 +++ awx/ui/client/src/search/main.js | 9 + awx/ui/client/src/search/tagSearch.block.less | 239 ++++++++++++++++++ .../client/src/search/tagSearch.controller.js | 105 ++++++++ .../client/src/search/tagSearch.directive.js | 30 +++ .../client/src/search/tagSearch.partial.html | 78 ++++++ awx/ui/client/src/search/tagSearch.service.js | 148 +++++++++++ .../src/shared/branding/colors.default.less | 2 +- .../list-generator/list-generator.factory.js | 39 +-- 12 files changed, 649 insertions(+), 38 deletions(-) create mode 100644 awx/ui/client/src/search/getSearchHtml.service.js create mode 100644 awx/ui/client/src/search/main.js create mode 100644 awx/ui/client/src/search/tagSearch.block.less create mode 100644 awx/ui/client/src/search/tagSearch.controller.js create mode 100644 awx/ui/client/src/search/tagSearch.directive.js create mode 100644 awx/ui/client/src/search/tagSearch.partial.html create mode 100644 awx/ui/client/src/search/tagSearch.service.js diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index 87d3b6b06b..8b5f7ec78d 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -51,6 +51,7 @@ import activityStream from './activity-stream/main'; import standardOut from './standard-out/main'; import lookUpHelper from './lookup/main'; import JobTemplates from './job-templates/main'; +import search from './search/main'; import {ScheduleEditController} from './controllers/Schedules'; import {ProjectsList, ProjectsAdd, ProjectsEdit} from './controllers/Projects'; import OrganizationsList from './organizations/list/organizations-list.controller'; @@ -109,6 +110,7 @@ var tower = angular.module('Tower', [ standardOut.name, access.name, JobTemplates.name, + search.name, 'templates', 'Utilities', 'OrganizationFormDefinition', diff --git a/awx/ui/client/src/helpers/search.js b/awx/ui/client/src/helpers/search.js index 33f7ec5c86..de68732ed1 100644 --- a/awx/ui/client/src/helpers/search.js +++ b/awx/ui/client/src/helpers/search.js @@ -392,9 +392,6 @@ export default scope[iterator + 'HoldInput' + modifier] = true; if ($('#search-widget-container' + modifier) && list.fields[scope[iterator + 'SearchField' + modifier]] && !list.fields[scope[iterator + 'SearchField' + modifier]].searchObject) { - - // if the search widget exists and its value is not an object, add its parameters to the query - if (scope[iterator + 'SearchValue' + modifier]) { // if user typed a value in the input box, show the reset link scope[iterator + 'ShowStartBtn' + modifier] = false; diff --git a/awx/ui/client/src/lists/Projects.js b/awx/ui/client/src/lists/Projects.js index cd76d48dcd..43f9983339 100644 --- a/awx/ui/client/src/lists/Projects.js +++ b/awx/ui/client/src/lists/Projects.js @@ -30,6 +30,7 @@ export default icon: "icon-job-{{ project.statusIcon }}", columnClass: "List-staticColumn--smallStatus", nosort: true, + searchLabel: 'Status', searchType: 'select', searchOptions: [], //set in the controller excludeModal: true diff --git a/awx/ui/client/src/search/getSearchHtml.service.js b/awx/ui/client/src/search/getSearchHtml.service.js new file mode 100644 index 0000000000..2e941ce381 --- /dev/null +++ b/awx/ui/client/src/search/getSearchHtml.service.js @@ -0,0 +1,31 @@ +export default [function() { + // given the list, return the fields that need searching + this.getList = function(list) { + return JSON.stringify(Object + .keys(list.fields) + .filter(function(i) { + return (list.fields[i] + .searchable !== false); + }).map(function(i) { + return {[i]: list.fields[i]}; + }).reduce(function (acc, i) { + var key = Object.keys(i); + acc[key] = i[key]; + return acc; + })); + }; + + // given the list config object, return the basepath + this.getEndpoint = function(list) { + return list.basePath || list.name; + }; + + // inject the directive with the list and endpoint + this.inject = function(list, endpoint) { + return ""; + }; + + return this; +}]; diff --git a/awx/ui/client/src/search/main.js b/awx/ui/client/src/search/main.js new file mode 100644 index 0000000000..69c7dbf40a --- /dev/null +++ b/awx/ui/client/src/search/main.js @@ -0,0 +1,9 @@ +import tagSearchDirective from './tagSearch.directive'; +import tagSearchService from './tagSearch.service'; +import getSearchHtml from './getSearchHtml.service'; + +export default + angular.module('search', []) + .directive('tagSearch', tagSearchDirective) + .factory('tagSearchService', tagSearchService) + .factory('getSearchHtml', getSearchHtml); diff --git a/awx/ui/client/src/search/tagSearch.block.less b/awx/ui/client/src/search/tagSearch.block.less new file mode 100644 index 0000000000..7e89864562 --- /dev/null +++ b/awx/ui/client/src/search/tagSearch.block.less @@ -0,0 +1,239 @@ +@import "../shared/branding/colors.default.less"; + +.TagSearch { + margin-bottom: 10px; +} + +.TagSearch-bar { + display: flex; + padding: 0; + font-size: 12px; + height: 35px; + align-items: stretch; + margin-bottom: 10px; +} + +.TagSearch-bar i { + font-size: 16px; + color: @default-icon; +} + +.TagSearch-typeDropdown { + color: @default-interface-txt; + flex: initial; + border: 1px solid @default-second-border; + border-top-left-radius: 5px; + border-bottom-left-radius: 5px; + padding: 0px 10px; + display: flex; + white-space: nowrap; + align-items: center; + max-height: 400px; + overflow-y: scroll; + width: 100px; + cursor: pointer; +} + +.TagSearch-typeDropdown.is-open { + border-bottom-left-radius: 0; +} + +.TagSearch-typeDropdownName { + width: 66px; + text-overflow: ellipsis; + display: block; + overflow: hidden; +} + +.TagSearch-selectDownIcon { + margin-left: 10px; +} + +.TagSearch-dropdownContainer { + position: absolute; + left: 15px; + top: 34px; + font-size: 14px; + border-radius: 5px; + border: 1px solid @default-second-border; + background: white; + padding: 5px 0; + border-top-left-radius: 0px; + border-top-right-radius: 0px; + z-index: 50000; + max-height: 200px; + overflow-y: scroll; + box-shadow: 2px 2px 2px 0px rgba(0, 0, 0, 0.1); +} + +.TagSearch-dropdownContainer--searchTypes { + min-width: 96px; +} + +.TagSearch-dropdownContainer--typeOptions { + right: 15px; + left: initial; + width: ~"calc(100% - 123px)"; +} + +.TagSearch-dropdownItem { + padding: 5px 10px; + cursor: pointer; +} + +.TagSearch-dropdownItem:hover { + background-color: @default-tertiary-bg; +} + +.TagSearch-dropdownItem.is-selected { + background-color: @default-button-hov; +} + +.TagSearch-searchTermContainer { + flex: initial; + width: ~"calc(100% - 70px)"; + border: 1px solid @default-second-border; + border-left: 0px; + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; + display: flex; + background-color: @default-secondary-bg; +} + +.TagSearch-searchTermContainer.is-open { + border-bottom-right-radius: 0; +} + +.TagSearch-searchTermContainer input { + flex: 1 0 auto; + margin: 0 10px; + border: none; + font-size: 14px; +} + +.TagSearch-searchTermContainer input:focus, +.TagSearch-searchTermContainer input:active { + outline: 0; +} + +.TagSearch-searchTermSelect { + padding: 0px 10px !important; + display: flex; + align-items: center; + width: 100%; +} + +.TagSearch-searchTermSelectPlaceholder { + color: @default-icon !important; + text-transform: uppercase; + font-size: 14px; + flex: 1 0 auto; +} + +.TagSearch-searchTermContainer input:placeholder-shown { + color: @default-icon !important; + text-transform: uppercase; +} + +.TagSearch-searchTermSelect { + padding: 10px; + background-color: @default-bg; + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; + cursor: pointer; +} + +.TagSearch-searchButton { + flex: initial; + margin-left: auto; + padding: 8px 10px; + border-left: 1px solid @default-second-border; + background-color: @default-bg; + cursor: pointer; + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; +} + +.TagSearch-searchButton:hover { + background-color: @default-tertiary-bg; +} + +.TagSearch-flexContainer { + display: flex; + width: 100%; + flex-wrap: wrap; +} + +.TagSearch-tagContainer { + display: flex; + max-width: 100%; + margin-bottom: 10px; +} + +.TagSearch-tag { + border-radius: 5px; + padding: 2px 10px; + margin: 4px 0px; + border: 1px solid @default-second-border; + font-size: 12px; + color: @default-interface-txt; + text-transform: uppercase; + background-color: @default-bg; + margin-right: 5px; + max-width: 100%; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + +.TagSearch-tag--deletable { + margin-right: 0px; + border-top-right-radius: 0px; + border-bottom-right-radius: 0px; + border-right: 0; + max-wdith: ~"calc(100% - 23px)"; +} + +.TagSearch-deleteContainer { + border: 1px solid @default-second-border; + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; + padding: 0 5px; + margin: 4px 0px; + margin-right: 5px; + align-items: center; + display: flex; + cursor: pointer; +} + +.TagSearch-tagDelete { + font-size: 13px; + color: @default-icon; +} + +.TagSearch-name { + flex: initial; + max-width: 100%; +} + +.TagSearch-tag--deletable > .TagSearch-name { + max-width: ~"calc(100% - 23px)"; +} + +.TagSearch-deleteContainer:hover, { + border-color: @default-err; + background-color: @default-err; +} + +.TagSearch-deleteContainer:hover > .TagSearch-tagDelete { + color: @default-bg; +} + +// TODO: Actually Remove the old list search widgets from the partials +.List-searchWidget { + display: none !important; +} + +.List-searchRow { + display: none !important; +} diff --git a/awx/ui/client/src/search/tagSearch.controller.js b/awx/ui/client/src/search/tagSearch.controller.js new file mode 100644 index 0000000000..a265873a5e --- /dev/null +++ b/awx/ui/client/src/search/tagSearch.controller.js @@ -0,0 +1,105 @@ +export default ['$scope', 'Refresh', 'tagSearchService', + function($scope, Refresh, tagSearchService) { + // JSONify passed field elements that can be searched + $scope.list = JSON.parse($scope.list); + + // Hotfix: GetBasePath to work with inventories + $scope.$watch("endpoint", function(val) { + if (val === 'inventories') { + $scope.endpoint = 'inventory'; + } + }); + + // Grab options for the left-dropdown of the searchbar + tagSearchService.getSearchTypes($scope.list, $scope.endpoint) + .then(function(searchTypes) { + $scope.searchTypes = searchTypes; + + // currently selected option of the left-dropdown + $scope.currentSearchType = $scope.searchTypes[0]; + }); + + // shows/hide the search type dropdown + $scope.toggleTypeDropdown = function() { + $scope.showTypeDropdown = !$scope.showTypeDropdown; + }; + + // sets the search type dropdown and hides it + $scope.setSearchType = function(type) { + $scope.currentSearchType = type; + $scope.showTypeDropdown = false; + }; + + // if the current search type uses a list instead + // of a text input, this show hides that list + $scope.toggleCurrentSearchDropdown = function() { + $scope + .showCurrentSearchDropdown = !$scope + .showCurrentSearchDropdown; + }; + + $scope.updateSearch = function(tags) { + var iterator = $scope.$parent.list.iterator; + var pageSize = $scope + .$parent[iterator + "_page_size"]; + var set = $scope.$parent.list.name; + var listScope = $scope.$parent; + var url = tagSearchService + .updateFilteredUrl($scope.endpoint, tags, pageSize); + + $scope.$parent[iterator + "_active_search"] = true; + + Refresh({ + scope: listScope, + set: set, + iterator: iterator, + url: url + }); + + $scope.currentSearchFilters = tags; + }; + + // triggers a refilter of the list with the newTag + $scope.addTag = function(type) { + var newTag = tagSearchService + .getTag($scope.currentSearchType, + $scope.newSearchTag, + type); + + // reset the search bar + $scope.resetSearchBar(); + + // make a clone of the currentSearchFilters + // array and push the newTag to this array + var tags = tagSearchService + .getCurrentTags($scope + .currentSearchFilters); + + if (!tagSearchService.isDuplicate(tags, newTag)) { + tags.push(newTag); + $scope.updateSearch(tags); + } + }; + + // triggers a refilter of the list without the oldTag + $scope.deleteTag = function(oldTag) { + // make a clone of the currentSearchFilters + // array and remove oldTag from the array + var tags = tagSearchService + .getCurrentTags($scope + .currentSearchFilters) + .filter(function(tag) { + return tag.url !== oldTag.url; + }); + + $scope.updateSearch(tags); + }; + + // make sure all stateful UI triggers are reset + $scope.resetSearchBar = function() { + delete $scope.currentSearchSelectedOption; + $scope.newSearchTag = null; + $scope.showTypeDropdown = false; + $scope.showCurrentSearchDropdown = false; + }; + }]; diff --git a/awx/ui/client/src/search/tagSearch.directive.js b/awx/ui/client/src/search/tagSearch.directive.js new file mode 100644 index 0000000000..d1515ea03c --- /dev/null +++ b/awx/ui/client/src/search/tagSearch.directive.js @@ -0,0 +1,30 @@ +import tagSearchController from './tagSearch.controller'; + +/* jshint unused: vars */ +export default + ['templateUrl', + function(templateUrl) { + return { + restrict: 'E', + scope: { + list: '@', + endpoint: '@', + iterator: '@' + }, + controller: tagSearchController, + templateUrl: templateUrl('search/tagSearch'), + link: function(scope, element, attrs) { + // make the enter button work as if clicking the + // search icon + element + .find('.TagSearch-searchTermInput') + .bind('keypress', function (e) { + var code = e.keyCode || e.which; + if (code === 13) { + scope.addTag(); + } + }); + } + }; + } + ]; diff --git a/awx/ui/client/src/search/tagSearch.partial.html b/awx/ui/client/src/search/tagSearch.partial.html new file mode 100644 index 0000000000..0e557dca2a --- /dev/null +++ b/awx/ui/client/src/search/tagSearch.partial.html @@ -0,0 +1,78 @@ +
+
+
+
+ + {{ currentSearchType.label || "Foo bar"}} + + +
+
+
+ {{ type.label }} +
+
+
+ +
+ +
+ +
+
+ Filter +
+ + +
+
+
+ {{ type.label }} +
+
+
+
+
+ +
+
+
+
+
+ {{ tag.name }} + +
+
+ +
+
+
+
+
+
diff --git a/awx/ui/client/src/search/tagSearch.service.js b/awx/ui/client/src/search/tagSearch.service.js new file mode 100644 index 0000000000..347e2df0f0 --- /dev/null +++ b/awx/ui/client/src/search/tagSearch.service.js @@ -0,0 +1,148 @@ +export default ['Rest', '$q', 'GetBasePath', function(Rest, $q, GetBasePath) { + var that = this; + // parse the field config object to return + // one of the searchTypes (for the left dropdown) + this.buildType = function (field, key, id) { + // build the value (key) + var value; + if (typeof(field.key) === String) { + value = field.key; + } else { + value = key; + } + + // build the label + var label = field.searchLabel || field.label; + + // build the search type + var type, typeOptions; + if (field.searchType === 'select') { + type = 'select'; + typeOptions = []; + } else if (field.searchType === 'boolean') { + type = 'select'; + typeOptions = [{label: "Yes", value: true}, + {label: "No", value: false}]; + } else { + type = 'text'; + } + + // return the built option + if (type === 'select') { + return { + id: id, + value: value, + label: label, + type: type, + typeOptions: typeOptions + }; + } else { + return { + id: id, + value: value, + label: label, + type: type + }; + } + }; + + // given the fields that are searchable, + // return searchTypes in the format the view can use + this.getSearchTypes = function(list, basePath) { + var defer = $q.defer(); + + var options = Object + .keys(list) + .map(function(key, id) { + return that.buildType(list[key], key, id); + }); + + var needsRequest, passThrough; + + // splits off options that need a request from + // those that don't + var partitionedOptions = _.partition(options, function(opt) { + return (opt.typeOptions && !opt.typeOptions + .length) ? true : false; + }); + + needsRequest = partitionedOptions[0]; + passThrough = partitionedOptions[1]; + + var joinOptions = function() { + return _.sortBy(_ + .flatten([needsRequest, passThrough]), function(opt) { + return opt.id; + }); + }; + + if (needsRequest.length) { + // make the options request to reutrn the typeOptions + Rest.setUrl(GetBasePath(basePath)); + Rest.options() + .success(function (data) { + var options = data.actions.GET; + needsRequest = needsRequest + .map(function (option) { + option.typeOptions = options[option + .value] + .choices + .map(function(i) { + return { + value: i[0], + label: i[1] + }; + }); + + return option; + }); + + defer.resolve(joinOptions()); + }); + } else { + defer.resolve(joinOptions()); + } + + return defer.promise; + }; + + // returns the url with filter params + this.updateFilteredUrl = function(basePath, tags, pageSize) { + return GetBasePath(basePath) + "?" + + (tags || []).map(function (t) { + return t.url; + }).join("&") + "&page_size=" + pageSize; + }; + + // given the field and input filters, create the tag object + this.getTag = function(field, textVal, selectVal) { + var tag = _.clone(field); + if (tag.type === "text") { + tag.url = tag.value + "__icontains=" + textVal; + tag.name = textVal; + } else { + tag.url = tag.value + "=" + selectVal.value; + tag.name = selectVal.label; + } + return tag; + }; + + // returns true if the newTag is already in the list of tags + this.isDuplicate = function(tags, newTag) { + return (tags + .filter(function(tag) { + return (tag.url === newTag.url); + }).length > 0); + }; + + // returns an array of tags (or empty array if there are none) + // .slice(0) is used so the currentTags variable is not directly mutated + this.getCurrentTags = function(currentTags) { + if (currentTags && currentTags.length) { + return currentTags.slice(0); + } + return []; + }; + + return this; +}]; diff --git a/awx/ui/client/src/shared/branding/colors.default.less b/awx/ui/client/src/shared/branding/colors.default.less index dd53943e88..1713540c93 100644 --- a/awx/ui/client/src/shared/branding/colors.default.less +++ b/awx/ui/client/src/shared/branding/colors.default.less @@ -4,7 +4,7 @@ @default-icon: #B7B7B7; @default-icon-hov: #D7D7D7; // also selected button @default-border: #E8E8E8; -@default-second-border: #E1E1E1; +@default-second-border: #D7D7D7; @default-bg: #FFFFFF; // also selected btn txt @default-secondary-bg: #FCFCFC; // page/input field bg, just adds depth @default-tertiary-bg: #FAFAFA; // hover bg, alt-list diff --git a/awx/ui/client/src/shared/list-generator/list-generator.factory.js b/awx/ui/client/src/shared/list-generator/list-generator.factory.js index 2205f84390..d507fb8782 100644 --- a/awx/ui/client/src/shared/list-generator/list-generator.factory.js +++ b/awx/ui/client/src/shared/list-generator/list-generator.factory.js @@ -98,9 +98,9 @@ import {templateUrl} from '../../shared/template-url/template-url.factory'; -export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'PaginateWidget', 'Attr', 'Icon', +export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'PaginateWidget', 'Attr', 'Icon', 'getSearchHtml', 'Column', 'DropDown', 'NavigationLink', 'SelectIcon', - function ($location, $compile, $rootScope, SearchWidget, PaginateWidget, Attr, Icon, Column, DropDown, NavigationLink, + function ($location, $compile, $rootScope, SearchWidget, PaginateWidget, Attr, Icon, getSearchHtml, Column, DropDown, NavigationLink, SelectIcon) { return { @@ -362,38 +362,9 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate html += (list.emptyListText) ? list.emptyListText : "PLEASE ADD ITEMS TO THIS LIST"; html += ""; if (options.showSearch=== undefined || options.showSearch === true) { - // Only show the search bar if we are loading results or if we have at least 1 base result - html += "
0)\">\n"; - if (options.searchSize) { - html += SearchWidget({ - iterator: list.iterator, - template: list, - mini: true, - size: options.searchSize, - searchWidgets: list.searchWidgets - }); - } else if (options.mode === 'summary') { - html += SearchWidget({ - iterator: list.iterator, - template: list, - mini: true, - size: 'col-lg-6' - }); - } else if (options.mode === 'lookup' || options.id !== undefined) { - html += SearchWidget({ - iterator: list.iterator, - template: list, - mini: true, - size: 'col-lg-8' - }); - } else { - html += SearchWidget({ - iterator: list.iterator, - template: list, - mini: true - }); - } - html += "
\n"; + html += getSearchHtml + .inject(getSearchHtml.getList(list), + getSearchHtml.getEndpoint(list)); // Message for when a search returns no results. This should only get shown after a search is executed with no results. html += "
\n"; From 36a1efe4ea2d57c8b263434395c599258de7eb08 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Mon, 4 Apr 2016 11:39:42 -0400 Subject: [PATCH 2/4] updates to tag search ui implementation --- awx/ui/client/src/forms/Projects.js | 1 + awx/ui/client/src/helpers/Jobs.js | 14 +---- awx/ui/client/src/lists/AllJobs.js | 4 +- awx/ui/client/src/partials/jobs.html | 4 -- .../src/search/getSearchHtml.service.js | 21 +++++--- .../client/src/search/tagSearch.controller.js | 11 +--- .../client/src/search/tagSearch.directive.js | 1 + awx/ui/client/src/search/tagSearch.service.js | 4 +- awx/ui/client/src/shared/form-generator.js | 54 +++++++++++-------- .../list-generator/list-generator.factory.js | 15 ++++-- 10 files changed, 67 insertions(+), 62 deletions(-) diff --git a/awx/ui/client/src/forms/Projects.js b/awx/ui/client/src/forms/Projects.js index 69f34cdbaa..05e40e6d08 100644 --- a/awx/ui/client/src/forms/Projects.js +++ b/awx/ui/client/src/forms/Projects.js @@ -246,6 +246,7 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition']) related: { permissions: { + basePath: 'projects/:id/access_list/', type: 'collection', title: 'Permissions', iterator: 'permission', diff --git a/awx/ui/client/src/helpers/Jobs.js b/awx/ui/client/src/helpers/Jobs.js index dd7e83d57f..6ceb59494b 100644 --- a/awx/ui/client/src/helpers/Jobs.js +++ b/awx/ui/client/src/helpers/Jobs.js @@ -368,20 +368,11 @@ export default spinner = (params.spinner === undefined) ? true : params.spinner, e, html, key; - // Add the search widget. We want it arranged differently, so we're injecting and compiling it separately - html = SearchWidget({ - iterator: list.iterator, - template: params.list, - includeSize: false - }); - e = angular.element(document.getElementById(id + '-search-container')).append(html); - $compile(e)(scope); - GenerateList.inject(list, { mode: 'edit', id: id, scope: scope, - showSearch: false, + showSearch: true, title: false }); @@ -408,9 +399,6 @@ export default JobsControllerInit({ scope: scope, parent_scope: parent_scope }); JobsListUpdate({ scope: scope, parent_scope: parent_scope, list: list }); parent_scope.$emit('listLoaded'); - // setTimeout(function(){ - // scope.$apply(); - // }, 300); }); if (base === 'jobs' && list.name === 'all_jobs') { diff --git a/awx/ui/client/src/lists/AllJobs.js b/awx/ui/client/src/lists/AllJobs.js index 4cc7a995c6..f2964e4186 100644 --- a/awx/ui/client/src/lists/AllJobs.js +++ b/awx/ui/client/src/lists/AllJobs.js @@ -10,15 +10,15 @@ export default .value( 'AllJobsList', { name: 'all_jobs', + basePath: 'unified_jobs', iterator: 'all_job', editTitle: 'All Jobs', index: false, hover: true, well: false, - fields: { status: { - label: '', + label: 'Status', columnClass: 'List-staticColumn--smallStatus', awToolTip: "{{ all_job.status_tip }}", awTipPlacement: "right", diff --git a/awx/ui/client/src/partials/jobs.html b/awx/ui/client/src/partials/jobs.html index 2d3cc442a9..c37b3f8495 100644 --- a/awx/ui/client/src/partials/jobs.html +++ b/awx/ui/client/src/partials/jobs.html @@ -25,10 +25,6 @@
-
-
-
-
diff --git a/awx/ui/client/src/search/getSearchHtml.service.js b/awx/ui/client/src/search/getSearchHtml.service.js index 2e941ce381..82484abfc9 100644 --- a/awx/ui/client/src/search/getSearchHtml.service.js +++ b/awx/ui/client/src/search/getSearchHtml.service.js @@ -1,13 +1,15 @@ -export default [function() { +export default ['GetBasePath', function(GetBasePath) { // given the list, return the fields that need searching this.getList = function(list) { + var f = _.clone(list.fields); return JSON.stringify(Object - .keys(list.fields) + .keys(f) .filter(function(i) { - return (list.fields[i] + return (f[i] .searchable !== false); }).map(function(i) { - return {[i]: list.fields[i]}; + delete f[i].awToolTip; + return {[i]: f[i]}; }).reduce(function (acc, i) { var key = Object.keys(i); acc[key] = i[key]; @@ -17,14 +19,19 @@ export default [function() { // given the list config object, return the basepath this.getEndpoint = function(list) { - return list.basePath || list.name; + var endPoint = (list.basePath || list.name); + if (endPoint === 'inventories') { + endPoint = 'inventory'; + } + return GetBasePath(endPoint); }; // inject the directive with the list and endpoint - this.inject = function(list, endpoint) { + this.inject = function(list, endpoint, set, iterator) { return ""; + "' set='" + set + + "' iterator='" + iterator + "'>"; }; return this; diff --git a/awx/ui/client/src/search/tagSearch.controller.js b/awx/ui/client/src/search/tagSearch.controller.js index a265873a5e..8edcfdce12 100644 --- a/awx/ui/client/src/search/tagSearch.controller.js +++ b/awx/ui/client/src/search/tagSearch.controller.js @@ -3,13 +3,6 @@ export default ['$scope', 'Refresh', 'tagSearchService', // JSONify passed field elements that can be searched $scope.list = JSON.parse($scope.list); - // Hotfix: GetBasePath to work with inventories - $scope.$watch("endpoint", function(val) { - if (val === 'inventories') { - $scope.endpoint = 'inventory'; - } - }); - // Grab options for the left-dropdown of the searchbar tagSearchService.getSearchTypes($scope.list, $scope.endpoint) .then(function(searchTypes) { @@ -39,10 +32,10 @@ export default ['$scope', 'Refresh', 'tagSearchService', }; $scope.updateSearch = function(tags) { - var iterator = $scope.$parent.list.iterator; + var iterator = $scope.iterator; var pageSize = $scope .$parent[iterator + "_page_size"]; - var set = $scope.$parent.list.name; + var set = $scope.set; var listScope = $scope.$parent; var url = tagSearchService .updateFilteredUrl($scope.endpoint, tags, pageSize); diff --git a/awx/ui/client/src/search/tagSearch.directive.js b/awx/ui/client/src/search/tagSearch.directive.js index d1515ea03c..1f5e562b30 100644 --- a/awx/ui/client/src/search/tagSearch.directive.js +++ b/awx/ui/client/src/search/tagSearch.directive.js @@ -9,6 +9,7 @@ export default scope: { list: '@', endpoint: '@', + set: '@', iterator: '@' }, controller: tagSearchController, diff --git a/awx/ui/client/src/search/tagSearch.service.js b/awx/ui/client/src/search/tagSearch.service.js index 347e2df0f0..93348d89c3 100644 --- a/awx/ui/client/src/search/tagSearch.service.js +++ b/awx/ui/client/src/search/tagSearch.service.js @@ -78,7 +78,7 @@ export default ['Rest', '$q', 'GetBasePath', function(Rest, $q, GetBasePath) { if (needsRequest.length) { // make the options request to reutrn the typeOptions - Rest.setUrl(GetBasePath(basePath)); + Rest.setUrl(basePath); Rest.options() .success(function (data) { var options = data.actions.GET; @@ -108,7 +108,7 @@ export default ['Rest', '$q', 'GetBasePath', function(Rest, $q, GetBasePath) { // returns the url with filter params this.updateFilteredUrl = function(basePath, tags, pageSize) { - return GetBasePath(basePath) + "?" + + return basePath + "?" + (tags || []).map(function (t) { return t.url; }).join("&") + "&page_size=" + pageSize; diff --git a/awx/ui/client/src/shared/form-generator.js b/awx/ui/client/src/shared/form-generator.js index 0f3ea7445a..f5d0d83316 100644 --- a/awx/ui/client/src/shared/form-generator.js +++ b/awx/ui/client/src/shared/form-generator.js @@ -142,10 +142,10 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat .factory('GenerateForm', ['$rootScope', '$location', '$compile', 'generateList', 'SearchWidget', 'PaginateWidget', 'Attr', 'Icon', 'Column', 'NavigationLink', 'HelpCollapse', 'DropDown', 'Empty', 'SelectIcon', - 'Store', 'ActionButton', + 'Store', 'ActionButton', 'getSearchHtml', '$state', function ($rootScope, $location, $compile, GenerateList, SearchWidget, PaginateWidget, Attr, Icon, Column, NavigationLink, HelpCollapse, - DropDown, Empty, SelectIcon, Store, ActionButton) { + DropDown, Empty, SelectIcon, Store, ActionButton, getSearchHtml, $state) { return { setForm: function (form) { this.form = form; }, @@ -1692,28 +1692,38 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat html += "Hint: " + collection.instructions + "\n"; html += "
\n"; } + var rootID = $location.$$path.split("/")[2]; + var endpoint = "/api/v1/" + collection.basePath + .replace(":id", rootID); + var tagSearch = getSearchHtml + .inject(getSearchHtml.getList(collection), + endpoint, itm, collection.iterator); - //html += "
\n"; - html += "
\n"; + var actionButtons = ""; + Object.keys(collection.actions || {}) + .forEach(act => { + actionButtons += ActionButton(collection + .actions[act]); + }); - html += SearchWidget({ - iterator: collection.iterator, - template: collection, - mini: true, - ngShow: collection.iterator + "Loading == true || " + collection.iterator + "_active_search == true || (" + collection.iterator + "Loading == false && " + collection.iterator + "_active_search == false && " + collection.iterator + "_total_rows > 0)" - }); - - html += "
\n"; - html += "
\n"; - - for (act in collection.actions) { - action = collection.actions[act]; - html += ActionButton(action); - } - - html += "
\n"; - html += "
\n"; - html += "
\n"; + html += ` +
+
0 + )\"> + ${tagSearch} +
+
+
+ ${actionButtons} +
+
+
+ `; // Message for when a search returns no results. This should only get shown after a search is executed with no results. html += "
\n"; diff --git a/awx/ui/client/src/shared/list-generator/list-generator.factory.js b/awx/ui/client/src/shared/list-generator/list-generator.factory.js index d507fb8782..2c15e72679 100644 --- a/awx/ui/client/src/shared/list-generator/list-generator.factory.js +++ b/awx/ui/client/src/shared/list-generator/list-generator.factory.js @@ -362,10 +362,19 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate html += (list.emptyListText) ? list.emptyListText : "PLEASE ADD ITEMS TO THIS LIST"; html += "
"; if (options.showSearch=== undefined || options.showSearch === true) { - html += getSearchHtml + var tagSearch = getSearchHtml .inject(getSearchHtml.getList(list), - getSearchHtml.getEndpoint(list)); - + getSearchHtml.getEndpoint(list), + list.name, + list.iterator); + html += ` +
+ ${tagSearch} +
+ `; // Message for when a search returns no results. This should only get shown after a search is executed with no results. html += "
\n"; html += "
No records matched your search.
\n"; From 967194a3f3aac12835b9afd8d35d1e1411487a80 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Wed, 6 Apr 2016 15:38:48 -0400 Subject: [PATCH 3/4] fixes to tag search ui implementation --- awx/ui/client/src/forms/Inventories.js | 1 + awx/ui/client/src/lists/InventoryGroups.js | 2 +- awx/ui/client/src/search/getSearchHtml.service.js | 1 + awx/ui/client/src/search/tagSearch.partial.html | 7 ++++--- awx/ui/client/src/search/tagSearch.service.js | 13 ++++++++++--- 5 files changed, 17 insertions(+), 7 deletions(-) diff --git a/awx/ui/client/src/forms/Inventories.js b/awx/ui/client/src/forms/Inventories.js index 746c8a49a9..aa6d6f06e3 100644 --- a/awx/ui/client/src/forms/Inventories.js +++ b/awx/ui/client/src/forms/Inventories.js @@ -79,6 +79,7 @@ export default related: { scan_job_templates: { + basePath: 'inventories/:id/scan_job_templates', type: 'collection', title: 'Scan Job Templates', iterator: 'scan_job_template', diff --git a/awx/ui/client/src/lists/InventoryGroups.js b/awx/ui/client/src/lists/InventoryGroups.js index 53881f3d7c..db3f2bc7a0 100644 --- a/awx/ui/client/src/lists/InventoryGroups.js +++ b/awx/ui/client/src/lists/InventoryGroups.js @@ -73,7 +73,7 @@ export default }, last_update_failed: { label: 'Update failed?', - searchType: 'select', + searchType: 'boolean', searchSingleValue: true, searchValue: 'failed', searchOnly: true, diff --git a/awx/ui/client/src/search/getSearchHtml.service.js b/awx/ui/client/src/search/getSearchHtml.service.js index 82484abfc9..8570237b32 100644 --- a/awx/ui/client/src/search/getSearchHtml.service.js +++ b/awx/ui/client/src/search/getSearchHtml.service.js @@ -9,6 +9,7 @@ export default ['GetBasePath', function(GetBasePath) { .searchable !== false); }).map(function(i) { delete f[i].awToolTip; + delete f[i].ngClass; return {[i]: f[i]}; }).reduce(function (acc, i) { var key = Object.keys(i); diff --git a/awx/ui/client/src/search/tagSearch.partial.html b/awx/ui/client/src/search/tagSearch.partial.html index 0e557dca2a..de379cf3d9 100644 --- a/awx/ui/client/src/search/tagSearch.partial.html +++ b/awx/ui/client/src/search/tagSearch.partial.html @@ -3,9 +3,10 @@
- - {{ currentSearchType.label || "Foo bar"}} + ng-class="{'is-open': showTypeDropdown}" + ng-cloak> + + {{ currentSearchType.label }}
diff --git a/awx/ui/client/src/search/tagSearch.service.js b/awx/ui/client/src/search/tagSearch.service.js index 93348d89c3..93d4819901 100644 --- a/awx/ui/client/src/search/tagSearch.service.js +++ b/awx/ui/client/src/search/tagSearch.service.js @@ -1,4 +1,4 @@ -export default ['Rest', '$q', 'GetBasePath', function(Rest, $q, GetBasePath) { +export default ['Rest', '$q', 'GetBasePath', 'Wait', 'ProcessErrors', function(Rest, $q, GetBasePath, Wait, ProcessErrors) { var that = this; // parse the field config object to return // one of the searchTypes (for the left dropdown) @@ -18,7 +18,7 @@ export default ['Rest', '$q', 'GetBasePath', function(Rest, $q, GetBasePath) { var type, typeOptions; if (field.searchType === 'select') { type = 'select'; - typeOptions = []; + typeOptions = field.searchOptions || []; } else if (field.searchType === 'boolean') { type = 'select'; typeOptions = [{label: "Yes", value: true}, @@ -49,6 +49,7 @@ export default ['Rest', '$q', 'GetBasePath', function(Rest, $q, GetBasePath) { // given the fields that are searchable, // return searchTypes in the format the view can use this.getSearchTypes = function(list, basePath) { + Wait("start"); var defer = $q.defer(); var options = Object @@ -95,11 +96,17 @@ export default ['Rest', '$q', 'GetBasePath', function(Rest, $q, GetBasePath) { }); return option; + }) + .error(function (data, status) { + ProcessErrors(null, data, status, null, { + hdr: 'Error!', + msg: 'Getting type options failed'}); }); - + Wait("stop"); defer.resolve(joinOptions()); }); } else { + Wait("stop"); defer.resolve(joinOptions()); } From c2d9cf466d88493fa5d570caf95542f6c6ea0d9f Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Wed, 6 Apr 2016 15:48:00 -0400 Subject: [PATCH 4/4] fixed error handling of options --- awx/ui/client/src/search/tagSearch.service.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/awx/ui/client/src/search/tagSearch.service.js b/awx/ui/client/src/search/tagSearch.service.js index 93d4819901..4f8f7fa48a 100644 --- a/awx/ui/client/src/search/tagSearch.service.js +++ b/awx/ui/client/src/search/tagSearch.service.js @@ -93,18 +93,19 @@ export default ['Rest', '$q', 'GetBasePath', 'Wait', 'ProcessErrors', function(R value: i[0], label: i[1] }; - }); - - return option; + }); + return option; + }); + Wait("stop"); + defer.resolve(joinOptions()); }) .error(function (data, status) { + Wait("stop"); + defer.reject("options request failed"); ProcessErrors(null, data, status, null, { hdr: 'Error!', msg: 'Getting type options failed'}); }); - Wait("stop"); - defer.resolve(joinOptions()); - }); } else { Wait("stop"); defer.resolve(joinOptions());