Skip to content

Commit

Permalink
Added full navigation definition in UserConfiguration (Group/Link) (#…
Browse files Browse the repository at this point in the history
…2270)

Added full navigation definition in UserConfiguration.  This means that the navigation bar is totally customizable from the configuration. It even adds support for links created directly through configuration.  With the combination of app tokens and links one could completely redefine the navigation bar.
  • Loading branch information
abujeda authored Sep 7, 2022
1 parent 44baafd commit 1b38c8e
Show file tree
Hide file tree
Showing 19 changed files with 582 additions and 28 deletions.
22 changes: 22 additions & 0 deletions apps/dashboard/app/apps/app_recategorizer.rb
Original file line number Diff line number Diff line change
@@ -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
112 changes: 112 additions & 0 deletions apps/dashboard/app/apps/nav_bar.rb
Original file line number Diff line number Diff line change
@@ -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
24 changes: 18 additions & 6 deletions apps/dashboard/app/apps/ood_app_group.rb
Original file line number Diff line number Diff line change
@@ -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?
Expand Down Expand Up @@ -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
Expand Down
31 changes: 31 additions & 0 deletions apps/dashboard/app/apps/ood_app_link.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

6 changes: 5 additions & 1 deletion apps/dashboard/app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
9 changes: 5 additions & 4 deletions apps/dashboard/app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions apps/dashboard/app/models/user_configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
14 changes: 10 additions & 4 deletions apps/dashboard/app/views/layouts/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,16 @@

<div class="collapse navbar-collapse" id="navbar">
<ul class="navbar-nav mr-auto">
<%= render partial: 'layouts/nav/featured_apps', locals: { group: @featured_group } if @featured_group.present? %>
<%= render partial: 'layouts/nav/group', collection: @nav_groups %>
<%= render partial: 'layouts/nav/sessions', nav_groups: @nav_groups if Configuration.app_development_enabled? || @nav_groups.any?(&:has_batch_connect_apps?) %>
<%= render partial: 'layouts/nav/all_apps' if @user_configuration.show_all_apps_link %>
<% if @nav_bar.empty? %>
<%= render partial: 'layouts/nav/featured_apps' if @featured_group.present? %>
<%= render partial: 'layouts/nav/group', collection: @nav_groups %>
<%= render partial: 'layouts/nav/sessions', nav_groups: @nav_groups if Configuration.app_development_enabled? || @nav_groups.any?(&:has_batch_connect_apps?) %>
<%= render partial: 'layouts/nav/all_apps' if @user_configuration.show_all_apps_link %>
<% else %>
<% @nav_bar.each do |nav_item| %>
<%= render partial: nav_item.partial_path, locals: nav_item.to_h %>
<% end %>
<% end %>
</ul>

<ul class="navbar-nav">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<ul class="dropdown-menu dropdown-menu-right" role="menu">
<%= nav_link(t('dashboard.nav_restart_server'), "sync", restart_url) %>
<%= nav_link(t('dashboard.nav_develop_docs'), "book", Configuration.developer_docs_url, target: "_blank") %>
<%= nav_link(t('dashboard.nav_develop_docs'), "book", Configuration.developer_docs_url, new_tab: true) %>
<%= nav_link(products_title(:dev), "cog", products_path(type: "dev")) %>
<% if Configuration.app_sharing_enabled? %>
<%= nav_link(products_title(:usr), "share-alt", products_path(type: "usr")) %>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<% group = @featured_group %>
<li class="nav-item dropdown" title="<%= group.title %>" >
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><%= group.title %><span class="caret"></span></a>

Expand Down
2 changes: 1 addition & 1 deletion apps/dashboard/app/views/layouts/nav/_group.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><%= group.title %><span class="caret"></span></a>

