diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000000..395b83ed650e --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +app/javascript/oldjs/locale/*.json -text -diff diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9fb707c58676..597aac9aa2b4 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,8 +1 @@ -# trees -app/presenters/tree_node/ @skateman -app/presenters/tree_builder_* @skateman -# styling and images -app/assets/stylesheets/ @epwinchell -app/assets/images/ @epwinchell -# topology services -app/services/*topology_service.rb @skateman +* @ManageIQ/committers-ui diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d8c34ad3d747..34aac6351c35 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,19 +1,18 @@ +--- name: CI - on: push: pull_request: workflow_dispatch: schedule: - - cron: '0 0 * * *' - + - cron: 0 0 * * * jobs: ci: runs-on: ubuntu-latest strategy: matrix: ruby-version: - - '2.7' + - '3.0' node-version: - 14 test-suite: @@ -24,7 +23,7 @@ jobs: - spec:jest - spec:routes include: - - ruby-version: '2.6' + - ruby-version: '2.7' node-version: 14 test-suite: spec services: @@ -34,29 +33,31 @@ jobs: POSTGRESQL_USER: root POSTGRESQL_PASSWORD: smartvm POSTGRESQL_DATABASE: vmdb_test - options: --health-cmd pg_isready --health-interval 2s --health-timeout 5s --health-retries 5 + options: "--health-cmd pg_isready --health-interval 2s --health-timeout 5s + --health-retries 5" ports: - 5432:5432 env: - TEST_SUITE: ${{ matrix.test-suite }} + TEST_SUITE: "${{ matrix.test-suite }}" PGHOST: localhost PGPASSWORD: smartvm - CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} + CC_TEST_REPORTER_ID: "${{ secrets.CC_TEST_REPORTER_ID }}" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up system run: bin/before_install - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: ${{ matrix.ruby-version }} + ruby-version: "${{ matrix.ruby-version }}" bundler-cache: true timeout-minutes: 30 - name: Set up Node - if: ${{ matrix.test-suite == 'spec:compile' || matrix.test-suite == 'spec:javascript' || matrix.test-suite == 'spec:jest' }} + if: "${{ matrix.test-suite == 'spec:compile' || matrix.test-suite == 'spec:javascript' + || matrix.test-suite == 'spec:jest' }}" uses: actions/setup-node@v2 with: - node-version: ${{ matrix.node-version }} + node-version: "${{ matrix.node-version }}" cache: yarn registry-url: https://npm.manageiq.org/ - name: Prepare tests @@ -64,6 +65,7 @@ jobs: - name: Run tests run: bundle exec rake - name: Report code coverage - if: ${{ github.ref == 'refs/heads/master' && matrix.ruby-version == '2.7' && matrix.test-suite == 'spec' }} + if: "${{ github.ref == 'refs/heads/master' && matrix.ruby-version == '2.7' && + matrix.test-suite == 'spec' }}" continue-on-error: true uses: paambaati/codeclimate-action@v3.0.0 diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 07f7b4514fc8..91d86ab05efa 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1006,6 +1006,7 @@ def get_sort_col def process_saved_reports(saved_reports, task) success_count = 0 failure_count = 0 + params[:miq_grid_checks] = params[:miq_grid_checks]&.split(",") MiqReportResult.for_user(current_user).where(:id => saved_reports).order(MiqReportResult.arel_table[:name].lower).each do |rep| rep.public_send(task) if rep.respond_to?(task) # Run the task rescue StandardError @@ -1019,6 +1020,7 @@ def process_saved_reports(saved_reports, task) :target_class => "MiqReportResult", :userid => current_userid ) + params[:miq_grid_checks]&.delete(rep[:id].to_s) success_count += 1 else add_flash(_("\"%{record}\": %{task} successfully initiated") % {:record => rep.name, :task => task}) @@ -1032,6 +1034,7 @@ def process_saved_reports(saved_reports, task) add_flash(n_("Error during Saved Report delete from the %{product} Database", "Error during Saved Reports delete from the %{product} Database", failure_count) % {:product => Vmdb::Appliance.PRODUCT_NAME}) end + params[:miq_grid_checks] || [] end # Common timeprofiles button handler routines diff --git a/app/controllers/application_controller/compare.rb b/app/controllers/application_controller/compare.rb index ae6328f5f99a..a8c528a2bb2b 100644 --- a/app/controllers/application_controller/compare.rb +++ b/app/controllers/application_controller/compare.rb @@ -13,6 +13,8 @@ def create_compare_view rpt = get_compare_report(@sb[:compare_db]) session[:miq_sections] = MiqCompare.sections(rpt) + selected_sections = session[:miq_sections]&.select { |_key, value| value[:checked] == true } + session[:selected_sections] = selected_sections ? selected_sections.keys.map(&:to_s) : [] ids = session[:miq_selected].collect(&:to_i) @compare = MiqCompare.new({:ids => ids, :include => session[:miq_sections]}, @@ -413,13 +415,11 @@ def sections_field_changed end def set_checked_sections - session[:selected_sections] = [] - params[:all_checked]&.each do |a| - s = a.split(':') - if s.length > 1 - session[:selected_sections].push(s[1]) - end + selections = [] + params[:all_checked]&.each do |item| + add_selections!(selection_names(item), selections) end + session[:selected_sections] = update_selections(params[:id], selections, params[:check]) end # Toggle exists/details view @@ -1851,4 +1851,54 @@ def section_field_compare_values(view, section, field, base_val) unset_same_flag unless fld.nil? || base_val == fld[:_value_] end end + + def validate_name(name) + name&.starts_with?('xx-group') + end + + # Method to get the selection names from :all_checked and :id params. + # individual selected item suggests that the variable name contains ':' + # else, the child_names are retrived from the selected_parent item. + def selection_names(name) + if validate_name(name) + count = name.scan(/xx-group/).count + count == 1 ? child_names(name) : [name.split(':')[1]] + end + end + + # Method to get the child names of a selected parent item. + # session[:miq_sections] contains all tree names and the keys are filters using the selected group_name[1]. + def child_names(name) + child_names = [] + group_name = name.split('_') + if group_name[1] + child_sections = session[:miq_sections]&.select { |_key, value| value[:group] == group_name[1] } + if child_sections && child_sections.length > 1 + child_names = child_sections.keys.map(&:to_s) + end + end + child_names + end + + # Method to add or remove selections if an id is present. + def update_selections(param_id, selections, check) + if param_id + names = selection_names(CGI.unescape(param_id)) + case check + when "true" then add_selections!(names, selections) + when "false" then remove_selections!(names, selections) + end + end + selections + end + + # Method to add names to the selections. + def add_selections!(names, selections) + selections.replace(selections | names) + end + + # Method to remove names to the selections. + def remove_selections!(names, selections) + selections.replace(selections - names) + end end diff --git a/app/controllers/application_controller/timelines/options.rb b/app/controllers/application_controller/timelines/options.rb index 5220cbf0410f..7c5ad88d9883 100644 --- a/app/controllers/application_controller/timelines/options.rb +++ b/app/controllers/application_controller/timelines/options.rb @@ -34,6 +34,8 @@ def update_from_params(params) self.levels = params[:tl_levels]&.map(&:to_sym) || group_levels self.categories = {} params.fetch(:tl_categories, []).each do |category_display_name| + next if category_display_name == "Other" + group_data = event_groups[events[category_display_name]] category = { :display_name => category_display_name, diff --git a/app/controllers/catalog_controller.rb b/app/controllers/catalog_controller.rb index 3697f8544eee..3590f0b81095 100644 --- a/app/controllers/catalog_controller.rb +++ b/app/controllers/catalog_controller.rb @@ -710,6 +710,9 @@ def svc_catalog_provision ra, st, svc_catalog_provision_finish_submit_endpoint ) @in_a_form = true + @dialog_locals = options[:dialog_locals] + # require 'byebug' + # byebug replace_right_cell(:action => "dialog_provision", :dialog_locals => options[:dialog_locals]) else # if catalog item has no dialog and provision button was pressed from list view diff --git a/app/controllers/cloud_network_controller.rb b/app/controllers/cloud_network_controller.rb index 8f23ecafdd8d..6f0d0e300a0c 100644 --- a/app/controllers/cloud_network_controller.rb +++ b/app/controllers/cloud_network_controller.rb @@ -45,7 +45,7 @@ def create when "add" options = form_params ems = ExtManagementSystem.find(options[:ems_id]) - if CloudNetwork.class_by_ems(ems).supports?(:create) + if CloudNetwork.class_by_ems(ems)&.supports?(:create) options.delete(:ems_id) task_id = ems.create_cloud_network_queue(session[:userid], options) unless task_id.kind_of?(Integer) diff --git a/app/controllers/container_dashboard_controller.rb b/app/controllers/container_dashboard_controller.rb index 72aa98bfb840..1efe00133e21 100644 --- a/app/controllers/container_dashboard_controller.rb +++ b/app/controllers/container_dashboard_controller.rb @@ -112,7 +112,7 @@ def breadcrumbs_options :breadcrumbs => [ {:title => _("Compute")}, {:title => _("Containers")}, - {:title => _("Overview"), :url => controller_url}, + {:title => _("Overview"), :url => "#{controller_url}/show"}, ], } end diff --git a/app/controllers/ems_storage_dashboard_controller.rb b/app/controllers/ems_storage_dashboard_controller.rb index 937f7097b79e..862d98ff57b0 100644 --- a/app/controllers/ems_storage_dashboard_controller.rb +++ b/app/controllers/ems_storage_dashboard_controller.rb @@ -23,6 +23,11 @@ def aggregate_status_data render :json => {:data => aggregate_status(params[:id])} end + def aggregate_event_data + assert_privileges('ems_storage_show') # TODO: might be ems_event_show + render :json => {:data => aggregate_event(params[:id])} + end + private def block_storage_heatmap_data(ems_id) @@ -33,6 +38,10 @@ def aggregate_status(ems_id) EmsStorageDashboardService.new(ems_id, self, EmsStorage).aggregate_status_data end + def aggregate_event(ems_id) + EmsStorageDashboardService.new(ems_id, self, EmsStorage).aggregate_event_data + end + def get_session_data @layout = "ems_storage_dashboard" end diff --git a/app/controllers/floating_ip_controller.rb b/app/controllers/floating_ip_controller.rb index 12fc0d9ca7a6..2793b62532cb 100644 --- a/app/controllers/floating_ip_controller.rb +++ b/app/controllers/floating_ip_controller.rb @@ -44,7 +44,7 @@ def create options = form_params ems = ExtManagementSystem.find(options[:ems_id]) - if FloatingIp.class_by_ems(ems).supports?(:create) + if FloatingIp.class_by_ems(ems)&.supports?(:create) options.delete(:ems_id) task_id = ems.create_floating_ip_queue(session[:userid], options) diff --git a/app/controllers/generic_object_definition_controller.rb b/app/controllers/generic_object_definition_controller.rb index 5df73885b7ff..d295fd66bbb2 100644 --- a/app/controllers/generic_object_definition_controller.rb +++ b/app/controllers/generic_object_definition_controller.rb @@ -276,6 +276,7 @@ def replace_right_cell @explorer = false node = x_node || params[:id] + @node_type = node.split('-').first v_tb, c_tb = case node_type(node) when :root then process_root_node(presenter) diff --git a/app/controllers/mixins/actions/vm_actions/associate_floating_ip.rb b/app/controllers/mixins/actions/vm_actions/associate_floating_ip.rb index 1d2a0f7285ca..b88fb7a116f0 100644 --- a/app/controllers/mixins/actions/vm_actions/associate_floating_ip.rb +++ b/app/controllers/mixins/actions/vm_actions/associate_floating_ip.rb @@ -57,10 +57,6 @@ def associate_floating_ip_form_fields def associate_floating_ip_vm assert_privileges("instance_associate_floating_ip") @record = find_record_with_rbac(VmCloud, params[:id]) - case params[:button] - when "cancel" then associate_handle_cancel_button - when "submit" then associate_handle_submit_button - end if @sb[:explorer] replace_right_cell @@ -72,38 +68,6 @@ def associate_floating_ip_vm end end end - - def associate_handle_cancel_button - add_flash(_("Association of Floating IP with Instance \"%{name}\" was cancelled by the user") % {:name => @record.name}) - @record = @sb[:action] = nil - end - - def associate_handle_submit_button - if @record.supports?(:associate_floating_ip) - floating_ip = params[:floating_ip][:address] - begin - @record.associate_floating_ip_queue(session[:userid], floating_ip) - add_flash(_("Associating Floating IP %{address} with Instance \"%{name}\"") % { - :address => floating_ip, - :name => @record.name - }) - rescue StandardError => ex - add_flash(_("Unable to associate Floating IP %{address} with Instance \"%{name}\": %{details}") % { - :address => floating_ip, - :name => @record.name, - :details => get_error_message_from_fog(ex.to_s) - }, :error) - end - else - add_flash(_("Unable to associate Floating IP with Instance \"%{name}\": %{details}") % { - :name => @record.name, - :details => @record.unsupported_reason(:associate_floating_ip) - }, :error) - end - params[:id] = @record.id.to_s # reset id in params for show - @record = nil - @sb[:action] = nil - end end end end diff --git a/app/controllers/mixins/actions/vm_actions/disassociate_floating_ip.rb b/app/controllers/mixins/actions/vm_actions/disassociate_floating_ip.rb index 5b15ad0d2030..0b68130d86d8 100644 --- a/app/controllers/mixins/actions/vm_actions/disassociate_floating_ip.rb +++ b/app/controllers/mixins/actions/vm_actions/disassociate_floating_ip.rb @@ -61,63 +61,6 @@ def disassociate_floating_ip_form_fields def disassociate_floating_ip_vm assert_privileges("instance_disassociate_floating_ip") @record = find_record_with_rbac(VmCloud, params[:id]) - - case params[:button] - when "cancel" then disassociate_handle_cancel_button - when "submit" then disassociate_handle_submit_button - end - end - - private - - def disassociate_handle_cancel_button - add_flash(_("Disassociation of Floating IP from Instance \"%{name}\" was cancelled by the user") % {:name => @record.name}) - @record = @sb[:action] = nil - if @sb[:explorer] - replace_right_cell - else - flash_to_session - render :update do |page| - page << javascript_prologue - page.redirect_to(previous_breadcrumb_url) - end - end - end - - def disassociate_handle_submit_button - if @record.supports?(:disassociate_floating_ip) - floating_ip = params[:floating_ip][:address] - begin - @record.disassociate_floating_ip_queue(session[:userid], floating_ip) - add_flash(_("Disassociating Floating IP %{address} from Instance \"%{name}\"") % { - :address => floating_ip, - :name => @record.name - }) - rescue StandardError => ex - add_flash(_("Unable to disassociate Floating IP %{address} from Instance \"%{name}\": %{details}") % { - :address => floating_ip, - :name => @record.name, - :details => get_error_message_from_fog(ex.to_s) - }, :error) - end - else - add_flash(_("Unable to disassociate Floating IP from Instance \"%{name}\": %{details}") % { - :name => @record.name, - :details => @record.unsupported_reason(:disassociate_floating_ip) - }, :error) - end - params[:id] = @record.id.to_s # reset id in params for show - @record = nil - @sb[:action] = nil - if @sb[:explorer] - replace_right_cell - else - flash_to_session - render :update do |page| - page << javascript_prologue - page.redirect_to(previous_breadcrumb_url) - end - end end end end diff --git a/app/controllers/mixins/actions/vm_actions/resize.rb b/app/controllers/mixins/actions/vm_actions/resize.rb index acd2a300eead..45cf9e446610 100644 --- a/app/controllers/mixins/actions/vm_actions/resize.rb +++ b/app/controllers/mixins/actions/vm_actions/resize.rb @@ -56,79 +56,6 @@ def resize @resize = true render :action => "show" unless @explorer end - - def resize_vm - assert_privileges("instance_resize") - @record = find_record_with_rbac(VmOrTemplate, params[:objectId]) - if params[:id] && params[:id] != 'new' - @request_id = params[:id] - end - - case params[:button] - when "cancel" - add_flash(_("Reconfigure of Instance \"%{name}\" was cancelled by the user") % {:name => @record.name}) - @record = @sb[:action] = nil - when "submit" - if @record.supports?(:resize) - begin - flavor_id = params['flavor_id'] - flavor = find_record_with_rbac(Flavor, flavor_id) - old_flavor_name = @record.flavor.try(:name) || _("unknown") - options = {:src_ids => [@record.id], - :instance_type => flavor_id} - VmCloudReconfigureRequest.make_request(@request_id, options, current_user) - add_flash(_("Reconfiguring Instance \"%{name}\" from %{old_flavor} to %{new_flavor}") % { - :name => @record.name, - :old_flavor => old_flavor_name, - :new_flavor => flavor.name - }) - rescue StandardError => ex - add_flash(_("Unable to reconfigure Instance \"%{name}\": %{details}") % { - :name => @record.name, - :details => get_error_message_from_fog(ex.to_s) - }, :error) - end - else - add_flash(_("Unable to reconfigure Instance \"%{name}\": %{details}") % { - :name => @record.name, - :details => @record.unsupported_reason(:resize) - }, :error) - end - params[:id] = @record.id.to_s # reset id in params for show - @record = nil - @sb[:action] = nil - end - if @sb[:explorer] && !(@breadcrumbs.length >= 2 && previous_breadcrumb_url.include?('miq_request')) - @sb[:explorer] = nil - replace_right_cell - else - flash_to_session - javascript_redirect(previous_breadcrumb_url) - end - end - - def resize_form_fields - assert_privileges("instance_resize") - - @request_id = params[:id] - @record = find_record_with_rbac(VmOrTemplate, params[:objectId]) - flavors = [] - # include only flavors with root disks at least as big as the instance's current root disk. - @record.ext_management_system&.flavors&.each do |ems_flavor| - # include only flavors with root disks at least as big as the instance's current root disk. - if @record.flavor.nil? || ((ems_flavor != @record.flavor) && (ems_flavor.root_disk_size >= @record.flavor.root_disk_size)) - flavors << {:name => ems_flavor.name_with_details, :id => ems_flavor.id} - end - end - resize_values = { - :flavors => flavors - } - unless @request_id == 'new' - @req = MiqRequest.find_by(:id => @request_id) - resize_values[:flavor_id] = @req.options[:instance_type].to_i - end - render :json => resize_values - end end end end diff --git a/app/controllers/mixins/ems_common.rb b/app/controllers/mixins/ems_common.rb index 433b0dcaf2b7..6f99bda4e5c6 100644 --- a/app/controllers/mixins/ems_common.rb +++ b/app/controllers/mixins/ems_common.rb @@ -103,6 +103,7 @@ def display_methods physical_servers_with_host physical_switches physical_storages + placement_groups security_groups security_policies security_policy_rules diff --git a/app/controllers/ops_controller/ops_rbac.rb b/app/controllers/ops_controller/ops_rbac.rb index 4ba08e28a8ad..1627b6d1dcd3 100644 --- a/app/controllers/ops_controller/ops_rbac.rb +++ b/app/controllers/ops_controller/ops_rbac.rb @@ -140,35 +140,13 @@ def rbac_tenant_edit end def rbac_tenant_manage_quotas_cancel - @tenant = Tenant.find(params[:id]) - add_flash(_("Manage quotas for %{model}\ \"%{name}\" was cancelled by the user") % - {:model => tenant_type_title_string(@tenant.divisible), :name => @tenant.name}) get_node_info(x_node) replace_right_cell(:nodetype => x_node) end def rbac_tenant_manage_quotas_save_add - tenant = Tenant.find(params[:id]) - begin - tenant.set_quotas(rbac_tenant_manage_quotas_params.to_h.deep_symbolize_keys) - rescue => bang - add_flash(_("Error when saving tenant quota: %{message}") % {:message => bang.message}, :error) - javascript_flash - else - add_flash(_("Quotas for %{model} \"%{name}\" were saved") % - {:model => tenant_type_title_string(tenant.divisible), :name => tenant.name}) - get_node_info(x_node) - replace_right_cell(:nodetype => "root", :replace_trees => [:rbac]) - end - end - - private def rbac_tenant_manage_quotas_params - if params[:quotas] - permitted_attrs = TenantQuota::NAMES.index_with { %i[unit value warn_value] } - params.require(:quotas).permit(permitted_attrs) - else - {} - end + get_node_info(x_node) + replace_right_cell(:nodetype => "root", :replace_trees => [:rbac]) end def rbac_tenant_manage_quotas_reset @@ -177,7 +155,6 @@ def rbac_tenant_manage_quotas_reset @edit = {:tenant_id => @tenant.id} session[:edit] = {:key => "tenant_manage_quotas__#{@tenant.id}"} session[:changed] = false - add_flash(_("All changes have been reset"), :warning) if params[:button] == 'reset' replace_right_cell(:nodetype => "tenant_manage_quotas") end @@ -193,17 +170,6 @@ def rbac_tenant_manage_quotas end end - def tenant_quotas_form_fields - assert_privileges("rbac_tenant_manage_quotas") - - tenant = Tenant.find(params[:id]) - tenant_quotas = tenant.get_quotas - render :json => { - :name => tenant.name, - :quotas => tenant_quotas - } - end - # Edit user or group tags def rbac_tenant_tags_edit case params[:button] diff --git a/app/controllers/placement_group_controller.rb b/app/controllers/placement_group_controller.rb new file mode 100644 index 000000000000..7fccc6a813a8 --- /dev/null +++ b/app/controllers/placement_group_controller.rb @@ -0,0 +1,55 @@ +class PlacementGroupController < ApplicationController + before_action :check_privileges + before_action :get_session_data + after_action :cleanup_action + after_action :set_session_data + + include Mixins::GenericListMixin + include Mixins::GenericSessionMixin + include Mixins::MoreShowActions + include Mixins::GenericShowMixin + include Mixins::EmsCommon + include Mixins::BreadcrumbsMixin + + def self.display_methods + %w[instances] + end + + def breadcrumb_name(_model) + _("Placement Groups") + end + + def self.table_name + @table_name ||= "placement_group" + end + + def download_data + assert_privileges('placement_group_view') + super + end + + def download_summary_pdf + assert_privileges('placement_group_view') + super + end + + private + + def textual_group_list + [%i[relationships properties], %i[tags]] + end + helper_method :textual_group_list + + def breadcrumbs_options + { + :breadcrumbs => [ + {:title => _("Compute")}, + {:title => _("Clouds")}, + {:title => _("Placement Groups"), :url => controller_url}, + ], + } + end + + menu_section :clo + feature_for_actions "#{controller_name}_show_list", *ADV_SEARCH_ACTIONS +end diff --git a/app/controllers/report_controller/saved_reports.rb b/app/controllers/report_controller/saved_reports.rb index 293656a19be5..3463d3cd84e0 100644 --- a/app/controllers/report_controller/saved_reports.rb +++ b/app/controllers/report_controller/saved_reports.rb @@ -125,7 +125,7 @@ def saved_report_delete @report = nil r = MiqReportResult.for_user(current_user).find(savedreports[0]) @sb[:miq_report_id] = r.miq_report_id - process_saved_reports(savedreports, "destroy") unless savedreports.empty? + params[:miq_grid_checks] = process_saved_reports(savedreports, "destroy") unless savedreports.empty? add_flash(_("The selected Saved Report was deleted")) if @flash_array.nil? @report_deleted = true end diff --git a/app/controllers/security_group_controller.rb b/app/controllers/security_group_controller.rb index b0734b4863a8..41b65ff4f2a4 100644 --- a/app/controllers/security_group_controller.rb +++ b/app/controllers/security_group_controller.rb @@ -55,7 +55,7 @@ def create @security_group = SecurityGroup.new options = form_params ems = ExtManagementSystem.find(options[:ems_id]) - if SecurityGroup.class_by_ems(ems).supports?(:create) + if SecurityGroup.class_by_ems(ems)&.supports?(:create) options.delete(:ems_id) task_id = ems.create_security_group_queue(session[:userid], options) diff --git a/app/controllers/vm_common.rb b/app/controllers/vm_common.rb index 1c783a21b36b..abab8be2368e 100644 --- a/app/controllers/vm_common.rb +++ b/app/controllers/vm_common.rb @@ -358,6 +358,10 @@ def floating_ips show_association('floating_ips', _('Floating IPs'), :floating_ips, FloatingIp) end + def placement_group + show_association('placement_groups', _('Placement Groups'), :placement_groups, PlacementGroup) + end + def cloud_subnets show_association('cloud_subnets', _('Subnets'), :cloud_subnets, CloudSubnet) end @@ -1273,7 +1277,6 @@ def set_right_cell_vars(options = {}) when "resize" partial = "vm_common/resize" header = _("Reconfiguring %{vm_or_template} \"%{name}\"") % {:vm_or_template => ui_lookup(:table => table), :name => name} - action = "resize_vm" when "retire" partial = "shared/views/retire" header = _("Set/Remove retirement date for %{vm_or_template}") % {:vm_or_template => ui_lookup(:table => table)} diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index a7a7a83dd624..1da8be8d44a2 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -353,7 +353,7 @@ def db_to_controller(db, action = "show") when "ActionSet" controller = "miq_action" action = "show_set" - when "AutomationRequest" + when "AutomationRequest", "MiqProvision" controller = "miq_request" action = "show" when "ConditionSet" @@ -391,6 +391,9 @@ def db_to_controller(db, action = "show") when "MiqAeInstance" controller = "miq_ae_class" action = "show_details" + when "PlacementGroup" + controller = "placement_group" + action = "show" when "SecurityGroup" controller = "security_group" action = "show" @@ -777,6 +780,7 @@ def display_adv_search? orchestration_stack persistent_volume physical_server + placement_group provider_foreman resource_pool retired @@ -1092,6 +1096,7 @@ def pdf_page_size_style physical_storage physical_server persistent_volume + placement_group policy policy_group security_policy @@ -1335,9 +1340,10 @@ def miq_toolbar(toolbars) end def miq_structured_list(data) - react('MiqStructuredList', {:title => data[:title], - :headers => data[:headers], - :rows => data[:rows], - :message => data[:message], :mode => ["miq_summary", data[:mode]].join(' ')}) + react('MiqStructuredList', {:title => data[:title], + :headers => data[:headers], + :rows => data[:rows], + :message => data[:message], + :mode => "miq_summary #{data[:mode]}"}) end end diff --git a/app/helpers/application_helper/button/cloud_network_new.rb b/app/helpers/application_helper/button/cloud_network_new.rb index 9e297d906794..71a89d0b4955 100644 --- a/app/helpers/application_helper/button/cloud_network_new.rb +++ b/app/helpers/application_helper/button/cloud_network_new.rb @@ -1,6 +1,6 @@ class ApplicationHelper::Button::CloudNetworkNew < ApplicationHelper::Button::ButtonNewDiscover def supports_button_action? - ::EmsNetwork.all.any? { |ems| CloudNetwork.class_by_ems(ems).supports?(:create) } + ::EmsNetwork.all.any? { |ems| CloudNetwork.class_by_ems(ems)&.supports?(:create) } end def role_allows_feature? diff --git a/app/helpers/application_helper/button/cloud_subnet_new.rb b/app/helpers/application_helper/button/cloud_subnet_new.rb index ed0275f37b2a..a0322cc92865 100644 --- a/app/helpers/application_helper/button/cloud_subnet_new.rb +++ b/app/helpers/application_helper/button/cloud_subnet_new.rb @@ -12,6 +12,6 @@ def role_allows_feature? # disable button if no active providers support create action def disabled? - ::EmsNetwork.all.none? { |ems| CloudSubnet.class_by_ems(ems).supports?(:create) } + ::EmsNetwork.all.none? { |ems| CloudSubnet.class_by_ems(ems)&.supports?(:create) } end end diff --git a/app/helpers/application_helper/button/floating_ip_new.rb b/app/helpers/application_helper/button/floating_ip_new.rb index c37835c74283..dd59e0b7e1df 100644 --- a/app/helpers/application_helper/button/floating_ip_new.rb +++ b/app/helpers/application_helper/button/floating_ip_new.rb @@ -8,6 +8,6 @@ def calculate_properties # disable button if no active providers support create action def disabled? - ::EmsNetwork.all.none? { |ems| ::FloatingIp.class_by_ems(ems).supports?(:create) } + ::EmsNetwork.all.none? { |ems| ::FloatingIp.class_by_ems(ems)&.supports?(:create) } end end diff --git a/app/helpers/application_helper/button/network_router_new.rb b/app/helpers/application_helper/button/network_router_new.rb index b8cd83e643b2..2d7410a27d21 100644 --- a/app/helpers/application_helper/button/network_router_new.rb +++ b/app/helpers/application_helper/button/network_router_new.rb @@ -12,6 +12,6 @@ def role_allows_feature? # disable button if no active providers support create action def disabled? - ::EmsNetwork.all.none? { |ems| NetworkRouter.class_by_ems(ems).supports?(:create) } + ::EmsNetwork.all.none? { |ems| NetworkRouter.class_by_ems(ems)&.supports?(:create) } end end diff --git a/app/helpers/application_helper/button/network_service_entry_new.rb b/app/helpers/application_helper/button/network_service_entry_new.rb index 79547275a1c0..f3acf38b7c6a 100644 --- a/app/helpers/application_helper/button/network_service_entry_new.rb +++ b/app/helpers/application_helper/button/network_service_entry_new.rb @@ -1,6 +1,6 @@ class ApplicationHelper::Button::NetworkServiceEntryNew < ApplicationHelper::Button::ButtonNewDiscover def supports_button_action? - ::EmsNetwork.all.any? { |ems| NetworkServiceEntry.class_by_ems(ems).supports?(:create) } + ::EmsNetwork.all.any? { |ems| NetworkServiceEntry.class_by_ems(ems)&.supports?(:create) } end def role_allows_feature? diff --git a/app/helpers/application_helper/button/network_service_new.rb b/app/helpers/application_helper/button/network_service_new.rb index 9b5a2726b552..db2cecfdff86 100644 --- a/app/helpers/application_helper/button/network_service_new.rb +++ b/app/helpers/application_helper/button/network_service_new.rb @@ -1,6 +1,6 @@ class ApplicationHelper::Button::NetworkServiceNew < ApplicationHelper::Button::ButtonNewDiscover def supports_button_action? - ::EmsNetwork.all.any? { |ems| NetworkService.class_by_ems(ems).supports?(:create) } + ::EmsNetwork.all.any? { |ems| NetworkService.class_by_ems(ems)&.supports?(:create) } end def role_allows_feature? diff --git a/app/helpers/application_helper/button/security_group_new.rb b/app/helpers/application_helper/button/security_group_new.rb index 34c208963015..656d70f940c6 100644 --- a/app/helpers/application_helper/button/security_group_new.rb +++ b/app/helpers/application_helper/button/security_group_new.rb @@ -8,6 +8,6 @@ def calculate_properties # disable button if no active providers support create action def disabled? - ::EmsNetwork.all.none? { |ems| SecurityGroup.class_by_ems(ems).supports?(:create) } + ::EmsNetwork.all.none? { |ems| SecurityGroup.class_by_ems(ems)&.supports?(:create) } end end diff --git a/app/helpers/application_helper/button/security_policy_new.rb b/app/helpers/application_helper/button/security_policy_new.rb index 583ce24e728a..ef07036f7334 100644 --- a/app/helpers/application_helper/button/security_policy_new.rb +++ b/app/helpers/application_helper/button/security_policy_new.rb @@ -1,6 +1,6 @@ class ApplicationHelper::Button::SecurityPolicyNew < ApplicationHelper::Button::ButtonNewDiscover def supports_button_action? - ::EmsNetwork.all.any? { |ems| SecurityPolicy.class_by_ems(ems).supports?(:create) } + ::EmsNetwork.all.any? { |ems| SecurityPolicy.class_by_ems(ems)&.supports?(:create) } end def role_allows_feature? diff --git a/app/helpers/application_helper/button/security_policy_rule_new.rb b/app/helpers/application_helper/button/security_policy_rule_new.rb index 5b419a7dd3e8..9310c168f2bb 100644 --- a/app/helpers/application_helper/button/security_policy_rule_new.rb +++ b/app/helpers/application_helper/button/security_policy_rule_new.rb @@ -1,6 +1,6 @@ class ApplicationHelper::Button::SecurityPolicyRuleNew < ApplicationHelper::Button::ButtonNewDiscover def supports_button_action? - ::EmsNetwork.all.any? { |ems| SecurityPolicyRule.class_by_ems(ems).supports?(:create) } + ::EmsNetwork.all.any? { |ems| SecurityPolicyRule.class_by_ems(ems)&.supports?(:create) } end def role_allows_feature? diff --git a/app/helpers/application_helper/listnav.rb b/app/helpers/application_helper/listnav.rb index e667f2020f2e..6248a9373edb 100644 --- a/app/helpers/application_helper/listnav.rb +++ b/app/helpers/application_helper/listnav.rb @@ -4,6 +4,7 @@ def render_listnav_filename common_layouts = %w[ physical_storage auth_key_pair_cloud + placement_group automation_manager_configured_system availability_zone cloud_database diff --git a/app/helpers/application_helper/page_layouts.rb b/app/helpers/application_helper/page_layouts.rb index 5b2d0124e5bb..54732de4225c 100644 --- a/app/helpers/application_helper/page_layouts.rb +++ b/app/helpers/application_helper/page_layouts.rb @@ -221,6 +221,7 @@ def show_adv_search? persistent_volume physical_server physical_storage + placement_group resource_pool retired security_group diff --git a/app/helpers/application_helper/toolbar/ems_infra_center.rb b/app/helpers/application_helper/toolbar/ems_infra_center.rb index cb0de4a20913..837c3926c97e 100644 --- a/app/helpers/application_helper/toolbar/ems_infra_center.rb +++ b/app/helpers/application_helper/toolbar/ems_infra_center.rb @@ -143,7 +143,7 @@ class ApplicationHelper::Toolbar::EmsInfraCenter < ApplicationHelper::Toolbar::B button( :ems_native_console, 'pficon pficon-screen fa-lg', - N_('Open a native console for this cloud provider'), + N_('Open a native console for this infrastructure provider'), N_('Native Console'), :keepSpinner => true, :url => "launch_console", diff --git a/app/helpers/application_helper/toolbar/hosts_center.rb b/app/helpers/application_helper/toolbar/hosts_center.rb index 3536a65a863e..a30ebcef9c7c 100644 --- a/app/helpers/application_helper/toolbar/hosts_center.rb +++ b/app/helpers/application_helper/toolbar/hosts_center.rb @@ -204,7 +204,9 @@ class ApplicationHelper::Toolbar::HostsCenter < ApplicationHelper::Toolbar::Basi :send_checked => true, :confirm => N_("Power On the selected items?"), :klass => ApplicationHelper::Button::HostFeatureButton, - :options => {:feature => :start}), + :options => {:feature => :start}, + :enabled => false, + :onwhen => "1+"), button( :host_stop, nil, @@ -215,7 +217,9 @@ class ApplicationHelper::Toolbar::HostsCenter < ApplicationHelper::Toolbar::Basi :send_checked => true, :confirm => N_("Power Off the selected items?"), :klass => ApplicationHelper::Button::HostFeatureButton, - :options => {:feature => :stop}), + :options => {:feature => :stop}, + :enabled => false, + :onwhen => "1+"), button( :host_reset, nil, @@ -226,7 +230,9 @@ class ApplicationHelper::Toolbar::HostsCenter < ApplicationHelper::Toolbar::Basi :send_checked => true, :confirm => N_("Reset the selected items?"), :klass => ApplicationHelper::Button::HostFeatureButtonWithDisable, - :options => {:feature => :reset}), + :options => {:feature => :reset}, + :enabled => false, + :onwhen => "1+"), ] ), ]) diff --git a/app/helpers/application_helper/toolbar/placement_group_center.rb b/app/helpers/application_helper/toolbar/placement_group_center.rb new file mode 100644 index 000000000000..53a97d76cd0d --- /dev/null +++ b/app/helpers/application_helper/toolbar/placement_group_center.rb @@ -0,0 +1,2 @@ +class ApplicationHelper::Toolbar::PlacementGroupCenter < ApplicationHelper::Toolbar::Basic +end diff --git a/app/helpers/application_helper/toolbar/placement_groups_center.rb b/app/helpers/application_helper/toolbar/placement_groups_center.rb new file mode 100644 index 000000000000..e87920bba8e3 --- /dev/null +++ b/app/helpers/application_helper/toolbar/placement_groups_center.rb @@ -0,0 +1,2 @@ +class ApplicationHelper::Toolbar::PlacementGroupsCenter < ApplicationHelper::Toolbar::Basic +end diff --git a/app/helpers/application_helper/toolbar/x_vm_center.rb b/app/helpers/application_helper/toolbar/x_vm_center.rb index 7ff8e249bc35..4da6ce85a95e 100644 --- a/app/helpers/application_helper/toolbar/x_vm_center.rb +++ b/app/helpers/application_helper/toolbar/x_vm_center.rb @@ -290,6 +290,16 @@ class ApplicationHelper::Toolbar::XVmCenter < ApplicationHelper::Toolbar::Basic :url => "native_console", :klass => ApplicationHelper::Button::VmNativeConsole ), + button( + :vm_native_console, + 'pficon pficon-screen fa-lg', + N_('Open a management console for this VM'), + N_('Management Console'), + :keepSpinner => true, + :url => "management_console", + :klass => ApplicationHelper::Button::GenericFeatureButton, + :options => {:feature => :native_console} + ), ] ), ]) diff --git a/app/helpers/application_helper/toolbar_chooser.rb b/app/helpers/application_helper/toolbar_chooser.rb index 4d83c8fd4bba..d6356a196dea 100644 --- a/app/helpers/application_helper/toolbar_chooser.rb +++ b/app/helpers/application_helper/toolbar_chooser.rb @@ -437,6 +437,7 @@ def center_toolbar_filename_classic physical_server physical_switch physical_storage + placement_group container_template resource_pool timeline diff --git a/app/helpers/catalog_helper.rb b/app/helpers/catalog_helper.rb index c83871080586..62dbaa47f3b7 100644 --- a/app/helpers/catalog_helper.rb +++ b/app/helpers/catalog_helper.rb @@ -1,5 +1,6 @@ module CatalogHelper include_concern 'TextualSummary' + include RequestInfoHelper def miq_catalog_resource(resources) headers = ["", _("Name"), _("Description"), _("Action Order"), _("Provision Order"), _("Action Start"), _("Action Stop"), _("Delay (mins) Start"), _("Delay (mins) Stop")] @@ -55,9 +56,246 @@ def service_catalog_summary(record, sb_data) miq_structured_list(data) end + def catalog_tab_configuration(record) + condition = catalog_tab_conditions(record) + tab_labels = [tab_label(:basic)] + tab_labels.push(tab_label(:detail)) if condition[:detail] + + if condition[:resource] + tab_labels.push(tab_label(:resource)) + elsif condition[:request] + tab_labels.push(tab_label(:request)) + end + + if condition[:provision] + tab_labels.push(tab_label(:provision)) + tab_labels.push(tab_label(:retirement)) if condition[:retirement] + end + + return tab_labels, condition + end + + def catalog_tab_edit_configuration(record) + condition = catalog_tab_edit_conditions(record) + tab_labels = [tab_label(:basic)] + tab_labels.push(tab_label(:detail)) if condition[:detail] + tab_labels.push(tab_label(:resource)) if condition[:resource] + tab_labels.push(tab_label(:request)) if condition[:request] + return tab_labels, condition + end + + def catalog_tab_edit_generic_configuration + [tab_label(:basic), tab_label(:provision), tab_label(:retirement)] + end + + def catalog_tab_content(key_name, &block) + if catalog_tabs_types[key_name] + class_name = key_name == :basic ? 'tab_content active' : 'tab_content' + content_tag(:div, :id => key_name, :class => class_name, &block) + end + end + + def catalog_basic_information(record, sb_params, tenants_tree) + prov_types = catalog_provision_types + prov_data = [prov_types[:template], prov_types[:ovf]].include?(record.prov_type) && catalog_provision?(record, :playbook) ? provisioning : nil + data = {:title => _('Basic Information'), :mode => "miq_catalog_basic_information"} + rows = [] + rows.push(row_data(_('Name / Description'), "#{record.name} / #{record.description}")) + rows.push(row_data(_('Display in Catalog'), {:input => "checkbox", :name => "display", :checked => record.display, :disabled => true, :label => ''})) + rows.push(row_data(_('Catalog'), h(record.service_template_catalog ? record.service_template_catalog.name : _('Unassigned')))) + rows.push(row_data(_('Zone'), record.zone ? record.zone.name : '')) unless record.composite? + rows.push(row_data(_('Dialog'), h(sb_params[:dialog_label]))) unless catalog_provision?(record, :playbook) + rows.push(row_data(_("Price / Month (in %{currency})") % {:currency => record.currency.code}, record.price)) if record.currency + rows.push(row_data(_('Item Type'), h(_(ServiceTemplate.all_catalog_item_types[record.prov_type])))) if record.prov_type + rows.push(row_data(_('Subtype'), h(_(ServiceTemplate::GENERIC_ITEM_SUBTYPES[record[:generic_subtype]]) || _("Custom")))) if catalog_provision?(record, :generic) + + if catalog_provision?(record, :orchestration) + rows.push(row_data(_('Orchestration Template'), h(record.try(:orchestration_template).try(:name)))) + rows.push(row_data(_('Provider'), h(record.orchestration_manager.name))) if record.orchestration_manager + elsif catalog_provision?(record, :tower) + rows.push(row_data(_('Ansible Tower Template'), h(record.try(:job_template).try(:name)))) + elsif catalog_provision?(record, :template) + rows.push(row_data(_('Provider'), provision_data(prov_data, :provider_name))) + rows.push(row_data(_('Container Template'), provision_data(prov_data, :template_name))) + end + + unless catalog_provision?(record, :playbook) + entry_points = [[_("Provisioning"), :fqname]] + unless record.prov_type.try(:start_with?, "generic_") + entry_points.push([_("Reconfigure"), :reconfigure_fqname], [_("Retirement"), :retire_fqname]) + end + entry_points.each do |entry_points_op| + rows.push(row_data("#{entry_points_op[0]} %s" % _('Entry Point'), h(sb_params[entry_points_op[1]]))) + end + end + + rows.push(row_data(_('Tenant'), h(record.tenant.name))) if User.current_user.super_admin_user? + rows.push(row_data(_('Owner'), h(record.try(:evm_owner).try(:name)))) + rows.push(row_data(_('Ownership Group'), h(record.try(:miq_group).try(:name)))) + rows.push(row_data(_('Additional Tenants'), {:input => 'component', :component => 'TREE_VIEW_REDUX', :props => tenants_tree.locals_for_render})) if role_allows?(:feature => 'rbac_tenant_view') + + if catalog_provision?(record, :ovf) + options = record.config_info[:provision] + rows.push(row_data(_('OVF Template'), provision_data(prov_data, :ovf_template_name))) + rows.push(row_data(_('VM Name'), options[:vm_name])) + rows.push(row_data(_('Accept EULA'), {:input => "checkbox", :name => "accept_ecula", :checked => options[:accept_all_eula], :disabled => true, :label => ''})) + rows.push(row_data(_('Datacenter'), provision_data(prov_data, :datacenter_name))) + rows.push(row_data(_('Resource Pool'), provision_data(prov_data, :resource_pool_name))) + rows.push(row_data(_('Folder'), provision_data(prov_data, :ems_folder_name))) + rows.push(row_data(_('Host'), provision_data(prov_data, :host_name))) + rows.push(row_data(_('Storage'), provision_data(prov_data, :storage_name))) + rows.push(row_data(_('Disk Format'), provision_data(prov_data, :disk_format))) + rows.push(row_data(_('Virtual Network'), provision_data(prov_data, :network_name))) + end + + data[:rows] = rows + miq_structured_list(data) + end + + def catalog_smart_management(record) + smart_mgnt = textual_tags_render_data(record) + data = {:title => smart_mgnt[:title], :mode => "miq_catalog-smart_management"} + rows = [] + smart_mgnt[:items].each do |item| + row = row_data(item[:label], item[:value]) + row[:cells][:icon] = item[:icon] if item[:icon] + rows.push(row) + end + data[:rows] = rows + miq_structured_list(data) + end + + def catalog_custom_image(record) + picture = record.picture ? "#{record.picture.url_path}?#{rand(99_999_999)}" : nil + data = {:title => _('Custom Image'), :mode => "miq_catalog_custom_image"} + data[:rows] = [row_data('', {:input => 'component', :component => 'CATALOG_CUSTOM_IMAGE', :props => {:recordId => record.id, :image => picture}})] + miq_structured_list(data) + end + + def catalog_details(record) + data = {:title => _('Details'), :mode => "miq_catalog_details"} + data[:rows] = [row_data(_('Long Description'), record.long_description)] + miq_structured_list(data) + end + + def catalog_resources(record) + resources = record.service_resources + data = {:title => _('Resources'), :mode => "miq_catalog_resources"} + data[:rows] = [row_data('', {:input => 'component', :component => 'CATALOG_RESOURCE', :props => {:initialData => miq_catalog_resource(resources)}})] + miq_structured_list(data) + end + + def catalog_generic_ansible_playbook_info(type, record, info) + list_type = type == :provision ? 'provisioning' : 'retirement' + data = {:title => "#{list_type.camelize} %s" % _('Info'), :mode => "miq_catalog_playbook_info"} + rows = [] + rows.push(row_data(_('Repository'), h(info[:repository]))) + rows.push(row_data(_('Playbook'), h(info[:playbook]))) + rows.push(row_data(_('Machine Credential'), h(info[:machine_credential]))) + rows.push(row_data(_('Vault Credential'), h(info[:vault_credential]))) + rows.push(row_data(_('Vault Credential'), h(info[:vault_credential]))) + rows.push(row_data(_('Cloud Credential'), h(info[:cloud_credential]))) + rows.push(row_data(_('Max TTL (mins)'), h(record.config_info[type][:execution_ttl]))) + rows.push(row_data(_('Hosts'), h(record.config_info[type][:hosts]))) + rows.push(row_data(_('Logging Output'), h(ViewHelper::LOG_OUTPUT_LEVELS[info[:log_output]]))) + rows.push(row_data(_('Escalate Privilege'), h(info[:become_enabled]))) + rows.push(row_data(_('Verbosity'), _(ViewHelper::VERBOSITY_LEVELS[info[:verbosity]]))) + data[:rows] = rows + miq_structured_list(data) + end + + def catalog_variables_default_data(type, record) + data = {:title => _("Variables & Default Values"), :mode => "miq_catalog_variable_data"} + data[:headers] = [_("Variable"), _("Default value")] + rows = [] + extra_vars = record.config_info[type][:extra_vars] + if extra_vars + extra_vars.each do |key, value| + rows.push({:cells => [{:value => h(key)}, {:value => h(value[:default])}]}) + end + else + data[:message] = _("No variables & default values available") + end + data[:rows] = rows + miq_structured_list(data) + end + + def catalog_dialog(provisioning) + rows = [] + data = {:title => _("Dialog"), :mode => "miq_catalog_dialog"} + if provisioning[:dialog_id] + if role_allows?(:feature => "dialog_accord", :any => true) + rows.push({ + :cells => [{:value => provisioning[:dialog]}], + :title => provisioning[:dialog], + :onclick => "DoNav('/miq_ae_customization/show/dg-#{provisioning[:dialog_id]}');", + }) + else + rows.push(row_data('', provisioning[:dialog])) + end + end + data[:rows] = rows + miq_structured_list(data) + end + + def catalog_provision_types + {:generic => "generic", + :orchestration => "generic_orchestration", + :ovf => "generic_ovf_template", + :playbook => "generic_ansible_playbook", + :tower => "generic_ansible_tower", + :template => "generic_container_template"}.freeze + end + private + def catalog_tabs_types + { + :basic => _('Basic Information'), + :detail => _('Details'), + :resource => _('Selected Resources'), + :request => _('Request Info'), + :provision => _('Provisioning'), + :retirement => _('Retirement') + } + end + + def catalog_tab_conditions(record) + { + :detail => record.display && !record.prov_type.try(:start_with?, "generic_"), + :resource => record.composite?, + :request => !record.prov_type || (record.prov_type && need_prov_dialogs?(record.prov_type)), + :provision => record.prov_type == catalog_provision_types[:playbook], + :retirement => record.config_info.fetch_path(:retirement) + } + end + + def catalog_tab_edit_conditions(record) + resource = request = false + detail = !!record[:display] + unless record[:st_prov_type].try(:start_with?, "generic_") + if record[:service_type] == "composite" + resource = true + elsif record[:service_type] == "atomic" && need_prov_dialogs?(record[:st_prov_type]) + request = true + end + end + {:detail => detail, :resource => resource, :request => request} + end + + def provision_data(data, type) + data && data[type] + end + def row_data(label, value) {:cells => {:label => label, :value => value}} end + + def catalog_provision?(record, type) + record.prov_type == catalog_provision_types[type] + end + + def tab_label(item) + {:name => item, :text => catalog_tabs_types[item]} + end end diff --git a/app/helpers/ems_cloud_helper/textual_summary.rb b/app/helpers/ems_cloud_helper/textual_summary.rb index dc0949473ca9..b468b72fa0db 100644 --- a/app/helpers/ems_cloud_helper/textual_summary.rb +++ b/app/helpers/ems_cloud_helper/textual_summary.rb @@ -19,7 +19,7 @@ def textual_group_relationships _("Relationships"), %i[ ems_infra network_manager availability_zones host_aggregates cloud_tenants flavors - security_groups instances images cloud_volumes orchestration_stacks storage_managers cloud_databases + security_groups placement_groups instances images cloud_volumes orchestration_stacks storage_managers cloud_databases custom_button_events tenant ] ) @@ -121,6 +121,16 @@ def textual_storage_managers h end + def textual_placement_groups + num = @record.try(:placement_groups) ? @record.number_of(:placement_groups) : 0 + h = {:label => _('Placement Groups'), :icon => "fa fa-database", :value => num} + if num.positive? + h[:title] = _("Show all Placement Groups") + h[:link] = ems_cloud_path(@record.id, :display => 'placement_groups') + end + h + end + def textual_cloud_databases num = @record.try(:cloud_databases) ? @record.number_of(:cloud_databases) : 0 h = {:label => _('Cloud Databases'), :icon => "fa fa-database", :value => num} diff --git a/app/helpers/generic_object_definition_helper.rb b/app/helpers/generic_object_definition_helper.rb index b713621620fd..2a69fbc35196 100644 --- a/app/helpers/generic_object_definition_helper.rb +++ b/app/helpers/generic_object_definition_helper.rb @@ -1,3 +1,57 @@ module GenericObjectDefinitionHelper include_concern 'TextualSummary' + + def generic_object_definition_button_summary(button) + style = button.options.key?(:button_color) ? button.options[:button_color].to_s : nil + data = {:title => _('Basic Information')} + data[:rows] = [ + row_data(_('Name'), button.name), + row_data(_('Display in Catalog'), {:input => "checkbox", :name => "display", :checked => button.options[:display], :disabled => true, :label => ''}), + row_data(_('Descrption'), button.description), + row_data(_('Image'), button.options[:button_icon], style, :icon => true), + ] + miq_structured_list(data) + end + + def generic_object_definition_button_group_summary(button_group) + @custom_buttons_table_data = custom_buttons_table_data(button_group.set_data[:button_order].map { |id| CustomButton.find_by(:id => id) }) + style = button_group.set_data[:button_color] ? button_group.set_data[:button_color].to_s : nil + icon = button_group.set_data[:button_icon].to_s == "" ? "pficon-folder-close" : button_group.set_data[:button_icon].to_s + data = {:title => _('Basic Information')} + data[:rows] = [ + row_data(_('Name'), button_group.name), + row_data(_('Display in Catalog'), {:input => "checkbox", :name => "display", :checked => button_group.set_data[:display], :disabled => true, :label => ''}), + row_data(_('Descrption'), button_group.description), + row_data(_('Image'), icon, style, :icon => true), + ] + miq_structured_list(data) + end + + private + + def row_data(label, value, style = "", icon: false) + if icon + data = {:cells => {:label => label, :icon => value, :color => style}} + else + data = {:cells => {:label => label, :value => value}} + data[:style] = style if style.present? + end + data + end + + def custom_buttons_table_data(button_group) + rows = [] + button_group.each do |button| + rows.push( + { + :name => button.name, + :id => button.id, + :description => button.description, + :button_icon => button.options[:button_icon], + :button_color => button.options[:button_color] + } + ) + end + rows + end end diff --git a/app/helpers/generic_object_definition_helper/textual_summary.rb b/app/helpers/generic_object_definition_helper/textual_summary.rb index f40147032c5f..1cacb5fb000e 100644 --- a/app/helpers/generic_object_definition_helper/textual_summary.rb +++ b/app/helpers/generic_object_definition_helper/textual_summary.rb @@ -33,7 +33,7 @@ def textual_generic_objects def textual_group_attribute_details_list if @record.property_attributes.count.zero? - TextualEmpty.new(_('Attributes'), _('No Attributes defined')) + empty_record_properties_list(_('Attributes')) else record_properties_list( _('Attributes (%{count})') % {:count => @record.property_attributes.count}, @@ -44,7 +44,7 @@ def textual_group_attribute_details_list def textual_group_association_details_list if @record.property_associations.count.zero? - TextualEmpty.new(_('Associations'), _('No Associations defined')) + empty_record_properties_list(_('Associations')) else record_properties_list( _('Associations (%{count})') % {:count => @record.property_associations.count }, @@ -62,9 +62,18 @@ def record_properties_list(type_and_count, type, labels) ) end + def empty_record_properties_list(title) + TextualMultilabel.new( + title, + :additional_table_class => "table-fixed", + :labels => nil, + :values => nil + ) + end + def textual_group_method_details_list if @record.property_methods.count.zero? - TextualEmpty.new(_('Methods'), _('No Methods defined')) + empty_record_properties_list(_('Methods')) else TextualMultilabel.new( _('Methods (%{count})') % {:count => @record.property_methods.count}, diff --git a/app/helpers/miq_ae_class_helper.rb b/app/helpers/miq_ae_class_helper.rb index 3c350a508c63..39ae8e12001b 100644 --- a/app/helpers/miq_ae_class_helper.rb +++ b/app/helpers/miq_ae_class_helper.rb @@ -314,4 +314,14 @@ def datastore_data(type, data) class_field_data(data) end end + + def datastore_form(ae_ns, sb_data, type) + domain = ae_ns.domain? + react('DatastoreForm', {:type => DATASTORE_TYPES[type], + :domain => domain, + :namespacePath => domain ? "" : sb_data[:namespace_path], + :namespaceId => ae_ns.id || 'new', + :nameReadOnly => domain && !ae_ns.editable_property?(:name), + :descReadOnly => domain && !ae_ns.editable_property?(:description)}) + end end diff --git a/app/helpers/miq_request_helper.rb b/app/helpers/miq_request_helper.rb new file mode 100644 index 000000000000..556a80f3c4b7 --- /dev/null +++ b/app/helpers/miq_request_helper.rb @@ -0,0 +1,3 @@ +module MiqRequestHelper + include RequestInfoHelper +end diff --git a/app/helpers/placement_group.rb b/app/helpers/placement_group.rb new file mode 100644 index 000000000000..b14aee7d87c9 --- /dev/null +++ b/app/helpers/placement_group.rb @@ -0,0 +1,3 @@ +module PlacementGroupHelper + include_concern 'TextualSummary' +end diff --git a/app/helpers/placement_group_helper.rb b/app/helpers/placement_group_helper.rb new file mode 100644 index 000000000000..b14aee7d87c9 --- /dev/null +++ b/app/helpers/placement_group_helper.rb @@ -0,0 +1,3 @@ +module PlacementGroupHelper + include_concern 'TextualSummary' +end diff --git a/app/helpers/placement_group_helper/textual_summary.rb b/app/helpers/placement_group_helper/textual_summary.rb new file mode 100644 index 000000000000..b54472f93d1a --- /dev/null +++ b/app/helpers/placement_group_helper/textual_summary.rb @@ -0,0 +1,38 @@ +module PlacementGroupHelper::TextualSummary + include TextualMixins::TextualEmsCloud + include TextualMixins::TextualGroupTags + include TextualMixins::TextualName + include TextualMixins::TextualCustomButtonEvents + # + # Groups + # + + def textual_group_relationships + TextualGroup.new(_("Relationships"), %i[ems_cloud instances]) + end + + def textual_group_properties + TextualGroup.new(_("Properties"), %i[name policy]) + end + + # + # Items + # + def textual_type + ui_lookup(:model => @record.type) + end + + def textual_policy + ui_lookup(:model => @record.policy) + end + + def textual_instances + num = @record.number_of(:vms) + h = {:label => _('Instances'), :icon => "pficon pficon-virtual-machine", :value => num} + if num.positive? && role_allows?(:feature => "vm_show_list") + h[:link] = url_for_only_path(:action => 'show', :id => @record, :display => 'instances') + h[:title] = _("Show all Instances") + end + h + end +end diff --git a/app/helpers/request_info_helper.rb b/app/helpers/request_info_helper.rb new file mode 100644 index 000000000000..73695415fe60 --- /dev/null +++ b/app/helpers/request_info_helper.rb @@ -0,0 +1,122 @@ +module RequestInfoHelper + private + + def provision_tab_configuration(workflow) + prov_tab_labels = workflow.provisioning_tab_list.map do |dialog| + {:name => dialog[:name], :text => dialog[:description]} + end + return prov_tab_labels, workflow.get_dialog_order + end + + def prov_vm_grid_data(edit, vms, vm) + none_index = '__VM__NONE__' + rows = [] + clones = [:clone_to_template, :clone_to_vm] + if vms + unless clones.include?(edit[:wf].request_type) + rows.push({:id => none_index, :clickable => true, :cells => none_cells(edit[:vm_headers].length - 1)}) + vms.each do |data| + rows.push({:id => data.id.to_s, :clickable => true, :cells => prov_vm_grid_cells(data, edit)}) + end + end + else + rows.push({:id => vm.id.to_s, :clickable => true, :cells => prov_vm_grid_cells(vm, edit)}) + end + { + :headers => prov_grid_vm_header(edit, clones, vms), + :rows => rows, + :selected => selected_vm(edit).presence || none_index, + :recordId => edit[:req_id] || "new", + } + end + + def prov_host_grid_data(edit, options_data, hosts) + none_index = '__HOST__NONE__' + rows = [{:id => none_index, :clickable => true, :cells => none_cells(5)}] + options = edit || options_data + rows += hosts.map do |h| + {:id => h.id.to_s, :clickable => true, :cells => prov_host_grid_cells(h, options)} + end + { + :headers => prov_grid_host_header(edit, options), + :rows => rows, + :selected => selected_host(edit).presence || none_index, + :recordId => (edit && edit[:req_id]) || "new", + } + end + + def prov_grid_vm_header(edit, clones, vms) + headers = [] + edit[:vm_columns].each_with_index do |h, index| + item = {:text => edit[:vm_headers][h], :header_text => edit[:vm_headers][h]} + if vms && clones.exclude?(edit[:wf].request_type) + item[:sort_choice] = h + item[:sort_data] = sort_data(edit, index, 'vm') + end + headers.push(item) + end + headers + end + + def prov_grid_host_header(edit, options) + headers = [] + options && options[:host_columns].each_with_index do |h, index| + item = {:text => options[:host_headers][h], :header_text => options[:host_headers][h]} + if edit + item[:sort_choice] = h + item[:sort_data] = sort_data(edit, index, 'host') + end + headers.push(item) + end + headers + end + + def cell_data(data) + {:text => data} + end + + def selected_vm(edit) + edit[:new][:src_vm_id] && edit[:new][:src_vm_id][0].to_s + end + + def selected_host(edit) + edit[:new][:placement_host_name] && edit[:new][:placement_host_name][0].to_s + end + + def sort_data(edit, index, type) + sort = {:isFilteredBy => false} + if edit["#{type}_columns".to_sym][index] == edit["#{type}_sortcol".to_sym] + sort = {:isFilteredBy => true, :sortDirection => edit["#{type}_sortdir".to_sym] == 'ASC' ? 'ASC' : 'DESC'} + end + sort + end + + def none_cells(count) + Array.new(count) { cell_data(" ") }.unshift(cell_data("<#{_('None')}>")) + end + + def prov_vm_grid_cells(data, edit) + cells = [ + cell_data(data.name), + cell_data(h(data.operating_system.try(:product_name))), + cell_data(h(data.platform)), + cell_data(h(data.cpu_total_cores)), + cell_data(h(number_to_human_size(data.mem_cpu.to_i * 1024 * 1024))), + cell_data(h(number_to_human_size(data.allocated_disk_storage))), + ] + cells.push(cell_data(h(data.deprecated ? _("true") : _("false")))) if edit[:vm_headers].key?('deprecated') + ext_name = data.ext_management_system ? h(data.ext_management_system.name) : "" + cells.push(cell_data(ext_name)) + cells.push(cell_data(h(data.v_total_snapshots))) + if edit[:vm_headers].key?('cloud_tenant') + cells.push(cell_data(h(data.cloud_tenant ? data.cloud_tenant.name : _('None')))) + end + cells + end + + def prov_host_grid_cells(data, options) + options[:host_columns].map do |col| + cell_data(h(data.send(col))) + end + end +end diff --git a/app/helpers/vm_cloud_helper.rb b/app/helpers/vm_cloud_helper.rb index bc18bfecaa61..8674c39bca88 100644 --- a/app/helpers/vm_cloud_helper.rb +++ b/app/helpers/vm_cloud_helper.rb @@ -1,4 +1,5 @@ module VmCloudHelper include VmHelper + include RequestInfoHelper include_concern 'TextualSummary' end diff --git a/app/helpers/vm_helper/textual_summary.rb b/app/helpers/vm_helper/textual_summary.rb index 625a3c6f32f2..136a865754bd 100644 --- a/app/helpers/vm_helper/textual_summary.rb +++ b/app/helpers/vm_helper/textual_summary.rb @@ -70,8 +70,8 @@ def textual_group_vm_cloud_relationships _("Relationships"), %i[ ems ems_infra cluster host availability_zone cloud_tenant flavor vm_template drift scan_history service genealogy - cloud_network cloud_subnet orchestration_stack cloud_networks cloud_subnets network_routers security_groups - floating_ips network_ports cloud_volumes custom_button_events + cloud_network cloud_subnet placement_group orchestration_stack cloud_networks cloud_subnets network_routers + security_groups floating_ips network_ports cloud_volumes custom_button_events ] ) end @@ -401,6 +401,19 @@ def textual_cloud_subnets h end + def textual_placement_group + my_placement_group = @record.placement_group + h = {:label => _('Placement Group'), + :icon => "pficon-flavor", + :value => (my_placement_group.nil? ? _("None") : my_placement_group.name)} + if !my_placement_group.nil? && role_allows?(:feature => "placement_group_show") + h[:title] = _("Show Placement Group") + textual_link(@record.placement_group, :label => _('Placement Group')) + h[:link] = url_for_only_path(:controller => 'placement_group', :action => 'show', :id => my_placement_group.id) + end + h + end + def textual_network_ports num = @record.number_of(:network_ports) h = {:label => _('Network Ports'), :icon => "ff ff-network-port", :value => num} diff --git a/app/javascript/components/async-credentials/password-field.jsx b/app/javascript/components/async-credentials/password-field.jsx index c270287a3663..9e9cdc4efc55 100644 --- a/app/javascript/components/async-credentials/password-field.jsx +++ b/app/javascript/components/async-credentials/password-field.jsx @@ -12,7 +12,7 @@ const PasswordField = ({ helperText, edit, parent, - componentclass, + componentClass, ...rest }) => { const formOptions = useFormApi(); @@ -24,8 +24,8 @@ const PasswordField = ({ validateOnMount: rest.validateOnMount, helperText, ...rest, - component: edit ? 'edit-password-field' : componentclass, - componentclass, + component: edit ? 'edit-password-field' : componentClass, + ...(edit) && { componentClass }, }; const newProps = { ...secretField }; @@ -88,7 +88,7 @@ PasswordField.propTypes = { helperText: PropTypes.string, edit: PropTypes.bool, parent: PropTypes.string, - componentclass: PropTypes.string, + componentClass: PropTypes.string, }; PasswordField.defaultProps = { @@ -97,7 +97,7 @@ PasswordField.defaultProps = { helperText: undefined, edit: false, parent: undefined, - componentclass: componentTypes.TEXT_FIELD, + componentClass: componentTypes.TEXT_FIELD, }; export default PasswordField; diff --git a/app/javascript/components/carbon-charts/stackBarChart.js b/app/javascript/components/carbon-charts/stackBarChart.js index 02a02a4c5784..dd4738f67548 100644 --- a/app/javascript/components/carbon-charts/stackBarChart.js +++ b/app/javascript/components/carbon-charts/stackBarChart.js @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { StackedBarChart } from '@carbon/charts-react'; -const StackBarChartGraph = ({ data, title }) => { +const StackBarChartGraph = ({ data, title, chart_options=null }) => { const options = { title, axes: { @@ -24,7 +24,7 @@ const StackBarChartGraph = ({ data, title }) => { }; return ( - + ); }; diff --git a/app/javascript/components/dashboard-widgets/widget-report/index.jsx b/app/javascript/components/dashboard-widgets/widget-report/index.jsx index 542a8910be9d..ea2278315296 100644 --- a/app/javascript/components/dashboard-widgets/widget-report/index.jsx +++ b/app/javascript/components/dashboard-widgets/widget-report/index.jsx @@ -6,7 +6,7 @@ const WidgetReport = ({ widgetModel }) => { let widget; if (widgetModel) { // eslint-disable-next-line react/no-danger - widget = (
); + widget = (
); } else { widget = (

diff --git a/app/javascript/components/data-store-form/data-store-form.schema.js b/app/javascript/components/data-store-form/data-store-form.schema.js new file mode 100644 index 000000000000..52dc4fc87f60 --- /dev/null +++ b/app/javascript/components/data-store-form/data-store-form.schema.js @@ -0,0 +1,64 @@ +import { componentTypes, validatorTypes } from '@@ddf'; + +/** Namespace path field schema */ +const namespacePathField = () => ({ + component: componentTypes.TEXT_FIELD, + id: 'namespacePath', + name: 'namespacePath', + label: __('Fully Qualified Name'), + isDisabled: true, +}); + +/** Name field schema */ +const nameField = (readOnly) => ({ + component: componentTypes.TEXT_FIELD, + id: 'name', + name: 'name', + label: __('Name'), + maxLength: 128, + validate: [{ type: validatorTypes.REQUIRED }], + isRequired: true, + isDisabled: readOnly, +}); + +/** Description field schema */ +const descriptionField = (readOnly) => ({ + component: componentTypes.TEXT_FIELD, + id: 'description', + name: 'description', + label: __('Description'), + maxLength: 128, + validate: [{ type: validatorTypes.REQUIRED }], + isDisabled: readOnly, +}); + +/** Enabled field schema */ +const enabledField = () => ({ + component: componentTypes.CHECKBOX, + id: 'enabled', + name: 'enabled', + label: __('Enabled'), +}); + +/** Function to generate the form fields based on conditions */ +const generateFields = (domain, namespacePath, nameReadOnly, descReadOnly) => { + const fields = []; + if (!domain) { fields.push(namespacePathField(namespacePath)); } + fields.push(nameField(nameReadOnly)); + fields.push(descriptionField(descReadOnly)); + if (domain) { fields.push(enabledField()); } + return fields; +}; + +const createSchema = (_type, domain, namespacePath, nameReadOnly, descReadOnly) => ({ + fields: [ + { + component: componentTypes.SUB_FORM, + id: 'datastore-form-wrapper', + name: 'datastore-form-wrapper', + title: __('Info'), + fields: generateFields(domain, namespacePath, nameReadOnly, descReadOnly), + }], +}); + +export default createSchema; diff --git a/app/javascript/components/data-store-form/index.jsx b/app/javascript/components/data-store-form/index.jsx new file mode 100644 index 000000000000..64cd91c94488 --- /dev/null +++ b/app/javascript/components/data-store-form/index.jsx @@ -0,0 +1,80 @@ +import React, { useState, useEffect } from 'react'; +import MiqFormRenderer from '@@ddf'; +import PropTypes from 'prop-types'; +import createSchema from './data-store-form.schema'; +import miqRedirectBack from '../../helpers/miq-redirect-back'; +import { http } from '../../http_api'; + +const DatastoreForm = ({ + type, domain, namespacePath, namespaceId, nameReadOnly, descReadOnly, +}) => { + const isEdit = namespaceId !== 'new'; + const submitLabel = isEdit ? __('Save') : __('Add'); + + const [{ initialValues, isLoading }, setState] = useState({ + initialValues: { + namespacePath, enabled: true, + }, + isLoading: isEdit, + }); + + useEffect(() => { + if (isEdit) { + http.get(`/miq_ae_class/namespace/${namespaceId}`).then((response) => { + setState({ + initialValues: { ...initialValues, ...response }, + loading: false, + }); + }); + } + }, [namespaceId]); + + const onSubmit = (values) => { + miqSparkleOn(); + const url = (isEdit) + ? `/miq_ae_class/update_namespace/${namespaceId}?button=save` + : '/miq_ae_class/create_namespace/new?button=add'; + miqAjaxButton(url, values, { complete: false }); + }; + + const onCancel = () => { + miqSparkleOn(); + let message = ''; + if (isEdit) { + message = domain + ? sprintf(__(`Edit of Domain "%s" was cancelled by user.`), initialValues.name) + : sprintf(__(`Edit of Namespace "%s" was cancelled by user.`), initialValues.name); + } else { + message = domain + ? __('Add of Domain was cancelled by user.') + : __('Add of Namespace was cancelled by user.'); + } + + miqRedirectBack(message, 'warning', '/miq_ae_class/explorer'); + }; + + return !isLoading && ( + + ); +}; + +DatastoreForm.propTypes = { + type: PropTypes.string.isRequired, + domain: PropTypes.bool.isRequired, + namespacePath: PropTypes.string.isRequired, + namespaceId: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + ]).isRequired, + nameReadOnly: PropTypes.bool.isRequired, + descReadOnly: PropTypes.bool.isRequired, +}; + +export default DatastoreForm; diff --git a/app/javascript/components/data-tables/button-list/helper.js b/app/javascript/components/data-tables/button-list/helper.js index 67243188cada..a9bb387fb33c 100644 --- a/app/javascript/components/data-tables/button-list/helper.js +++ b/app/javascript/components/data-tables/button-list/helper.js @@ -4,25 +4,30 @@ const tableHeaders = () => [ { key: 'hoverText', header: __('Description') }, ]; -const generateNodeKey = (nodeType, itemId, treeId) => (nodeType.split('-')[1] === 'ub' ? `${nodeType}_cb-${itemId}` : `${treeId}_cb-${itemId}`); +const generateNodeKey = (nodeType, itemId, treeId, shortNodeKey) => { + if (shortNodeKey) { + return `cb-${itemId}`; + } + return (nodeType.split('-')[1] === 'ub' ? `${nodeType}_cb-${itemId}` : `${treeId}_cb-${itemId}`); +}; /** Function to generate table body's row contents */ -const tableRows = (list, nodeType, treeId, treeBox) => list.map((item, index) => ({ +const tableRows = (list, nodeType, treeId, treeBox, shortNodeKey) => list.map((item, index) => ({ id: index.toString(), name: { text: item.name, icon: item.button_icon, props: { style: { color: item.button_color } } }, hoverText: item.description, treeBox, - nodeKey: generateNodeKey(nodeType, item.id, treeId), + nodeKey: generateNodeKey(nodeType, item.id, treeId, shortNodeKey), clickable: true, })); /** Function to generate table's header and row contents */ export const tableData = ( { - list, nodeType, treeId, treeBox, + list, nodeType, treeId, treeBox, shortNodeKey, } ) => { const headers = tableHeaders(); - const rows = tableRows(list, nodeType, treeId, treeBox); + const rows = tableRows(list, nodeType, treeId, treeBox, shortNodeKey); return { headers, rows }; }; diff --git a/app/javascript/components/dynamic-select/index.js b/app/javascript/components/dynamic-select/index.js new file mode 100644 index 000000000000..a72b7c202137 --- /dev/null +++ b/app/javascript/components/dynamic-select/index.js @@ -0,0 +1,121 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { prepareProps } from '@data-driven-forms/carbon-component-mapper'; +import { Button, Select } from 'carbon-components-react'; +import { + componentTypes, FormSpy, useFieldApi, useFormApi, +} from '@data-driven-forms/react-form-renderer'; +import MiqFormRenderer from '@@ddf'; +import { Renew32 } from '@carbon/icons-react'; + +const DynamicSelect = (props) => { + // const { + // initialValues, options, id, titleText, label, placeholder, + // ...rest + // } = useFieldApi(prepareProps(props)); + const { + customProp, + label, + input, + isRequired, + meta: { error, touched }, + FieldArrayProvider, + dataType, + ...rest + } = useFieldApi(props); + console.log(input); + console.log(props); + // const [defaultValues, setDefaultValues] = useState(initialValues || []); + + console.log(props.initialValue); + + const fields = { + fields: [ + { + component: componentTypes.SELECT, + id: props.id, + name: props.name, + label: props.label, + // options: props.options, + options: [{ value: '0', label: 'test' }, { value: '1', label: '1' }], + onChange: (val) => { + input.value = val; + props.setDynamicFieldValues({ ...props.dynamicFieldValues, [props.name]: input.value }); + // props.setDynamicFieldValues({ [props.name]: input.value }); + console.log(input.value); + }, + resolveProps: (fieldProps, { meta, input }, formOptions) => { + // console.log(fieldProps); + // console.log(meta); + // console.log(input); + // console.log(formOptions); + // props.setDynamicFieldValues({ ...props.dynamicFieldValues, [props.name]: input.value }); + // props.setDynamicFieldValues({ [props.name]: input.value }); + }, + }, + ], + }; + + return ( + //
+ // + // + // {touched && error &&

{error}

} + // {customProp &&

This is a custom prop and has nothing to do with form schema

} + // + //
+ } + schema={fields} + /> + ); +}; +const verifyIsDisabled = (valid) => { + let isDisabled = true; + if (valid) { + isDisabled = false; + } + return isDisabled; +}; + +const FormTemplate = ({ formFields }) => { + const { + handleSubmit, onCancel, getState, + } = useFormApi(); + const { valid } = getState(); + return ( +
+ {formFields} + + {() => ( +
+
+ )} +
+
+ ); +}; + +DynamicSelect.propTypes = { +}; + +DynamicSelect.defaultProps = { + initialValues: [], +}; + +export default DynamicSelect; diff --git a/app/javascript/components/host-initiator-form/host-initiator-form.schema.js b/app/javascript/components/host-initiator-form/host-initiator-form.schema.js index e1d32700c3c4..293094d99589 100644 --- a/app/javascript/components/host-initiator-form/host-initiator-form.schema.js +++ b/app/javascript/components/host-initiator-form/host-initiator-form.schema.js @@ -20,6 +20,13 @@ const loadStorages = (id) => API.get(`/api/providers/${id}?attributes=type,physi value: id, }))); +const loadGroups = (id) => API.get(`/api/physical_storages/${id}?attributes=host_initiator_groups`) + .then(({ host_initiator_groups }) => { + let groupOptions = host_initiator_groups.map(({ id, name }) => ({ label: name, value: id })); + groupOptions.unshift({ label: `<${__('None')}>`, value: '' }); + return groupOptions; + }); + const loadWwpns = (id) => API.get(`/api/physical_storages/${id}?attributes=wwpn_candidates`) // eslint-disable-next-line camelcase .then(({ wwpn_candidates }) => wwpn_candidates.map(({ candidate }) => @@ -234,6 +241,11 @@ const createSchema = (state, setState, ems, initialValues, storageId, setStorage type: "exact-length", threshold: 16, message: __('The length of the WWPN should be exactly 16 characters.') + }, + { + type: "pattern", + pattern: "^[0-9A-Fa-f]+$", + message: __('The WWPN should be a hexadecimal expression (0-9, A-F)') } ], }, @@ -242,6 +254,18 @@ const createSchema = (state, setState, ems, initialValues, storageId, setStorage or: [{ when: 'port_type', is: 'FC' }, { when: 'port_type', is: 'NVMeFC' }], }, }, + { + component: componentTypes.SELECT, + id: 'host_initiator_group', + name: 'host_initiator_group', + label: __('Host Initiator Group:'), + isRequired: false, + // includeEmpty: true, + validate: [{ type: validatorTypes.REQUIRED }], + loadOptions: () => (storageId ? loadGroups(storageId) : Promise.resolve([])), + isSearchable: true, + condition: { and: [{ when: 'physical_storage_id', isNotEmpty: true }, { when: 'port_type', isNotEmpty: true }] }, + }, ], }); }; diff --git a/app/javascript/components/miq-custom-tab/index.jsx b/app/javascript/components/miq-custom-tab/index.jsx new file mode 100644 index 000000000000..f01271452704 --- /dev/null +++ b/app/javascript/components/miq-custom-tab/index.jsx @@ -0,0 +1,77 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { Tabs, Tab } from 'carbon-components-react'; + +const MiqCustomTab = ({ containerId, tabLabels, type }) => { + const [data, setData] = useState({ loading: false }); + const tabConfigurations = (name) => [ + { type: 'CATALOG_SUMMARY' }, + { type: 'CATALOG_EDIT' }, + { type: 'CATALOG_REQUEST_INFO', url: `/miq_request/prov_field_changed?tab_id=${name}&edit_mode=true` }, + ]; + + const configuration = (name) => tabConfigurations(name).find((item) => item.type === type); + + const tabIdentifier = (name) => { + const config = configuration(name); + const cType = config && config.url ? 'dynamic' : 'static'; + return `${type.toLowerCase()}_${cType}`; + }; + + /** Function to load the tab contents which are already available within the page. */ + const staticContents = (name) => { + const container = document.getElementById(containerId); + const tabs = container.getElementsByClassName('tab_content'); + tabs.forEach((child) => { + if (child.parentElement.id === containerId) { + child.classList.remove('active'); + if (child.id === `${name}`) { + child.classList.add('active'); + } + } + }); + miqSparkleOff(); + }; + + /** Function to load tab contents after a url is executed. + * After the url is executed, the selected tab contents are displayes using the staticContents function. + */ + const dynamicContents = (name, url) => { + window.miqJqueryRequest(url).then(() => { + staticContents(name); + setData({ loading: false }); + }); + }; + + /** Function to hande tab click events. */ + const onTabSelect = (name) => { + if (!data.loading) { + miqSparkleOn(); + const config = configuration(name); + return config && config.url ? dynamicContents(name, config.url) : staticContents(name); + } + return data; + }; + + /** Function to render the tabs from the tabLabels props */ + const renderTabs = () => tabLabels.map(({ name, text }) => ( + onTabSelect(name)} /> + )); + + return ( + + {renderTabs()} + + ); +}; + +export default MiqCustomTab; + +MiqCustomTab.propTypes = { + containerId: PropTypes.string.isRequired, + tabLabels: PropTypes.arrayOf(PropTypes.shape({ + name: PropTypes.string, + text: PropTypes.string, + })).isRequired, + type: PropTypes.string.isRequired, +}; diff --git a/app/javascript/components/miq-data-table/helper.js b/app/javascript/components/miq-data-table/helper.js index 31d1bc2ddeb0..01fd0b1e1aae 100644 --- a/app/javascript/components/miq-data-table/helper.js +++ b/app/javascript/components/miq-data-table/helper.js @@ -5,6 +5,8 @@ export const CellElements = { icon: 'icon', image: 'image', text: 'text', + textinput: 'is_textinput', + toggle: 'is_toggle', }; /** Click events in a cell. */ @@ -36,6 +38,8 @@ export const isNumber = (data) => typeof (data) === 'number'; export const isNull = (data) => (data === null); export const hasImage = (keys, data) => keys.includes(CellElements.image) && data.image && data.image.length > 0; export const hasButton = (keys) => keys.includes(CellElements.button); +export const hasTextInput = (keys) => keys.includes(CellElements.textinput); +export const hasToggle = (keys) => keys.includes(CellElements.toggle); export const hasText = (data) => Object.keys(data).includes(CellElements.text); const hasValue = (data) => Object.keys(data).includes('value'); diff --git a/app/javascript/components/miq-data-table/index.jsx b/app/javascript/components/miq-data-table/index.jsx index 9ab0774ebc76..0fdcbed094aa 100644 --- a/app/javascript/components/miq-data-table/index.jsx +++ b/app/javascript/components/miq-data-table/index.jsx @@ -143,7 +143,7 @@ const MiqDataTable = ({ {...getRowProps({ row })} title={(item && item.clickable) ? __('Click to view details') : ''} className={classNameRow(item)} - tabIndex={(item && item.clickable === false) ? '' : '0'} + tabIndex={(item && item.clickable === false) ? '' : index.toString()} onKeyPress={(event) => onCellClick(row, CellAction.itemClick, event)} > {rowCheckBox && renderRowCheckBox(getSelectionProps, row)} diff --git a/app/javascript/components/miq-data-table/miq-table-cell.jsx b/app/javascript/components/miq-data-table/miq-table-cell.jsx index 62cadb363105..5910fe779ea1 100644 --- a/app/javascript/components/miq-data-table/miq-table-cell.jsx +++ b/app/javascript/components/miq-data-table/miq-table-cell.jsx @@ -1,9 +1,9 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Button, TableCell } from 'carbon-components-react'; +import { Button, TableCell, TextInput, Toggle } from 'carbon-components-react'; import classNames from 'classnames'; import { - CellAction, hasIcon, hasImage, hasButton, isObject, isArray, isNumber, decimalCount, + CellAction, hasIcon, hasImage, hasButton, hasTextInput, hasToggle, isObject, isArray, isNumber, decimalCount, } from './helper'; const MiqTableCell = ({ @@ -92,10 +92,46 @@ const MiqTableCell = ({

); + /** Function to render a Text Box inside cell. */ + /* eslint-disable no-eval */ + const cellTextInput = (item, id) => ( +
+ +
+ ); + + /** Function to render a Toggle inside cell. */ + /* eslint-disable no-eval */ + const cellToggle = (item, id) => ( +
+ (item.ontoggle() ? item.ontoggle() : undefined)} + disabled={item.disabled} + tabIndex={0} + /> +
+ ); + /** Determines which component has to be rendered inside a cell. * Also to determine if a click event necesseary for a cell or its component . */ const cellComponent = () => { - const { data } = cell; + const { data, id } = cell; const keys = Object.keys(data); const content = { component: '', cellClick: !!onCellClick, showText: true }; if (isObject(data)) { @@ -108,6 +144,10 @@ const MiqTableCell = ({ if (hasButton(keys)) return { ...content, component: cellButton(data), cellClick: false }; + if (hasToggle(keys)) return { ...content, component: cellToggle(data, id), cellClick: false }; + + if (hasTextInput(keys)) return { ...content, component: cellTextInput(data, id), cellClick: false }; + return { ...content, component: cellText() }; } return { ...content, component: cellText() }; diff --git a/app/javascript/components/miq-structured-list/helpers.js b/app/javascript/components/miq-structured-list/helpers.js index 63d160026b42..2269d83bff81 100644 --- a/app/javascript/components/miq-structured-list/helpers.js +++ b/app/javascript/components/miq-structured-list/helpers.js @@ -1,4 +1,17 @@ /* eslint-disable no-eval */ +import { TreeViewRedux } from '../tree-view'; +import CatalogResource from '../data-tables/catalog-resource'; + +export const InputTypes = { + TEXTAREA: 'text_area', + CHECKBOX: 'checkbox', + COMPONENT: 'component', +}; + +export const DynamicReactComponents = { + TREE_VIEW_REDUX: TreeViewRedux, + CATALOG_RESOURCE: CatalogResource, +}; const dataType = (data) => (data ? data.constructor.name.toString() : undefined); diff --git a/app/javascript/components/miq-structured-list/index.jsx b/app/javascript/components/miq-structured-list/index.jsx index c7ad1a76f07a..f7d5fb178c43 100644 --- a/app/javascript/components/miq-structured-list/index.jsx +++ b/app/javascript/components/miq-structured-list/index.jsx @@ -10,10 +10,11 @@ import { StructuredListCell, Accordion, AccordionItem, - TextArea, Link, } from 'carbon-components-react'; + import MiqStructuredListHeader from './miq-structured-list-header'; +import MiqStructuredListInputs from './miq-structured-list-inputs'; import { rowClickEvent, isObject, isArray, isSubItem, hasClickEvents, hasInput, } from './helpers'; @@ -31,7 +32,7 @@ const MiqStructuredList = ({ /** Function to render an icon in the cell. */ const renderIcon = (row) => (
- +
); @@ -105,12 +106,8 @@ const MiqStructuredList = ({
); - const renderInputContent = (row) => { - if (row.value.input === 'text_area') { - return (