diff --git a/app/presenters/menu/item.rb b/app/presenters/menu/item.rb index 0e324fd66c5..82d5e3140ba 100644 --- a/app/presenters/menu/item.rb +++ b/app/presenters/menu/item.rb @@ -1,5 +1,5 @@ module Menu - Item = Struct.new(:id, :name, :feature, :rbac_feature, :href, :type, :parent_id, :defaults) do + Item = Struct.new(:id, :name, :feature, :rbac_feature, :href, :type, :parent_id, :defaults, :icon) do extend ActiveModel::Naming def self.base_class @@ -10,7 +10,7 @@ def self.base_model model_name end - def initialize(an_id, a_name, features, rbac_feature, href, type = :default, parent_id = nil, defaults = nil) + def initialize(an_id, a_name, features, rbac_feature, href, type = :default, parent_id = nil, defaults = nil, icon = nil) super @parent = nil @name = a_name.kind_of?(Proc) ? a_name : -> { a_name } @@ -58,5 +58,21 @@ def parent_path def item(item_id) item_id == id ? self : nil end + + def placement + @parent&.placement || :default + end + + def contains_item_id?(item_id) + item_id == id + end + + def subsection? + false + end + + def items + [] + end end end diff --git a/app/presenters/menu/manager.rb b/app/presenters/menu/manager.rb index c6c98e9d5e3..6546596ccc3 100644 --- a/app/presenters/menu/manager.rb +++ b/app/presenters/menu/manager.rb @@ -26,6 +26,8 @@ def items def item(item_id) @menu.each do |menu_section| + return menu_section if menu_section.kind_of?(Menu::Item) && menu_section.id == item_id + menu_section.items.each do |el| the_item = el.item(item_id) return the_item if the_item.present? @@ -73,6 +75,7 @@ def initialize load_default_items load_custom_items(Menu::YamlLoader) load_custom_items(Menu::CustomLoader) + load_custom_items(Menu::SettingsLoader) end def merge_sections(sections) @@ -100,10 +103,12 @@ def merge_sections(sections) def merge_items(items) items.each do |item| parent = @id_to_section[item.parent_id] - raise InvalidMenuDefinition, 'Invalid parent' if parent.nil? - - parent.items << item - item.parent = parent + if parent.nil? + @menu << item + else + parent.items << item + item.parent = parent + end end end @@ -123,7 +128,7 @@ def preprocess_sections @id_to_section = @menu.index_by(&:id) # recursively add subsections to the @id_to_section hash @menu.each do |section| - section.preprocess_sections(@id_to_section) + section.preprocess_sections(@id_to_section) if section.respond_to?(:preprocess_sections) end end diff --git a/app/presenters/menu/settings_loader.rb b/app/presenters/menu/settings_loader.rb new file mode 100644 index 00000000000..cae13998413 --- /dev/null +++ b/app/presenters/menu/settings_loader.rb @@ -0,0 +1,39 @@ +module Menu + class SettingsLoader < YamlLoader + include Singleton + include Vmdb::Logging + + def self.load + instance.load_from_settings + end + + def load_from_settings + begin + settings = ::Settings.ui.custom_menu + items = (settings || []).map { |i| create_custom_item(HashWithIndifferentAccess.new(i)) } + rescue StandardError => e + # if we encounter an error while loading the menus, we ignore the whole settings + _log.error("Error loading custom menu from settings: #{e}") + _log.error("Settings were: #{settings}") + return [[], []] + end + [[], items] + end + + private + + def create_custom_item(item) + # only alow: + # * items, + # * displayed in the iframe, + # * and at the top menu level. + create_custom_menu_item( + item.merge( + 'type' => 'items', + 'item_type' => 'big_iframe', + 'parent' => nil + ) + ) + end + end +end diff --git a/app/presenters/menu/yaml_loader.rb b/app/presenters/menu/yaml_loader.rb index e6127833dbb..0745af66a2d 100644 --- a/app/presenters/menu/yaml_loader.rb +++ b/app/presenters/menu/yaml_loader.rb @@ -26,15 +26,26 @@ def load_custom_item(file_name) end end + # In case `rbac` is a Hash, convert keys to symbols. + # Example: { :feature => 'vm_explorer', :any => true } + # + # Else assume string and return: + # { :feature => rbac } + def parse_rbac_property(rbac) + rbac.kind_of?(Hash) ? rbac.symbolize_keys : { :feature => rbac } + end + def create_custom_menu_item(properties) - rbac = properties['rbac'].each_with_object({}) { |(k, v), h| h[k.to_sym] = v } - item_type = properties.key?('item_type') ? properties['item_type'].to_sym : :default - %w[id name rbac parent].each do |property| + %w[id name rbac].each do |property| if properties[property].blank? raise Menu::Manager::InvalidMenuDefinition, "incomplete definition -- missing #{property}" end end + + rbac = parse_rbac_property(properties['rbac']) + item_type = properties.fetch('item_type', :default).to_sym + item = Item.new( properties['id'], properties['name'], @@ -42,7 +53,9 @@ def create_custom_menu_item(properties) rbac, properties['href'], item_type, - properties['parent'].to_sym + properties['parent']&.to_sym, + nil, + properties['icon'] ) item end @@ -53,6 +66,7 @@ def create_custom_menu_section(properties) before = properties.key?('before') ? properties['before'].to_sym : nil section_type = properties.key?('section_type') ? properties['section_type'].to_sym : :default href = properties.key?('href') ? properties['href'].to_sym : nil + # no parent_id here? Section.new(properties['id'].to_sym, properties['name'], icon, [], placement, before, section_type, href) end end diff --git a/app/presenters/tree_builder_ops_rbac_features.rb b/app/presenters/tree_builder_ops_rbac_features.rb index dbf1904b453..a42b6c67978 100644 --- a/app/presenters/tree_builder_ops_rbac_features.rb +++ b/app/presenters/tree_builder_ops_rbac_features.rb @@ -19,7 +19,9 @@ def initialize(name, sandbox, build, **params) private def x_get_tree_roots(count_only = false, _options) - top_nodes = Menu::Manager.items.select { |section| Vmdb::PermissionStores.instance.can?(section.id) } + top_nodes = Menu::Manager.items.select do |section| + Vmdb::PermissionStores.instance.can?(section.id) && !section.kind_of?(Menu::Item) + end top_nodes += %w[all_vm_rules api_exclusive sui ops_explorer].collect do |additional_feature| MiqProductFeature.obj_features[additional_feature] && diff --git a/app/views/layouts/_vertical_navbar.html.haml b/app/views/layouts/_vertical_navbar.html.haml index 5850bdc01ca..4f0a0af24e3 100644 --- a/app/views/layouts/_vertical_navbar.html.haml +++ b/app/views/layouts/_vertical_navbar.html.haml @@ -1,7 +1,15 @@ .nav-pf-vertical.nav-pf-vertical-with-sub-menus.nav-pf-vertical-collapsible-menus %ul#maintab.list-group - Menu::Manager.menu do |menu_section| - - if menu_section.visible? + - next unless menu_section.visible? + - if Menu::Item === menu_section + %li.list-group-item{:class => item_nav_class(menu_section), :id => "menu_item_#{menu_section.id}"} + %a{menu_section.link_params} + %span{:class => menu_section.icon} + %span.list-group-item-value + = _(menu_section.name) + + - else %li.list-group-item.secondary-nav-item-pf{"data-target" => "#menu-#{menu_section.id}", :class => section_nav_class(menu_section)} %a{menu_section.link_params} %span{:class => menu_section.icon} diff --git a/config/settings.yml b/config/settings.yml new file mode 100644 index 00000000000..8f647b088f5 --- /dev/null +++ b/config/settings.yml @@ -0,0 +1,2 @@ +:ui: + :custom_menu: diff --git a/spec/presenters/menu/menu_manager_spec.rb b/spec/presenters/menu/menu_manager_spec.rb index a93c92fdabc..b6e961ba519 100644 --- a/spec/presenters/menu/menu_manager_spec.rb +++ b/spec/presenters/menu/menu_manager_spec.rb @@ -38,4 +38,15 @@ end end end + + context "menu" do + it "knows about custom items from settings" do + ::Settings.ui.custom_menu = settings_custom_items + count = 0 + Menu::Manager.menu do |item| + count += 1 if item.kind_of?(Menu::Item) && item.name =~ /^Custom Item/ + end + expect(count).to eq(2) + end + end end diff --git a/spec/presenters/menu/settings_loader_spec.rb b/spec/presenters/menu/settings_loader_spec.rb new file mode 100644 index 00000000000..348916bd1b6 --- /dev/null +++ b/spec/presenters/menu/settings_loader_spec.rb @@ -0,0 +1,10 @@ +describe Menu::SettingsLoader do + include Spec::Support::MenuHelper + it "loads custom menu items" do + ::Settings.ui.custom_menu = settings_custom_items + sections, items = described_class.load + + expect(sections.length).to be(0) + expect(items.length).to be(2) + end +end diff --git a/spec/support/menu_helper.rb b/spec/support/menu_helper.rb index b4fb1afecbd..1db2df0d430 100644 --- a/spec/support/menu_helper.rb +++ b/spec/support/menu_helper.rb @@ -36,6 +36,27 @@ def section_file def item_file create_temp_file(ITEM_DEF) end + + def settings_custom_items + [ + { + :type => 'item', + :icon => 'fa fa-bug', + :id => 'custom_i1', + :name => 'Custom Item 1', + :href => 'https://www.redhat.com', + :rbac => 'vm_explorer' + }, + { + :type => 'item', + :icon => 'pficon pficon-help', + :id => 'custom_i2', + :name => 'Custom Item 2', + :href => 'https://www.hmpf.cz', + :rbac => 'vm_explorer' + } + ] + end end end end