ManageIQ/manageiq-ui-classic

View on GitHub
app/presenters/tree_builder.rb

Summary

Maintainability
C
7 hrs
Test Coverage
A
99%
class TreeBuilder
  include TreeKids

  attr_reader :name, :options, :tree_nodes, :bs_tree

  def self.class_for_type(type)
    raise('Obsolete tree type.') if type == :filter
    @x_tree_node_classes ||= {}
    @x_tree_node_classes[type] ||= LEFT_TREE_CLASSES[type].constantize
  end

  def initialize(name, sandbox, build = true, **_params)
    @tree_state = TreeState.new(sandbox)
    @sb = sandbox # FIXME: some subclasses still access @sb

    @locals_for_render  = {}
    @name               = name.to_sym # includes _tree
    @options            = tree_init_options
    @tree_nodes         = []

    add_to_sandbox
    build_tree if build
  end

  def node_by_tree_id(id)
    model, rec_id, prefix = self.class.extract_node_model_and_id(id)

    case model
    when 'Hash' # create a fake hash node
      {:type => prefix, :id => rec_id, :full_id => id}
    when nil # no model, probably super() called from a redefinition
      nil
    else
      model.constantize.find(rec_id)
    end
  end

  # Get the children of a tree node that is being expanded (autoloaded)
  def x_get_child_nodes(id)
    parents = [] # FIXME: parent ids should be provided on autoload as well

    object = node_by_tree_id(id)

    # Save node as open
    open_node(id)

    x_get_tree_objects(object, false, parents).map do |o|
      x_build_node(o, id)
    end
  end

  # The possible options are
  # * full_ids - whether to generate full node IDs or not
  # * open_all - expand all expandable nodes
  # * lazy - is the tree lazily-loadable
  # * checkboxes - show checkboxes for the nodes
  # * allow_reselect - fire the onclick event if a selected node is reselected
  # * three_checks - hierarchically check the parent if all children are checked
  # * post_check - some kind of post-processing hierarchical checks
  # * silent_activate - whether to activate the active_node silently or not (by default for explorers)
  def tree_init_options
    $log.warn("MIQ(#{self.class.name}) - TreeBuilder descendants should have their own tree_init_options")
    {}
  end

  # Get nodes model (folder, Vm, Cluster, etc)
  def self.get_model_for_prefix(node_prefix)
    X_TREE_NODE_PREFIXES[node_prefix]
  end

  def self.get_prefix_for_model(model)
    model = model.to_s unless model.kind_of?(String)
    X_TREE_NODE_PREFIXES_INVERTED[model]
  end

  def self.build_node_id(record)
    prefix = get_prefix_for_model(record.class.base_model)
    "#{prefix}-#{record.id}"
  end

  # return this nodes model and record id
  def self.extract_node_model_and_id(node_id)
    prefix, record_id = node_id.split("_").last.split('-')
    model = get_model_for_prefix(prefix)
    [model, record_id, prefix]
  end

  def locals_for_render
    @locals_for_render.update(:select_node => @tree_state.x_node(@name).to_s)
  end

  def reload!
    build_tree
  end

  def open_node(id)
    opened_nodes.push(id) unless opened_nodes.include?(id)
  end

  def expand_node?(key)
    # A based on the tree state, a node should be expanded in three cases:
    # - the open_all setting is present
    # - it has been already expanded
    # - the node is set as active_node
    !!@options[:open_all] || opened_nodes.include?(key) || @tree_state.x_tree(@name)[:active_node] == key
  end

  # Add child nodes to a tree below node 'id'
  def self.tree_add_child_nodes(sandbox:, klass_name:, name:, id:)
    tree = klass_name.constantize.new(name, sandbox, false)
    tree.x_get_child_nodes(id)
  end

  private

  # Post-process partial checkbox state
  def post_check(tree)
    stack = tree.map(&:itself)
    nodes = []
    parents = []

    # Collect nodes in a flat structure
    while stack.any?
      node = stack.pop
      nodes.push(node)

      if node[:nodes]&.any?
        parents.push(node)
        node[:nodes].each { |child| stack.push(child) }
      end
    end

    # Process nodes top-to-bottom
    nodes.reverse!
    while nodes.any?
      parent = nodes.pop

      parent[:nodes]&.each do |child|
        if parent.try(:[], :state).try(:[], :checked)
          child[:state] ||= {}
          child[:state][:checked] = true
        end
      end
    end

    # Process nodes bottom-to-top
    while parents.any?
      parent = parents.pop
      parent[:state] ||= {}
      parent[:state][:checked] = parent[:nodes].map { |node| node.try(:[], :state).try(:[], :checked) }.reduce { |acc, curr| acc == curr ? acc : 'undefined' }
    end
  end

  def build_tree
    @tree_nodes = x_build_tree
    post_check(@tree_nodes) if @options[:post_check] && @options[:three_checks]
    active_node_set(@tree_nodes)
    @bs_tree = @tree_nodes.to_json
    @locals_for_render = set_locals_for_render
  end

  def opened_nodes
    # If the open_nodes is not set, Array(nil) will always return a different object, therefore,
    # for stateless trees it has a performance drawback of creating a new array after each time
    # this method is called. By memoizing the first result, only a single array gets allocated.
    @opened_nodes ||= Array(@tree_state.x_tree(@name)[:open_nodes])
  end

  # Subclass this method if active node on initial load is different than root node.
  def active_node_set(tree_nodes)
    @tree_state.x_node_set(tree_nodes.first[:key], @name) unless @tree_state.x_node(@name)
  end

  def add_to_sandbox
    @tree_state.add_tree(
      @options.reverse_merge(
        :tree       => @name,
        :klass_name => self.class.name,
        :open_nodes => []
      )
    )
  end

  def set_locals_for_render
    {
      :tree_id            => "#{@name}box",
      :tree_name          => @name.to_s,
      :bs_tree            => @bs_tree,
      :checkboxes         => @options[:checkboxes],
      :autoload           => @options[:lazy],
      :allow_reselect     => @options[:allow_reselect],
      :hierarchical_check => @options[:three_checks],
      :onclick            => @options[:onclick],
      :oncheck            => @options[:oncheck],
      :click_url          => @options[:click_url],
      :check_url          => @options[:check_url],
      :silent_activate    => @options[:silent_activate]
    }.compact
  end

  # Build an explorer tree, from scratch
  def x_build_tree
    nodes = x_get_tree_objects(nil, false, []).map do |child|
      # already a node? FIXME: make a class for node
      if child.kind_of?(Hash) && child.key?(:text) && child.key?(:key) && child.key?(:image)
        child
      else
        x_build_node(child, nil)
      end
    end

    return nodes unless respond_to?(:root_options, true)

    [TreeNode::Root.new(root_options, nil, self).to_h.merge(:nodes => nodes)]
  end

  # determine if this is an ancestry node, and return the approperiate object
  #
  # @param object [Hash,Array,Object] object that is possibly an ancestry node
  # @returns [Object, Hash] The object of interest from this ancestry tree, and the children
  #
  # Ancestry trees are of the form:
  #
  #   {Object => {Object1 => {}, Object2 => {Object2a => {}}}}
  #
  # Since `build_tree` and x_build_node uses enumeration, it comes in as:
  #   [Object, {Object1 => {}, Object2 => {Object2a => {}}}]
  #
  def object_from_ancestry(object)
    if object.kind_of?(Array) && object.size == 2 && (object[1].kind_of?(Hash) || object[1].kind_of?(Array))
      object
    else
      [object, nil]
    end
  end

  def x_get_tree_objects(parent, count_only, parents)
    children_or_count = parent.nil? ? x_get_tree_roots : x_get_tree_kids(parent, count_only, parents)
    children_or_count || (count_only ? 0 : [])
  end

  # @param object the current node object (or an ancestry tree hash)
  # @param pid [String|Nil] parent id root nodes are nil
  # @returns [Hash] display hash for this node and all children
  def x_build_node(object, pid)
    parents = pid.to_s.split('_')

    object, ancestry_kids = object_from_ancestry(object)
    node = TreeNode.new(object, pid, self)
    override(node, object) if self.class.method_defined?(:override) || self.class.private_method_defined?(:override)

    if ancestry_kids || node.expanded || !@options[:lazy]
      (ancestry_kids || x_get_tree_objects(object, false, parents)).each do |o|
        node.nodes.push(x_build_node(o, node.key))
      end
    elsif x_get_tree_objects(object, true, parents).positive?
      node.lazy = true # set child flag if children exist
    end

    node.to_h
  end

  # Handle custom tree nodes (object is a Hash)
  def x_get_tree_custom_kids(_object, count_only)
    count_only ? 0 : []
  end

  def count_only_or_objects(count_only, objects, sort_by = nil)
    if count_only
      objects.respond_to?(:order) ? objects.except(:order).size : objects.size
    elsif sort_by.kind_of?(Proc)
      objects.sort_by(&sort_by)
    elsif sort_by
      objects.sort_by { |o| Array(sort_by).collect { |sb| o.deep_send(sb).to_s.downcase } }
    else
      objects
    end
  end

  def count_only_or_objects_filtered(count_only, objects, sort_by = nil, options = {}, &block)
    count_only_or_objects(count_only, Rbac.filtered(objects, options), sort_by, &block)
  end

  def prefixed_title(prefix, title)
    ViewHelper.capture do
      ViewHelper.concat_tag(:strong, "#{prefix}:")
      ViewHelper.concat(' ')
      ViewHelper.concat(title)
    end
  end

  LEFT_TREE_CLASSES = {
    # Overview
    ## Reports
    ### Saved Reports
    :savedreports                    => "TreeBuilderReportSavedReports",
    ### Reports
    :reports                         => "TreeBuilderReportReports",
    ### Schedules
    :schedules                       => "TreeBuilderReportSchedules",
    ### Dashboards
    :db                              => "TreeBuilderReportDashboards",
    ### Dashboard Widgets
    :widgets                         => "TreeBuilderReportWidgets",
    ### Edit Report Menus
    :roles                           => "TreeBuilderReportRoles",
    ### Import/Export
    :export                          => "TreeBuilderReportExport",
    ## Timelines (TODO)

    ## Utilization
    ### Utilization
    :utilization                     => "TreeBuilderUtilization",

    ## Chargeback
    ### Reports
    :cb_reports                      => "TreeBuilderChargebackReports",

    ## Catalogs
    ### Service Catalogs
    :svccat                          => "TreeBuilderServiceCatalog",
    ### Catalog Items
    :sandt                           => "TreeBuilderCatalogItems",
    ### Orchestration Templates
    :ot                              => "TreeBuilderOrchestrationTemplates",
    ### Catalogs
    :stcat                           => "TreeBuilderCatalogs",

    ## Workloads
    ### VMs & Instances
    :vms_instances_filter            => "TreeBuilderVmsInstancesFilter",
    ### Templates & Images
    :templates_images_filter         => "TreeBuilderTemplatesImagesFilter",

    # Compute
    ## Clouds
    ### Instances
    #### Instances by provider
    :instances                       => "TreeBuilderInstances",
    #### Images by provider
    :images                          => "TreeBuilderImages",
    #### Instances
    :instances_filter                => "TreeBuilderInstancesFilter",
    #### Images
    :images_filter                   => "TreeBuilderImagesFilter",

    ## Infrastructure
    ### Virtual Machines
    #### VMs & Templates
    :vandt                           => "TreeBuilderVandt",
    #### VMs
    :vms_filter                      => "TreeBuilderVmsFilter",
    #### Templates
    :templates_filter                => "TreeBuilderTemplateFilter",
    ### Datastores
    #### Datastores
    :storage                         => "TreeBuilderStorage",
    #### Datastore Clusters
    :storage_pod                     => "TreeBuilderStoragePod",
    ### PXE
    #### PXE Servers
    :pxe_servers                     => "TreeBuilderPxeServers",
    #### Customization Templates
    :customization_templates         => "TreeBuilderPxeCustomizationTemplates",
    #### System Image Types
    :pxe_image_types                 => "TreeBuilderPxeImageTypes",
    #### ISO Datastores
    :iso_datastores                  => "TreeBuilderIsoDatastores",

    ### Networking
    #### Switches
    :infra_networking                => "TreeBuilderInfraNetworking",

    # Configuration
    ## Management
    ### Providers
    :configuration_manager_providers => "TreeBuilderConfigurationManager",

    # Control
    ## Explorer
    ### Policies
    :policy                          => "TreeBuilderPolicy",
    ### Actions
    :action                          => "TreeBuilderAction",
    ### Alert Profiles
    :alert_profile                   => "TreeBuilderAlertProfile",

    ## Automate
    ### Explorer
    #### Datastore
    :ae                              => "TreeBuilderAeClass",
    ### Customization
    #### Provisioning Dialogs
    :old_dialogs                     => "TreeBuilderProvisioningDialogs",
    #### Service Dialogs
    :dialogs                         => "TreeBuilderServiceDialogs",
    #### Buttons
    :ab                              => "TreeBuilderButtons",
    #### Import/Export
    :dialog_import_export            => "TreeBuilderAeCustomization",
    ### Generic Objects
    :generic_object_definition       => "TreeBuilderGenericObjectDefinition",

    # OPS (Configuration)
    ## Settings
    :settings                        => "TreeBuilderOpsSettings",
    ## Access Control
    :rbac                            => "TreeBuilderOpsRbac",
    ## Diagnostics
    :diagnostics                     => "TreeBuilderOpsDiagnostics",
  }.freeze

  # Tree node prefixes for generic explorers
  X_TREE_NODE_PREFIXES = {
    "a"    => "MiqAction",
    "aec"  => "MiqAeClass",
    "aei"  => "MiqAeInstance",
    "aem"  => "MiqAeMethod",
    "aen"  => "MiqAeNamespace",
    "ap"   => "MiqAlertSet",
    "asr"  => "AssignedServerRole",
    "az"   => "AvailabilityZone",
    "azu"  => "ManageIQ::Providers::Azure::CloudManager::OrchestrationTemplate",
    "azs"  => "ManageIQ::Providers::AzureStack::CloudManager::OrchestrationTemplate",
    "at"   => "ManageIQ::Providers::ExternalAutomationManager",
    "cl"   => "Classification",
    "cfp"  => "ConfigurationScriptPayload",
    "cw"   => "ConfigurationWorkflow",
    "cnt"  => "Container",
    "co"   => "Condition",
    "cbg"  => "CustomButtonSet",
    "cb"   => "CustomButton",
    "cfn"  => "ManageIQ::Providers::Amazon::CloudManager::OrchestrationTemplate",
    "cm"   => "Compliance",
    "cd"   => "ComplianceDetail",
    "cp"   => "ConfigurationProfile",
    "cr"   => "ChargebackRate",
    "cs"   => "ConfiguredSystem",
    "ct"   => "CustomizationTemplate",
    "dc"   => "Datacenter",
    "dg"   => "Dialog",
    "ds"   => "Storage",
    "dsc"  => "StorageCluster",
    "e"    => "ExtManagementSystem",
    "ev"   => "MiqEventDefinition",
    "c"    => "EmsCluster",
    "csa"  => "ManageIQ::Providers::AnsibleTower::AutomationManager::ConfiguredSystem",
    "f"    => "EmsFolder",
    "g"    => "MiqGroup",
    "gd"   => "GuestDevice",
    "god"  => "GenericObjectDefinition",
    "h"    => "Host",
    "hot"  => "ManageIQ::Providers::Openstack::CloudManager::OrchestrationTemplate",
    "isi"  => "IsoImage",
    "l"    => "Lan",
    "me"   => "MiqEnterprise",
    "mr"   => "MiqRegion",
    "msc"  => "MiqSchedule",
    "ms"   => "MiqSearch",
    "odg"  => "MiqDialog",
    "ot"   => "OrchestrationTemplate",
    "phys" => "PhysicalServer",
    "pi"   => "PxeImage",
    "pit"  => "PxeImageType",
    "ps"   => "PxeServer",
    "pp"   => "MiqPolicySet",
    "p"    => "MiqPolicy",
    "rep"  => "MiqReport",
    "rr"   => "MiqReportResult",
    "svr"  => "MiqServer",
    "ur"   => "MiqUserRole",
    "r"    => "ResourcePool",
    "s"    => "Service",
    "sa"   => "StorageAdapter",
    'sn'   => 'Snapshot',
    "sl"   => "MiqScsiLun",
    "sg"   => "MiqScsiTarget",
    "sis"  => "ScanItemSet",
    "role" => "ServerRole",
    "st"   => "ServiceTemplate",
    "stc"  => "ServiceTemplateCatalog",
    "sr"   => "ServiceResource",
    "sw"   => "Switch",
    "t"    => "MiqTemplate",
    "tn"   => "Tenant",
    "u"    => "User",
    "v"    => "Vm",
    "vap"  => "ManageIQ::Providers::Vmware::CloudManager::OrchestrationTemplate",
    "vnf"  => "ManageIQ::Providers::Openstack::CloudManager::VnfdTemplate",
    "wi"   => "WindowsImage",
    "ws"   => "MiqWidgetSet",
    "xx"   => "Hash", # For custom (non-CI) nodes, specific to each tree
    "z"    => "Zone"
  }.freeze

  X_TREE_NODE_PREFIXES_INVERTED = X_TREE_NODE_PREFIXES.invert
end