diff --git a/awx/ui/client/legacy-styles/lists.less b/awx/ui/client/legacy-styles/lists.less index 484ea4440c..6aae815332 100644 --- a/awx/ui/client/legacy-styles/lists.less +++ b/awx/ui/client/legacy-styles/lists.less @@ -60,7 +60,7 @@ table, tbody { height: 40px; font-size: 14px; color: @list-item; - border-bottom: 1px solid @default-white-button-bord; + border-bottom: 1px solid @default-border; } .List-tableRow:last-of-type { @@ -176,6 +176,27 @@ table, tbody { text-transform: uppercase; } +.List-exitHolder { + justify-content: flex-end; + display:flex; +} + +.List-exit { + cursor:pointer; + padding:0px; + border: none; + height:20px; + font-size: 20px; + background-color:@default-bg; + color:@d7grey; + transition: color 0.2s; + line-height:1; +} + +.List-exit:hover{ + color:@default-icon; +} + .List-actionHolder { justify-content: flex-end; display: flex; diff --git a/awx/ui/client/src/bread-crumb/bread-crumb.block.less b/awx/ui/client/src/bread-crumb/bread-crumb.block.less index 7f080e7b8b..f75894afeb 100644 --- a/awx/ui/client/src/bread-crumb/bread-crumb.block.less +++ b/awx/ui/client/src/bread-crumb/bread-crumb.block.less @@ -34,6 +34,13 @@ .BreadCrumb-menuLink:hover { color: @bc-link-icon-focus; } +.BreadCrumb-menuLink { + .BreadCrumb-menuLinkImage.fa-refresh { + &:hover { + color: @default-link; + } + } +} .BreadCrumb-menuLinkImage { font-size: 18px; color: @bc-link-icon; diff --git a/awx/ui/client/src/bread-crumb/bread-crumb.directive.js b/awx/ui/client/src/bread-crumb/bread-crumb.directive.js index 86b604e2cc..93d17a00c9 100644 --- a/awx/ui/client/src/bread-crumb/bread-crumb.directive.js +++ b/awx/ui/client/src/bread-crumb/bread-crumb.directive.js @@ -12,6 +12,7 @@ export default scope.showActivityStreamButton = false; scope.showRefreshButton = false; + scope.alwaysShowRefreshButton = false; scope.loadingLicense = true; scope.$on("$stateChangeSuccess", function updateActivityStreamButton(event, toState, toParams, fromState, fromParams) { @@ -48,6 +49,7 @@ export default } scope.showRefreshButton = (streamConfig && streamConfig.refreshButton) ? true : false; + scope.alwaysShowRefreshButton = (streamConfig && streamConfig.alwaysShowRefreshButton) ? true: false; }); // scope.$on('featuresLoaded', function(){ diff --git a/awx/ui/client/src/bread-crumb/bread-crumb.partial.html b/awx/ui/client/src/bread-crumb/bread-crumb.partial.html index 563e6c4f79..606eec8b04 100644 --- a/awx/ui/client/src/bread-crumb/bread-crumb.partial.html +++ b/awx/ui/client/src/bread-crumb/bread-crumb.partial.html @@ -8,7 +8,7 @@ data-trigger="hover" data-container="body" ng-hide= "loadingLicense || licenseMissing" - ng-if="socketStatus === 'error' && showRefreshButton" + ng-if="(socketStatus === 'error' && showRefreshButton) || alwaysShowRefreshButton" ng-click="refresh()"> diff --git a/awx/ui/client/src/instance-groups/capacity-bar/capacity-bar.block.less b/awx/ui/client/src/instance-groups/capacity-bar/capacity-bar.block.less new file mode 100644 index 0000000000..f8e9af45ee --- /dev/null +++ b/awx/ui/client/src/instance-groups/capacity-bar/capacity-bar.block.less @@ -0,0 +1,28 @@ +@import "../../shared/branding/colors.default.less"; + +capacity-bar { + + width: 50%; + margin-right: 10px; + min-width: 100px; + + .CapacityBar { + background-color: @default-bg; + display: flex; + flex: 0 0 auto; + height: 10px; + border: 1px solid @default-link; + width: 100%; + border-radius: 100vw; + overflow: hidden; + } + + .CapacityBar-remaining { + background-color: @default-link; + flex: 0 0 auto; + } + + .CapacityBar-consumed { + flex: 0 0 auto; + } +} \ No newline at end of file diff --git a/awx/ui/client/src/instance-groups/capacity-bar/capacity-bar.directive.js b/awx/ui/client/src/instance-groups/capacity-bar/capacity-bar.directive.js new file mode 100644 index 0000000000..37fd496349 --- /dev/null +++ b/awx/ui/client/src/instance-groups/capacity-bar/capacity-bar.directive.js @@ -0,0 +1,18 @@ +export default ['templateUrl', + function (templateUrl) { + return { + scope: { + capacity: '=' + }, + templateUrl: templateUrl('instance-groups/capacity-bar/capacity-bar'), + restrict: 'E', + link: function(scope) { + scope.$watch('capacity', function() { + scope.PercentRemainingStyle = { + 'flex-grow': scope.capacity * 0.01 + }; + }, true); + } + }; + } +]; diff --git a/awx/ui/client/src/instance-groups/capacity-bar/capacity-bar.partial.html b/awx/ui/client/src/instance-groups/capacity-bar/capacity-bar.partial.html new file mode 100644 index 0000000000..aac4ccd7b0 --- /dev/null +++ b/awx/ui/client/src/instance-groups/capacity-bar/capacity-bar.partial.html @@ -0,0 +1,4 @@ +
+
+
+
\ No newline at end of file diff --git a/awx/ui/client/src/instance-groups/capacity-bar/main.js b/awx/ui/client/src/instance-groups/capacity-bar/main.js new file mode 100644 index 0000000000..e330c7080c --- /dev/null +++ b/awx/ui/client/src/instance-groups/capacity-bar/main.js @@ -0,0 +1,5 @@ +import capacityBar from './capacity-bar.directive'; + +export default + angular.module('capacityBarDirective', []) + .directive('capacityBar', capacityBar); \ No newline at end of file diff --git a/awx/ui/client/src/instance-groups/instance-group.block.less b/awx/ui/client/src/instance-groups/instance-group.block.less new file mode 100644 index 0000000000..8588e0f2de --- /dev/null +++ b/awx/ui/client/src/instance-groups/instance-group.block.less @@ -0,0 +1,56 @@ +@import "../shared/branding/colors.default.less"; + +.InstanceGroups { + + .BreadCrumb-menuLinkImage:hover { + color: @default-link; + } + + .List-details { + align-self: flex-end; + color: @default-interface-txt; + display: flex; + flex: 0 0 auto; + font-size: 12px; + margin-right:20px; + text-transform: uppercase; + } + + .Capacity-details { + display: flex; + margin-right: 20px; + align-items: center; + + .Capacity-details--label { + color: @default-interface-txt; + margin: 0 10px 0 0; + } + + .Capacity-details--percentage { + color: @default-data-txt; + } + } + + .RunningJobs-details { + align-items: center; + display: flex; + + .RunningJobs-details--label { + margin: 0 10px 0 0; + } + } + + .List-tableCell--capacityRemainingColumn { + display: flex; + height: 40px; + align-items: center; + } + + .List-noItems { + margin-top: 20px; + } + + .List-tableRow .List-titleBadge { + margin: 0 0 0 5px; + } +} \ No newline at end of file diff --git a/awx/ui/client/src/instance-groups/instance-group.partial.html b/awx/ui/client/src/instance-groups/instance-group.partial.html new file mode 100644 index 0000000000..9fe3b98d34 --- /dev/null +++ b/awx/ui/client/src/instance-groups/instance-group.partial.html @@ -0,0 +1,34 @@ +
+
+
+
+
+
{{ instanceGroupName }}
+
+
+
+

Capacity

+ + {{ instanceGroupCapacity }}% +
+
+

Running Jobs

+ + {{ instanceGroupJobsRunning }} + +
+
+
+ +
+
+
+
INSTANCES
+
JOBS
+
+
+
+
+
\ No newline at end of file diff --git a/awx/ui/client/src/instance-groups/instance-groups-multiselect/instance-groups.block.less b/awx/ui/client/src/instance-groups/instance-groups-multiselect/instance-groups.block.less deleted file mode 100644 index a3aab73a19..0000000000 --- a/awx/ui/client/src/instance-groups/instance-groups-multiselect/instance-groups.block.less +++ /dev/null @@ -1,4 +0,0 @@ -#InstanceGroups { - display: flex; - padding: 0 12px; -} \ No newline at end of file diff --git a/awx/ui/client/src/instance-groups/instance-groups.list.js b/awx/ui/client/src/instance-groups/instance-groups.list.js index a6827a9291..096d2652bf 100644 --- a/awx/ui/client/src/instance-groups/instance-groups.list.js +++ b/awx/ui/client/src/instance-groups/instance-groups.list.js @@ -14,8 +14,10 @@ export default ['i18n', function(i18n) { label: i18n._('Name'), columnClass: 'col-md-3 col-sm-9 col-xs-9', modalColumnClass: 'col-md-8', + uiSref: 'instanceGroups.instances.list({instance_group_id: instance_group.id})', + ngClass: "{'isActive' : isActive()}" }, - capacity: { + percent_capacity_remaining: { label: i18n._('Capacity'), nosort: true, }, diff --git a/awx/ui/client/src/instance-groups/instance-groups.partial.html b/awx/ui/client/src/instance-groups/instance-groups.partial.html new file mode 100644 index 0000000000..baeaf59f00 --- /dev/null +++ b/awx/ui/client/src/instance-groups/instance-groups.partial.html @@ -0,0 +1,11 @@ +
+ + +
+ +
+ +
+
+
+
diff --git a/awx/ui/client/src/instance-groups/instance-groups.route.js b/awx/ui/client/src/instance-groups/instance-groups.route.js new file mode 100644 index 0000000000..89cd484dbb --- /dev/null +++ b/awx/ui/client/src/instance-groups/instance-groups.route.js @@ -0,0 +1,41 @@ +import {templateUrl} from '../shared/template-url/template-url.factory'; +import { N_ } from '../i18n'; + +export default { + name: 'instanceGroups', + url: '/instance_groups', + searchPrefix: 'instance_group', + ncyBreadcrumb: { + parent: 'setup', + label: N_('INSTANCE GROUPS') + }, + params: { + instance_group_search: { + value: { + page_size: '10', + order_by: 'name' + } + } + }, + data: { + alwaysShowRefreshButton: true, + }, + views: { + '@': { + templateUrl: templateUrl('./instance-groups/instance-groups'), + }, + 'list@instanceGroups': { + templateUrl: templateUrl('./instance-groups/list/instance-groups-list'), + controller: 'InstanceGroupsList' + + } + }, + resolve: { + Dataset: ['InstanceGroupList', 'QuerySet', '$stateParams', 'GetBasePath', + function(list, qs, $stateParams, GetBasePath) { + let path = GetBasePath(list.basePath) || GetBasePath(list.name); + return qs.search(path, $stateParams[`${list.iterator}_search`]); + } + ] + } +}; \ No newline at end of file diff --git a/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs-list.route.js b/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs-list.route.js new file mode 100644 index 0000000000..dcff49f2d0 --- /dev/null +++ b/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs-list.route.js @@ -0,0 +1,40 @@ +import { N_ } from '../../../i18n'; + +export default { + name: 'instanceGroups.instances.list.job.list', + url: '/jobs', + searchPrefix: 'instance_job', + ncyBreadcrumb: { + parent: 'instanceGroups.instances.list', + label: N_('{{ breadcrumb.instance_name }}') + }, + params: { + instance_job_search: { + value: { + page_size: '10', + order_by: '-finished', + not__launch_type: 'sync' + } + } + }, + views: { + 'list@instanceGroups.instances.list.job': { + templateProvider: function(InstanceJobsList, generateList) { + let html = generateList.build({ + list: InstanceJobsList + }); + return html; + }, + controller: 'InstanceJobsController' + } + }, + + resolve: { + Dataset: ['InstanceJobsList', 'QuerySet', '$stateParams', 'GetBasePath', + function(list, qs, $stateParams, GetBasePath) { + let path = `${GetBasePath('instances')}${$stateParams.instance_id}/jobs`; + return qs.search(path, $stateParams[`${list.iterator}_search`]); + } + ], + } +}; \ No newline at end of file diff --git a/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.controller.js b/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.controller.js new file mode 100644 index 0000000000..a7d50764f5 --- /dev/null +++ b/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.controller.js @@ -0,0 +1,82 @@ +export default ['$scope','InstanceJobsList', 'GetBasePath', 'Rest', 'Dataset','Find', '$state', '$q', + function($scope, InstanceJobsList, GetBasePath, Rest, Dataset, Find, $state, $q) { + + let list = InstanceJobsList; + + init(); + + function init(){ + $scope.optionsDefer = $q.defer(); + $scope.list = list; + $scope[`${list.iterator}_dataset`] = Dataset.data; + $scope[list.name] = $scope[`${list.iterator}_dataset`].results; + } + + $scope.$on(`${list.iterator}_options`, function(event, data){ + $scope.options = data.data.actions.GET; + optionsRequestDataProcessing(); + }); + + // iterate over the list and add fields like type label, after the + // OPTIONS request returns, or the list is sorted/paginated/searched + function optionsRequestDataProcessing(){ + + if($scope[list.name] && $scope[list.name].length > 0) { + $scope[list.name].forEach(function(item, item_idx) { + var itm = $scope[list.name][item_idx]; + + if(item.summary_fields && item.summary_fields.source_workflow_job && + item.summary_fields.source_workflow_job.id){ + item.workflow_result_link = `/#/workflows/${item.summary_fields.source_workflow_job.id}`; + } + + // Set the item type label + if (list.fields.type && $scope.options && + $scope.options.hasOwnProperty('type')) { + $scope.options.type.choices.forEach(function(choice) { + if (choice[0] === item.type) { + itm.type_label = choice[1]; + } + }); + } + buildTooltips(itm); + }); + } + } + + function buildTooltips(job) { + job.status_tip = 'Job ' + job.status + ". Click for details."; + } + + $scope.viewjobResults = function(job) { + var goTojobResults = function(state) { + $state.go(state, { id: job.id }, { reload: true }); + }; + switch (job.type) { + case 'job': + goTojobResults('jobResult'); + break; + case 'ad_hoc_command': + goTojobResults('adHocJobStdout'); + break; + case 'system_job': + goTojobResults('managementJobStdout'); + break; + case 'project_update': + goTojobResults('scmUpdateStdout'); + break; + case 'inventory_update': + goTojobResults('inventorySyncStdout'); + break; + case 'workflow_job': + goTojobResults('workflowResults'); + break; + } + }; + + $scope.$watchCollection(`${$scope.list.name}`, function() { + optionsRequestDataProcessing(); + } + ); + } +]; \ No newline at end of file diff --git a/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.list.js b/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.list.js new file mode 100644 index 0000000000..58476f0054 --- /dev/null +++ b/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.list.js @@ -0,0 +1,78 @@ +export default ['i18n', function(i18n) { + return { + + name: 'instance_jobs', + iterator: 'instance_job', + index: false, + hover: false, + well: false, + emptyListText: i18n._('No jobs have yet run.'), + title: false, + basePath: 'api/v2/instances/{{$stateParams.instance_id}}/jobs', + + fields: { + status: { + label: '', + columnClass: 'col-lg-1 col-md-1 col-sm-2 col-xs-2 List-staticColumn--smallStatus', + dataTipWatch: 'instance_job.status_tip', + awToolTip: "{{ instance_job.status_tip }}", + awTipPlacement: "right", + dataTitle: "{{ instance_job.status_popover_title }}", + icon: 'icon-job-{{ instance_job.status }}', + iconOnly: true, + ngClick:"viewjobResults(instance_job)", + nosort: true + }, + id: { + label: i18n._('ID'), + ngClick:"viewjobResults(instance_job)", + columnClass: 'col-lg-1 col-md-1 col-sm-2 col-xs-2 List-staticColumnAdjacent', + awToolTip: "{{ instance_job.status_tip }}", + dataPlacement: 'top', + noLink: true + }, + name: { + label: i18n._('Name'), + columnClass: 'col-lg-2 col-md-3 col-sm-4 col-xs-6', + ngClick: "viewjobResults(instance_job)", + nosort: true, + badgePlacement: 'right', + badgeCustom: true, + badgeIcon: ` + + W + + ` + }, + type: { + label: i18n._('Type'), + ngBind: 'instance_job.type_label', + link: false, + columnClass: "col-lg-2 hidden-md hidden-sm hidden-xs", + nosort: true + }, + finished: { + label: i18n._('Finished'), + noLink: true, + filter: "longDate", + columnClass: "col-lg-2 col-md-3 col-sm-3 hidden-xs", + key: true, + desc: true, + nosort: true + }, + labels: { + label: i18n._('Labels'), + type: 'labels', + nosort: true, + showDelete: false, + columnClass: 'List-tableCell col-lg-4 col-md-4 hidden-sm hidden-xs', + sourceModel: 'labels', + sourceField: 'name', + }, + } + }; +}]; diff --git a/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.partial.html b/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.partial.html new file mode 100644 index 0000000000..163a2e25fe --- /dev/null +++ b/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.partial.html @@ -0,0 +1,33 @@ +
+
+
+
+
+
{{ instanceName }}
+
+
+
+

Capacity

+ + {{ instanceCapacity }}% +
+
+

Running Jobs

+ + {{ instanceJobsRunning }} + +
+
+
+ +
+
+
+
JOBS
+
+
+
+
+
\ No newline at end of file diff --git a/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.route.js b/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.route.js new file mode 100644 index 0000000000..73af9259b5 --- /dev/null +++ b/awx/ui/client/src/instance-groups/instances/instance-jobs/instance-jobs.route.js @@ -0,0 +1,37 @@ +import { templateUrl } from '../../../shared/template-url/template-url.factory'; + +export default { + name: 'instanceGroups.instances.list.job', + url: '/:instance_id', + abstract: true, + ncyBreadcrumb: { + skip: true + }, + views: { + 'instanceJobs@instanceGroups': { + templateUrl: templateUrl('./instance-groups/instances/instance-jobs/instance-jobs'), + controller: function($scope, $rootScope, instance) { + $scope.instanceName = instance.hostname; + $scope.instanceCapacity = instance.percent_capacity_remaining; + $scope.instanceJobsRunning = instance.jobs_running; + $rootScope.breadcrumb.instance_name = instance.hostname; + } + } + }, + resolve: { + instance: ['GetBasePath', 'Rest', 'ProcessErrors', '$stateParams', function(GetBasePath, Rest, ProcessErrors, $stateParams) { + let url = GetBasePath('instances') + $stateParams.instance_id; + Rest.setUrl(url); + return Rest.get() + .then(({data}) => { + return data; + }) + .catch(({data, status}) => { + ProcessErrors(null, data, status, null, { + hdr: 'Error!', + msg: 'Failed to get instance groups info. GET returned status: ' + status + }); + }); + }] + } +}; \ No newline at end of file diff --git a/awx/ui/client/src/instance-groups/instances/instances-list.partial.html b/awx/ui/client/src/instance-groups/instances/instances-list.partial.html new file mode 100644 index 0000000000..ac75597b2a --- /dev/null +++ b/awx/ui/client/src/instance-groups/instances/instances-list.partial.html @@ -0,0 +1,43 @@ +
+ + + +
PLEASE ADD ITEMS TO THIS LIST
+
+ + + + + + + + + + + + + + + + +
+ "{{'Name' | translate}}" + + + Capacity + + Running Jobs +
+ {{ instance.hostname }} + {{ instance.percent_capacity_remaining }}% + + + {{ instance.jobs_running }} + +
+
+
diff --git a/awx/ui/client/src/instance-groups/instances/instances-list.route.js b/awx/ui/client/src/instance-groups/instances/instances-list.route.js new file mode 100644 index 0000000000..89e572f3d7 --- /dev/null +++ b/awx/ui/client/src/instance-groups/instances/instances-list.route.js @@ -0,0 +1,34 @@ +import {templateUrl} from '../../shared/template-url/template-url.factory'; +import { N_ } from '../../i18n'; + +export default { + name: 'instanceGroups.instances.list', + url: '/instances', + searchPrefix: 'instance', + ncyBreadcrumb: { + parent: 'instanceGroups', + label: N_('{{breadcrumb.instance_group_name}}') + }, + params: { + instance_search: { + value: { + page_size: '10', + order_by: 'hostname' + } + } + }, + views: { + 'list@instanceGroups.instances': { + templateUrl: templateUrl('./instance-groups/instances/instances-list'), + controller: 'InstanceListController' + } + }, + resolve: { + Dataset: ['InstanceList', 'QuerySet', '$stateParams', 'GetBasePath', + function(list, qs, $stateParams, GetBasePath) { + let path = `${GetBasePath('instance_groups')}${$stateParams.instance_group_id}/instances`; + return qs.search(path, $stateParams[`${list.iterator}_search`]); + } + ] + } +}; \ No newline at end of file diff --git a/awx/ui/client/src/instance-groups/instances/instances.controller.js b/awx/ui/client/src/instance-groups/instances/instances.controller.js new file mode 100644 index 0000000000..0481d84263 --- /dev/null +++ b/awx/ui/client/src/instance-groups/instances/instances.controller.js @@ -0,0 +1,20 @@ +export default ['$scope', 'InstanceList', 'GetBasePath', 'Rest', 'Dataset','Find', '$state', '$q', + function($scope, InstanceList, GetBasePath, Rest, Dataset, Find, $state, $q) { + let list = InstanceList; + + init(); + + function init(){ + $scope.optionsDefer = $q.defer(); + $scope.list = list; + $scope[`${list.iterator}_dataset`] = Dataset.data; + $scope[list.name] = $scope[`${list.iterator}_dataset`].results; + } + + $scope.isActive = function(id) { + let selected = parseInt($state.params.instance_id); + return id === selected; + }; + + } +]; \ No newline at end of file diff --git a/awx/ui/client/src/instance-groups/instances/instances.list.js b/awx/ui/client/src/instance-groups/instances/instances.list.js new file mode 100644 index 0000000000..b3966884d8 --- /dev/null +++ b/awx/ui/client/src/instance-groups/instances/instances.list.js @@ -0,0 +1,29 @@ +export default ['i18n', function(i18n) { + return { + name: 'instances' , + iterator: 'instance', + listTitle: false, + index: false, + hover: false, + tabs: true, + well: true, + + fields: { + hostname: { + key: true, + label: i18n._('Name'), + columnClass: 'col-md-3 col-sm-9 col-xs-9', + modalColumnClass: 'col-md-8', + uiSref: 'instanceGroups.instances.list.job({instance_id: instance.id})' + }, + percent_capacity_remaining: { + label: i18n._('Capacity'), + nosort: true, + }, + jobs_running: { + label: i18n._('Running Jobs'), + nosort: true, + }, + } + }; +}]; diff --git a/awx/ui/client/src/instance-groups/instances/instances.route.js b/awx/ui/client/src/instance-groups/instances/instances.route.js new file mode 100644 index 0000000000..0662052c67 --- /dev/null +++ b/awx/ui/client/src/instance-groups/instances/instances.route.js @@ -0,0 +1,34 @@ +import {templateUrl} from '../../shared/template-url/template-url.factory'; + +export default { + name: 'instanceGroups.instances', + url: '/:instance_group_id', + abstract: true, + views: { + 'instances@instanceGroups': { + templateUrl: templateUrl('./instance-groups/instance-group'), + controller: function($scope, $rootScope, instanceGroup) { + $scope.instanceGroupName = instanceGroup.name; + $scope.instanceGroupCapacity = instanceGroup.percent_capacity_remaining; + $scope.instanceGroupJobsRunning = instanceGroup.jobs_running; + $rootScope.breadcrumb.instance_group_name = instanceGroup.name; + } + } + }, + resolve: { + instanceGroup: ['GetBasePath', 'Rest', 'ProcessErrors', '$stateParams', function(GetBasePath, Rest, ProcessErrors, $stateParams) { + let url = GetBasePath('instance_groups') + $stateParams.instance_group_id; + Rest.setUrl(url); + return Rest.get() + .then(({data}) => { + return data; + }) + .catch(({data, status}) => { + ProcessErrors(null, data, status, null, { + hdr: 'Error!', + msg: 'Failed to get instance groups info. GET returned status: ' + status + }); + }); + }] + } +}; \ No newline at end of file diff --git a/awx/ui/client/src/instance-groups/jobs/jobs-list.route.js b/awx/ui/client/src/instance-groups/jobs/jobs-list.route.js new file mode 100644 index 0000000000..7dc5230339 --- /dev/null +++ b/awx/ui/client/src/instance-groups/jobs/jobs-list.route.js @@ -0,0 +1,40 @@ +import { N_ } from '../../i18n'; + +export default { + name: 'instanceGroups.instances.jobs', + url: '/jobs', + searchPrefix: 'job', + ncyBreadcrumb: { + parent: 'instanceGroups.instances.list', + label: N_('JOBS') + }, + params: { + job_search: { + value: { + page_size: '10', + order_by: '-finished', + not__launch_type: 'sync' + } + }, + instance_group_id: null + }, + views: { + 'list@instanceGroups.instances': { + templateProvider: function(JobsList, generateList) { + let html = generateList.build({ + list: JobsList + }); + return html; + }, + controller: 'JobsListController' + } + }, + resolve: { + Dataset: ['JobsList', 'QuerySet', '$stateParams', 'GetBasePath', + function(list, qs, $stateParams, GetBasePath) { + let path = `${GetBasePath('instance_groups')}${$stateParams.instance_group_id}/jobs`; + return qs.search(path, $stateParams[`${list.iterator}_search`]); + } + ] + } +}; \ No newline at end of file diff --git a/awx/ui/client/src/instance-groups/jobs/jobs.controller.js b/awx/ui/client/src/instance-groups/jobs/jobs.controller.js new file mode 100644 index 0000000000..cfe2f73327 --- /dev/null +++ b/awx/ui/client/src/instance-groups/jobs/jobs.controller.js @@ -0,0 +1,82 @@ +export default ['$scope','JobsList', 'GetBasePath', 'Rest', 'Dataset','Find', '$state', '$q', + function($scope, JobsList, GetBasePath, Rest, Dataset, Find, $state, $q) { + + let list = JobsList; + + init(); + + function init(){ + $scope.optionsDefer = $q.defer(); + $scope.list = list; + $scope[`${list.iterator}_dataset`] = Dataset.data; + $scope[list.name] = $scope[`${list.iterator}_dataset`].results; + } + + $scope.$on(`${list.iterator}_options`, function(event, data){ + $scope.options = data.data.actions.GET; + optionsRequestDataProcessing(); + }); + + // iterate over the list and add fields like type label, after the + // OPTIONS request returns, or the list is sorted/paginated/searched + function optionsRequestDataProcessing(){ + + if($scope[list.name] && $scope[list.name].length > 0) { + $scope[list.name].forEach(function(item, item_idx) { + var itm = $scope[list.name][item_idx]; + if(item.summary_fields && item.summary_fields.source_workflow_job && + item.summary_fields.source_workflow_job.id){ + item.workflow_result_link = `/#/workflows/${item.summary_fields.source_workflow_job.id}`; + } + + // Set the item type label + if (list.fields.type && $scope.options && + $scope.options.hasOwnProperty('type')) { + $scope.options.type.choices.forEach(function(choice) { + if (choice[0] === item.type) { + itm.type_label = choice[1]; + } + }); + } + buildTooltips(itm); + }); + } + } + + function buildTooltips(job) { + job.status_tip = 'Job ' + job.status + ". Click for details."; + } + + $scope.viewjobResults = function(job) { + var goTojobResults = function(state) { + $state.go(state, { id: job.id }, { reload: true }); + }; + switch (job.type) { + case 'job': + goTojobResults('jobResult'); + break; + case 'ad_hoc_command': + goTojobResults('adHocJobStdout'); + break; + case 'system_job': + goTojobResults('managementJobStdout'); + break; + case 'project_update': + goTojobResults('scmUpdateStdout'); + break; + case 'inventory_update': + goTojobResults('inventorySyncStdout'); + break; + case 'workflow_job': + goTojobResults('workflowResults'); + break; + } + + }; + + $scope.$watchCollection(`${$scope.list.name}`, function() { + optionsRequestDataProcessing(); + } + ); + } +]; \ No newline at end of file diff --git a/awx/ui/client/src/instance-groups/jobs/jobs.list.js b/awx/ui/client/src/instance-groups/jobs/jobs.list.js new file mode 100644 index 0000000000..59e14ba19b --- /dev/null +++ b/awx/ui/client/src/instance-groups/jobs/jobs.list.js @@ -0,0 +1,76 @@ +export default ['i18n', function (i18n) { + return { + name: 'jobs', + iterator: 'job', + basePath: 'api/v2/instance_groups/{{$stateParams.instance_group_id}}/jobs/', + index: false, + hover: false, + well: true, + emptyListText: i18n._('No jobs have yet run.'), + listTitle: false, + + fields: { + status: { + label: '', + columnClass: 'col-lg-1 col-md-1 col-sm-2 col-xs-2 List-staticColumn--smallStatus', + dataTipWatch: 'job.status_tip', + awToolTip: "{{ job.status_tip }}", + awTipPlacement: "right", + dataTitle: "{{ job.status_popover_title }}", + icon: 'icon-job-{{ job.status }}', + iconOnly: true, + ngClick: "viewjobResults(job)", + nosort: true + }, + id: { + label: i18n._('ID'), + ngClick: "viewjobResults(job)", + columnClass: 'col-lg-1 col-md-1 col-sm-2 col-xs-2 List-staticColumnAdjacent', + awToolTip: "{{ job.status_tip }}", + dataPlacement: 'top', + noLink: true + }, + name: { + label: i18n._('Name'), + columnClass: 'col-lg-2 col-md-3 col-sm-4 col-xs-6', + ngClick: "viewjobResults(job)", + badgePlacement: 'right', + badgeCustom: true, + nosort: true, + badgeIcon: ` + + W + + ` + }, + type: { + label: i18n._('Type'), + ngBind: 'job.type_label', + columnClass: "col-lg-2 hidden-md hidden-sm hidden-xs", + nosort: true + }, + finished: { + label: i18n._('Finished'), + noLink: true, + filter: "longDate", + columnClass: "col-lg-2 col-md-3 col-sm-3 hidden-xs", + key: true, + desc: true, + nosort: true + }, + labels: { + label: i18n._('Labels'), + type: 'labels', + nosort: true, + showDelete: false, + columnClass: 'List-tableCell col-lg-4 col-md-4 hidden-sm hidden-xs', + sourceModel: 'labels', + sourceField: 'name' + }, + } + }; +}]; diff --git a/awx/ui/client/src/instance-groups/list/instance-groups-list.controller.js b/awx/ui/client/src/instance-groups/list/instance-groups-list.controller.js index ffb46ac5c0..381e2419bf 100644 --- a/awx/ui/client/src/instance-groups/list/instance-groups-list.controller.js +++ b/awx/ui/client/src/instance-groups/list/instance-groups-list.controller.js @@ -1,42 +1,19 @@ -export default ['$scope', 'InstanceGroupList', 'GetBasePath', 'Rest', 'Dataset','Find', '$state', '$q', - function($scope, InstanceGroupList, GetBasePath, Rest, Dataset, Find, $state, $q) { +export default ['$scope', 'InstanceGroupList', 'GetBasePath', 'Rest', 'Dataset','Find', '$state', + function($scope, InstanceGroupList, GetBasePath, Rest, Dataset, Find, $state) { let list = InstanceGroupList; init(); function init(){ - $scope.optionsDefer = $q.defer(); $scope.list = list; $scope[`${list.iterator}_dataset`] = Dataset.data; $scope[list.name] = $scope[`${list.iterator}_dataset`].results; + $scope.instanceGroupCount = Dataset.data.count; } - // iterate over the list and add fields like type label, after the - // OPTIONS request returns, or the list is sorted/paginated/searched - function optionsRequestDataProcessing(){ - $scope.optionsDefer.promise.then(function(options) { - if($scope.list.name === 'instance_groups'){ - if ($scope[list.name] !== undefined) { - $scope[list.name].forEach(function(item, item_idx) { - var itm = $scope[list.name][item_idx]; - // Set the item type label - if (list.fields.kind && options && options.actions && options.actions.GET && options.actions.GET.kind) { - options.actions.GET.kind.choices.forEach(function(choice) { - if (choice[0] === item.kind) { - itm.kind_label = choice[1]; - } - }); - } - - }); - } - } - }); - } - - $scope.$watchCollection(`${$scope.list.name}`, function() { - optionsRequestDataProcessing(); - } - ); + $scope.isActive = function(id) { + let selected = parseInt($state.params.instance_group_id); + return id === selected; + }; } ]; \ No newline at end of file diff --git a/awx/ui/client/src/instance-groups/list/instance-groups-list.partial.html b/awx/ui/client/src/instance-groups/list/instance-groups-list.partial.html new file mode 100644 index 0000000000..b6d3679a57 --- /dev/null +++ b/awx/ui/client/src/instance-groups/list/instance-groups-list.partial.html @@ -0,0 +1,63 @@ +
+
+
+ INSTANCE GROUPS +
+ + {{ instanceGroupCount }} + +
+
+ + + + +
PLEASE ADD ITEMS TO THIS LIST
+ +
+ + + + + + + + + + + + + + + + +
+ "{{'Name' | translate}}" + + + Capacity + + Running Jobs +
+ {{ instance_group.name }} + {{ instance_group.instances }} + + {{ instance_group.percent_capacity_remaining }}% + + + {{ instance_group.jobs_running }} + +
+
+ + + diff --git a/awx/ui/client/src/instance-groups/main.js b/awx/ui/client/src/instance-groups/main.js index c73e7069b8..024444c7d1 100644 --- a/awx/ui/client/src/instance-groups/main.js +++ b/awx/ui/client/src/instance-groups/main.js @@ -1,35 +1,58 @@ import InstanceGroupsList from './list/instance-groups-list.controller'; -import instanceGroupsMultiselect from './instance-groups-multiselect/instance-groups.directive'; -import instanceGroupsModal from './instance-groups-multiselect/instance-groups-modal/instance-groups-modal.directive'; +import instanceGroupsMultiselect from '../shared/instance-groups-multiselect/instance-groups.directive'; +import instanceGroupsModal from '../shared/instance-groups-multiselect/instance-groups-modal/instance-groups-modal.directive'; +import instanceGroupsRoute from './instance-groups.route'; +import instancesListRoute from './instances/instances-list.route'; +import JobsList from './jobs/jobs.list'; +import jobsListRoute from './jobs/jobs-list.route'; +import JobsListController from './jobs/jobs.controller'; +import InstanceList from './instances/instances.list'; +import instancesRoute from './instances/instances.route'; +import InstanceListController from './instances/instances.controller'; +import InstanceJobsList from './instances/instance-jobs/instance-jobs.list'; +import instanceJobsRoute from './instances/instance-jobs/instance-jobs.route'; +import instanceJobsListRoute from './instances/instance-jobs/instance-jobs-list.route'; +import InstanceJobsController from './instances/instance-jobs/instance-jobs.controller'; +import CapacityBar from './capacity-bar/main'; import list from './instance-groups.list'; import service from './instance-groups.service'; -import { N_ } from '../i18n'; export default -angular.module('instanceGroups', []) +angular.module('instanceGroups', [CapacityBar.name]) .service('InstanceGroupsService', service) .factory('InstanceGroupList', list) + .factory('JobsList', JobsList) + .factory('InstanceList', InstanceList) + .factory('InstanceJobsList', InstanceJobsList) .controller('InstanceGroupsList', InstanceGroupsList) + .controller('JobsListController', JobsListController) + .controller('InstanceListController', InstanceListController) + .controller('InstanceJobsController', InstanceJobsController) .directive('instanceGroupsMultiselect', instanceGroupsMultiselect) .directive('instanceGroupsModal', instanceGroupsModal) - .config(['$stateProvider', 'stateDefinitionsProvider', - function($stateProvider, stateDefinitionsProvider) { - let stateDefinitions = stateDefinitionsProvider.$get(); + .config(['$stateProvider', 'stateDefinitionsProvider', '$stateExtenderProvider', + function($stateProvider, stateDefinitionsProvider, $stateExtenderProvider) { + let stateExtender = $stateExtenderProvider.$get(); + + + function generateInstanceGroupsStates() { + return new Promise((resolve) => { + resolve({ + states: [ + stateExtender.buildDefinition(instanceGroupsRoute), + stateExtender.buildDefinition(instancesRoute), + stateExtender.buildDefinition(instancesListRoute), + stateExtender.buildDefinition(jobsListRoute), + stateExtender.buildDefinition(instanceJobsRoute), + stateExtender.buildDefinition(instanceJobsListRoute) + ] + }); + }); + } $stateProvider.state({ name: 'instanceGroups', url: '/instance_groups', - lazyLoad: () => stateDefinitions.generateTree({ - parent: 'instanceGroups', - list: 'InstanceGroupList', - controllers: { - list: 'InstanceGroupsList' - }, - ncyBreadcrumb: { - parent: 'setup', - label: N_('INSTANCE GROUPS') - } - }) + lazyLoad: () => generateInstanceGroupsStates() }); - } - ]); \ No newline at end of file + }]); diff --git a/awx/ui/client/src/shared/generator-helpers.js b/awx/ui/client/src/shared/generator-helpers.js index a30f847d76..f4d6aade11 100644 --- a/awx/ui/client/src/shared/generator-helpers.js +++ b/awx/ui/client/src/shared/generator-helpers.js @@ -382,6 +382,8 @@ angular.module('GeneratorHelpers', [systemStatus.name]) html += " diff --git a/awx/ui/client/src/instance-groups/instance-groups-multiselect/instance-groups-multiselect.controller.js b/awx/ui/client/src/shared/instance-groups-multiselect/instance-groups-multiselect.controller.js similarity index 100% rename from awx/ui/client/src/instance-groups/instance-groups-multiselect/instance-groups-multiselect.controller.js rename to awx/ui/client/src/shared/instance-groups-multiselect/instance-groups-multiselect.controller.js diff --git a/awx/ui/client/src/shared/instance-groups-multiselect/instance-groups.block.less b/awx/ui/client/src/shared/instance-groups-multiselect/instance-groups.block.less new file mode 100644 index 0000000000..77612ebf3b --- /dev/null +++ b/awx/ui/client/src/shared/instance-groups-multiselect/instance-groups.block.less @@ -0,0 +1,22 @@ +@import "../../shared/branding/colors.default.less"; + +#InstanceGroups { + display: flex; + padding: 0 12px; +} + +#instance-groups-panel { + table { + overflow: hidden; + } + .List-header { + margin-bottom: 20px; + } + .isActive { + border-left: 10px solid @list-row-select-bord; + } + .instances-list, + .instance-jobs-list { + margin-top: 20px; + } +} diff --git a/awx/ui/client/src/instance-groups/instance-groups-multiselect/instance-groups.directive.js b/awx/ui/client/src/shared/instance-groups-multiselect/instance-groups.directive.js similarity index 86% rename from awx/ui/client/src/instance-groups/instance-groups-multiselect/instance-groups.directive.js rename to awx/ui/client/src/shared/instance-groups-multiselect/instance-groups.directive.js index 2396a97ab5..2a0277c45e 100644 --- a/awx/ui/client/src/instance-groups/instance-groups-multiselect/instance-groups.directive.js +++ b/awx/ui/client/src/shared/instance-groups-multiselect/instance-groups.directive.js @@ -6,7 +6,7 @@ export default ['templateUrl', '$compile', instanceGroups: '=' }, restrict: 'E', - templateUrl: templateUrl('instance-groups/instance-groups-multiselect/instance-groups'), + templateUrl: templateUrl('shared/instance-groups-multiselect/instance-groups'), controller: instanceGroupsMultiselectController, link: function(scope) { scope.openInstanceGroupsModal = function() { diff --git a/awx/ui/client/src/instance-groups/instance-groups-multiselect/instance-groups.partial.html b/awx/ui/client/src/shared/instance-groups-multiselect/instance-groups.partial.html similarity index 75% rename from awx/ui/client/src/instance-groups/instance-groups-multiselect/instance-groups.partial.html rename to awx/ui/client/src/shared/instance-groups-multiselect/instance-groups.partial.html index b4e4021205..cc4b689308 100644 --- a/awx/ui/client/src/instance-groups/instance-groups-multiselect/instance-groups.partial.html +++ b/awx/ui/client/src/shared/instance-groups-multiselect/instance-groups.partial.html @@ -1,6 +1,6 @@
- -