diff --git a/apps/dashboard/app/apps/app_recategorizer.rb b/apps/dashboard/app/apps/app_recategorizer.rb new file mode 100644 index 0000000000..512b71f4ea --- /dev/null +++ b/apps/dashboard/app/apps/app_recategorizer.rb @@ -0,0 +1,22 @@ +# Wrapper around OodApp to override category and subcategory +# This is to create artificial groups and subgroups for a custom navigation +class AppRecategorizer < SimpleDelegator + + def initialize(ood_app, category: nil, subcategory: nil) + super(ood_app) + @inner_category = category + @inner_subcategory = subcategory + end + + def category + inner_category + end + + def subcategory + inner_subcategory + end + + private + + attr_reader :inner_category, :inner_subcategory +end \ No newline at end of file diff --git a/apps/dashboard/app/apps/nav_bar.rb b/apps/dashboard/app/apps/nav_bar.rb new file mode 100644 index 0000000000..9d7193a9a8 --- /dev/null +++ b/apps/dashboard/app/apps/nav_bar.rb @@ -0,0 +1,112 @@ +# Creates navigation items based on user configuration. +# +class NavBar + + STATIC_LINKS = { + all_apps: "layouts/nav/all_apps", + featured_apps: "layouts/nav/featured_apps", + sessions: "layouts/nav/sessions", + } + + def self.items(nav_config) + nav_config.map do |nav_item| + if nav_item.is_a?(String) + item_from_token(nav_item) + elsif nav_item.is_a?(Hash) + if nav_item.fetch(:links, nil) + extend_group(nav_menu(nav_item)) + elsif nav_item.fetch(:url, nil) + extend_link(nav_link(nav_item, nil, nil)) + elsif nav_item.fetch(:apps, nil) + matched_apps = nav_apps(nav_item, nil, nil) + extend_link(matched_apps.first.links.first) if matched_apps.first && matched_apps.first.links.first + elsif nav_item.fetch(:profile, nil) + extend_link(nav_profile(nav_item, nil, nil)) + end + end + end.flatten.compact + end + + private + + def self.nav_menu(hash_item) + menu_title = hash_item.fetch(:title, '') + menu_items = hash_item.fetch(:links, []) + + group_title = '' + apps = menu_items.map do |item| + if item.is_a?(String) + item = { apps: item } + end + + group_title = item.fetch(:group, group_title) + if item.fetch(:url, nil) + nav_link(item, menu_title, group_title) + elsif item.fetch(:apps, nil) + nav_apps(item, menu_title, group_title) + elsif item.fetch(:profile, nil) + nav_profile(item, menu_title, group_title) + end + end.flatten.compact + + OodAppGroup.new(apps: apps, title: menu_title, sort: false) + end + + def self.nav_link(item, category, subcategory) + OodAppLink.new(item).categorize(category: category, subcategory: subcategory) + end + + def self.nav_apps(item, category, subcategory) + app_configs = Array.wrap(item.fetch(:apps, [])) + app_configs.map do |config_string| + matched_apps = Router.pinned_apps_from_token(config_string, SysRouter.apps) + matched_apps.map do |reg_app| + AppRecategorizer.new(reg_app, category: category, subcategory: subcategory) + end + end.flatten + end + + def self.nav_profile(item, category, subcategory) + profile = item.fetch(:profile) + profile_data = item.clone + profile_data[:title] = profile unless item.fetch(:title, nil) + profile_data[:url] = Rails.application.routes.url_helpers.settings_path('settings[profile]' => profile) + profile_data[:data] = { method: 'post' } + profile_data[:new_tab] = false + OodAppLink.new(profile_data).categorize(category: category, subcategory: subcategory) + end + + def self.item_from_token(token) + static_link_template = STATIC_LINKS.fetch(token.to_sym, nil) + if static_link_template + return NavItemDecorator.new({}, static_link_template) + end + + matched_apps = Router.pinned_apps_from_token(token, SysRouter.apps) + if matched_apps.size == 1 + extend_link(matched_apps.first.links.first) + elsif matched_apps.size > 1 + extend_group(OodAppGroup.groups_for(apps: matched_apps).first) + else + group = OodAppGroup.groups_for(apps: SysRouter.apps).select { |g| g.title.downcase == token.downcase }.first + group.nil? ? nil : extend_group(group) + end + end + + def self.extend_group(item) + NavItemDecorator.new(item, 'layouts/nav/group') + end + + def self.extend_link(item) + NavItemDecorator.new(item, 'layouts/nav/link') + end + + class NavItemDecorator < SimpleDelegator + attr_reader :partial_path + + def initialize(nav_item, partial_path) + super(nav_item) + @partial_path = partial_path + end + end +end \ No newline at end of file diff --git a/apps/dashboard/app/apps/ood_app_group.rb b/apps/dashboard/app/apps/ood_app_group.rb index 6ac2a14745..f554ffbbf9 100644 --- a/apps/dashboard/app/apps/ood_app_group.rb +++ b/apps/dashboard/app/apps/ood_app_group.rb @@ -1,10 +1,11 @@ class OodAppGroup - attr_accessor :apps, :title + attr_accessor :apps, :title, :sort - def initialize(title: "", apps: [], nav_limit: nil) + def initialize(title: "", apps: [], nav_limit: nil, sort: true) @apps = apps @title = title @nav_limit = nav_limit + @sort = sort end def has_apps? @@ -38,14 +39,25 @@ def nav_limit @nav_limit || apps.size end + def to_h + { + :group => self, + :apps => apps, + :title => title, + :nav_limit => nav_limit, + :sort => sort + } + end + # given an array of apps, group those apps by app category (or the attribute) # specified by 'group_by', sorting both groups and apps arrays by title - def self.groups_for(apps: [], group_by: :category, nav_limit: nil) - apps.group_by { |app| + def self.groups_for(apps: [], group_by: :category, nav_limit: nil, sort: true) + groups = apps.group_by { |app| app.respond_to?(group_by) ? app.send(group_by) : app.metadata[group_by] }.map { |k,v| - OodAppGroup.new(title: k, apps: v.sort_by { |a| a.title }, nav_limit: nav_limit) - }.sort_by { |g| [ g.title.nil? ? 1 : 0, g.title ] } # make sure that the ungroupable app is always last + OodAppGroup.new(title: k, apps: sort ? v.sort_by { |a| a.title } : v, nav_limit: nav_limit) + } + sort ? groups.sort_by { |g| [ g.title.nil? ? 1 : 0, g.title ] } : groups # make sure that the ungroupable app is always last end # select a subset of groups by the specified array of titles diff --git a/apps/dashboard/app/apps/ood_app_link.rb b/apps/dashboard/app/apps/ood_app_link.rb index a5056ff44b..beec978f58 100644 --- a/apps/dashboard/app/apps/ood_app_link.rb +++ b/apps/dashboard/app/apps/ood_app_link.rb @@ -23,5 +23,36 @@ def initialize(config = {}) def new_tab? @new_tab end + + def to_h + instance_variables.each_with_object({}) do |var, hash| + hash[var.to_s.gsub('@', '').to_sym] = instance_variable_get(var) + end + end + + def categorize(category: nil, subcategory: nil) + LinkCategorizer.new(self, category: category, subcategory: subcategory) + end + + private + + # make an OodAppLink look like an OodApp + class LinkCategorizer < SimpleDelegator + attr_reader :category, :subcategory + + def initialize(link, category: nil, subcategory: nil) + super(link) + @category = category + @subcategory = subcategory + end + + def links + [self] + end + + def metadata + {} + end + end end diff --git a/apps/dashboard/app/controllers/application_controller.rb b/apps/dashboard/app/controllers/application_controller.rb index 1a462a7c4d..140b94456f 100644 --- a/apps/dashboard/app/controllers/application_controller.rb +++ b/apps/dashboard/app/controllers/application_controller.rb @@ -5,7 +5,7 @@ class ApplicationController < ActionController::Base before_action :set_user, :set_user_configuration, :set_pinned_apps, :set_nav_groups, :set_announcements before_action :set_my_balances, only: [:index, :new, :featured] - before_action :set_featured_group + before_action :set_featured_group, :set_custom_navigation def set_user @user = CurrentUser @@ -15,6 +15,10 @@ def set_user_configuration @user_configuration ||= UserConfiguration.new(request_hostname: request.hostname) end + def set_custom_navigation + @nav_bar = NavBar.items(@user_configuration.nav_bar) + end + def set_nav_groups #TODO: for AweSim, what if we added the shared apps here? @nav_groups = filter_groups(sys_app_groups) diff --git a/apps/dashboard/app/helpers/application_helper.rb b/apps/dashboard/app/helpers/application_helper.rb index bbf15daeb5..6d3c0d1db3 100644 --- a/apps/dashboard/app/helpers/application_helper.rb +++ b/apps/dashboard/app/helpers/application_helper.rb @@ -20,12 +20,13 @@ def restart_url # @param icon [String] favicon icon name (i.e. "refresh" "for "fa "fa-refresh") # @param url [#to_s, nil] url to access # @param role [String] app role i.e. "vdi", "shell", etc. - # @param method [String] change the method used in this link. + # @param data [Hash] data parameter for the link_to helper. # @return nil if url not set or the HTML string for the bootstrap nav link - def nav_link(title, icon, url, target: '', role: nil, method: nil) + def nav_link(title, icon, url, new_tab: false, role: nil, data: nil) if url + icon_uri = URI("fa://#{icon}") render partial: 'layouts/nav/link', - locals: { title: title, faicon: icon, url: url.to_s, target: target, role: role, method: method } + locals: { title: title, class: 'dropdown-item', icon_uri: icon_uri, url: url.to_s, new_tab: new_tab, role: role, data: data } end end @@ -80,7 +81,7 @@ def profile_links def profile_link(profile_info) profile_id = profile_info[:id] - nav_link(profile_info.fetch(:name, profile_id), profile_info.fetch(:icon, "user"), settings_path("settings[profile]" => profile_id), method: "post") if profile_id + nav_link(profile_info.fetch(:name, profile_id), profile_info.fetch(:icon, "user"), settings_path("settings[profile]" => profile_id), data: {method: "post"}) if profile_id end def custom_css_paths diff --git a/apps/dashboard/app/models/user_configuration.rb b/apps/dashboard/app/models/user_configuration.rb index 5ade1ab3e6..5b5bad3bd4 100644 --- a/apps/dashboard/app/models/user_configuration.rb +++ b/apps/dashboard/app/models/user_configuration.rb @@ -60,6 +60,8 @@ class UserConfiguration # Navigation properties ConfigurationProperty.with_boolean_mapper(name: :show_all_apps_link, default_value: false, read_from_env: true, env_names: ['SHOW_ALL_APPS_LINK']), + # New navigation definition property + ConfigurationProperty.property(name: :nav_bar, default_value: []), ].freeze def initialize(request_hostname: nil) diff --git a/apps/dashboard/app/views/layouts/application.html.erb b/apps/dashboard/app/views/layouts/application.html.erb index 42d23c6905..4555066f09 100644 --- a/apps/dashboard/app/views/layouts/application.html.erb +++ b/apps/dashboard/app/views/layouts/application.html.erb @@ -37,10 +37,16 @@