diff --git a/awx/ui/client/features/output/_index.less b/awx/ui/client/features/output/_index.less index d661afae97..989b532ab6 100644 --- a/awx/ui/client/features/output/_index.less +++ b/awx/ui/client/features/output/_index.less @@ -150,3 +150,52 @@ white-space: pre-wrap; } + +// Search --------------------------------------------------------------------------------- +@at-jobz-top-search-key: @at-space-2x; +@at-jobz-bottom-search-key: @at-space-3x; + +.jobz-searchKeyPaneContainer { + margin-top: @at-jobz-top-search-key; + margin-bottom: @at-jobz-bottom-search-key; +} + +.jobz-searchKeyPane { + // background-color: @at-gray-f6; + background-color: @login-notice-bg; + color: @login-notice-text; + border-radius: @at-border-radius; + border: 1px solid @at-gray-b7; + // color: @at-gray-848992; + padding: 6px @at-padding-input 6px @at-padding-input; +} + +.jobz-searchClearAll { + font-size: 10px; + padding-bottom: @at-space; +} + +.jobz-Button-searchKey { + .at-mixin-Button(); + + background-color: @at-blue; + border-color: at-color-button-border-default; + color: @at-white; + + &:hover, &:active { + color: @at-white; + background-color: @at-blue-hover; + box-shadow: none; + } + + &:focus { + color: @at-white; + } +} + +.jobz-tagz { + margin-top: @at-space; + display: flex; + width: 100%; + flex-wrap: wrap; +} diff --git a/awx/ui/client/features/output/index.controller.js b/awx/ui/client/features/output/index.controller.js index e2aeae2d90..7e524a26f4 100644 --- a/awx/ui/client/features/output/index.controller.js +++ b/awx/ui/client/features/output/index.controller.js @@ -9,6 +9,8 @@ let page; let render; let scroll; let resource; +let $state; +let qs; let chain; @@ -19,7 +21,9 @@ function JobsIndexController ( _render_, _$scope_, _$compile_, - _$q_ + _$q_, + _$state_, + _qs_, ) { vm = this || {}; @@ -59,6 +63,23 @@ function JobsIndexController ( const stream = false; // TODO: Set in route chain = $q.resolve(); + + // search + $state = _$state_; + qs = _qs_; + + vm.searchValue = ''; + vm.searchRejected = null; + vm.searchKey = false; + vm.searchKeyExamples = searchKeyExamples; + vm.searchKeyFields = searchKeyFields; + + vm.clearSearch = clearSearch; + vm.search = search; + vm.toggleSearchKey = toggleSearchKey; + vm.removeSearchTag = removeSearchTag; + vm.searchTags = getSearchTags(getCurrentQueryset()); + render.requestAnimationFrame(() => init()); } @@ -318,6 +339,62 @@ function toggle (uuid, menu) { lines.removeClass('hidden'); } + +// +// Search +// + +const searchReloadOptions = { reload: true, inherit: false }; +const searchKeyExamples = ['id:>1', 'task:set', 'created:>=2000-01-01']; +const searchKeyFields = ['changed', 'failed', 'host_name', 'stdout', 'task', 'role', 'playbook', 'play']; + +function toggleSearchKey () { + vm.searchKey = !vm.searchKey; +} + +function getCurrentQueryset() { + const { job_event_search } = $state.params; + + return qs.decodeArr(job_event_search); +} + +function getSearchTags (queryset) { + return qs.createSearchTagsFromQueryset(queryset) + .filter(tag => !tag.startsWith('event')) + .filter(tag => !tag.startsWith('-event')) + .filter(tag => !tag.startsWith('page_size')) + .filter(tag => !tag.startsWith('order_by')); +} + +function removeSearchTag (index) { + const searchTerm = vm.searchTags[index]; + + const currentQueryset = getCurrentQueryset(); + const modifiedQueryset = qs.removeTermsFromQueryset(currentQueryset, searchTerm); + + vm.searchTags = getSearchTags(modifiedQueryset); + + $state.params.job_event_search = qs.encodeArr(modifiedQueryset); + $state.transitionTo($state.current, $state.params, searchReloadOptions); +} + +function search () { + const searchInputQueryset = qs.getSearchInputQueryset(vm.searchValue); + + const currentQueryset = getCurrentQueryset(); + const modifiedQueryset = qs.mergeQueryset(currentQueryset, searchInputQueryset); + + vm.searchTags = getSearchTags(modifiedQueryset); + + $state.params.job_event_search = qs.encodeArr(modifiedQueryset); + $state.transitionTo($state.current, $state.params, searchReloadOptions); +} + +function clearSearch () { + vm.searchTags = []; + + $state.params.job_event_search = ''; + $state.transitionTo($state.current, $state.params, searchReloadOptions); } JobsIndexController.$inject = [ @@ -327,7 +404,9 @@ JobsIndexController.$inject = [ 'JobRenderService', '$scope', '$compile', - '$q' + '$q', + '$state', + 'QuerySet', ]; module.exports = JobsIndexController; diff --git a/awx/ui/client/features/output/index.js b/awx/ui/client/features/output/index.js index bc73ca83c7..593b7b1097 100644 --- a/awx/ui/client/features/output/index.js +++ b/awx/ui/client/features/output/index.js @@ -6,6 +6,7 @@ import Controller from '~features/output/index.controller'; import PageService from '~features/output/page.service'; import RenderService from '~features/output/render.service'; import ScrollService from '~features/output/scroll.service'; +import SearchKeyDirective from '~features/output/search-key.directive'; const Template = require('~features/output/index.view.html'); @@ -15,8 +16,8 @@ const PAGE_LIMIT = 3; const PAGE_SIZE = 100; const WS_PREFIX = 'ws'; -function resolveResource (Job, ProjectUpdate, AdHocCommand, SystemJob, WorkflowJob, $stateParams) { - const { id, type } = $stateParams; +function resolveResource (Job, ProjectUpdate, AdHocCommand, SystemJob, WorkflowJob, $stateParams, qs, Wait) { + const { id, type, job_event_search } = $stateParams; let Resource; let related = 'events'; @@ -43,14 +44,20 @@ function resolveResource (Job, ProjectUpdate, AdHocCommand, SystemJob, WorkflowJ return null; } + const params = { page_size: PAGE_SIZE, order_by: 'start_line' }; + + if (job_event_search) { + const searchParams = qs.encodeQuerysetObject(qs.decodeArr(job_event_search)); + + Object.assign(params, searchParams); + } + + Wait('start'); return new Resource('get', id) .then(model => model.extend(related, { pageCache: PAGE_CACHE, pageLimit: PAGE_LIMIT, - params: { - page_size: PAGE_SIZE, - order_by: 'start_line' - } + params, })) .then(model => { return { @@ -67,7 +74,9 @@ function resolveResource (Job, ProjectUpdate, AdHocCommand, SystemJob, WorkflowJ pageLimit: PAGE_LIMIT } }; - }); + }) + .catch(({ data, status }) => qs.error(data, status)) + .finally(() => Wait('stop')); } function resolveWebSocketConnection (SocketService, $stateParams) { @@ -131,8 +140,8 @@ function getWebSocketResource (type) { function JobsRun ($stateRegistry) { const state = { name: 'jobz', - url: '/jobz/:type/:id', - route: '/jobz/:type/:id', + url: '/jobz/:type/:id?job_event_search', + route: '/jobz/:type/:id?job_event_search', data: { activityStream: true, activityStreamTarget: 'jobs' @@ -152,6 +161,8 @@ function JobsRun ($stateRegistry) { 'SystemJobModel', 'WorkflowJobModel', '$stateParams', + 'QuerySet', + 'Wait', resolveResource ], ncyBreadcrumb: [ @@ -179,6 +190,7 @@ angular .service('JobStrings', Strings) .service('JobPageService', PageService) .service('JobScrollService', ScrollService) + .directive('atSearchKey', SearchKeyDirective) .run(JobsRun); export default MODULE_NAME; diff --git a/awx/ui/client/features/output/index.view.html b/awx/ui/client/features/output/index.view.html index 3c1a276826..2516ede6c7 100644 --- a/awx/ui/client/features/output/index.view.html +++ b/awx/ui/client/features/output/index.view.html @@ -7,6 +7,51 @@
+ +
+
+ + + + + + +
+
+ +
+
+
{{ tag }}
+
+ +
+
+
CLEAR ALL
+
+ + + +
{ + vm.examples = scope.examples || []; + vm.fields = scope.fields || []; + vm.relatedFields = scope.relatedFields || []; + } +} + +AtSearchKeyController.$inject = ['$scope']; + + +function atSearchKey () { + return { + templateUrl, + restrict: 'E', + require: ['atSearchKey'], + controllerAs: 'vm', + link: atSearchKeyLink, + controller: AtSearchKeyController, + scope: { + examples: '=', + fields: '=', + relatedFields: '=', + }, + }; +} + +export default atSearchKey; diff --git a/awx/ui/client/features/output/search-key.partial.html b/awx/ui/client/features/output/search-key.partial.html new file mode 100644 index 0000000000..d2790d285f --- /dev/null +++ b/awx/ui/client/features/output/search-key.partial.html @@ -0,0 +1,20 @@ + +
+
+
+
+
EXAMPLES:
+ +
+
+
+ FIELDS: + {{ field }}, +
+
+ ADDITIONAL INFORMATION: + For additional information on advanced search search syntax please see the Ansible Tower + documentation. +
+
+