diff --git a/.eslintrc.json b/.eslintrc.json index dfe10c8a5..282aa5064 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -158,7 +158,7 @@ "no-bitwise": "error", "no-caller": "error", "no-catch-shadow": "error", - "no-confusing-arrow": "error", + "no-confusing-arrow": ["error", {"allowParens": true}], "no-continue": "off", "no-div-regex": "error", "no-duplicate-imports": "error", diff --git a/client/app/services/service-explorer/service-explorer.component.js b/client/app/services/service-explorer/service-explorer.component.js index 730dd3af9..38f84c61e 100644 --- a/client/app/services/service-explorer/service-explorer.component.js +++ b/client/app/services/service-explorer/service-explorer.component.js @@ -11,7 +11,7 @@ export const ServiceExplorerComponent = { /** @ngInject */ function ComponentController($state, ServicesState, Language, ListView, Chargeback, TaggingService, TagEditorModal, EventNotifications, ModalService, PowerOperations, lodash, Polling, POLLING_INTERVAL) { - var vm = this; + const vm = this; vm.$onDestroy = function() { Polling.stop('serviceListPolling'); @@ -19,14 +19,13 @@ function ComponentController($state, ServicesState, Language, ListView, Chargeba vm.$onInit = () => { vm.permissions = ServicesState.getPermissions(); - if ($state.params.filter) { - ServicesState.services.setFilters($state.params.filter); - ServicesState.services.filterApplied = true; - } else { - ServicesState.services.setFilters([]); - ServicesState.services.filterApplied = false; - } + $state.params.filter ? ServicesState.services.setFilters($state.params.filter) : ServicesState.services.setFilters([]); ServicesState.services.setSort({id: "created_at", title: "Created", sortType: "numeric"}, false); + + TaggingService.queryAvailableTags().then((response) => { + vm.filterTags = response; + }); + angular.extend(vm, { loading: false, title: __('Services'), @@ -53,17 +52,22 @@ function ComponentController($state, ServicesState, Language, ListView, Chargeba cardConfig: getCardConfig(), listConfig: getListConfig(), listActions: getListActions(), - headerConfig: getHeaderConfig(), + toolbarConfig: { + sortConfig: serviceSortConfig(), + filterConfig: serviceFilterConfig(), + actionsConfig: { + actionsInclude: true, + }, + }, menuActions: getMenuActions(), serviceChildrenListConfig: createServiceChildrenListConfig(), pollingInterval: POLLING_INTERVAL, }); vm.offset = 0; - Language.fixState(ServicesState.services, vm.headerConfig); + Language.fixState(ServicesState.services, vm.toolbarConfig); resolveServices(vm.limit, 0); - Polling.start('serviceListPolling', pollUpdateServicesList, vm.pollingInterval); }; function getCardConfig() { @@ -86,7 +90,7 @@ function ComponentController($state, ServicesState, Language, ListView, Chargeba function handleSelectionChange() { vm.selectedItemsList = vm.servicesList.filter((service) => service.selected); - vm.headerConfig.filterConfig.selectedCount = vm.selectedItemsList.length; + vm.toolbarConfig.filterConfig.selectedCount = vm.selectedItemsList.length; } function isAnsibleService(service) { @@ -173,30 +177,24 @@ function ComponentController($state, ServicesState, Language, ListView, Chargeba } - function getHeaderConfig() { - var serviceFilterConfig = { - fields: getServiceFilterFields(), + function serviceFilterConfig() { + return { + fields: [], resultsCount: 0, totalCount: 0, selectedCount: 0, - appliedFilters: ServicesState.services.filterApplied ? ServicesState.services.getFilters() : [], + appliedFilters: ServicesState.services.getFilters() || [], onFilterChange: filterChange, }; + } - var serviceSortConfig = { + function serviceSortConfig() { + return { fields: getServiceSortFields(), onSortChange: sortChange, isAscending: ServicesState.services.getSort().isAscending, currentField: ServicesState.services.getSort().currentField, }; - - return { - sortConfig: serviceSortConfig, - filterConfig: serviceFilterConfig, - actionsConfig: { - actionsInclude: true, - }, - }; } function createServiceChildrenListConfig() { @@ -318,14 +316,15 @@ function ComponentController($state, ServicesState, Language, ListView, Chargeba } function getServiceFilterFields() { + const filterTags = lodash(vm.filterTags.map((tag) => (tag.name.match(/\//g).length === 3 ? tag.name : false))) + .compact() + .uniqBy() + .value(); + return [ ListView.createFilterField('name', __('Name'), __('Filter by Name'), 'text'), ListView.createFilterField('description', __('Description'), __('Filter by Description'), 'text'), - - // TODO: find a way to filter on virtual attributes - // ListView.createFilterField('chargeback_relative_cost', __('Relative Cost'), __('Filter by Relative Cost'), 'select', dollars), - // TODO: find a good way to filter on date other than string - // ListView.createFilterField('owner', __('Created'), __('Filter by Created On'), 'text'), + ListView.createFilterField('tags.name', __('Tag Category/Value'), __('Filter by Tag Category/Value'), 'select', filterTags), ]; } @@ -334,9 +333,6 @@ function ComponentController($state, ServicesState, Language, ListView, Chargeba ListView.createSortField('created_at', __('Created'), 'numeric'), ListView.createSortField('name', __('Name'), 'alpha'), ListView.createSortField('retires_on', __('Retirement Date'), 'numeric'), - - // TODO: Find a way to sort by charback cost - // ListView.createSortField('chargeback_report.used_cost_sum', __('Relative Cost'), 'alpha'), ]; } @@ -347,7 +343,7 @@ function ComponentController($state, ServicesState, Language, ListView, Chargeba function querySuccess(result) { vm.filterCount = result.subcount; - vm.headerConfig.filterConfig.resultsCount = vm.filterCount; + vm.toolbarConfig.filterConfig.resultsCount = vm.filterCount; resolve(); } @@ -360,31 +356,27 @@ function ComponentController($state, ServicesState, Language, ListView, Chargeba } function resolveServices(limit, offset, refresh) { - if (!refresh) { - vm.loading = true; - } else { - vm.loading = false; - } + Polling.stop('serviceListPolling'); + vm.loading = !refresh; vm.offset = offset; getFilterCount().then(() => { ServicesState.getServices( limit, offset, - ServicesState.services.getFilters(), - ServicesState.services.getSort().currentField, - ServicesState.services.getSort().isAscending, refresh).then(querySuccess, queryFailure); }); function querySuccess(result) { + Polling.start('serviceListPolling', pollUpdateServicesList, vm.pollingInterval); vm.loading = false; vm.services = []; var existingServices = (angular.isDefined(vm.servicesList) && refresh ? angular.copy(vm.servicesList) : []); vm.selectedItemsList = []; - vm.headerConfig.filterConfig.totalCount = result.subcount; - vm.headerConfig.filterConfig.selectedCount = 0; + vm.toolbarConfig.filterConfig.fields = getServiceFilterFields(); + vm.toolbarConfig.filterConfig.totalCount = result.subcount; + vm.toolbarConfig.filterConfig.selectedCount = 0; - angular.forEach(result.resources, function(item) { + result.resources.forEach((item) => { if (angular.isUndefined(item.service_id)) { item.disableRowExpansion = item.all_service_children.length < 1; item.power_state = PowerOperations.getPowerState(item); @@ -426,9 +418,9 @@ function ComponentController($state, ServicesState, Language, ListView, Chargeba return (powerState !== 'on' && powerState !== 'off' ? powerStates.unknown : powerStates[powerState]); } - function queryFailure(_error) { + function queryFailure(response) { vm.loading = false; - EventNotifications.error(__('There was an error loading the services.')); + EventNotifications.error(__('There was an error loading the services. ') + response.data.error.message); } } diff --git a/client/app/services/service-explorer/service-explorer.component.spec.js b/client/app/services/service-explorer/service-explorer.component.spec.js index bfe49331c..1eec32ab4 100644 --- a/client/app/services/service-explorer/service-explorer.component.spec.js +++ b/client/app/services/service-explorer/service-explorer.component.spec.js @@ -52,13 +52,13 @@ describe('Component: serviceExplorer', () => { }); it('should set toolbar', () => { - expect(ctrl.headerConfig.sortConfig.fields).to.have.lengthOf(3); - expect(ctrl.headerConfig.sortConfig.currentField).to.eql({id: 'created_at', title: 'Created', sortType: 'numeric'}); + expect(ctrl.toolbarConfig.sortConfig.fields).to.have.lengthOf(3); + expect(ctrl.toolbarConfig.sortConfig.currentField).to.eql({id: 'created_at', title: 'Created', sortType: 'numeric'}); }); it('should allow for sorting to be able to be updated', () => { const catalogSpy = sinon.spy(ServicesState.services, 'setSort'); - ctrl.headerConfig.sortConfig.onSortChange('name', true); + ctrl.toolbarConfig.sortConfig.onSortChange('name', true); expect(catalogSpy).to.have.been.called.once; }); diff --git a/client/app/services/service-explorer/service-explorer.html b/client/app/services/service-explorer/service-explorer.html index 25965ad22..5c8431bca 100644 --- a/client/app/services/service-explorer/service-explorer.html +++ b/client/app/services/service-explorer/service-explorer.html @@ -14,12 +14,12 @@ - + @@ -29,10 +29,10 @@
+ items="vm.servicesList" + menu-actions="vm.menuActions" + update-menu-action-for-item-fn="vm.updateMenuActionForItemFn" + custom-scope="vm">
@@ -70,7 +70,8 @@
- @@ -121,11 +122,11 @@
+ class="explorer-children-list" + config="vm.serviceChildrenListConfig" + items="$parent.item.all_service_children" + custom-scope="vm" + update-menu-action-for-item-fn="vm.updateMenuActionForItemFn">
@@ -179,19 +180,24 @@
- @@ -199,5 +205,6 @@
- + diff --git a/client/app/services/services-state.service.js b/client/app/services/services-state.service.js index f29c29cb9..073e4ac4f 100644 --- a/client/app/services/services-state.service.js +++ b/client/app/services/services-state.service.js @@ -66,7 +66,7 @@ export function ServicesStateFactory(ListConfiguration, CollectionsApi, RBAC) { return CollectionsApi.query('services', options); } - function getServices(limit, offset, filters, sortField, sortAscending, refresh) { + function getServices(limit, offset, refresh) { const options = { expand: 'resources', limit: limit, @@ -87,15 +87,13 @@ export function ServicesStateFactory(ListConfiguration, CollectionsApi, RBAC) { 'service_resources', 'tags', ], - filter: getQueryFilters(filters), + filter: getQueryFilters(services.getFilters()), auto_refresh: refresh, }; - if (angular.isDefined(sortField)) { - options.sort_by = services.getSort().currentField.id; - options.sort_options = services.getSort().currentField.sortType === 'alpha' ? 'ignore_case' : ''; - options.sort_order = sortAscending ? 'asc' : 'desc'; - } + options.sort_by = services.getSort().currentField.id || ''; + options.sort_options = services.getSort().currentField.sortType === 'alpha' ? 'ignore_case' : ''; + options.sort_order = services.getSort().isAscending ? 'asc' : 'desc'; return CollectionsApi.query('services', options); } diff --git a/client/app/services/services-state.service.spec.js b/client/app/services/services-state.service.spec.js index 50df5f203..4cabef46e 100644 --- a/client/app/services/services-state.service.spec.js +++ b/client/app/services/services-state.service.spec.js @@ -1,142 +1,142 @@ describe('Services-state Service', function() { -let successResponse = { - 'status':'success' -}; -let permissionsSpy; + let successResponse = { + 'status': 'success' + }; + let permissionsSpy; - describe('basic service functions - ', ()=>{ + describe('basic service functions - ', () => { beforeEach(function() { - module('app.services'); - bard.inject('ServicesState', '$http','CollectionsApi','ListConfiguration','RBAC'); - }); + module('app.services'); + bard.inject('ServicesState', '$http', 'CollectionsApi', 'ListConfiguration', 'RBAC'); + }); - it('should allow a service to be retrieved', () =>{ - const serviceId = '12345'; - const collectionsApiSpy = sinon.stub(CollectionsApi, 'get').returns(Promise.resolve(successResponse)); - ServicesState.getService(serviceId,false); - const expectedOptions = { - attributes: [ - 'name', 'guid', 'created_at', 'type', 'description', 'picture', 'picture.image_href', 'evm_owner.name', 'evm_owner.userid', - 'miq_group.description', 'all_service_children', 'aggregate_all_vm_cpus', 'aggregate_all_vm_memory', 'aggregate_all_vm_disk_count', - 'aggregate_all_vm_disk_space_allocated', 'aggregate_all_vm_disk_space_used', 'aggregate_all_vm_memory_on_disk', 'retired', - 'retirement_state', 'retirement_warn', 'retires_on', 'actions', 'custom_actions', 'provision_dialog', 'service_resources', - 'chargeback_report', 'service_template', 'parent_service', 'power_state', 'power_status', 'options', 'vms.ipaddresses', - 'vms.snapshots', 'vms.v_total_snapshots', 'vms.v_snapshot_newest_name', 'vms.v_snapshot_newest_timestamp', 'vms.v_snapshot_newest_total_size', - 'vms.supports_console?', 'vms.supports_launch_cockpit?', 'vms.max_mem_usage_absolute_average_avg_over_time_period', 'vms.hardware', - 'vms.hardware.aggregate_cpu_speed', 'vms.cpu_usagemhz_rate_average_avg_over_time_period', - ], - expand: ['vms', 'orchestration_stacks'], - auto_refresh: false, - }; - expect(collectionsApiSpy).to.have.been.calledWith('services',serviceId, expectedOptions) + it('should allow a service to be retrieved', () => { + const serviceId = '12345'; + const collectionsApiSpy = sinon.stub(CollectionsApi, 'get').returns(Promise.resolve(successResponse)); + ServicesState.getService(serviceId, false); + const expectedOptions = { + attributes: [ + 'name', 'guid', 'created_at', 'type', 'description', 'picture', 'picture.image_href', 'evm_owner.name', 'evm_owner.userid', + 'miq_group.description', 'all_service_children', 'aggregate_all_vm_cpus', 'aggregate_all_vm_memory', 'aggregate_all_vm_disk_count', + 'aggregate_all_vm_disk_space_allocated', 'aggregate_all_vm_disk_space_used', 'aggregate_all_vm_memory_on_disk', 'retired', + 'retirement_state', 'retirement_warn', 'retires_on', 'actions', 'custom_actions', 'provision_dialog', 'service_resources', + 'chargeback_report', 'service_template', 'parent_service', 'power_state', 'power_status', 'options', 'vms.ipaddresses', + 'vms.snapshots', 'vms.v_total_snapshots', 'vms.v_snapshot_newest_name', 'vms.v_snapshot_newest_timestamp', 'vms.v_snapshot_newest_total_size', + 'vms.supports_console?', 'vms.supports_launch_cockpit?', 'vms.max_mem_usage_absolute_average_avg_over_time_period', 'vms.hardware', + 'vms.hardware.aggregate_cpu_speed', 'vms.cpu_usagemhz_rate_average_avg_over_time_period', + ], + expand: ['vms', 'orchestration_stacks'], + auto_refresh: false, + }; + expect(collectionsApiSpy).to.have.been.calledWith('services', serviceId, expectedOptions) }); it('should be able to get a record count', () => { - const collectionsApiSpy = sinon.stub(CollectionsApi, 'query').returns(Promise.resolve(successResponse)); - ServicesState.getServicesMinimal(); - expect(collectionsApiSpy).to.have.been.calledWith('services', { filter: ['ancestry=null'] }); + const collectionsApiSpy = sinon.stub(CollectionsApi, 'query').returns(Promise.resolve(successResponse)); + ServicesState.getServicesMinimal(); + expect(collectionsApiSpy).to.have.been.calledWith('services', {filter: ['ancestry=null']}); }); it('should be able to get service credentials', () => { - const collectionsApiSpy = sinon.stub(CollectionsApi, 'get').returns(Promise.resolve(successResponse)); - const serviceCredential = '12345'; - ServicesState.getServiceCredential(serviceCredential); - expect(collectionsApiSpy).to.have.been.calledWith('authentications',serviceCredential, {}); + const collectionsApiSpy = sinon.stub(CollectionsApi, 'get').returns(Promise.resolve(successResponse)); + const serviceCredential = '12345'; + ServicesState.getServiceCredential(serviceCredential); + expect(collectionsApiSpy).to.have.been.calledWith('authentications', serviceCredential, {}); }); it('should be able to get service repository', () => { - const collectionsApiSpy = sinon.stub(CollectionsApi, 'get').returns(Promise.resolve(successResponse)); - const serviceRepository = '12345'; - ServicesState.getServiceRepository(serviceRepository); - expect(collectionsApiSpy).to.have.been.calledWith('configuration_script_sources',serviceRepository, {}); + const collectionsApiSpy = sinon.stub(CollectionsApi, 'get').returns(Promise.resolve(successResponse)); + const serviceRepository = '12345'; + ServicesState.getServiceRepository(serviceRepository); + expect(collectionsApiSpy).to.have.been.calledWith('configuration_script_sources', serviceRepository, {}); }); it('should be able to get ServiceJobsStdout', () => { - const collectionsApiSpy = sinon.stub(CollectionsApi, 'get').returns(Promise.resolve(successResponse)); - const serviceId = '12345'; - const stackId = '4567'; - const expectedAttributes = { attributes: ['job_plays', 'stdout'], format_attributes: 'stdout=html' }; - ServicesState.getServiceJobsStdout(serviceId, stackId); + const collectionsApiSpy = sinon.stub(CollectionsApi, 'get').returns(Promise.resolve(successResponse)); + const serviceId = '12345'; + const stackId = '4567'; + const expectedAttributes = {attributes: ['job_plays', 'stdout'], format_attributes: 'stdout=html'}; + ServicesState.getServiceJobsStdout(serviceId, stackId); - expect(collectionsApiSpy).to.have.been.calledWith('services/12345/orchestration_stacks', stackId, expectedAttributes); + expect(collectionsApiSpy).to.have.been.calledWith('services/12345/orchestration_stacks', stackId, expectedAttributes); + }); + it('should get users permissions', () => { + const expectedPermissions = { + 'edit': false, + 'delete': false, + 'reconfigure': false, + 'setOwnership': false, + 'retire': false, + 'setRetireDate': false, + 'editTags': false, + 'viewAnsible': false, + 'instanceStart': false, + 'instanceStop': false, + 'instanceSuspend': false, + 'instanceRetire': false, + 'cockpit': false, + 'console': false, + 'viewSnapshots': false, + 'vm_snapshot_add': false, + 'vm_snapshot_show_list': false, + 'ems_infra_show': false, + 'ems_cluster_show': false, + 'host_show': false, + 'resource_pool_show': false, + 'storage_show_list': false, + 'instance_show': false, + 'vm_drift': false, + 'vm_check_compliance': false + }; + const actualPermissions = ServicesState.getPermissions(); + expect(actualPermissions).to.eql(expectedPermissions); }); - it('should get users permissions', () => { - const expectedPermissions = { - 'edit': false, - 'delete': false, - 'reconfigure': false, - 'setOwnership': false, - 'retire': false, - 'setRetireDate': false, - 'editTags': false, - 'viewAnsible': false, - 'instanceStart': false, - 'instanceStop': false, - 'instanceSuspend': false, - 'instanceRetire': false, - 'cockpit': false, - 'console': false, - 'viewSnapshots': false, - 'vm_snapshot_add': false, - 'vm_snapshot_show_list': false, - 'ems_infra_show': false, - 'ems_cluster_show': false, - 'host_show': false, - 'resource_pool_show': false, - 'storage_show_list': false, - 'instance_show': false, - 'vm_drift': false, - 'vm_check_compliance': false - }; - const actualPermissions = ServicesState.getPermissions(); - expect (actualPermissions).to.eql(expectedPermissions); - }); it('should allow services to be retrieved', () => { - const collectionsApiSpy = sinon.stub(CollectionsApi, 'query').returns(Promise.resolve(successResponse)); - ServicesState.services.setSort({'id':'name', 'title':'Name','sortType':'alpha'}, 'asc'); - const nameFilter = { - id: 'name', - title: 'Name', - placeholder: 'Filter by Name', - filterType: 'text', - value: 'test', - }; - const expectedOptions = { - attributes: ['picture', 'picture.image_href', 'chargeback_report', 'evm_owner.userid', 'miq_group.description', 'v_total_vms', 'power_state', 'power_states', 'power_status', 'all_service_children', 'all_vms', 'custom_actions', 'service_resources', 'tags'], - auto_refresh: false, - expand: 'resources', - filter: ['ancestry=null', "name='%test%'"], - limit: 5, - offset: '0', - sort_by: 'name', - sort_options: 'ignore_case', - sort_order: 'asc' - }; + const collectionsApiSpy = sinon.stub(CollectionsApi, 'query').returns(Promise.resolve(successResponse)); + ServicesState.services.setSort({'id': 'name', 'title': 'Name', 'sortType': 'alpha'}, 'asc'); + const nameFilter = { + id: 'name', + title: 'Name', + placeholder: 'Filter by Name', + filterType: 'text', + value: 'test', + }; + const expectedOptions = { + attributes: ["picture", "picture.image_href", "chargeback_report", "evm_owner.userid", "miq_group.description", "v_total_vms", "power_state", "power_states", "power_status", "all_service_children", "all_vms", "custom_actions", "service_resources", "tags"], + auto_refresh: false, + expand: "resources", + filter: ["ancestry=null"], + limit: 5, + offset: "0", + sort_by: "name", + sort_options: "ignore_case", + sort_order: "asc" + }; - ServicesState.getServices(5, 0,[nameFilter],'name',true, false ); - expect(collectionsApiSpy).to.have.been.calledWith('services',expectedOptions); + ServicesState.getServices(5, 0, false); + expect(collectionsApiSpy).to.have.been.calledWith('services', expectedOptions); }); - }); - describe('Permission based functions - ', () => { + }); + describe('Permission based functions - ', () => { beforeEach(function() { - module('app.services'); - bard.inject('RBAC'); - permissionsSpy = sinon.stub(RBAC,'hasAny').returns(true); + module('app.services'); + bard.inject('RBAC'); + permissionsSpy = sinon.stub(RBAC, 'hasAny').returns(true); - bard.inject('ServicesState', '$http','CollectionsApi','ListConfiguration'); - }); + bard.inject('ServicesState', '$http', 'CollectionsApi', 'ListConfiguration'); + }); it('should get getLifeCycleCustomDropdown', () => { - const dropDown = ServicesState.getLifeCycleCustomDropdown({},{}); - expect(dropDown.actions.length).to.eq(2) + const dropDown = ServicesState.getLifeCycleCustomDropdown({}, {}); + expect(dropDown.actions.length).to.eq(2) }); it('should get getPolicyCustomDropdown', () => { - const dropDown = ServicesState.getPolicyCustomDropdown({}); - expect(dropDown.title).to.eq('Policy'); - expect(dropDown.actions.length).to.eq(1); + const dropDown = ServicesState.getPolicyCustomDropdown({}); + expect(dropDown.title).to.eq('Policy'); + expect(dropDown.actions.length).to.eq(1); }); it('should get getConfigurationCustomDropdown', () => { - const dropDown = ServicesState.getConfigurationCustomDropdown({},{},{}); - expect(dropDown.title).to.eq('Configuration'); - expect(dropDown.actions.length).to.eq(3); + const dropDown = ServicesState.getConfigurationCustomDropdown({}, {}, {}); + expect(dropDown.title).to.eq('Configuration'); + expect(dropDown.actions.length).to.eq(3); }); - }); + }); }) diff --git a/client/assets/sass/_overrides.sass b/client/assets/sass/_overrides.sass index 5b21227db..8da6d1252 100644 --- a/client/assets/sass/_overrides.sass +++ b/client/assets/sass/_overrides.sass @@ -10,7 +10,6 @@ button margin-bottom: 4px - // This should be in patternfly .dropdown-menu-appended-to-body &::before, @@ -43,10 +42,19 @@ .form-group margin-bottom: 0 !important // sass-lint:disable-line no-important + // pf-toolbar-dropdown goes crazy when given sufficent cause to, this keeps it inline + .toolbar-apf-filter + .dropdown-menu + max-height: 78em + max-width: 325px + overflow-y: scroll + + li + overflow-x: scroll + // Override patternfly-timeline default icon size .timeline-pf-drop font-size: 20px !important // sass-lint:disable-line no-important &:hover font-size: 30px !important // sass-lint:disable-line no-important -