diff --git a/app/assets/javascripts/components/widget-chart.js b/app/assets/javascripts/components/widget-chart.js index 0e0b07cbd8d..4b022a7b46f 100644 --- a/app/assets/javascripts/components/widget-chart.js +++ b/app/assets/javascripts/components/widget-chart.js @@ -1,29 +1,11 @@ ManageIQ.angular.app.component('widgetChart', { bindings: { widgetId: '@', + widgetModel: '<', }, controllerAs: 'vm', - controller: ['$http', 'miqService', '$sce', function($http, miqService, $sce) { - var vm = this; - vm.widgetChartModel = {}; - - this.$onInit = function() { - $http.get('/dashboard/widget_chart_data/' + vm.widgetId) - .then(vm.getData) - .catch(miqService.handleFailure); - vm.div_id = "dd_w" + vm.widgetId + "_box"; - }; - - vm.getData = function(response) { - vm.widgetChartModel.state = response.data.state; - if (response.data.content !== null) { - vm.widgetChartModel.content = $sce.trustAsHtml(response.data.content); - } - }; - }], template: [ - '
', - '
', + '
', '
', ' ', ' ', @@ -32,7 +14,7 @@ ManageIQ.angular.app.component('widgetChart', { __('No chart data found.'), ' ', '
', - '
', + '
', '
', ' ', ' ', @@ -44,10 +26,9 @@ ManageIQ.angular.app.component('widgetChart', { __('Invalid chart data. Try regenerating the widgets.'), '

', '
', - '
', - '
', + '
', + '
', '
', '
', - '
', ].join("\n"), }); diff --git a/app/assets/javascripts/components/widget-empty.js b/app/assets/javascripts/components/widget-empty.js new file mode 100644 index 00000000000..1d85802315e --- /dev/null +++ b/app/assets/javascripts/components/widget-empty.js @@ -0,0 +1,15 @@ +ManageIQ.angular.app.component('widgetEmpty', { + template: [ + '
', + '
', + ' ', + '
', + '

', + __('No data found.'), + '

', + '

', + __('If this widget is new or has just been added to your dashboard, the data is being generated and should be available soon.'), + '

', + '
', + ].join("\n"), +}); diff --git a/app/assets/javascripts/components/widget-error.js b/app/assets/javascripts/components/widget-error.js new file mode 100644 index 00000000000..5ca595e6710 --- /dev/null +++ b/app/assets/javascripts/components/widget-error.js @@ -0,0 +1,12 @@ +ManageIQ.angular.app.component('widgetError', { + template: [ + '
', + '
', + ' ', + '
', + '

', + __('Error: Request for data failed.'), + '

', + '
', + ].join("\n"), +}); diff --git a/app/assets/javascripts/components/widget-footer.js b/app/assets/javascripts/components/widget-footer.js new file mode 100644 index 00000000000..f8ee56ae00f --- /dev/null +++ b/app/assets/javascripts/components/widget-footer.js @@ -0,0 +1,16 @@ +ManageIQ.angular.app.component('widgetFooter', { + bindings: { + widgetLastRun: '@', + widgetNextRun: '@', + }, + controllerAs: 'vm', + template: [ + '', + ].join("\n"), +}); diff --git a/app/assets/javascripts/components/widget-menu.js b/app/assets/javascripts/components/widget-menu.js index 38d1bc36eaa..d2a466b7cf8 100644 --- a/app/assets/javascripts/components/widget-menu.js +++ b/app/assets/javascripts/components/widget-menu.js @@ -1,39 +1,30 @@ ManageIQ.angular.app.component('widgetMenu', { bindings: { widgetId: '@', + widgetModel: '<', }, controllerAs: 'vm', - controller: ['$http', 'miqService', function($http, miqService) { + controller: function() { var vm = this; - vm.widgetMenuModel = {shortcuts: []}; vm.shortcutsMissing = function() { - return vm.widgetMenuModel.shortcuts.length === 0; + return vm.widgetModel.shortcuts.length === 0; }; - - this.$onInit = function() { - $http.get('/dashboard/widget_menu_data/' + vm.widgetId) - .then(function(response) { vm.widgetMenuModel = response.data; }) - .catch(miqService.handleFailure); - vm.div_id = 'dd_w' + vm.widgetId + '_box'; - }; - }], + }, template: [ - '
', - ' ', - ' ', - '
', + '
', + ' ', + '
', __('No shortcuts are authorized for this user, contact your Administrator'), - '
', - ' ', - ' ', + ' ', - ' ', - ' ', - '
', - ' ', + ' ', + '
', + ' ', '{{shortcut.description}}', - ' ', - '
', - '
', + ' ', + ' ', + ' ', + ' ', + '', ].join("\n"), }); diff --git a/app/assets/javascripts/components/widget-report.js b/app/assets/javascripts/components/widget-report.js index 0cb9874be87..94152440d91 100644 --- a/app/assets/javascripts/components/widget-report.js +++ b/app/assets/javascripts/components/widget-report.js @@ -1,39 +1,28 @@ ManageIQ.angular.app.component('widgetReport', { bindings: { widgetId: '@', + widgetModel: '<', }, controllerAs: 'vm', - controller: ['$http', 'miqService', '$sce', function($http, miqService, $sce) { + controller: function() { var vm = this; - vm.widgetReportModel = {}; - - this.$onInit = function() { - $http.get('/dashboard/widget_report_data/' + vm.widgetId) - .then(function(response) { vm.widgetReportModel.content = $sce.trustAsHtml(response.data.content);}) - .catch(miqService.handleFailure); - vm.div_id = "dd_w" + vm.widgetId + "_box"; - }; - vm.contentPresent = function() { - return vm.widgetReportModel.content !== undefined; + return vm.widgetModel && vm.widgetModel.content !== undefined; }; - }], + }, template: [ - '
', - '
', - '
', - ' ', - ' ', - '

', + '
', + '
', + ' ', + ' ', + '

', __('No report data found.'), - '

', - '
', + '

', '
', - '
', - '
', - '
', + '
', + '
', + '
', '
', '
', - ].join("\n"), }); diff --git a/app/assets/javascripts/components/widget-rss.js b/app/assets/javascripts/components/widget-rss.js index a5cb45cdf39..cd6dd6e3acb 100644 --- a/app/assets/javascripts/components/widget-rss.js +++ b/app/assets/javascripts/components/widget-rss.js @@ -1,37 +1,27 @@ ManageIQ.angular.app.component('widgetRss', { bindings: { widgetId: '@', + widgetModel: '<', }, controllerAs: 'vm', - controller: ['$http', 'miqService', '$sce', function($http, miqService, $sce) { + controller: function() { var vm = this; - vm.widgetRssModel = {}; - - this.$onInit = function() { - $http.get('/dashboard/widget_rss_data/' + vm.widgetId) - .then(function(response) { vm.widgetRssModel.content = $sce.trustAsHtml(response.data.content); }) - .catch(miqService.handleFailure); - vm.div_id = "dd_w" + vm.widgetId + "_box"; - }; - vm.contentPresent = function() { - return vm.widgetRssModel.content !== undefined; + return vm.widgetModel && vm.widgetModel.content !== undefined; }; - }], + }, template: [ - '
', - '
', - '
', - ' ', - ' ', - '

', + '
', + '
', + ' ', + ' ', + '

', __('No RSS Feed data found'), - '

', - '
', + '

', '
', - '
', - '
', - '
', + '
', + '
', + '
', '
', '
', ].join("\n"), diff --git a/app/assets/javascripts/components/widget-spinner.js b/app/assets/javascripts/components/widget-spinner.js new file mode 100644 index 00000000000..f21cd7d228f --- /dev/null +++ b/app/assets/javascripts/components/widget-spinner.js @@ -0,0 +1,9 @@ +ManageIQ.angular.app.component('widgetSpinner', { + template: [ + '
', + '
', + ' ', + '
', + '
', + ].join("\n"), +}); diff --git a/app/assets/javascripts/components/widget-wrapper.js b/app/assets/javascripts/components/widget-wrapper.js new file mode 100644 index 00000000000..322406f10b0 --- /dev/null +++ b/app/assets/javascripts/components/widget-wrapper.js @@ -0,0 +1,85 @@ +ManageIQ.angular.app.component('widgetWrapper', { + bindings: { + widgetId: '@', + widgetType: '@', + widgetButtons: '@', + widgetBlank: '@', + widgetTitle: '@', + widgetLastRun: '@', + widgetNextRun: '@', + }, + controllerAs: 'vm', + controller: ['$http', 'miqService', '$sce', function($http, miqService, $sce) { + var vm = this; + + var widgetTypeUrl = { + menu: '/dashboard/widget_menu_data/', + report: '/dashboard/widget_report_data/', + chart: '/dashboard/widget_chart_data/', + rss: '/dashboard/widget_rss_data/', + }; + + var deferred = miqDeferred(); + vm.promise = deferred.promise; + + this.$onInit = function() { + vm.divId = "w_" + vm.widgetId; + vm.innerDivId = 'dd_w' + vm.widgetId + '_box'; + if (vm.widgetBlank === 'false') { + $http.get(vm.widgetUrl()) + .then(function(response) { + vm.widgetModel = response.data; + // if there's html make it passable + if (vm.widgetModel.content) { + vm.widgetModel.content = $sce.trustAsHtml(vm.widgetModel.content); + } + deferred.resolve(); + }) + .catch(function(e) { + vm.error = true; + miqService.handleFailure(e); + deferred.reject(); + }); + } + }; + + vm.widgetUrl = function() { + if (widgetTypeUrl.hasOwnProperty(vm.widgetType)) { + return [widgetTypeUrl[vm.widgetType], vm.widgetId].join('/'); + } else { + console.log('Something went wrong. There is no support for widget type of ', vm.widgetType); + } + }; + }], + template: [ + '
', + '
', + '
', + '
', + ' ', + ' ', + '

', + "{{vm.widgetTitle}}", + '

', + '
', + '
', + ' ', + ' ', + '
', + ' ', + '
', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '
', + ' ', + '
', + '
', + '
', + ].join("\n"), +}); diff --git a/app/helpers/dashboard_helper.rb b/app/helpers/dashboard_helper.rb index 149f145ccc9..7fb04ae3373 100644 --- a/app/helpers/dashboard_helper.rb +++ b/app/helpers/dashboard_helper.rb @@ -3,4 +3,12 @@ def ext_auth?(auth_option = nil) return false unless ::Settings.authentication.mode == 'httpd' auth_option ? ::Settings.authentication[auth_option] : true end + + def last_next_run(widget) + last_run_on = widget.last_run_on_for_user(current_user) + next_run_on = widget.next_run_on + last_run = last_run_on ? format_timezone(last_run_on, session[:user_tz], "widget_footer") : _('Never') + next_run = next_run_on ? format_timezone(next_run_on, session[:user_tz], "widget_footer") : _('Unknown') + [last_run, next_run] + end end diff --git a/app/javascript/spec/dashboards/mocks.js b/app/javascript/spec/dashboards/mocks.js new file mode 100644 index 00000000000..33a0f26d328 --- /dev/null +++ b/app/javascript/spec/dashboards/mocks.js @@ -0,0 +1,43 @@ +require('angular'); +require('angular-mocks'); +const module = window.module; +const inject = window.inject; + +window.ManageIQ = { + angular: { + app: angular.module('ManageIQ', []), + }, +}; +window.__ = (x) => x; + +// FIXME: app/assets/javascripts/services/ +ManageIQ.angular.app.service('miqService', function () { + this.handleFailure = () => null; +}); + +// FIXME: miq_application.js +window.miqDeferred = () => { + var deferred = {}; + + deferred.promise = new Promise(function (resolve, reject) { + deferred.resolve = resolve; + deferred.reject = reject; + }); + + return deferred; +}; + +// FIXME: don't mock PF functions +$.fn.setupVerticalNavigation = function() {}; + +require('../../../assets/javascripts/components/widget-chart'); +require('../../../assets/javascripts/components/widget-empty'); +require('../../../assets/javascripts/components/widget-error'); +require('../../../assets/javascripts/components/widget-footer'); +require('../../../assets/javascripts/components/widget-menu'); +require('../../../assets/javascripts/components/widget-report'); +require('../../../assets/javascripts/components/widget-rss'); +require('../../../assets/javascripts/components/widget-spinner'); +require('../../../assets/javascripts/components/widget-wrapper'); + +export { module, inject }; diff --git a/app/javascript/spec/dashboards/widget-empty.test.js b/app/javascript/spec/dashboards/widget-empty.test.js new file mode 100644 index 00000000000..ef0e372e670 --- /dev/null +++ b/app/javascript/spec/dashboards/widget-empty.test.js @@ -0,0 +1,36 @@ +import { module, inject } from './mocks'; + +describe('widget-empty', function () { + let $scope; + let element; + let $compile; + + beforeEach(module('ManageIQ')); + beforeEach(inject(function (_$compile_, $rootScope, $templateCache) { + // FIXME: templateRequest is using $http to get the template, but angular-mocks prevents it + $templateCache.put('/static/dropdown-menu.html.haml', '
'); + $scope = $rootScope; + $scope.miqButtonClicked = function () {}; + $scope.validForm = true; + $compile = _$compile_; + })); + + it('is rendered in widget-wrapper if widget-blank is set to true', function (done) { + element = angular.element( + '
' + + '' + + '
' + ); + element = $compile(element)($scope); + + $scope.$digest(); + + setTimeout(function () { + const widget = element.find("widget-empty"); + expect(widget.length).toBe(1); + + done(); + }); + }); +} +); diff --git a/app/javascript/spec/dashboards/widget-wrapper.test.js b/app/javascript/spec/dashboards/widget-wrapper.test.js new file mode 100644 index 00000000000..86982f24940 --- /dev/null +++ b/app/javascript/spec/dashboards/widget-wrapper.test.js @@ -0,0 +1,59 @@ +import { module, inject } from './mocks'; + +describe('widget-wrapper', function () { + let $scope; + let element; + let $compile; + const widgetTypes = ['chart', 'menu', 'report', 'rss']; + + beforeEach(module('ManageIQ')); + + beforeEach(inject(function (_$compile_, $rootScope, $templateCache, $http) { + // FIXME: templateRequest is using $http to get the template, but angular-mocks prevents it + $templateCache.put('/static/dropdown-menu.html.haml', '
'); + + $scope = $rootScope; + + $compile = _$compile_; + spyOn($http, 'get').and.callFake(function (url) { + if (url === '/static/dropdown-menu.html.haml') { + return Promise.resolve({ + data: "
", + status: 200, + statusText: 'OK', + }); + } else { + return Promise.resolve({ + data: { + content: "
", + minimized: false, + shortcuts: [], + }, + status: 200, + statusText: 'OK', + }); + } + }); + })); + + widgetTypes.forEach(function (widget) { + it(`renders widget-${widget} when widget-type is ${widget}`, function (done) { + element = angular.element( + '
' + + ' ' + + '
' + ); + element = $compile(element)($scope); + $scope.$digest(); + + const $ctrl = element.find('widget-wrapper').find('div').scope().vm; + $ctrl.promise.catch(function () {}).then(function () { + $scope.$digest(); + + const widgetElement = element.find("widget-".concat(widget)); + expect(widgetElement.length).toBe(1); + done(); + }); + }); + }); +}); diff --git a/app/views/dashboard/_widget.html.haml b/app/views/dashboard/_widget.html.haml index 2dc16d3f8c1..0921e5d80ac 100644 --- a/app/views/dashboard/_widget.html.haml +++ b/app/views/dashboard/_widget.html.haml @@ -1,25 +1,13 @@ --# Parameters: --# widget MiqWidget object -%div{:id => "w_#{presenter.widget.id}"} - .card-pf.card-pf-view - .card-pf-body - .card-pf-heading-kebab - %dropdown-menu{"widget-id" => presenter.widget.id, "buttons-data" => presenter.widget_buttons} - %h2.card-pf-title.sortable-handle{:style => "cursor:move"} - = h(presenter.widget.title) - - - if presenter.widget.content_type == "menu" - %widget-menu{:id => presenter.widget.id, "widget-id" => presenter.widget.id} - - elsif presenter.widget.contents_for_user(current_user).blank? - = render :partial => 'widget_blank', :locals => {:widget => presenter.widget} - - elsif presenter.widget.content_type == "report" - %widget-report{:id => presenter.widget.id, "widget-id" => presenter.widget.id} - - elsif presenter.widget.content_type == "chart" - %widget-chart{:id => presenter.widget.id, "widget-id" => presenter.widget.id} - - elsif presenter.widget.content_type == "rss" - %widget-rss{:id => presenter.widget.id, "widget-id" => presenter.widget.id} - - unless presenter.widget.content_type == "menu" - = render :partial => 'widget_footer', :locals => {:widget => presenter.widget} +%div{:id => "ww_#{presenter.widget.id}"} + - last_run, next_run = last_next_run(presenter.widget) + - widget_blank = presenter.widget.content_type == 'menu' ? false : presenter.widget.contents_for_user(current_user).blank? + %widget-wrapper{"widget-id" => presenter.widget.id, + "widget-type" => presenter.widget.content_type, + "widget-buttons" => presenter.widget_buttons, + "widget-blank" => widget_blank, + "widget-last-run" => last_run, + "widget-next-run" => next_run, + "widget-title" => presenter.widget.title} :javascript - miq_bootstrap("#w_#{presenter.widget.id}"); + miq_bootstrap("#ww_#{presenter.widget.id}"); diff --git a/app/views/dashboard/_widget_blank.html.haml b/app/views/dashboard/_widget_blank.html.haml deleted file mode 100644 index 2ef84b0ea97..00000000000 --- a/app/views/dashboard/_widget_blank.html.haml +++ /dev/null @@ -1,10 +0,0 @@ --# - Parameters: - widget -- MiqWidget object -.mc{:id => "dd_w#{widget.id}_box", -:style => "#{@sb[:dashboards][@sb[:active_db]][:minimized].include?(widget.id) ? 'display: none;' : ''}"} - .blank-slate-pf{:style => "padding: 10px"} - .blank-slate-pf-icon - %i.fa.fa-cog - %h1= _('No data found.') - %p= _('If this widget is new or has just been added to your dashboard, the data is being generated and should be available soon.') diff --git a/app/views/dashboard/_widget_footer.html.haml b/app/views/dashboard/_widget_footer.html.haml deleted file mode 100644 index df198451858..00000000000 --- a/app/views/dashboard/_widget_footer.html.haml +++ /dev/null @@ -1,17 +0,0 @@ --# - Parameters: - widget -- MiqWidget object -.card-pf-footer - = _('Updated') - - last_run_on = widget.last_run_on_for_user(current_user) - - if last_run_on - = format_timezone(last_run_on, session[:user_tz], "widget_footer") - - else - = _('Never') -   |   - = _('Next') - - next_run_on = widget.next_run_on - - if next_run_on - = format_timezone(next_run_on, session[:user_tz], "widget_footer") - - else - = _('Unknown') diff --git a/app/views/dashboard/_zoomed_chart.html.haml b/app/views/dashboard/_zoomed_chart.html.haml index 7c3c657e8f7..ea4e8a92df2 100644 --- a/app/views/dashboard/_zoomed_chart.html.haml +++ b/app/views/dashboard/_zoomed_chart.html.haml @@ -1,6 +1,7 @@ -# Parameters: widget -- MiqWidget object +- last_run, next_run = last_next_run(widget) #zoomed_chart_div .card-pf .card-pf-heading @@ -11,4 +12,6 @@ %i.fa.fa-close.pull-right .card-pf-body = chart_remote('dashboard', :id => 'my_chart', :zoomed => true) - = render :partial => 'widget_footer', :locals => {:widget => widget} + %widget-footer{:id => "zoomed_chart_footer_#{widget.id}", 'widget-last-run' => last_run, 'widget-next-run' => next_run} +:javascript + miq_bootstrap("#zoomed_chart_footer_#{widget.id}"); diff --git a/app/views/layouts/_header.html.haml b/app/views/layouts/_header.html.haml index ac4878edc09..c89ff7de2b8 100644 --- a/app/views/layouts/_header.html.haml +++ b/app/views/layouts/_header.html.haml @@ -31,12 +31,11 @@ = _(menu_item.name) = render :partial => "layouts/user_options" - - = render :partial => "layouts/spinner" - = render :partial => "layouts/lightbox_panel" = render :partial => "layouts/notifications_drawer" = render :partial => "layouts/toast_list" += render :partial => "layouts/spinner" += render :partial => "layouts/lightbox_panel" :javascript miq_bootstrap('#notification-app', 'miq.notifications');