diff --git a/app/assets/javascripts/controllers/toolbar_controller.js b/app/assets/javascripts/controllers/toolbar_controller.js deleted file mode 100644 index 9d26d0cd541..00000000000 --- a/app/assets/javascripts/controllers/toolbar_controller.js +++ /dev/null @@ -1,214 +0,0 @@ -(function() { - function isButton(item) { - return item.type === 'button'; - } - - function isButtonTwoState(item) { - return item.type === 'buttonTwoState' && item.id.indexOf('view') === -1; - } - - /** - * Private method for subscribing to rxSubject. - * For success functuon @see ToolbarController#onRowSelect() - * @returns {undefined} - */ - function subscribeToSubject() { - listenToRx(function(event) { - if (event.eventType === 'updateToolbarCount') { - this.MiQToolbarSettingsService.setCount(event.countSelected); - } else if (event.rowSelect) { - this.onRowSelect(event.rowSelect); - } else if (event.redrawToolbar) { - this.onUpdateToolbar(event.redrawToolbar); - } else if (event.update) { - this.onUpdateItem(event); - } else if (typeof event.setCount !== 'undefined') { - this.onSetCount(event.setCount); - } - - // sync changes - if (!this.$scope.$$phase) { - this.$scope.$digest(); - } - }.bind(this), - function(err) { - console.error('Angular RxJs Error: ', err); - }, - function() { - console.debug('Angular RxJs subject completed, no more events to catch.'); - }); - } - - /** - * Private method for setting rootPoint of MiQEndpointsService. - * @param {Object} MiQEndpointsService service responsible for endpoits. - * @returns {undefined} - */ - function initEndpoints(MiQEndpointsService) { - var urlPrefix = '/' + location.pathname.split('/')[1]; - MiQEndpointsService.rootPoint = urlPrefix; - } - - /** - * Constructor of angular's miqToolbarController. - * @param {Object} MiQToolbarSettingsService toolbarSettings service from ui-components. - * @param {Object} MiQEndpointsService endpoits service from ui-components. - * @param {Object} $scope service for managing $scope (for apply and digest reasons). - * @param {Object} $location service for managing browser's location. - * this contructor will assign all params to `this`, it will init endpoits, set if toolbar is used on list page. - * @returns {undefined} - */ - var ToolbarController = function(MiQToolbarSettingsService, MiQEndpointsService, $scope, $location) { - this.MiQToolbarSettingsService = MiQToolbarSettingsService; - this.MiQEndpointsService = MiQEndpointsService; - this.$scope = $scope; - this.$location = $location; - initEndpoints(this.MiQEndpointsService); - this.isList = location.pathname.includes('show_list'); - }; - - /** - * Public method which is executed after row in gtl is selected. - * @param {Object} data selected row - * @returns {undefined} - */ - ToolbarController.prototype.onRowSelect = function(data) { - this.MiQToolbarSettingsService.checkboxClicked(data.checked); - }; - - /** - * Public method for setting up url of data views, based on last path param (e.g. /show_list). - * @returns {undefined} - */ - ToolbarController.prototype.defaultViewUrl = function() { - this.dataViews.forEach(function(item) { - if (item.url === '') { - var lastSlash = location.pathname.lastIndexOf('/'); - item.url = (lastSlash !== -1) ? location.pathname.substring(lastSlash) : ''; - } - }); - }; - - /** - * Method which will retrieves toolbar settings from server. - * @see MiQToolbarSettingsService#getSettings for more info. - * Settings is called with this.isList and $location search object with value of `type`. - * No need to worry about multiple search params and no complicated function for parsing is needed. - * @param {function} getData callbalc for retireving toolbar data - * @returns {undefined} - */ - ToolbarController.prototype.fetchData = function(getData) { - return this.MiQToolbarSettingsService - .getSettings(getData) - .then(function(toolbarItems) { - this.toolbarItems = toolbarItems.items; - this.dataViews = toolbarItems.dataViews; - }.bind(this)); - }; - - ToolbarController.prototype.onSetCount = function(count) { - this.MiQToolbarSettingsService.setCount(count); - if (!this.$scope.$$phase) { - this.$scope.$digest(); - } - }; - - ToolbarController.prototype.setClickHandler = function() { - _.chain(this.toolbarItems) - .flatten() - .map(function(item) { - return (item && item.hasOwnProperty('items')) ? item.items : item; - }) - .flatten() - .filter(function(item) { - return item.type && - (isButton(item) || isButtonTwoState(item)); - }) - .each(function(item) { - item.eventFunction = function($event) { - // clicking on disabled or hidden things shouldn't do anything - if (item.hidden === true || item.enabled === false) { - return; - } - - sendDataWithRx({toolbarEvent: 'itemClicked'}); - Promise.resolve(miqToolbarOnClick.bind($event.delegateTarget)($event)).then(function(data) { - sendDataWithRx({type: 'TOOLBAR_CLICK_FINISH', payload: data}); - }); - }; - }) - .value(); - }; - - /** - * Public method for changing view over data. - * @param {Object} item clicked view object - * @param {Object} $event angular synthetic mouse event - * @returns {undefined} - */ - ToolbarController.prototype.onViewClick = function(item, $event) { - if (item.url.indexOf('/') === 0) { - var delimiter = (item.url === '/') ? '' : '/'; - var tail = (ManageIQ.record.recordId) ? delimiter + ManageIQ.record.recordId : ''; - - location.replace('/' + ManageIQ.controller + item.url + tail + item.url_parms); - } else { - miqToolbarOnClick.bind($event.delegateTarget)($event); - } - }; - - ToolbarController.prototype.initObject = function(toolbarString) { - subscribeToSubject.bind(this)(); - this.updateToolbar(JSON.parse(toolbarString)); - }; - - ToolbarController.prototype.onUpdateToolbar = function(toolbarObject) { - this.updateToolbar(toolbarObject); - }; - - ToolbarController.prototype.onUpdateItem = function(updateData) { - var toolbarItem = _.find(_.flatten(this.toolbarItems), {id: updateData.update}); - if (toolbarItem && toolbarItem.hasOwnProperty(updateData.type)) { - toolbarItem[updateData.type] = updateData.value; - } - }; - - ToolbarController.prototype.updateToolbar = function(toolbarObject) { - var toolbarItems = this.MiQToolbarSettingsService.generateToolbarObject(toolbarObject); - this.toolbarItems = toolbarItems.items; - this.dataViews = toolbarItems.dataViews; - this.defaultViewUrl(); - this.setClickHandler(); - this.showOrHide(); - }; - - ToolbarController.prototype.anyToolbarVisible = function() { - if (!this.toolbarItems || !this.toolbarItems.length) { - return false; - } - - var nonEmpty = this.toolbarItems.filter(function(ary) { - if (!ary || !ary.length) { - return false; - } - - return _.some(ary, function(item) { - return !item.hidden; - }); - }); - - return !!nonEmpty.length; - }; - - ToolbarController.prototype.showOrHide = function() { - if (this.anyToolbarVisible()) { - $('#toolbar').show(); - } else { - $('#toolbar').hide(); - } - }; - - ToolbarController.$inject = ['MiQToolbarSettingsService', 'MiQEndpointsService', '$scope', '$location']; - - angular.module('ManageIQ').controller('miqToolbarController', ToolbarController); -})(); diff --git a/app/assets/javascripts/miq_application.js b/app/assets/javascripts/miq_application.js index 60bd1ed7e03..067c1a212fb 100644 --- a/app/assets/javascripts/miq_application.js +++ b/app/assets/javascripts/miq_application.js @@ -20,11 +20,6 @@ function miqOnLoad() { miqBuildCalendar(); - // Init the toolbars - if (typeof miqInitToolbars === 'function') { - miqInitToolbars(); - } - // Initialize the dashboard widget pulldown if (miqDomElementExists('widget_select_div')) { miqInitWidgetPulldown(); @@ -61,12 +56,6 @@ function miqPrepRightCellForm(tree) { miqDimDiv(tree + '_div', true); } -// Initialize the widget pulldown on the dashboard -function miqInitWidgetPulldown() { - $('#dashboard_dropdown button:not(.dropdown-toggle), #toolbar ul.dropdown-menu > li > a').off('click'); - $('#dashboard_dropdown button:not(.dropdown-toggle), #toolbar ul.dropdown-menu > li > a').on('click', miqWidgetToolbarClick); -} - function miqCalendarDateConversion(server_offset) { return moment().utcOffset(Number(server_offset) / 60).toDate(); } @@ -1316,186 +1305,6 @@ function miqAccordionSwap(_collapse, expand) { ManageIQ.noCollapseEvent = false; } -// This function is called in miqOnLoad -function miqInitToolbars() { - $('#toolbar:not(.miq-toolbar-menu) button:not(.dropdown-toggle), ' + - '#toolbar:not(.miq-toolbar-menu) ul.dropdown-menu > li > a, ' + - '#toolbar:not(.miq-toolbar-menu) .toolbar-pf-view-selector > ul.list-inline > li > a' - ).off('click'); - $('#toolbar:not(.miq-toolbar-menu) button:not(.dropdown-toggle), ' + - '#toolbar:not(.miq-toolbar-menu) ul.dropdown-menu > li > a, ' + - '#toolbar:not(.miq-toolbar-menu) .toolbar-pf-view-selector > ul.list-inline > li > a' - ).click(function() { - miqToolbarOnClick.bind(this)(); - return false; - }); -} - -// Function to run transactions when toolbar button is clicked -function miqToolbarOnClick(_e) { - var tb_url; - var button = $(this); - var popup = false; - - // If it's a dropdown, collapse the parent container - var parent = button.parents('div.btn-group.dropdown.open'); - parent.removeClass('open'); - parent.children('button.dropdown-toggle').attr('aria-expanded', 'false'); - - if (button.hasClass('disabled') || button.parent().hasClass('disabled')) { - return; - } - - if (button.parents('#dashboard_dropdown').length > 0) { - return; - } - - if (button.data('confirm-tb') && !button.data('popup')) { - if (!confirm(button.data('confirm-tb'))) { - return; - } - } - - if (button.data('popup')) { - if (!button.data('confirm-tb') || confirm(button.data('confirm-tb'))) { - // popup windows are only supported for urls starting with '/' (non-ajax) - popup = true; - } - } - - if (button.data('url')) { - // See if a url is defined - if (button.data('url').indexOf('/') === 0) { - // If url starts with / it is non-ajax - tb_url = '/' + ManageIQ.controller + button.data('url'); - if (ManageIQ.record.recordId !== null) { - // remove last '/' if exist - tb_url = tb_url.replace(/\/$/, ''); - tb_url += '/' + ManageIQ.record.recordId; - } - if (button.data('url_parms')) { - tb_url += button.data('url_parms'); - } - if (popup) { - window.open(tb_url); - } else { - DoNav(encodeURI(tb_url)); - } - return; - } - - // An ajax url was defined - tb_url = '/' + ManageIQ.controller + '/' + button.data('url'); - if (button.data('url').indexOf('x_history') !== 0) { - // If not an explorer history button - if (ManageIQ.record.recordId !== null) { - tb_url += '/' + ManageIQ.record.recordId; - } - } - } else if (button.data('function')) { - // support data-function and data-function-data - var fn = new Function('return ' + button.data('function')); // eval - returns a function returning the right function - fn().call(button, button.data('functionData')); - return false; - } else { - // No url specified, run standard button ajax transaction - if (typeof button.data('explorer') !== 'undefined' && button.data('explorer')) { - // Use x_button method for explorer ajax - tb_url = '/' + ManageIQ.controller + '/x_button'; - } else { - tb_url = '/' + ManageIQ.controller + '/button'; - } - if (ManageIQ.record.recordId !== null) { - tb_url += '/' + ManageIQ.record.recordId; - } - tb_url += '?pressed='; - if (typeof button.data('pressed') === 'undefined' && button.data('click')) { - tb_url += button.data('click').split('__').pop(); - } else { - tb_url += button.data('pressed'); - } - } - - if (button.data('prompt')) { - tb_url = miqSupportCasePrompt(tb_url); - if (!tb_url) { - return false; - } - } - - // put url_parms into params var, if defined - var paramstring = getParams(button.data('url_parms'), !!button.data('send_checked')); - - // TODO: - // Checking for perf_reload button to not turn off spinning Q (will be done after charts are drawn). - // Checking for Report download button to allow controller method to turn off spinner - // Need to design this feature into the toolbar button support at a later time. - var no_complete = _.includes([ - 'perf_reload', - 'vm_perf_reload', - 'download_choice__render_report_csv', - 'download_choice__render_report_pdf', - 'download_choice__render_report_txt', - 'custom_button_vmdb_choice__ab_button_simulate', - 'catalogitem_button_vmdb_choice__ab_button_simulate', - ], button.attr('name')) || button.attr('name').match(/_console$/); - - var options = { - beforeSend: true, - complete: !no_complete, - data: paramstring, - }; - - return miqJqueryRequest(tb_url, options); - - function getParams(urlParms, sendChecked) { - var params = []; - - if (urlParms && (urlParms[0] === '?')) { - params.push( urlParms.slice(1) ); - } - - // FIXME - don't depend on length - // (but then params[:miq_grid_checks] || params[:id] does the wrong thing) - if (sendChecked && ManageIQ.gridChecks.length) { - params.push('miq_grid_checks=' + ManageIQ.gridChecks.join(',')); - } - - if (urlParms && urlParms.match('_div$')) { - params.push(miqSerializeForm(urlParms)); - } - - return _.filter(params).join('&') || undefined; - } -} - -function miqSupportCasePrompt(tb_url) { - var supportCase = prompt(__('Enter Support Case:'), ''); - if (supportCase === null) { - return false; - } else if (supportCase.trim() === '') { - alert(__('Support Case must be provided to collect logs')); - return false; - } - - var url = tb_url + '&support_case=' + encodeURIComponent(supportCase); - return url; -} - -// Handle chart context menu clicks -function miqWidgetToolbarClick(_e) { - var itemId = $(this).data('click'); - if (itemId === 'reset') { - if (confirm(__("Are you sure you want to reset this Dashboard's Widgets to the defaults?"))) { - miqAjax('/dashboard/reset_widgets'); - } - } else if (itemId === 'add_widget') { - return; - } else { - miqJqueryRequest('/dashboard/widget_add?widget=' + itemId, {beforeSend: true}); - } -} - function miqInitAccordions() { var height = $('#left_div').height() - $('#toolbar').outerHeight() - $('#breadcrumbs').outerHeight(); var panel = $('#left_div .panel-heading').outerHeight(); diff --git a/app/assets/javascripts/miq_explorer.js b/app/assets/javascripts/miq_explorer.js index 4c5939fc5e3..d2bd040d076 100644 --- a/app/assets/javascripts/miq_explorer.js +++ b/app/assets/javascripts/miq_explorer.js @@ -1,5 +1,5 @@ /* global miqAccordionSwap miqAddNodeChildren miqAsyncAjax miqBuildCalendar miqButtons miqDeleteTreeCookies miqDomElementExists miqExpandParentNodes miqInitDashboardCols - * miqInitAccordions miqInitMainContent miqInitToolbars miqRemoveNodeChildren miqSparkle miqSparkleOff miqTreeActivateNode miqTreeActivateNodeSilently miqTreeFindNodeByKey miqTreeObject load_c3_charts miqGtlSetExtraClasses */ + * miqInitAccordions miqInitMainContent miqRemoveNodeChildren miqSparkle miqSparkleOff miqTreeActivateNode miqTreeActivateNodeSilently miqTreeFindNodeByKey miqTreeObject load_c3_charts miqGtlSetExtraClasses */ ManageIQ.explorer = {}; ManageIQ.explorer.updateElement = function(element, options) { @@ -296,7 +296,6 @@ ManageIQ.explorer.processReplaceRightCell = function(data) { _.forEach(data.reloadToolbars, function(content, element) { $('#' + element).html(content); }); - miqInitToolbars(); } ManageIQ.record = data.record; diff --git a/app/assets/stylesheets/main.scss b/app/assets/stylesheets/main.scss index a630091f027..06aaffa9b65 100644 --- a/app/assets/stylesheets/main.scss +++ b/app/assets/stylesheets/main.scss @@ -42,6 +42,7 @@ $login-container-details-border-color-rgba: rgba(0, 0, 0, 0.5); @import "codemirror_editor"; @import "dynamic_prefix_form_input"; @import "report-data-table"; +@import "topology-toolbar"; .login-pf #brand img { // sets size of brand.svg on login screen (upstream only) height: 38px; diff --git a/app/assets/stylesheets/topology-toolbar.scss b/app/assets/stylesheets/topology-toolbar.scss new file mode 100644 index 00000000000..7c258cc1ecd --- /dev/null +++ b/app/assets/stylesheets/topology-toolbar.scss @@ -0,0 +1,11 @@ +.topology-tb .has-clear .clear { + background: rgba(255, 255, 255, 0); + border-width: 0; + height: 25px; + line-height: 1; + padding: 0; + position: absolute; + right: 1px; + top: 1px; + width: 28px; +} diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index debfd2df7fd..0e242e064f9 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -432,7 +432,7 @@ def db_to_controller(db, action = "show") # Method to create the center toolbar XML def build_toolbar(tb_name) - _toolbar_builder.call(tb_name) + _toolbar_builder.build_toolbar(tb_name) end def _toolbar_builder diff --git a/app/helpers/application_helper/button/basic.rb b/app/helpers/application_helper/button/basic.rb index ee3976a8892..e32ec6cfb90 100644 --- a/app/helpers/application_helper/button/basic.rb +++ b/app/helpers/application_helper/button/basic.rb @@ -30,7 +30,7 @@ def localized(key, value = nil) self[key] = value if value case self[key] - when NilClass then '' + when NilClass then nil when Proc then instance_eval(&self[key]) else _(self[key]) end diff --git a/app/helpers/application_helper/toolbar/base.rb b/app/helpers/application_helper/toolbar/base.rb index aa0ea7353ec..1b099a27c98 100644 --- a/app/helpers/application_helper/toolbar/base.rb +++ b/app/helpers/application_helper/toolbar/base.rb @@ -3,8 +3,8 @@ class ApplicationHelper::Toolbar::Base extend SingleForwardable delegate %i[api_button button select twostate separator definition button_group custom_content] => :instance - def custom_content(name, args) - @definition[name] = ApplicationHelper::Toolbar::Custom.new(name, args) + def custom_content(name, props = nil) + @definition[name] = ApplicationHelper::Toolbar::Custom.new(name, props) end def button_group(name, buttons) diff --git a/app/helpers/application_helper/toolbar/catalogitem_button_center.rb b/app/helpers/application_helper/toolbar/catalogitem_button_center.rb index b26d90ad0a7..a9dec75d7b6 100644 --- a/app/helpers/application_helper/toolbar/catalogitem_button_center.rb +++ b/app/helpers/application_helper/toolbar/catalogitem_button_center.rb @@ -29,9 +29,10 @@ class ApplicationHelper::Toolbar::CatalogitemButtonCenter < ApplicationHelper::T 'fa fa-play-circle-o fa-lg', N_('Simulate using Button details'), N_('Simulate'), - :klass => ApplicationHelper::Button::CatalogItemButton, - :url => "resolve", - :url_parms => "?button=simulate"), + :keepSpinner => true, + :klass => ApplicationHelper::Button::CatalogItemButton, + :url => "resolve", + :url_parms => "?button=simulate"), ] ), ]) diff --git a/app/helpers/application_helper/toolbar/cloud/instance_operations_button_group_mixin.rb b/app/helpers/application_helper/toolbar/cloud/instance_operations_button_group_mixin.rb index 5623b265e38..e7776d2e795 100644 --- a/app/helpers/application_helper/toolbar/cloud/instance_operations_button_group_mixin.rb +++ b/app/helpers/application_helper/toolbar/cloud/instance_operations_button_group_mixin.rb @@ -112,15 +112,17 @@ def self.included(included_class) 'pficon pficon-screen fa-lg', N_('Open a web-based HTML5 console for this VM'), N_('VM Console'), - :url => "html5_console", - :klass => ApplicationHelper::Button::VmHtml5Console), + :keepSpinner => true, + :url => "html5_console", + :klass => ApplicationHelper::Button::VmHtml5Console), included_class.button( :cockpit_console, 'pficon pficon-screen fa-lg', N_('Open a new browser window with Cockpit for this VM. This requires that Cockpit is pre-configured on the VM.'), N_('Web Console'), - :url => "launch_cockpit", - :klass => ApplicationHelper::Button::CockpitConsole + :keepSpinner => true, + :url => "launch_cockpit", + :klass => ApplicationHelper::Button::CockpitConsole ), ] ), diff --git a/app/helpers/application_helper/toolbar/container_node_center.rb b/app/helpers/application_helper/toolbar/container_node_center.rb index f543e3c20c3..07c485baae2 100644 --- a/app/helpers/application_helper/toolbar/container_node_center.rb +++ b/app/helpers/application_helper/toolbar/container_node_center.rb @@ -77,8 +77,9 @@ class ApplicationHelper::Toolbar::ContainerNodeCenter < ApplicationHelper::Toolb 'pficon pficon-screen fa-lg', N_('Open a new browser window with Cockpit for this VM. This requires that Cockpit is pre-configured on the VM.'), N_('Web Console'), - :url => "launch_cockpit", - :klass => ApplicationHelper::Button::CockpitConsole + :keepSpinner => true, + :url => "launch_cockpit", + :klass => ApplicationHelper::Button::CockpitConsole ), ]) end diff --git a/app/helpers/application_helper/toolbar/custom.rb b/app/helpers/application_helper/toolbar/custom.rb index 786d18d5c81..ad4e3a5b2aa 100644 --- a/app/helpers/application_helper/toolbar/custom.rb +++ b/app/helpers/application_helper/toolbar/custom.rb @@ -1,8 +1,6 @@ -ApplicationHelper::Toolbar::Custom = Struct.new(:name, :args) do - def render(view_context) - # FIXME: assigns? locals? view_binding? instance_data? - @content = view_context.render :partial => args[:partial] +ApplicationHelper::Toolbar::Custom = Struct.new(:name, :props) do + def evaluate(view_context) + # Collect properties for the React toolbar component from controller (view_context). + props ? view_context.instance_eval(&props) : {} end - - attr_reader :content end diff --git a/app/helpers/application_helper/toolbar/custom_button_center.rb b/app/helpers/application_helper/toolbar/custom_button_center.rb index 1daeeb11ef0..6b020da822f 100644 --- a/app/helpers/application_helper/toolbar/custom_button_center.rb +++ b/app/helpers/application_helper/toolbar/custom_button_center.rb @@ -27,8 +27,9 @@ class ApplicationHelper::Toolbar::CustomButtonCenter < ApplicationHelper::Toolba 'fa fa-play-circle-o fa-lg', N_('Simulate using Button details'), N_('Simulate'), - :url => "resolve", - :url_parms => "?button=simulate"), + :keepSpinner => true, + :url => "resolve", + :url_parms => "?button=simulate"), ] ), ]) diff --git a/app/helpers/application_helper/toolbar/dashboard_center.rb b/app/helpers/application_helper/toolbar/dashboard_center.rb index b23e1cc80e0..feb03bbc8b0 100644 --- a/app/helpers/application_helper/toolbar/dashboard_center.rb +++ b/app/helpers/application_helper/toolbar/dashboard_center.rb @@ -1,3 +1,13 @@ class ApplicationHelper::Toolbar::DashboardCenter < ApplicationHelper::Toolbar::Basic - custom_content('custom', :partial => 'dashboard/dropdownbar') + custom_content( + 'dashboard', + proc do + { + :allowAdd => @widgets_menu[:allow_add], + :allowReset => @widgets_menu[:allow_reset], + :locked => !!@widgets_menu[:locked], + :items => @widgets_menu[:items], + } + end + ) end diff --git a/app/helpers/application_helper/toolbar/ems_physical_infra_center.rb b/app/helpers/application_helper/toolbar/ems_physical_infra_center.rb index 4a6dee6e7ab..87566eed4bb 100644 --- a/app/helpers/application_helper/toolbar/ems_physical_infra_center.rb +++ b/app/helpers/application_helper/toolbar/ems_physical_infra_center.rb @@ -89,10 +89,11 @@ class ApplicationHelper::Toolbar::EmsPhysicalInfraCenter < ApplicationHelper::To 'pficon pficon-screen fa-lg', N_('Open a web-based console for this provider'), N_('Management Console'), - :url => "launch_console", - :confirm => N_("Open management console for this provider"), - :klass => ApplicationHelper::Button::PhysicalInfraConsole, - :options => {:feature => :console}) + :keepSpinner => true, + :url => "launch_console", + :confirm => N_("Open management console for this provider"), + :klass => ApplicationHelper::Button::PhysicalInfraConsole, + :options => {:feature => :console}) ] ), ]) diff --git a/app/helpers/application_helper/toolbar/logs_center.rb b/app/helpers/application_helper/toolbar/logs_center.rb index eaab70528a5..11d3b53daff 100644 --- a/app/helpers/application_helper/toolbar/logs_center.rb +++ b/app/helpers/application_helper/toolbar/logs_center.rb @@ -3,9 +3,7 @@ class ApplicationHelper::Toolbar::LogsCenter < ApplicationHelper::Toolbar::Basic button( :refresh_log, 'fa fa-refresh fa-lg', - proc do - _('Refresh this page') - end, + N_('Refresh this page'), nil), button( :fetch_log, diff --git a/app/helpers/application_helper/toolbar/physical_server_center.rb b/app/helpers/application_helper/toolbar/physical_server_center.rb index c2b947eea41..2bb801bd785 100644 --- a/app/helpers/application_helper/toolbar/physical_server_center.rb +++ b/app/helpers/application_helper/toolbar/physical_server_center.rb @@ -75,7 +75,6 @@ class ApplicationHelper::Toolbar::PhysicalServerCenter < ApplicationHelper::Tool :url => "provision", :url_parms => "main_div", :enabled => true, - :onwhen => "0+", :klass => ApplicationHelper::Button::ConfiguredSystemProvision ) ] @@ -120,10 +119,11 @@ class ApplicationHelper::Toolbar::PhysicalServerCenter < ApplicationHelper::Tool 'pficon pficon-screen fa-lg', N_('Open a remote console for this Physical Server'), N_('Physical Server Console'), - :url => "console", - :method => :get, - :enabled => true, - :options => {:feature => :physical_server_remote_access} + :keepSpinner => true, + :url => "console", + :method => :get, + :enabled => true, + :options => {:feature => :physical_server_remote_access} ) ], ), diff --git a/app/helpers/application_helper/toolbar/report_view.rb b/app/helpers/application_helper/toolbar/report_view.rb index 82185a71bf4..99292de9993 100644 --- a/app/helpers/application_helper/toolbar/report_view.rb +++ b/app/helpers/application_helper/toolbar/report_view.rb @@ -46,22 +46,25 @@ class ApplicationHelper::Toolbar::ReportView < ApplicationHelper::Toolbar::Basic 'fa fa-file-text-o fa-lg', N_('Download this report in text format'), N_('Download as Text'), - :url_parms => "?render_type=txt", - :klass => ApplicationHelper::Button::RenderReport), + :keepSpinner => true, + :url_parms => "?render_type=txt", + :klass => ApplicationHelper::Button::RenderReport), button( :render_report_csv, 'fa fa-file-text-o fa-lg', N_('Download this report in CSV format'), N_('Download as CSV'), - :url_parms => "?render_type=csv", - :klass => ApplicationHelper::Button::RenderReport), + :keepSpinner => true, + :url_parms => "?render_type=csv", + :klass => ApplicationHelper::Button::RenderReport), button( :render_report_pdf, 'pficon pficon-print fa-lg', N_('Print or export this report in PDF format'), N_('Print or export as PDF'), - :klass => ApplicationHelper::Button::RenderReport, - :url_parms => "?render_type=pdf"), + :keepSpinner => true, + :klass => ApplicationHelper::Button::RenderReport, + :url_parms => "?render_type=pdf"), ] ), ]) diff --git a/app/helpers/application_helper/toolbar/topology_center.rb b/app/helpers/application_helper/toolbar/topology_center.rb index 72b0e6153ce..7975dad59a0 100644 --- a/app/helpers/application_helper/toolbar/topology_center.rb +++ b/app/helpers/application_helper/toolbar/topology_center.rb @@ -1,3 +1,3 @@ class ApplicationHelper::Toolbar::TopologyCenter < ApplicationHelper::Toolbar::Basic - custom_content('custom', :partial => 'shared/topology_header_toolbar') + custom_content('topology') end diff --git a/app/helpers/application_helper/toolbar/vm_performance.rb b/app/helpers/application_helper/toolbar/vm_performance.rb index a1d95f1085d..c2424f6e54c 100644 --- a/app/helpers/application_helper/toolbar/vm_performance.rb +++ b/app/helpers/application_helper/toolbar/vm_performance.rb @@ -12,6 +12,7 @@ class ApplicationHelper::Toolbar::VmPerformance < ApplicationHelper::Toolbar::Ba 'fa fa-repeat fa-lg', N_('Reload the charts from the most recent C&U data'), nil, - :klass => ApplicationHelper::Button::PerfRefresh), + :keepSpinner => true, + :klass => ApplicationHelper::Button::PerfRefresh), ]) end diff --git a/app/helpers/application_helper/toolbar/x_vm_center.rb b/app/helpers/application_helper/toolbar/x_vm_center.rb index 67b6e5e0af4..f4d678de680 100644 --- a/app/helpers/application_helper/toolbar/x_vm_center.rb +++ b/app/helpers/application_helper/toolbar/x_vm_center.rb @@ -267,23 +267,26 @@ class ApplicationHelper::Toolbar::XVmCenter < ApplicationHelper::Toolbar::Basic 'pficon pficon-screen fa-lg', N_('Open a web-based HTML5 console for this VM'), N_('VM Console'), - :url => "html5_console", - :klass => ApplicationHelper::Button::VmHtml5Console), + :keepSpinner => true, + :url => "html5_console", + :klass => ApplicationHelper::Button::VmHtml5Console), button( :vm_vmrc_console, 'pficon pficon-screen fa-lg', N_('Open a VMRC console for this VM. This requires that VMRC is installed and pre-configured to work in your browser.'), N_('VMRC Console'), - :url => "vmrc_console", - :confirm => N_("Opening a VMRC console requires that VMRC is installed and pre-configured to work in your browser. Are you sure?"), + :keepSpinner => true, + :url => "vmrc_console", + :confirm => N_("Opening a VMRC console requires that VMRC is installed and pre-configured to work in your browser. Are you sure?"), :klass => ApplicationHelper::Button::VmVmrcConsole), button( :cockpit_console, 'pficon pficon-screen fa-lg', N_('Open a new browser window with Cockpit for this VM. This requires that Cockpit is pre-configured on the VM.'), N_('Web Console'), - :url => "launch_cockpit", - :klass => ApplicationHelper::Button::CockpitConsole + :keepSpinner => true, + :url => "launch_cockpit", + :klass => ApplicationHelper::Button::CockpitConsole ), ] ), diff --git a/app/helpers/application_helper/toolbar/x_vm_performance.rb b/app/helpers/application_helper/toolbar/x_vm_performance.rb index 28cc23458e3..d3f3e1b0757 100644 --- a/app/helpers/application_helper/toolbar/x_vm_performance.rb +++ b/app/helpers/application_helper/toolbar/x_vm_performance.rb @@ -12,6 +12,7 @@ class ApplicationHelper::Toolbar::XVmPerformance < ApplicationHelper::Toolbar::B 'fa fa-repeat fa-lg', N_('Reload the charts from the most recent C&U data'), nil, - :klass => ApplicationHelper::Button::PerfRefresh), + :keepSpinner => true, + :klass => ApplicationHelper::Button::PerfRefresh), ]) end diff --git a/app/helpers/application_helper/toolbar_builder.rb b/app/helpers/application_helper/toolbar_builder.rb index e222e4477e9..739638d8f8b 100644 --- a/app/helpers/application_helper/toolbar_builder.rb +++ b/app/helpers/application_helper/toolbar_builder.rb @@ -3,10 +3,6 @@ class ApplicationHelper::ToolbarBuilder include RestfulControllerMixin include ApplicationHelper::Toolbar::Mixins::CustomButtonToolbarMixin - def call(toolbar_name) - build_toolbar(toolbar_name) - end - # Loads the toolbar sent in parameter `toolbar_name`, and builds the buttons # in the toolbar, unless the group of buttons is meant to be skipped. # @@ -121,12 +117,11 @@ def apply_common_props(button, input) :icon => input[:icon], :name => button[:id], :onwhen => input[:onwhen], - :pressed => input[:pressed], :send_checked => input[:send_checked], ) - button[:enabled] = input[:enabled] - %i[title text confirm enabled].each do |key| + button[:enabled] = !!input[:enabled] if input.key?(:enabled) + %i[title text confirm].each do |key| if input[key].present? button[key] = button.localized(key, input[key]) end @@ -454,9 +449,7 @@ def build_toolbar_from_class(toolbar_class, record) build_button(bgi, group_index) end when ApplicationHelper::Toolbar::Custom - rendered_html = group.render(@view_context).tr('\'', '"') - group[:args][:html] = ERB::Util.html_escape(rendered_html).html_safe - @toolbar << group + @toolbar << { :custom => true, :name => group.name, :props => group.evaluate(@view_context) } end end diff --git a/app/helpers/toolbar_helper.rb b/app/helpers/toolbar_helper.rb index 43e008d44d5..32dbd25b86b 100644 --- a/app/helpers/toolbar_helper.rb +++ b/app/helpers/toolbar_helper.rb @@ -5,6 +5,6 @@ module ToolbarHelper def toolbar_from_hash calculate_toolbars.collect do |_div_id, toolbar_name| toolbar_name ? build_toolbar(toolbar_name) : nil - end + end.compact end end diff --git a/app/javascript/components/dashboard_toolbar.jsx b/app/javascript/components/dashboard_toolbar.jsx new file mode 100644 index 00000000000..4b5a24359db --- /dev/null +++ b/app/javascript/components/dashboard_toolbar.jsx @@ -0,0 +1,94 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Dropdown, MenuItem } from 'patternfly-react'; + +const addClick = item => + window.miqJqueryRequest(`widget_add?widget=${item.id}`, { beforeSend: true, complete: true }); + +const resetClick = () => { + /* eslint no-alert: off */ + if (window.confirm(__("Are you sure you want to reset this Dashboard's Widgets to the defaults?"))) { + window.miqJqueryRequest('reset_widgets', { beforeSend: true }); + } +}; + +const resetButton = () => ( + +); + +const addMenu = (items, locked) => { + const title = locked + ? __('Cannot add a Widget, this Dashboard has been locked by the Administrator') + : __('Add a widget'); + + return ( + + + + + + { items.map(item => ( + item.type === 'separator' + ? + : ( + addClick(item)}> + +   + {item.text} + + ) + ))} + + + ); +}; + +const renderDisabled = () => ( +
+ +
+); + +const DashboardToolbar = (props) => { + const { + items, allowAdd, allowReset, locked, + } = props; + + const renderContent = () => ( + + { allowAdd && addMenu(items, locked) } + { allowReset && resetButton() } + + ); + + return ( +
+
+ { items.length === 0 ? renderDisabled() : renderContent() } +
+
+ ); +}; + +DashboardToolbar.propTypes = { + allowAdd: PropTypes.bool.isRequired, + allowReset: PropTypes.bool.isRequired, + items: PropTypes.arrayOf(PropTypes.any).isRequired, + locked: PropTypes.bool.isRequired, +}; + +export default DashboardToolbar; diff --git a/app/javascript/components/miq-toolbar.jsx b/app/javascript/components/miq-toolbar.jsx new file mode 100644 index 00000000000..425c934fea5 --- /dev/null +++ b/app/javascript/components/miq-toolbar.jsx @@ -0,0 +1,272 @@ +import React, { useEffect, useReducer } from 'react'; +import PropTypes from 'prop-types'; + +import { Toolbar } from '@manageiq/react-ui-components/dist/toolbar'; +import '@manageiq/react-ui-components/dist/toolbar.css'; + +import DashboardToolbar from './dashboard_toolbar'; +import TopologyToolbar from './topology_toolbar'; + +/* global miqJqueryRequest, miqSerializeForm */ +/* eslint no-restricted-globals: ["off", "miqJqueryRequest", "miqSerializeForm"] */ + +/* eslint no-alert: "off" */ +const miqSupportCasePrompt = (tbUrl) => { + const supportCase = prompt(__('Enter Support Case:'), ''); + if (supportCase === null) { + return false; + } + + if (supportCase.trim() === '') { + alert(__('Support Case must be provided to collect logs')); + return false; + } + + return `${tbUrl}&support_case=${encodeURIComponent(supportCase)}`; +}; + +const getParams = (urlParms, sendChecked) => { + const params = []; + + if (urlParms && (urlParms[0] === '?')) { + params.push(urlParms.slice(1)); + } + + // FIXME - don't depend on length + // (but then params[:miq_grid_checks] || params[:id] does the wrong thing) + if (sendChecked && ManageIQ.gridChecks.length) { + params.push(`miq_grid_checks=${ManageIQ.gridChecks.join(',')}`); + } + + if (urlParms && urlParms.match('_div$')) { + params.push(miqSerializeForm(urlParms)); + } + + return _.filter(params).join('&') || undefined; +}; + +// Toolbar button onClick handler for all toolbar buttons. + +const onClick = (button) => { + let buttonUrl; + + if (button.confirm && !window.confirm(button.confirm)) { + // No handling unless confirmed. + return; + } + + if (button.url) { // A few buttons have an url. + if (button.url.indexOf('/') === 0) { + // If url starts with '/' it is non-ajax + buttonUrl = ['/', ManageIQ.controller, button.url].join(''); + + if (ManageIQ.record.recordId !== null) { + // Remove last '/' if exist. Add recordId. + buttonUrl = [buttonUrl.replace(/\/$/, ''), ManageIQ.record.recordId].join('/'); + } + + if (button.url_parms) { + buttonUrl += button.url_parms; + } + + // popup windows are only supported for urls starting with '/' (non-ajax) + if (button.popup) { + window.open(buttonUrl); + } else { + DoNav(encodeURI(buttonUrl)); + } + return; + } + + // An ajax url was defined (url w/o '/') + buttonUrl = `/${ManageIQ.controller}/${button.url}`; + if (button.url.indexOf('x_history') !== 0) { + // If not an explorer history button + if (ManageIQ.record.recordId !== null) { + buttonUrl += `/${ManageIQ.record.recordId}`; + } + } + } else if (button.function) { + // Client-side buttons use 'function' and 'function-data'. + // eval - returns a function returning the right function. + /* eslint no-new-func: "off" */ + const fn = new Function(`return ${button.function}`); + fn().call(button, button['function-data']); + return; + } else { // Most of (classic) buttons. + // If no url was specified, run standard button ajax transaction. + // Use x_button method for explorer ajax. + // Add recordId if defined. + // Pass button id as 'pressed'. + + buttonUrl = [ + `/${ManageIQ.controller}/${button.explorer ? 'x_button' : 'button'}`, + ManageIQ.record.recordId !== null ? `/${ManageIQ.record.recordId}` : '', + `?pressed=${button.id.split('__').pop()}`].join(''); + } + + if (button.prompt) { + buttonUrl = miqSupportCasePrompt(buttonUrl); + if (!buttonUrl) { + return; + } + } + + // put url_parms into params var, if defined + const paramstring = getParams(button.url_parms, !!button.send_checked); + + const options = { + beforeSend: true, + complete: !button.keepSpinner, + data: paramstring, + }; + + miqJqueryRequest(buttonUrl, options); +}; + +const onViewClick = (button) => { + if (button.url && (button.url.indexOf('/') === 0)) { + const delimiter = (button.url === '/') ? '' : '/'; + const tail = (ManageIQ.record.recordId) ? delimiter + ManageIQ.record.recordId : ''; + + window.location.replace(['/', ManageIQ.controller, button.url, tail, button.url_parms].join('')); + } else { + onClick(button); + } +}; + +const onRowSelect = (isChecked, dispatch) => { + dispatch({ type: isChecked ? 'INCREMENT' : 'DECREMENT' }); +}; + +const subscribeToSubject = dispatch => ( + listenToRx( + (event) => { + if (event.eventType === 'updateToolbarCount') { + dispatch({ type: 'SET', count: event.countSelected }); + } else if (event.rowSelect) { + onRowSelect(event.rowSelect.checked, dispatch); + } else if (event.redrawToolbar) { + dispatch({ type: 'TOOLBARS', toolbars: event.redrawToolbar }); + } else if (event.update) { + // TODO: originally probably for QE + // this.onUpdateItem(event); + console.log('Toolbar onUpdateItem called.', event); + } else if (typeof event.setCount !== 'undefined') { + dispatch({ type: 'SET', count: event.setCount }); + } + }, + err => console.error('Toolbar RxJs Error: ', err), + () => console.debug('Toolbar RxJs subject completed, no more events to catch.'), + ) +); + +const separateItems = (toolbarItems) => { + const separatedArray = []; + toolbarItems.forEach((items) => { + let arrayIndex = separatedArray.push([]); + items.forEach((item) => { + if (item.type !== 'separator') { + separatedArray[arrayIndex - 1].push(item); + } else { + arrayIndex = separatedArray.push([]); + } + }); + }); + return separatedArray; +}; + +const filterViews = toolbarItems => toolbarItems + .flat() + .filter(i => i && i.id && i.id.indexOf('view_') === 0); + +const toolbarReducer = (state, action) => { + switch (action.type) { + case 'INCREMENT': + return { + ...state, + count: state.count + 1, + }; + case 'DECREMENT': + return { + ...state, + count: state.count - 1, + }; + case 'SET': + return { + ...state, + count: action.count, + }; + case 'TOOLBARS': + return { + ...state, + toolbars: action.toolbars, + }; + default: + return state; + } +}; + +const initState = { + count: 0, + toolbars: [], +}; + +/* Wrapper class for generic toolbars and special toolbars. */ +const MiqToolbar = ({ toolbars }) => { + if (!toolbars || (toolbars.length === 0)) { + return null; + } + + const { custom, name, props } = toolbars[0][0]; + if (custom) { + switch (name) { + case 'dashboard': + return ; + case 'topology': + return ; + default: + return null; + } + } + + return ; +}; + +/* Generic toolbar class for toolbars optionally connected to GTL grids + * reacting to changes in number of selected items. */ +export const MiqGenericToolbar = ({ toolbars }) => { + const [state, dispatch] = useReducer(toolbarReducer, initState); + + useEffect(() => { + // Initiall toolbars are given in props. + // Later can be changed by an RxJs event. + dispatch({ type: 'TOOLBARS', toolbars }); + + const subscription = subscribeToSubject(dispatch); + return () => subscription.unsubscribe(); + }, []); + + const groups = separateItems(state.toolbars.filter(item => !!item)); + const views = filterViews(groups); + + return ( + + ); +}; + +MiqGenericToolbar.propTypes = { + toolbars: PropTypes.arrayOf(PropTypes.any).isRequired, +}; + +MiqToolbar.propTypes = { + toolbars: PropTypes.arrayOf(PropTypes.any).isRequired, +}; + +export default MiqToolbar; diff --git a/app/javascript/components/topology_toolbar.jsx b/app/javascript/components/topology_toolbar.jsx new file mode 100644 index 00000000000..68897de0398 --- /dev/null +++ b/app/javascript/components/topology_toolbar.jsx @@ -0,0 +1,60 @@ +import React from 'react'; + +const searchClick = (e) => { + e.preventDefault(); + sendDataWithRx({ service: 'topologyService', name: 'searchNode' }); +}; + +const refreshClick = () => { + sendDataWithRx({ name: 'refreshTopology' }); +}; + +const resetSearchClick = () => { + sendDataWithRx({ service: 'topologyService', name: 'resetSearch' }); +}; + +const TopologyToolbar = () => ( +
+
+
+ +
+
+ +
+
searchClick(e)} + > +
+
+ + + +
+
+
+ +
+
+
+
+); + +TopologyToolbar.propTypes = { +}; + +export default TopologyToolbar; diff --git a/app/javascript/packs/component-definitions-common.js b/app/javascript/packs/component-definitions-common.js index 0d544815345..632f8a21d74 100644 --- a/app/javascript/packs/component-definitions-common.js +++ b/app/javascript/packs/component-definitions-common.js @@ -3,6 +3,7 @@ import React from 'react'; import '@manageiq/react-ui-components/dist/tagging.css'; import { TagGroup, TableListView, GenericGroup } from '@manageiq/react-ui-components/dist/textual_summary'; import { TagView } from '@manageiq/react-ui-components/dist/tagging'; +import { Toolbar } from '@manageiq/react-ui-components/dist/toolbar'; import Breadcrumbs from '../components/breadcrumbs'; import CatalogForm from '../components/catalog-form/catalog-form'; @@ -18,6 +19,7 @@ import GraphQLExplorer from '../graphql-explorer'; import { HierarchicalTreeView } from '../components/tree-view'; import ImportDatastoreViaGit from '../components/automate-import-export-form/import-datastore-via-git'; import MiqAboutModal from '../components/miq-about-modal'; +import MiqToolbar from '../components/miq-toolbar'; import OptimizationList from '../optimization/optimization_list'; import OpsTenantForm from '../components/ops-tenant-form/ops-tenant-form'; import OrcherstrationTemplateForm from '../components/orchestration-template/orcherstration-template-form'; @@ -55,6 +57,7 @@ ManageIQ.component.addReact('GraphQLExplorer', GraphQLExplorer); ManageIQ.component.addReact('HierarchicalTreeView', HierarchicalTreeView); ManageIQ.component.addReact('ImportDatastoreViaGit', ImportDatastoreViaGit); ManageIQ.component.addReact('MiqAboutModal', MiqAboutModal); +ManageIQ.component.addReact('MiqToolbar', MiqToolbar); ManageIQ.component.addReact('OptimizationList', OptimizationList); ManageIQ.component.addReact('OpsTenantForm', OpsTenantForm); ManageIQ.component.addReact('OrcherstrationTemplateForm', OrcherstrationTemplateForm); @@ -70,6 +73,7 @@ ManageIQ.component.addReact('TagGroup', props => ); ManageIQ.component.addReact('TagView', TagView); ManageIQ.component.addReact('TaggingWrapperConnected', TaggingWrapperConnected); ManageIQ.component.addReact('TextualSummaryWrapper', TextualSummaryWrapper); +ManageIQ.component.addReact('Toolbar', Toolbar); ManageIQ.component.addReact('VmServerRelationshipForm', VmServerRelationshipForm); ManageIQ.component.addReact('VmSnapshotFormComponent', VmSnapshotFormComponent); ManageIQ.component.addReact('WorkersForm', WorkersForm); diff --git a/app/javascript/spec/toolbar/__snapshots__/dashboard_toolbar.spec.jsx.snap b/app/javascript/spec/toolbar/__snapshots__/dashboard_toolbar.spec.jsx.snap new file mode 100644 index 00000000000..0bb8ce27407 --- /dev/null +++ b/app/javascript/spec/toolbar/__snapshots__/dashboard_toolbar.spec.jsx.snap @@ -0,0 +1,173 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders ok 1`] = ` + +
+
+ + + +
+ + + + + + + + + +
+
+
+
+ +
+
+
+`; diff --git a/app/javascript/spec/toolbar/__snapshots__/miq-generic-toolbar.spec.jsx.snap b/app/javascript/spec/toolbar/__snapshots__/miq-generic-toolbar.spec.jsx.snap new file mode 100644 index 00000000000..36279b29934 --- /dev/null +++ b/app/javascript/spec/toolbar/__snapshots__/miq-generic-toolbar.spec.jsx.snap @@ -0,0 +1,100 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders ok 1`] = ` + + +
+ + + + + + + + +
+ +
+ + +`; diff --git a/app/javascript/spec/toolbar/__snapshots__/topology_toolbar.spec.jsx.snap b/app/javascript/spec/toolbar/__snapshots__/topology_toolbar.spec.jsx.snap new file mode 100644 index 00000000000..91516bab003 --- /dev/null +++ b/app/javascript/spec/toolbar/__snapshots__/topology_toolbar.spec.jsx.snap @@ -0,0 +1,101 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders ok 1`] = ` + +
+
+
+ +
+
+ +
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+`; diff --git a/app/javascript/spec/toolbar/dashboard_toolbar.spec.jsx b/app/javascript/spec/toolbar/dashboard_toolbar.spec.jsx new file mode 100644 index 00000000000..9f775d7d2fb --- /dev/null +++ b/app/javascript/spec/toolbar/dashboard_toolbar.spec.jsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import toJson from 'enzyme-to-json'; + +import DashboardToolbar from '../../components/dashboard_toolbar'; + +const dashboardProps = { + allowAdd: true, + allowReset: true, + locked: false, + items: [ + { + id: 31, + type: "button", + text: "add", + image: "fa fa-pie-chart fa-lg", + title: "Add this Chart Widget" + }, + ], +} + +describe('', () => { + it('renders ok', () => { + const t = mount(); + expect(toJson(t)).toMatchSnapshot(); + }); +}); diff --git a/app/javascript/spec/toolbar/miq-generic-toolbar.spec.jsx b/app/javascript/spec/toolbar/miq-generic-toolbar.spec.jsx new file mode 100644 index 00000000000..3ddbb7ddaf4 --- /dev/null +++ b/app/javascript/spec/toolbar/miq-generic-toolbar.spec.jsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import toJson from 'enzyme-to-json'; + +import { MiqGenericToolbar } from '../../components/miq-toolbar'; + +const genericTbProps = { + toolbars: [ + [ + { + id: 'summary_reload', + type: 'button', + icon: 'fa fa-refresh fa-lg', + name: 'summary_reload', + title: 'Refresh this page', + }, + ], + ], +}; + +describe('', () => { + it('renders ok', () => { + const t = mount(); + expect(toJson(t)).toMatchSnapshot(); + }); +}); diff --git a/app/javascript/spec/toolbar/miq-toolbar.spec.jsx b/app/javascript/spec/toolbar/miq-toolbar.spec.jsx new file mode 100644 index 00000000000..88d86636bed --- /dev/null +++ b/app/javascript/spec/toolbar/miq-toolbar.spec.jsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import MiqToolbar, { MiqGenericToolbar } from '../../components/miq-toolbar'; +import DashboardToolbar from '../../components/dashboard_toolbar'; +import TopologyToolbar from '../../components/topology_toolbar'; + +const dashboardData = [ + [ + { + custom: true, + name: 'dashboard', + props: { + allowAdd: true, + allowReset: true, + locked: false, + items: [ + { + id: 31, type: 'button', text: 'add', image: 'fa fa-pie-chart fa-lg', title: 'Add', + }, + ], + }, + }, + ], +]; + +const genericData = [ + [ + { + id: 'summary_reload', + type: 'button', + icon: 'fa fa-refresh fa-lg', + name: 'summary_reload', + title: 'Refresh this page', + }, + ], +]; + +const topologyData = [[{ custom: true, name: 'topology', props: {} }]]; + +describe('', () => { + beforeEach(() => { + window.matchMedia = jest.fn(); + }); + + it('renders DashboardToolbar', () => { + const t = shallow(); + expect(t.find(DashboardToolbar)).toHaveLength(1); + }); + + it('renders TopologyToolbar', () => { + const t = shallow(); + expect(t.find(TopologyToolbar)).toHaveLength(1); + }); + + it('renders MiqGenericToolbar', () => { + const t = shallow(); + expect(t.find(MiqGenericToolbar)).toHaveLength(1); + }); +}); diff --git a/app/javascript/spec/toolbar/topology_toolbar.spec.jsx b/app/javascript/spec/toolbar/topology_toolbar.spec.jsx new file mode 100644 index 00000000000..98fa5d03ce7 --- /dev/null +++ b/app/javascript/spec/toolbar/topology_toolbar.spec.jsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import toJson from 'enzyme-to-json'; + +import TopologyToolbar from '../../components/topology_toolbar'; + +describe('', () => { + it('renders ok', () => { + const t = mount(); + expect(toJson(t)).toMatchSnapshot(); + }); +}); diff --git a/app/views/dashboard/_widgets_menu.html.haml b/app/views/dashboard/_widgets_menu.html.haml deleted file mode 100644 index 22b88f85948..00000000000 --- a/app/views/dashboard/_widgets_menu.html.haml +++ /dev/null @@ -1,32 +0,0 @@ -.form-group - .btn-group.dropdown - - if @widgets_menu[:blank] - %button.disabled.btn.btn-default.dropdown-toggle{'data-toggle' => 'dropdown', :title => _('No Widgets available to add'), 'data-click' => 'add_widget'} - %i.fa.fa-reply.fa-lg - %span.caret - - else - - if @widgets_menu[:allow_add] - - if @widgets_menu[:locked] - - title = _("Cannot add a Widget, this Dashboard has been locked by the Administrator") - - enabled = false - - else - - title = _("Add a widget") - - enabled = true - - cls = enabled ? '' : 'disabled ' - %button{'data-toggle' => 'dropdown', :class => "#{cls}btn btn-default dropdown-toggle", :title => title} - %i.fa.fa-plus.fa-lg - %span.caret - - %ul.dropdown-menu.scrollable-menu - - @widgets_menu[:items].each do |item| - %li - - if item[:type] == :separator - .divider{:role => 'presentation'} - - else - %a{:href => '#', :title => item[:title], 'data-click' => item[:id]} - %i{:class => item[:image]} - = item[:text] - - - if @widgets_menu[:allow_reset] - %button.btn.btn-default{:title => _('Reset Dashboard Widgets to the defaults'), 'data-click' => 'reset'} - %i.fa.fa-reply.fa-lg diff --git a/app/views/layouts/_center_div_dashboard_no_listnav.html.haml b/app/views/layouts/_center_div_dashboard_no_listnav.html.haml index 122e4d9871f..005aee6ef54 100644 --- a/app/views/layouts/_center_div_dashboard_no_listnav.html.haml +++ b/app/views/layouts/_center_div_dashboard_no_listnav.html.haml @@ -5,7 +5,7 @@ .row.toolbar-pf#toolbar .col-sm-12 - if @widgets_menu - = render :partial => "layouts/angular/toolbar" + = react 'MiqToolbar', :toolbars => toolbar_from_hash .row#main-content.miq-body.miq-layout-center_div_dashboard_no_listnav .col-md-12 .spacer diff --git a/app/views/layouts/_center_div_no_listnav.html.haml b/app/views/layouts/_center_div_no_listnav.html.haml index 82b732afe7c..b2756ede592 100644 --- a/app/views/layouts/_center_div_no_listnav.html.haml +++ b/app/views/layouts/_center_div_no_listnav.html.haml @@ -7,7 +7,7 @@ - if !@in_a_form && taskbar_in_header? .row.toolbar-pf#toolbar .col-sm-12 - = render :partial => "layouts/angular/toolbar" + = react 'MiqToolbar', :toolbars => toolbar_from_hash .row#main-content.miq-layout-center_div_no_listnav{:class => @lastaction == "show_dashboard" || @layout == "monitor_alerts_overview" ? 'miq-body' : ''} .col-md-12 - if layout_uses_tabs? diff --git a/app/views/layouts/_center_div_with_listnav.html.haml b/app/views/layouts/_center_div_with_listnav.html.haml index c553294c109..515493ca9d9 100644 --- a/app/views/layouts/_center_div_with_listnav.html.haml +++ b/app/views/layouts/_center_div_with_listnav.html.haml @@ -7,7 +7,7 @@ .col-sm-12 - if @layout == "dashboard" = render :partial => "/layouts/tabs" - = render :partial => "layouts/angular/toolbar" + = react 'MiqToolbar', :toolbars => toolbar_from_hash .row.max-height .col-sm-8.col-md-9.col-sm-push-4.col-md-push-3.max-height #main-content.row.miq-layout-center_div_with_listnav diff --git a/app/views/layouts/_content.html.haml b/app/views/layouts/_content.html.haml index 41da5a73896..848c1624fb8 100644 --- a/app/views/layouts/_content.html.haml +++ b/app/views/layouts/_content.html.haml @@ -10,7 +10,7 @@ = render :partial => "layouts/breadcrumbs" .row.toolbar-pf#toolbar.miq-toolbar-menu .col-sm-12 - = render :partial => "layouts/angular/toolbar" + = react 'MiqToolbar', :toolbars => toolbar_from_hash .row.max-height .max-height{:class => simulate? ? 'col-sm-7 col-md-8 col-sm-push-5 col-md-push-4' : 'col-sm-8 col-md-9 col-sm-push-4 col-md-push-3'} #main-content.row.miq-layout-center_div_with_listnav diff --git a/app/views/layouts/angular/_toolbar.html.haml b/app/views/layouts/angular/_toolbar.html.haml deleted file mode 100644 index 2148b61381d..00000000000 --- a/app/views/layouts/angular/_toolbar.html.haml +++ /dev/null @@ -1,7 +0,0 @@ -%ng-csp#miq-toolbar-menu{"ng-controller" => "miqToolbarController as toolbarCtrl", - "ng-init" => "toolbarCtrl.initObject('#{j_str(toolbar_from_hash.to_json)}')"} - %miq-toolbar-menu{"toolbar-items" => "toolbarCtrl.toolbarItems", - "toolbar-views" => "toolbarCtrl.dataViews", - "on-view-click" => "toolbarCtrl.onViewClick(item, $event)"} -:javascript - miq_bootstrap('#miq-toolbar-menu'); diff --git a/app/views/shared/_topology_header_toolbar.html.haml b/app/views/shared/_topology_header_toolbar.html.haml deleted file mode 100644 index 459af802ae1..00000000000 --- a/app/views/shared/_topology_header_toolbar.html.haml +++ /dev/null @@ -1,39 +0,0 @@ -.form-group.text - %label.topology-checkbox.checkbox-inline - %input#box_display_names{:type => 'checkbox'} - = _("Display Names") - -.form-group - %button.btn.btn-default{:title => _('Refresh this page'), - 'data-function' => 'sendDataWithRx', - 'data-function-data' => {:name => 'refreshTopology'}.to_json} - %i.fa.fa-refresh.fa-lg - = _("Refresh") -%form.search-pf.topology-search{:role => 'form', - :onsubmit => 'sendDataWithRx({service: "topologyService", name: "searchNode", args: [] }); return false'} - .form-group.has-clear - .search-pf-input-group - %label.sr-only{:for => 'search'} - = _("Search") - - %input#search_topology.form-control{:type => 'search', - 'placeholder' => _("Search")} - - -# this hidden button is a workaround - -# pressing enter in ^input triggers a *click* event on the next button - -# without this, that button is resetSearch, which ..is undesirable :) - %button.hidden{'data-function' => 'sendDataWithRx', - 'data-function-data' => {:service => 'topologyService', - :name => 'searchNode'}.to_json} - - %button.clear{'aria-hidden' => 'true', - 'data-function' => 'sendDataWithRx', - 'data-function-data' => {:service => 'topologyService', - :name => 'resetSearch'}.to_json} - %span.pficon.pficon-close - - .form-group.search-button - %button.btn.btn-default.search-topology-button{'data-function' => 'sendDataWithRx', - 'data-function-data' => {:service => 'topologyService', - :name => 'searchNode'}.to_json} - %span.fa.fa-search diff --git a/config/jest.setup.js b/config/jest.setup.js index a3127fa2712..b65562b23cf 100644 --- a/config/jest.setup.js +++ b/config/jest.setup.js @@ -38,3 +38,11 @@ import initializeStore from '../app/javascript/miq-redux/store'; ManageIQ.redux.store = initializeStore(); ManageIQ.redux.store.injectReducers(); + +Object.defineProperty(Array.prototype, 'flat', { + value: function(depth = 1) { + return this.reduce(function (flat, toFlatten) { + return flat.concat((Array.isArray(toFlatten) && (depth>1)) ? toFlatten.flat(depth-1) : toFlatten); + }, []); + } +}); diff --git a/jest.config.js b/jest.config.js index d729c5c7a77..c9c426f63f8 100644 --- a/jest.config.js +++ b/jest.config.js @@ -13,15 +13,17 @@ module.exports = { testURL: 'http://localhost', transform: { '^.+\\.jsx?$': 'babel-jest', - '.(ts|tsx)': 'ts-jest' - }, - moduleNameMapper: { - '^moment$': resolveModule('moment'), // fix moment-strftime peerDependency issue + '.(ts|tsx)': 'ts-jest', }, moduleFileExtensions: [ 'ts', 'tsx', 'js', - 'jsx' + 'jsx', ], + moduleNameMapper: { + "\\.(css|scss)$": 'identity-obj-proxy', + '^react$': '/node_modules/react/', + '^moment$': resolveModule('moment'), // fix moment-strftime peerDependency issue + }, }; diff --git a/package.json b/package.json index 0e580dc26af..367053348fc 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "dependencies": { "@data-driven-forms/pf3-component-mapper": "^1.15.6", "@data-driven-forms/react-form-renderer": "^1.15.6", - "@manageiq/react-ui-components": "~0.11.43", + "@manageiq/react-ui-components": "~0.11.48", "@manageiq/ui-components": "~1.3.1", "@pf3/select": "~1.12.6", "@pf3/timeline": "~1.0.8", @@ -65,7 +65,7 @@ "numeral": "~2.0.6", "patternfly": "~3.59.1", "patternfly-bootstrap-treeview": "~2.1.7", - "patternfly-react": "~2.36.8", + "patternfly-react": "~2.39.1", "prop-types": "^15.6.0", "proxy-polyfill": "^0.1.7", "react": "~16.8.2", @@ -125,6 +125,7 @@ "fetch-mock": "^7.2.5", "file-loader": "~1.1.11", "glob": "~7.1.1", + "identity-obj-proxy": "^3.0.0", "imports-loader": "~0.8.0", "jasmine-jquery": "~2.1.1", "jest": "^22.0.6", diff --git a/spec/javascripts/controllers/toolbar_controller_spec.js b/spec/javascripts/controllers/toolbar_controller_spec.js deleted file mode 100644 index fe7ed80653d..00000000000 --- a/spec/javascripts/controllers/toolbar_controller_spec.js +++ /dev/null @@ -1,91 +0,0 @@ -describe('toolbarController', function() { - beforeEach(module('ManageIQ')); - - var middleware_toolbar_list = getJSONFixture('toolbar_middleware_server_list.json'); - var middleware_toolbar_detail = getJSONFixture('toolbar_middleware_server_detail.json'); - - var $controller, $scope; - - beforeEach(inject(function($injector) { - $scope = $injector.get('$rootScope').$new(); - var injectedCtrl = $injector.get('$controller'); - $controller = injectedCtrl('miqToolbarController', {$scope: $scope}); - })); - - describe('show list toolbar', function() { - beforeEach(function() { - $controller.initObject(JSON.stringify(middleware_toolbar_list)); - }); - - it('toolbar data loaded, toolbar items more than one, 3 toolbar items contain {url_parms: "main_div"}', function() { - var allItems = _ - .chain($controller.toolbarItems) - .flatten() - .map('items') - .flatten() - .value(); - - expect($controller.toolbarItems.length > 0).toBeTruthy(); - expect(_.filter(allItems, {url_parms: 'main_div'}).length >= 3).toBeTruthy(); - }); - - it('check one row and observe if toolbar items gets enabled', function() { - var inputCheckbox = document.createElement('input'); - inputCheckbox.setAttribute('type', 'checkbox'); - inputCheckbox.setAttribute('checked', true); - sendDataWithRx({rowSelect: inputCheckbox}); - var allItems = _ - .chain($controller.toolbarItems) - .flatten() - .map('items') - .flatten() - .value(); - expect(_.filter(allItems, {enabled: true}).length >= 3).toBeTruthy(); - }); - - it('Each dataView should have url set automatically', function() { - // there's at least one item in $controller.dataViews which means there's at least one object - // that has id that starts at view_ in toolbar_middleware_server_list.json - expect($controller.dataViews.length).not.toBe(0); - $controller.dataViews.forEach(function(dataView) { - expect(dataView.url).toBeTruthy(); - }); - }); - - it('Each button should have eventFunction set up', function() { - _.chain($controller.toolbarItems) - .flatten() - .map(function(item) { - return (item && item.hasOwnProperty('items')) ? item.items : item; - }) - .flatten() - .filter({type: 'button'}) - .each(function(item) { - expect(item.hasOwnProperty('eventFunction')).toBeTruthy(); - }) - .value(); - }); - }); - - describe('show detail toolbar', function() { - beforeEach(function() { - $controller.initObject(JSON.stringify(middleware_toolbar_detail)); - }); - - it('middleware server, it should be different than list toolbar', function() { - expect(middleware_toolbar_list !== $controller.toolbarItems).toBeTruthy(); - }); - }); - - describe('event data toolbar', function() { - beforeEach(function() { - spyOn($controller, 'onUpdateItem'); - $controller.initObject(JSON.stringify([[{id: 'someButton', hidden: false, type: 'button'}]])); - }); - - it('should call update toolbar', function() { - sendDataWithRx({update: 'someButton', type: 'hidden', value: true}); - expect($controller.onUpdateItem).toHaveBeenCalled(); - }); - }); -}); diff --git a/spec/javascripts/miq_application_spec.js b/spec/javascripts/miq_application_spec.js index eff1090b53f..0785091c146 100644 --- a/spec/javascripts/miq_application_spec.js +++ b/spec/javascripts/miq_application_spec.js @@ -10,58 +10,6 @@ describe('miq_application.js', function() { }); }); - describe('miqInitToolbars', function () { - beforeEach(function () { - var html = '
'; - setFixtures(html); - }); - - it('initializes the onclick event on a regular toolbar button', function () { - spyOn(window, "miqToolbarOnClick"); - miqInitToolbars(); - $('#first').click(); - expect(miqToolbarOnClick).toHaveBeenCalled(); - }); - - it('not initializes an onclick event on a dropdown toolbar button', function () { - spyOn(window, "miqToolbarOnClick"); - miqInitToolbars(); - $('#second').click(); - expect(miqToolbarOnClick).not.toHaveBeenCalled(); - }); - - it('initializes the onclick event on a dropdown toolbar link', function () { - spyOn(window, "miqToolbarOnClick"); - miqInitToolbars(); - $('#third').click(); - expect(miqToolbarOnClick).toHaveBeenCalled(); - }); - }); - - describe('miqToolbarOnClick', function () { - beforeEach(function () { - var html = '