<ul class="dropdown-menu">
<% OodAppGroup.groups_for(apps: group.apps, group_by: :subcategory).each_with_index do |g, index| %>
<% OodAppGroup.groups_for(apps: group.apps, group_by: :subcategory, sort: group.sort).each_with_index do |g, index| %>
<%= content_tag(:li, nil, class: ["dropdown-divider"], role: "separator") if index > 0 %>
<%= content_tag(:li, g.title, class: ["dropdown-header"]) unless g.title.empty? %>
<% g.apps.each do |app| %>
Expand Down
10 changes: 5 additions & 5 deletions apps/dashboard/app/views/layouts/nav/_help_dropdown.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
</a>

<ul class="dropdown-menu dropdown-menu-right" role="menu">
<%= nav_link(t('dashboard.nav_help_support'), "question-circle", support_url, target: "_blank") %>
<%= nav_link(t('dashboard.nav_help_docs'), "info-circle", docs_url, target: "_blank") %>
<%= nav_link(t('dashboard.nav_help_change_password'), "key", passwd_url, target: "_blank") %>
<%= nav_link(t('dashboard.nav_help_two_factor'), "mobile-alt", configure_2fa_url, target: "_blank") %>
<%= nav_link(t('dashboard.nav_help_support'), "question-circle", support_url, new_tab: true) %>
<%= nav_link(t('dashboard.nav_help_docs'), "info-circle", docs_url, new_tab: true) %>
<%= nav_link(t('dashboard.nav_help_change_password'), "key", passwd_url, new_tab: true) %>
<%= nav_link(t('dashboard.nav_help_two_factor'), "mobile-alt", configure_2fa_url, new_tab: true) %>
<% if help_custom_url %>
<%= nav_link(t('dashboard.nav_help_custom'), "question-circle", help_custom_url, target: "_blank" ) %>
<%= nav_link(t('dashboard.nav_help_custom'), "question-circle", help_custom_url, new_tab: true) %>
<% end %>
<%= nav_link(t('dashboard.nav_restart_server'), "sync", restart_url) %>
<% unless profile_links.empty? %>
Expand Down
26 changes: 20 additions & 6 deletions apps/dashboard/app/views/layouts/nav/_link.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,28 @@
locals:
title - required - text for the link
url - required - link url string
faicon - optional, default "cog" - font awesome icon to use
target - optional, default "_self" - link target
class - optional, default "nav-link" - main css class for the link.
icon_uri - optional, default "fas://cog" - font awesome icon to use
new_tab - optional, default false - link target
role - optional, default ""
method - optional, data-method for the link
data - optional, data parameter for the link_to helper
%>
<%= tag.a href: local_assigns.fetch(:url), target: local_assigns.fetch(:target, '_self'), class: "dropdown-item m-auto #{local_assigns.fetch(:role, nil)}", 'data-method': local_assigns.fetch(:method, nil) do %>
<%= tag.i class: "fas fa-fw fa-#{local_assigns.fetch(:faicon, 'cog')}" %>
<%
title = local_assigns.fetch(:title, 'No title')
url = local_assigns.fetch(:url)
%>
<%=
link_to(
url,
title: title,
class: "#{local_assigns.fetch(:class, 'nav-link')} m-auto #{local_assigns.fetch(:role, nil)}",
target: local_assigns.fetch(:new_tab, false) ? "_blank" : nil,
aria: ({ current: ('page' if (current_page?(url))) }),
data: local_assigns.fetch(:data, nil)
) do
%>
<%= icon_tag(local_assigns.fetch(:icon_uri, URI("fas://cog"))) %>
<%= tag.span do %>
<%= local_assigns.fetch(:title, 'No title') %>
<%= title %>
<% end %>
<% end %>
11 changes: 11 additions & 0 deletions apps/dashboard/test/apps/app_recategorizer_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
require 'test_helper'

class AppRecategorizerTest < ActiveSupport::TestCase

test "should create an OodApp with category and subcategory" do
result = AppRecategorizer.new(stub(), category: "test_category", subcategory: "test_subcategory")
assert_equal "test_category", result.category
assert_equal "test_subcategory", result.subcategory
end

end
Loading

0 comments on commit 1b38c8e

Please sign in to comment.