mirror of
https://github.com/ansible/awx.git
synced 2026-03-07 19:51:08 -03:30
Merge branch 'tagSearchUI' into devel
This commit is contained in:
@@ -51,6 +51,7 @@ import activityStream from './activity-stream/main';
|
|||||||
import standardOut from './standard-out/main';
|
import standardOut from './standard-out/main';
|
||||||
import lookUpHelper from './lookup/main';
|
import lookUpHelper from './lookup/main';
|
||||||
import JobTemplates from './job-templates/main';
|
import JobTemplates from './job-templates/main';
|
||||||
|
import search from './search/main';
|
||||||
import {ScheduleEditController} from './controllers/Schedules';
|
import {ScheduleEditController} from './controllers/Schedules';
|
||||||
import {ProjectsList, ProjectsAdd, ProjectsEdit} from './controllers/Projects';
|
import {ProjectsList, ProjectsAdd, ProjectsEdit} from './controllers/Projects';
|
||||||
import OrganizationsList from './organizations/list/organizations-list.controller';
|
import OrganizationsList from './organizations/list/organizations-list.controller';
|
||||||
@@ -110,6 +111,7 @@ var tower = angular.module('Tower', [
|
|||||||
access.name,
|
access.name,
|
||||||
JobTemplates.name,
|
JobTemplates.name,
|
||||||
portalMode.name,
|
portalMode.name,
|
||||||
|
search.name,
|
||||||
'templates',
|
'templates',
|
||||||
'Utilities',
|
'Utilities',
|
||||||
'OrganizationFormDefinition',
|
'OrganizationFormDefinition',
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ export default
|
|||||||
|
|
||||||
related: {
|
related: {
|
||||||
scan_job_templates: {
|
scan_job_templates: {
|
||||||
|
basePath: 'inventories/:id/scan_job_templates',
|
||||||
type: 'collection',
|
type: 'collection',
|
||||||
title: 'Scan Job Templates',
|
title: 'Scan Job Templates',
|
||||||
iterator: 'scan_job_template',
|
iterator: 'scan_job_template',
|
||||||
|
|||||||
@@ -246,6 +246,7 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
|
|||||||
|
|
||||||
related: {
|
related: {
|
||||||
permissions: {
|
permissions: {
|
||||||
|
basePath: 'projects/:id/access_list/',
|
||||||
type: 'collection',
|
type: 'collection',
|
||||||
title: 'Permissions',
|
title: 'Permissions',
|
||||||
iterator: 'permission',
|
iterator: 'permission',
|
||||||
|
|||||||
@@ -362,20 +362,16 @@ export default
|
|||||||
spinner = (params.spinner === undefined) ? true : params.spinner,
|
spinner = (params.spinner === undefined) ? true : params.spinner,
|
||||||
e, html, key;
|
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, {
|
GenerateList.inject(list, {
|
||||||
mode: 'edit',
|
mode: 'edit',
|
||||||
id: id,
|
id: id,
|
||||||
scope: scope,
|
scope: scope,
|
||||||
|
<<<<<<< HEAD
|
||||||
showSearch: false
|
showSearch: false
|
||||||
|
=======
|
||||||
|
showSearch: true,
|
||||||
|
title: false
|
||||||
|
>>>>>>> tagSearchUI
|
||||||
});
|
});
|
||||||
|
|
||||||
SearchInit({
|
SearchInit({
|
||||||
@@ -401,9 +397,6 @@ export default
|
|||||||
JobsControllerInit({ scope: scope, parent_scope: parent_scope });
|
JobsControllerInit({ scope: scope, parent_scope: parent_scope });
|
||||||
JobsListUpdate({ scope: scope, parent_scope: parent_scope, list: list });
|
JobsListUpdate({ scope: scope, parent_scope: parent_scope, list: list });
|
||||||
parent_scope.$emit('listLoaded');
|
parent_scope.$emit('listLoaded');
|
||||||
// setTimeout(function(){
|
|
||||||
// scope.$apply();
|
|
||||||
// }, 300);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (base === 'jobs' && list.name === 'all_jobs') {
|
if (base === 'jobs' && list.name === 'all_jobs') {
|
||||||
|
|||||||
@@ -392,9 +392,6 @@ export default
|
|||||||
scope[iterator + 'HoldInput' + modifier] = true;
|
scope[iterator + 'HoldInput' + modifier] = true;
|
||||||
if ($('#search-widget-container' + modifier) &&
|
if ($('#search-widget-container' + modifier) &&
|
||||||
list.fields[scope[iterator + 'SearchField' + modifier]] && !list.fields[scope[iterator + 'SearchField' + modifier]].searchObject) {
|
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 (scope[iterator + 'SearchValue' + modifier]) {
|
||||||
// if user typed a value in the input box, show the reset link
|
// if user typed a value in the input box, show the reset link
|
||||||
scope[iterator + 'ShowStartBtn' + modifier] = false;
|
scope[iterator + 'ShowStartBtn' + modifier] = false;
|
||||||
|
|||||||
@@ -10,15 +10,15 @@ export default
|
|||||||
.value( 'AllJobsList', {
|
.value( 'AllJobsList', {
|
||||||
|
|
||||||
name: 'all_jobs',
|
name: 'all_jobs',
|
||||||
|
basePath: 'unified_jobs',
|
||||||
iterator: 'all_job',
|
iterator: 'all_job',
|
||||||
editTitle: 'All Jobs',
|
editTitle: 'All Jobs',
|
||||||
index: false,
|
index: false,
|
||||||
hover: true,
|
hover: true,
|
||||||
well: false,
|
well: false,
|
||||||
|
|
||||||
fields: {
|
fields: {
|
||||||
status: {
|
status: {
|
||||||
label: '',
|
label: 'Status',
|
||||||
columnClass: 'List-staticColumn--smallStatus',
|
columnClass: 'List-staticColumn--smallStatus',
|
||||||
awToolTip: "{{ all_job.status_tip }}",
|
awToolTip: "{{ all_job.status_tip }}",
|
||||||
awTipPlacement: "right",
|
awTipPlacement: "right",
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export default
|
|||||||
},
|
},
|
||||||
last_update_failed: {
|
last_update_failed: {
|
||||||
label: 'Update failed?',
|
label: 'Update failed?',
|
||||||
searchType: 'select',
|
searchType: 'boolean',
|
||||||
searchSingleValue: true,
|
searchSingleValue: true,
|
||||||
searchValue: 'failed',
|
searchValue: 'failed',
|
||||||
searchOnly: true,
|
searchOnly: true,
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export default
|
|||||||
icon: "icon-job-{{ project.statusIcon }}",
|
icon: "icon-job-{{ project.statusIcon }}",
|
||||||
columnClass: "List-staticColumn--smallStatus",
|
columnClass: "List-staticColumn--smallStatus",
|
||||||
nosort: true,
|
nosort: true,
|
||||||
|
searchLabel: 'Status',
|
||||||
searchType: 'select',
|
searchType: 'select',
|
||||||
searchOptions: [], //set in the controller
|
searchOptions: [], //set in the controller
|
||||||
excludeModal: true
|
excludeModal: true
|
||||||
|
|||||||
@@ -25,10 +25,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="jobs-tab-content" class="Form-tabSection"
|
<div id="jobs-tab-content" class="Form-tabSection"
|
||||||
ng-class="{'is-selected': jobsSelected }">
|
ng-class="{'is-selected': jobsSelected }">
|
||||||
<div class= "row search-row">
|
|
||||||
<div class="col-lg-4 col-md-4 " id="active-jobs-search-container">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class= "job-list" id="active-jobs-container">
|
<div class= "job-list" id="active-jobs-container">
|
||||||
<div id="active-jobs" class= "job-list-target">
|
<div id="active-jobs" class= "job-list-target">
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
39
awx/ui/client/src/search/getSearchHtml.service.js
Normal file
39
awx/ui/client/src/search/getSearchHtml.service.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
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(f)
|
||||||
|
.filter(function(i) {
|
||||||
|
return (f[i]
|
||||||
|
.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);
|
||||||
|
acc[key] = i[key];
|
||||||
|
return acc;
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// given the list config object, return the basepath
|
||||||
|
this.getEndpoint = function(list) {
|
||||||
|
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, set, iterator) {
|
||||||
|
return "<tag-search list='" + list +
|
||||||
|
"' endpoint='" + endpoint +
|
||||||
|
"' set='" + set +
|
||||||
|
"' iterator='" + iterator + "'></tag-search>";
|
||||||
|
};
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}];
|
||||||
9
awx/ui/client/src/search/main.js
Normal file
9
awx/ui/client/src/search/main.js
Normal file
@@ -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);
|
||||||
239
awx/ui/client/src/search/tagSearch.block.less
Normal file
239
awx/ui/client/src/search/tagSearch.block.less
Normal file
@@ -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;
|
||||||
|
}
|
||||||
98
awx/ui/client/src/search/tagSearch.controller.js
Normal file
98
awx/ui/client/src/search/tagSearch.controller.js
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
export default ['$scope', 'Refresh', 'tagSearchService',
|
||||||
|
function($scope, Refresh, tagSearchService) {
|
||||||
|
// JSONify passed field elements that can be searched
|
||||||
|
$scope.list = JSON.parse($scope.list);
|
||||||
|
|
||||||
|
// 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.iterator;
|
||||||
|
var pageSize = $scope
|
||||||
|
.$parent[iterator + "_page_size"];
|
||||||
|
var set = $scope.set;
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
}];
|
||||||
31
awx/ui/client/src/search/tagSearch.directive.js
Normal file
31
awx/ui/client/src/search/tagSearch.directive.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import tagSearchController from './tagSearch.controller';
|
||||||
|
|
||||||
|
/* jshint unused: vars */
|
||||||
|
export default
|
||||||
|
['templateUrl',
|
||||||
|
function(templateUrl) {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
scope: {
|
||||||
|
list: '@',
|
||||||
|
endpoint: '@',
|
||||||
|
set: '@',
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
79
awx/ui/client/src/search/tagSearch.partial.html
Normal file
79
awx/ui/client/src/search/tagSearch.partial.html
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<div class="TagSearch row">
|
||||||
|
<div class="col-lg-4 col-md-8 col-sm-12 col-xs-12">
|
||||||
|
<div class="TagSearch-bar">
|
||||||
|
<div class="TagSearch-typeDropdown"
|
||||||
|
ng-click="toggleTypeDropdown()"
|
||||||
|
ng-class="{'is-open': showTypeDropdown}"
|
||||||
|
ng-cloak>
|
||||||
|
<span class="TagSearch-typeDropdownName" ng-cloak>
|
||||||
|
{{ currentSearchType.label }}
|
||||||
|
</span>
|
||||||
|
<i class="TagSearch-selectDownIcon fa fa-angle-down"></i>
|
||||||
|
</div>
|
||||||
|
<div aw-click-off="showTypeDropdown" class="TagSearch-dropdownContainer
|
||||||
|
TagSearch-dropdownContainer--searchTypes"
|
||||||
|
ng-show="showTypeDropdown">
|
||||||
|
<div class="TagSearch-dropdownItem"
|
||||||
|
ng-repeat="type in searchTypes track by $index"
|
||||||
|
ng-class="{'is-selected': (currentSearchType.value === type.value)}"
|
||||||
|
ng-click="setSearchType(type)">
|
||||||
|
{{ type.label }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="TagSearch-searchTermContainer"
|
||||||
|
ng-class="{'is-open': showCurrentSearchDropdown}">
|
||||||
|
<input class="TagSearch-searchTermInput"
|
||||||
|
ng-show="currentSearchType.type === 'text'"
|
||||||
|
ng-model="newSearchTag"
|
||||||
|
placeholder="Search">
|
||||||
|
<div class="TagSearch-searchButton"
|
||||||
|
ng-disabled="!newSearchTag"
|
||||||
|
ng-click="addTag()"
|
||||||
|
ng-show="currentSearchType.type === 'text'">
|
||||||
|
<i class="fa fa-search"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="TagSearch-searchTermSelect"
|
||||||
|
ng-show="currentSearchType.type === 'select'"
|
||||||
|
ng-click="toggleCurrentSearchDropdown()">
|
||||||
|
<div class="TagSearch-searchTermSelectPlaceholder">
|
||||||
|
Filter
|
||||||
|
</div>
|
||||||
|
<i class="TagSearch-selectDownIcon
|
||||||
|
fa fa-angle-down">
|
||||||
|
</i>
|
||||||
|
</div>
|
||||||
|
<div aw-click-off="showCurrentSearchDropdown"
|
||||||
|
class="TagSearch-dropdownContainer
|
||||||
|
TagSearch-dropdownContainer--typeOptions"
|
||||||
|
ng-show="showCurrentSearchDropdown &&
|
||||||
|
currentSearchType.type === 'select'">
|
||||||
|
<div class="TagSearch-dropdownItem"
|
||||||
|
ng-repeat="type in currentSearchType.typeOptions track by $index"
|
||||||
|
ng-click="addTag(type)">
|
||||||
|
{{ type.label }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-8 col-md-12 col-sm-12 col-xs-12">
|
||||||
|
<div class="TagSearch-tagSection">
|
||||||
|
<div class="TagSearch-flexContainer">
|
||||||
|
<div class="TagSearch-tagContainer"
|
||||||
|
ng-repeat="tag in currentSearchFilters track by $index">
|
||||||
|
<div class="TagSearch-tag TagSearch-tag--deletable"
|
||||||
|
<span class="TagSearch-name">
|
||||||
|
{{ tag.name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="TagSearch-deleteContainer"
|
||||||
|
ng-click="deleteTag(tag)">
|
||||||
|
<i class="fa fa-times TagSearch-tagDelete"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
156
awx/ui/client/src/search/tagSearch.service.js
Normal file
156
awx/ui/client/src/search/tagSearch.service.js
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
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)
|
||||||
|
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 = field.searchOptions || [];
|
||||||
|
} 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) {
|
||||||
|
Wait("start");
|
||||||
|
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(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;
|
||||||
|
});
|
||||||
|
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'});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Wait("stop");
|
||||||
|
defer.resolve(joinOptions());
|
||||||
|
}
|
||||||
|
|
||||||
|
return defer.promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
// returns the url with filter params
|
||||||
|
this.updateFilteredUrl = function(basePath, tags, pageSize) {
|
||||||
|
return 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;
|
||||||
|
}];
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
@default-icon: #B7B7B7;
|
@default-icon: #B7B7B7;
|
||||||
@default-icon-hov: #D7D7D7; // also selected button
|
@default-icon-hov: #D7D7D7; // also selected button
|
||||||
@default-border: #E8E8E8;
|
@default-border: #E8E8E8;
|
||||||
@default-second-border: #E1E1E1;
|
@default-second-border: #D7D7D7;
|
||||||
@default-bg: #FFFFFF; // also selected btn txt
|
@default-bg: #FFFFFF; // also selected btn txt
|
||||||
@default-secondary-bg: #FCFCFC; // page/input field bg, just adds depth
|
@default-secondary-bg: #FCFCFC; // page/input field bg, just adds depth
|
||||||
@default-tertiary-bg: #FAFAFA; // hover bg, alt-list
|
@default-tertiary-bg: #FAFAFA; // hover bg, alt-list
|
||||||
|
|||||||
@@ -142,10 +142,10 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
|
|||||||
.factory('GenerateForm', ['$rootScope', '$location', '$compile', 'generateList',
|
.factory('GenerateForm', ['$rootScope', '$location', '$compile', 'generateList',
|
||||||
'SearchWidget', 'PaginateWidget', 'Attr', 'Icon', 'Column',
|
'SearchWidget', 'PaginateWidget', 'Attr', 'Icon', 'Column',
|
||||||
'NavigationLink', 'HelpCollapse', 'DropDown', 'Empty', 'SelectIcon',
|
'NavigationLink', 'HelpCollapse', 'DropDown', 'Empty', 'SelectIcon',
|
||||||
'Store', 'ActionButton',
|
'Store', 'ActionButton', 'getSearchHtml', '$state',
|
||||||
function ($rootScope, $location, $compile, GenerateList, SearchWidget,
|
function ($rootScope, $location, $compile, GenerateList, SearchWidget,
|
||||||
PaginateWidget, Attr, Icon, Column, NavigationLink, HelpCollapse,
|
PaginateWidget, Attr, Icon, Column, NavigationLink, HelpCollapse,
|
||||||
DropDown, Empty, SelectIcon, Store, ActionButton) {
|
DropDown, Empty, SelectIcon, Store, ActionButton, getSearchHtml, $state) {
|
||||||
return {
|
return {
|
||||||
|
|
||||||
setForm: function (form) { this.form = form; },
|
setForm: function (form) { this.form = form; },
|
||||||
@@ -1692,28 +1692,38 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
|
|||||||
html += "<strong>Hint: </strong>" + collection.instructions + "\n";
|
html += "<strong>Hint: </strong>" + collection.instructions + "\n";
|
||||||
html += "</div>\n";
|
html += "</div>\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 += "<div class=\"well\">\n";
|
var actionButtons = "";
|
||||||
html += "<div class=\"row List-searchRow\">\n";
|
Object.keys(collection.actions || {})
|
||||||
|
.forEach(act => {
|
||||||
|
actionButtons += ActionButton(collection
|
||||||
|
.actions[act]);
|
||||||
|
});
|
||||||
|
|
||||||
html += SearchWidget({
|
html += `
|
||||||
iterator: collection.iterator,
|
<div class=\"row\">
|
||||||
template: collection,
|
<div class=\"col-lg-8\"
|
||||||
mini: true,
|
ng-show=\"${collection.iterator}Loading == 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)"
|
${collection.iterator}_active_search == true || (
|
||||||
});
|
${collection.iterator}Loading == false &&
|
||||||
|
${collection.iterator}_active_search == false &&
|
||||||
html += "<div class=\"col-lg-8\">\n";
|
${collection.iterator}_total_rows > 0
|
||||||
html += "<div class=\"list-actions\">\n";
|
)\">
|
||||||
|
${tagSearch}
|
||||||
for (act in collection.actions) {
|
</div>
|
||||||
action = collection.actions[act];
|
<div class=\"col-lg-4\">
|
||||||
html += ActionButton(action);
|
<div class=\"list-actions\">
|
||||||
}
|
${actionButtons}
|
||||||
|
</div>
|
||||||
html += "</div>\n";
|
</div>
|
||||||
html += "</div>\n";
|
</div>
|
||||||
html += "</div><!-- row -->\n";
|
`;
|
||||||
|
|
||||||
// Message for when a search returns no results. This should only get shown after a search is executed with no results.
|
// Message for when a search returns no results. This should only get shown after a search is executed with no results.
|
||||||
html += "<div class=\"row\" ng-show=\"" + collection.iterator + "Loading == false && " + collection.iterator + "_active_search == true && " + itm + ".length == 0\">\n";
|
html += "<div class=\"row\" ng-show=\"" + collection.iterator + "Loading == false && " + collection.iterator + "_active_search == true && " + itm + ".length == 0\">\n";
|
||||||
|
|||||||
@@ -98,9 +98,9 @@
|
|||||||
|
|
||||||
import {templateUrl} from '../../shared/template-url/template-url.factory';
|
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',
|
'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) {
|
SelectIcon) {
|
||||||
return {
|
return {
|
||||||
|
|
||||||
@@ -362,39 +362,19 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate
|
|||||||
html += (list.emptyListText) ? list.emptyListText : "PLEASE ADD ITEMS TO THIS LIST";
|
html += (list.emptyListText) ? list.emptyListText : "PLEASE ADD ITEMS TO THIS LIST";
|
||||||
html += "</div>";
|
html += "</div>";
|
||||||
if (options.showSearch=== undefined || options.showSearch === true) {
|
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
|
var tagSearch = getSearchHtml
|
||||||
html += "<div class=\"row List-searchRow\" ng-show=\"" + list.iterator + "Loading == true || " + list.iterator + "_active_search == true || (" + list.iterator + "Loading == false && " + list.iterator + "_active_search == false && " + list.iterator + "_total_rows > 0)\">\n";
|
.inject(getSearchHtml.getList(list),
|
||||||
if (options.searchSize) {
|
getSearchHtml.getEndpoint(list),
|
||||||
html += SearchWidget({
|
list.name,
|
||||||
iterator: list.iterator,
|
list.iterator);
|
||||||
template: list,
|
html += `
|
||||||
mini: true,
|
<div
|
||||||
size: options.searchSize,
|
ng-hide=\"${list.iterator}Loading == false &&
|
||||||
searchWidgets: list.searchWidgets
|
${list.iterator}_active_search == false &&
|
||||||
});
|
${list.iterator}_total_rows <1\">
|
||||||
} else if (options.mode === 'summary') {
|
${tagSearch}
|
||||||
html += SearchWidget({
|
</div>
|
||||||
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 += "</div><!-- row -->\n";
|
|
||||||
|
|
||||||
// Message for when a search returns no results. This should only get shown after a search is executed with no results.
|
// Message for when a search returns no results. This should only get shown after a search is executed with no results.
|
||||||
html += "<div class=\"row\" ng-show=\"" + list.iterator + "Loading == false && " + list.iterator + "_active_search == true && " + list.name + ".length == 0\">\n";
|
html += "<div class=\"row\" ng-show=\"" + list.iterator + "Loading == false && " + list.iterator + "_active_search == true && " + list.name + ".length == 0\">\n";
|
||||||
html += "<div class=\"col-lg-12 List-searchNoResults\">No records matched your search.</div>\n";
|
html += "<div class=\"col-lg-12 List-searchNoResults\">No records matched your search.</div>\n";
|
||||||
|
|||||||
Reference in New Issue
Block a user