From 6fe569349889caf117caaf9ae9179c5eee27191f Mon Sep 17 00:00:00 2001 From: Syphax bouazzouni Date: Wed, 11 Sep 2024 15:11:12 +0200 Subject: [PATCH 01/28] Feature: Federate browse page (#621) * simplify the header component to use content instead of a new section * update dropdown component to a custom title section instead of text * update browse page to use Dropdown component not bootsrap one * add color option to square badge component * add text and bg colors options to ontology browse card component * add federation helper code and config sample * add portals filters in the browse page * use federation helpers to get federation ontologies information * remove the filter using index code from submission filter as no used * refactor the filter_using_data function to be faster by using an hash * update the browse analytics cache to change depending on portals * add categories and groups ids on hover to know its origin when federated * use the last part of ids for browse counts independently of its origin * remove binding.pry from final federation code * add an error message if one of the external portal is down * remove $FEDERATED_PORTALS and use existent $PORTALS_INSTANCES variable * handle the merging of the same ontology from different portals * fix groups and categories selection at browse page init --- Gemfile | 2 +- Gemfile.lock | 12 ++- .../stylesheets/components/chip_button.scss | 1 + app/components/display/header_component.rb | 3 +- .../dropdown_container_component.rb | 5 +- .../dropdown_container_component.html.haml | 5 +- .../ontology_browse_card_component.rb | 30 +++++- .../ontology_browse_card_component.html.haml | 48 ++++++--- app/components/square_badge_component.rb | 6 +- app/controllers/application_controller.rb | 3 + app/controllers/concerns/submission_filter.rb | 100 ++++++++++-------- app/controllers/ontologies_controller.rb | 9 +- app/helpers/application_helper.rb | 2 +- app/helpers/federation_helper.rb | 97 +++++++++++++++++ app/views/home/index.html.haml | 4 +- app/views/home/tools.html.haml | 9 +- .../ontologies/browser/_ontologies.html.haml | 18 +++- app/views/ontologies/browser/browse.html.haml | 9 +- .../ontologies/sections/_metadata.html.haml | 16 ++- config/bioportal_config_env.rb.sample | 62 +++++++---- config/initializers/ontologies_api_client.rb | 1 + config/locales/en.yml | 2 +- config/locales/fr.yml | 2 +- 23 files changed, 324 insertions(+), 122 deletions(-) create mode 100644 app/helpers/federation_helper.rb diff --git a/Gemfile b/Gemfile index c40e3640c5..781e6379e2 100644 --- a/Gemfile +++ b/Gemfile @@ -96,7 +96,7 @@ gem 'flag-icons-rails', '~> 3.4' gem 'iso-639', '~> 0.3.6' # Custom API client -gem 'ontologies_api_client', git: 'https://github.com/ontoportal-lirmm/ontologies_api_ruby_client.git', branch: 'master' +gem 'ontologies_api_client', git: 'https://github.com/ontoportal-lirmm/ontologies_api_ruby_client.git', branch: 'development' # Ruby 2.7.8 pinned gems (to remove when migrating to Ruby >= 3.0) gem 'ffi', '~> 1.16.3' diff --git a/Gemfile.lock b/Gemfile.lock index 51f97081af..2c0afc74bc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,10 +1,10 @@ GIT remote: https://github.com/ontoportal-lirmm/ontologies_api_ruby_client.git - revision: 24fb2549f7b69841e052491439bc8375ed5acfd9 - branch: master + revision: 15ad7d41d9a414ed3570c073c2b75a3fb4cc19cf + branch: development specs: ontologies_api_client (2.2.0) - activesupport + activesupport (~> 7.0.4) excon faraday faraday-excon (~> 2.0.0) @@ -12,6 +12,8 @@ GIT lz4-ruby multi_json oj + parallel + request_store spawnling (= 2.1.5) GEM @@ -420,6 +422,8 @@ GEM regexp_parser (2.9.2) reline (0.5.10) io-console (~> 0.5) + request_store (1.7.0) + rack (>= 1.4) rest-client (2.1.0) http-accept (>= 1.7.0, < 2.0) http-cookie (>= 1.0.2, < 2.0) @@ -485,7 +489,7 @@ GEM simplecov-cobertura (2.1.0) rexml simplecov (~> 0.19) - simplecov-html (0.13.0) + simplecov-html (0.13.1) simplecov_json_formatter (0.1.4) snaky_hash (2.0.1) hashie diff --git a/app/assets/stylesheets/components/chip_button.scss b/app/assets/stylesheets/components/chip_button.scss index 1b19a29815..18916b7fec 100644 --- a/app/assets/stylesheets/components/chip_button.scss +++ b/app/assets/stylesheets/components/chip_button.scss @@ -10,6 +10,7 @@ } .chip-button-component-container { display: inline-block; + text-wrap: nowrap !important; } .chip-button-component-container svg path{ fill: var(--primary-color); diff --git a/app/components/display/header_component.rb b/app/components/display/header_component.rb index 76784068e1..d8b8ecebd0 100644 --- a/app/components/display/header_component.rb +++ b/app/components/display/header_component.rb @@ -4,7 +4,6 @@ class Display::HeaderComponent < ViewComponent::Base include ComponentsHelper - renders_one :text def initialize(text: nil, tooltip: nil) super @@ -14,7 +13,7 @@ def initialize(text: nil, tooltip: nil) def call content_tag(:div, class: 'header-component') do - out = content_tag(:p, text || @text) + out = content_tag(:p, content&.html_safe || @text) if @info && !@info.empty? out = out + info_tooltip(content_tag(:div, @info, style: 'max-width: 300px')) end diff --git a/app/components/dropdown_container_component.rb b/app/components/dropdown_container_component.rb index e06b3fa7ac..f9d8ce38d2 100644 --- a/app/components/dropdown_container_component.rb +++ b/app/components/dropdown_container_component.rb @@ -2,13 +2,16 @@ class DropdownContainerComponent < ViewComponent::Base renders_one :empty_state - def initialize(title:, id:, tooltip:nil, is_open: false) + renders_one :title + + def initialize(title: nil, id:, tooltip:nil, is_open: false) super @title = title @id = id @tooltip = tooltip @is_open = is_open end + def open_class @is_open ? "show" : "" end diff --git a/app/components/dropdown_container_component/dropdown_container_component.html.haml b/app/components/dropdown_container_component/dropdown_container_component.html.haml index 7b9d4965e8..398f3199a0 100644 --- a/app/components/dropdown_container_component/dropdown_container_component.html.haml +++ b/app/components/dropdown_container_component/dropdown_container_component.html.haml @@ -1,6 +1,9 @@ .dropdown-container .dropdown-title-bar{"data-toggle" => "collapse", "data-target" => "##{@id}"} - = render Display::HeaderComponent.new(text: @title, tooltip: @tooltip) + - if title? + = title + - else + = render Display::HeaderComponent.new(text: @title, tooltip: @tooltip) = image_tag("summary/arrow-down.svg", class: 'ml-2') .collapse{id: @id, class: open_class} diff --git a/app/components/ontology_browse_card_component.rb b/app/components/ontology_browse_card_component.rb index 84251d7f0d..dfc2c05d0e 100644 --- a/app/components/ontology_browse_card_component.rb +++ b/app/components/ontology_browse_card_component.rb @@ -1,14 +1,40 @@ # frozen_string_literal: true class OntologyBrowseCardComponent < ViewComponent::Base - include OntologiesHelper + include ApplicationHelper, OntologiesHelper, FederationHelper - def initialize(ontology: nil) + def initialize(ontology: nil, onto_link: nil, text_color: nil, bg_light_color: nil, portal_name: nil) super @ontology = ontology + @text_color = text_color + @bg_light_color = bg_light_color + @onto_link = onto_link || "/ontologies/#{@ontology[:acronym]}" if @ontology + @portal_name = portal_name end def ontology @ontology end + + def external_ontology? + !internal_ontology?(@ontology[:id]) || (Array(@ontology[:sources]).size > 1) + end + + def onto_link + @onto_link + end + + def style_text + external_ontology? ? "color: #{@text_color} !important" : '' + end + + def portal_color + @text_color + end + alias :color :portal_color + + def style_bg + external_ontology? ? "#{style_text} ; background-color: #{@bg_light_color}" : '' + end + end diff --git a/app/components/ontology_browse_card_component/ontology_browse_card_component.html.haml b/app/components/ontology_browse_card_component/ontology_browse_card_component.html.haml index aaf97e85fd..6a08ccc836 100644 --- a/app/components/ontology_browse_card_component/ontology_browse_card_component.html.haml +++ b/app/components/ontology_browse_card_component/ontology_browse_card_component.html.haml @@ -3,9 +3,11 @@ .d-flex .browse-ontology-description .browse-ontology-title-bar - %a.browse-ontology-title{:href => "/ontologies/#{ontology[:acronym]}", data: {'turbo': 'false'}} + %a.browse-ontology-title{:href => onto_link, data: {'turbo': 'false'} , style: style_text} = ontology[:name]+" ("+ontology[:acronym]+")" = private_ontology_icon(ontology[:private]) + - if external_ontology? + = render Display::InfoTooltipComponent.new(text: "Federated ontology from #{ontology[:sources].map{|x| link_to(x,x)}.join(', ')}", icon: 'external-link.svg') - if session[:user]&.admin? - ontology_status = status_string(ontology) = render Display::InfoTooltipComponent.new(text: ontology_status, icon: submission_status_icons(ontology_status)) @@ -21,27 +23,32 @@ %p.browse-fair-title = t('components.fair_score') .browse-progress-bar - .browse-faire-progress{:style => "width: #{ontology[:normalizedFairScore].to_s+"%"}"} + .browse-faire-progress{:style => "width: #{ontology[:normalizedFairScore].to_s+"%"}; #{color ? 'background-color:' + color : ''}"} %p.browse-fair-score = ontology[:fairScore] - %a.browse-fair-details{:href => "/ontologies/#{ontology[:acronym]}#fair-details", 'data-turbo': 'false'}= t('components.details_details') + %a.browse-fair-details{:href => "#{onto_link}#fair-details", 'data-turbo': 'false', style: style_text}= t('components.details_details') - .browse-ontology-cards - = render SquareBadgeComponent.new(label: t('components.classes'), count: ontology[:class_count_formatted], link: "/ontologies/#{ontology[:acronym]}?p=classes" ) + .browse-ontology-cards{style: color ? "color: #{color} !important" : ''} + = render SquareBadgeComponent.new(label: t('components.classes'), + count: ontology[:class_count_formatted], + link: "#{onto_link}?p=classes", + color: color) = render SquareBadgeComponent.new(label: ontology[:format] == 'SKOS' ? t('components.concepts') : t('components.instances'), - count: ontology[:individual_count_formatted], - link: "/ontologies/#{ontology[:acronym]}?p=#{ontology[:format] == 'SKOS' ? "classes" : "instances"}") + count: ontology[:individual_count_formatted], color: color, + link: "#{onto_link}?p=#{ontology[:format] == 'SKOS' ? "classes" : "instances"}") - = render SquareBadgeComponent.new(label: t('components.projects'), count: ontology[:project_count], link: "/ontologies/#{ontology[:acronym]}#projects_section" ) + = render SquareBadgeComponent.new(label: t('components.projects'), count: ontology[:project_count], color: color, + link: "#{onto_link}#projects_section" ) - = render SquareBadgeComponent.new(label: t('components.notes'), count: ontology[:note_count], link: "/ontologies/#{ontology[:acronym]}?p=notes" ) + = render SquareBadgeComponent.new(label: t('components.notes'), count: ontology[:note_count], color: color, + link: "#{onto_link}?p=notes" ) - .d-flex.align-items-baseline.mt-1 + .d-flex.w-100.mt-1.flex-wrap - if ontology[:creationDate] %span.mr-1 - = render ChipButtonComponent.new(type: "clickable") do + = render ChipButtonComponent.new(type: "clickable", style: style_bg) do %span.mr-1= t('components.submitted') %span.browse-uploaded-date{data:{controller: 'timeago', 'timeago-datetime-value': ontology[:creationDate], 'timeago-add-suffix-value': 'true'}} - if ontology[:contact] @@ -52,19 +59,32 @@ - if ontology[:released] - date = render DateTimeFieldComponent.new(value: ontology[:released]) %span{data:{controller:'tooltip'}, title: t('components.creation_date', date: date)} - = render ChipButtonComponent.new(type: "clickable") do + = render ChipButtonComponent.new(type: "clickable", style: style_bg) do = DateTime.parse(date).year rescue date - if ontology[:format] %span.mx-1 - = render ChipButtonComponent.new(type: "clickable") do + = render ChipButtonComponent.new(type: "clickable", style: style_bg) do = ontology[:format] - if ontology_retired?(ontology) %span.mx-1 = ontology_retired_badge(ontology) - if ontology[:viewOfOnt] %span.mx-1{data:{controller:'tooltip'}, title: t('components.view_of_the_ontology', ontology: ontology[:viewOfOnt].split('/').last )} - = render ChipButtonComponent.new(type: "clickable", text: t('components.view')) + = render ChipButtonComponent.new(type: "clickable", text: t('components.view'), style: style_bg) + + - if external_ontology? + - ontology[:sources].each do |id| + - config = ontology_portal_config(id)&.last || internal_portal_config(id) || {} + - unless config.blank? + %div.mx-1{title: content_tag(:div, "Source #{config[:name]}"), data:{controller: 'tooltip', 'tooltip-interactive-value': 'true'}} + = render ChipButtonComponent.new(type: "clickable" , style: style_bg) do + = link_to ontoportal_ui_link(id), target: '_top' do + %span.d-inline + %span.mr-1 + = inline_svg 'logo-white.svg', width: "20", height: "20" + %span + = config[:name] - if session[:user]&.admin? %div.mx-1{title: content_tag(:div, debug(ontology), style: 'height: 300px; overflow: scroll'), data:{controller: 'tooltip', 'tooltip-interactive-value': 'true'}} diff --git a/app/components/square_badge_component.rb b/app/components/square_badge_component.rb index 9d85a8c34f..bfbc95c6db 100644 --- a/app/components/square_badge_component.rb +++ b/app/components/square_badge_component.rb @@ -2,15 +2,17 @@ class SquareBadgeComponent < ViewComponent::Base - def initialize(label: , count: ,link: nil) + def initialize(label: , count: ,link: nil, color: nil) @label = label @count = count @link = link + @color = color end + def call return if @count.to_i.zero? - link_to(@link, class: 'browse-onology-card', 'data-turbo' => 'false') do + link_to(@link, class: 'browse-onology-card', 'data-turbo' => 'false', style: @color ? "color: #{@color} !important; border-color: #{@color}" : "") do concat(content_tag(:p, @count, class: 'browse-card-number')) concat(content_tag(:p, @label, class: 'browse-card-text')) end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index f75a395b75..291e2ae33d 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -175,6 +175,9 @@ def bp_config_json def rest_url helpers.rest_url end + def request_portals + helpers.request_portals + end def parse_response_body(response) return nil if response.nil? diff --git a/app/controllers/concerns/submission_filter.rb b/app/controllers/concerns/submission_filter.rb index 45746e36e2..b37beeb8d4 100644 --- a/app/controllers/concerns/submission_filter.rb +++ b/app/controllers/concerns/submission_filter.rb @@ -1,7 +1,7 @@ module SubmissionFilter extend ActiveSupport::Concern - include SearchContent + include FederationHelper BROWSE_ATTRIBUTES = ['ontology', 'submissionStatus', 'description', 'pullLocation', 'creationDate', 'contact', 'released', 'naturalLanguage', 'hasOntologyLanguage', @@ -21,17 +21,19 @@ def submissions_paginate_filter(params) filter_params = params.permit(@filters.keys).to_h init_filters(params) - @analytics = Rails.cache.fetch("ontologies_analytics-#{Time.now.year}-#{Time.now.month}") do + @analytics = Rails.cache.fetch("ontologies_analytics-#{Time.now.year}-#{Time.now.month}-#{request_portals.join('-')}") do helpers.ontologies_analytics end @ontologies = LinkedData::Client::Models::Ontology.all(include: 'all', also_include_views: true, display_links: false, display_context: false) + @ontologies, @errors = @ontologies.partition { |x| !x.errors } + # get fair scores of all ontologies @fair_scores = fairness_service_enabled? ? get_fair_score('all') : nil @total_ontologies = @ontologies.size - search_backend = params[:search_backend] + params = { query: @search, status: request_params[:status], show_views: @show_views, @@ -43,16 +45,12 @@ def submissions_paginate_filter(params) groups: request_params[:group], categories: request_params[:hasDomain], formats: request_params[:hasOntologyLanguage] } + submissions = filter_submissions(@ontologies, **params) - if search_backend.eql?('index') - submissions = filter_using_index(**params) - submissions = @ontologies.map{ |ont| ontology_hash(ont, submissions) } - else - submissions = filter_using_data(@ontologies, **params) - end + submissions = merge_by_acronym(submissions) - submissions = sort_submission_by(submissions, @sort_by, @search) + submissions = sort_submission_by(submissions, @sort_by, @search) @page = paginate_submissions(submissions, request_params[:page].to_i, request_params[:pagesize].to_i) @@ -67,49 +65,51 @@ def ontologies_with_filters_url(filters, page: 1, count: false) private - def filter_using_index(query:, status:, show_views:, private_only:, languages:, page_size:, formality_level:, is_of_type:, groups:, categories:, formats:) - search_ontologies( - query: query, - status: status, - show_views: show_views, - private_only: private_only, - languages: languages, - page_size: page_size, - formality_level: formality_level, - is_of_type: is_of_type, - groups: groups, categories: categories, - formats: formats - ) + def merge_by_acronym(submissions) + merged_submissions = [] + submissions.group_by { |x| x[:ontology]&.acronym }.each do |acronym, ontologies| + if ontologies.size.eql?(1) + ontology = ontologies.first + else + ontology = ontologies.select { |x| helpers.internal_ontology?(x[:id]) }.first || ontologies.first + end + ontology[:sources] = ontologies.map { |x| x[:id] } + merged_submissions << ontology + end + merged_submissions end - def filter_using_data(ontologies, query:, status:, show_views:, private_only:, languages:, page_size:, formality_level:, is_of_type:, groups:, categories:, formats:) + def filter_submissions(ontologies, query:, status:, show_views:, private_only:, languages:, page_size:, formality_level:, is_of_type:, groups:, categories:, formats:) submissions = LinkedData::Client::Models::OntologySubmission.all(include: BROWSE_ATTRIBUTES.join(','), also_include_views: true, display_links: false, display_context: false) + + submissions = submissions.map { |x| x[:ontology] ? [x[:ontology][:id], x] : nil }.compact.to_h + submissions = ontologies.map { |ont| ontology_hash(ont, submissions) } submissions.map do |s| - out = ((s.ontology.viewingRestriction.eql?('public') && !private_only) || private_only && s.ontology.viewingRestriction.eql?('private')) - out = out && (groups.blank? || (s.ontology.group.map { |x| helpers.link_last_part(x) } & groups.split(',')).any?) - out = out && (categories.blank? || (s.ontology.hasDomain.map { |x| helpers.link_last_part(x) } & categories.split(',')).any?) - out = out && (status.blank? || status.eql?('alpha,beta,production,retired') || status.split(',').include?(s.status)) - out = out && (formats.blank? || formats.split(',').any? { |f| s.hasOntologyLanguage.eql?(f) }) - out = out && (is_of_type.blank? || is_of_type.split(',').any? { |f| helpers.link_last_part(s.isOfType).eql?(f) }) - out = out && (formality_level.blank? || formality_level.split(',').any? { |f| helpers.link_last_part(s.hasFormalityLevel).eql?(f) }) - out = out && (languages.blank? || languages.split(',').any? { |f| s.naturalLanguage.any? { |n| helpers.link_last_part(n).eql?(f) } }) - out = out && (s.ontology.viewOf.blank? || (show_views && !s.ontology.viewOf.blank?)) - - out = out && (query.blank? || [s.description, s.ontology.name, s.ontology.acronym].any? { |x| (x|| '').downcase.include?(query.downcase) }) + out = ((s[:ontology].viewingRestriction.eql?('public') && !private_only) || private_only && s[:ontology].viewingRestriction.eql?('private')) + out = out && (groups.blank? || (s[:ontology].group.map { |x| helpers.link_last_part(x) } & groups.split(',')).any?) + out = out && (categories.blank? || (s[:ontology].hasDomain.map { |x| helpers.link_last_part(x) } & categories.split(',')).any?) + out = out && (status.blank? || status.eql?('alpha,beta,production,retired') || status.split(',').include?(s[:status])) + out = out && (formats.blank? || formats.split(',').any? { |f| s[:hasOntologyLanguage].eql?(f) }) + out = out && (is_of_type.blank? || is_of_type.split(',').any? { |f| helpers.link_last_part(s[:isOfType]).eql?(f) }) + out = out && (formality_level.blank? || formality_level.split(',').any? { |f| helpers.link_last_part(s[:hasFormalityLevel]).eql?(f) }) + out = out && (languages.blank? || languages.split(',').any? { |f| Array(s[:naturalLanguage]).any? { |n| helpers.link_last_part(n).eql?(f) } }) + out = out && (s[:ontology].viewOf.blank? || (show_views && !s[:ontology].viewOf.blank?)) + + out = out && (query.blank? || [s[:description], s[:ontology].name, s[:ontology].acronym].any? { |x| (x || '').downcase.include?(query.downcase) }) if out s[:rank] = 0 next s if query.blank? - s[:rank] += 3 if s.ontology.acronym && s.ontology.acronym.downcase.include?(query.downcase) + s[:rank] += 3 if s[:ontology].acronym && s[:ontology].acronym.downcase.include?(query.downcase) - s[:rank] += 2 if s.ontology.name && s.ontology.name.downcase.include?(query.downcase) + s[:rank] += 2 if s[:ontology].name && s[:ontology].name.downcase.include?(query.downcase) - s[:rank] += 1 if s.description && s.description.downcase.include?(query.downcase) + s[:rank] += 1 if s[:description] && s[:description].downcase.include?(query.downcase) s else @@ -131,7 +131,7 @@ def paginate_submissions(all_submissions, page, size) end def sort_submission_by(submissions, sort_by, query = nil) - return submissions.sort_by { |x| x[:rank] ? -x[:rank] : 0} unless query.blank? + return submissions.sort_by { |x| x[:rank] ? -x[:rank] : 0 } unless query.blank? if sort_by.eql?('visits') submissions = submissions.sort_by { |x| -(x[:popularity] || 0) } @@ -209,13 +209,17 @@ def filters_params(params, includes: BROWSE_ATTRIBUTES.join(','), page: 1, pages @filters[:search] = params[:search] end + unless params[:portals].blank? + @filters[:portals] = params[:portals] + end + request_params.delete(:order_by) if %w[visits fair].include?(request_params[:sort_by].to_s) request_params end def ontology_hash(ont, submissions) o = {} - sub = submissions.select{|x| x.ontology&.id.eql?(ont.id)}.first + sub = submissions[ont.id] o[:ontology] = ont @@ -225,7 +229,7 @@ def ontology_hash(ont, submissions) o[:hasOntologyLanguage] = sub&.hasOntologyLanguage - if sub&.metrics + if sub&.metrics && !sub.metrics.is_a?(String) o[:class_count] = sub.metrics.classes o[:individual_count] = sub.metrics.individuals else @@ -237,10 +241,10 @@ def ontology_hash(ont, submissions) o[:note_count] = ont.notes&.length || 0 o[:project_count] = ont.projects&.length || 0 - o[:popularity] = @analytics[ont.acronym] || 0 - o[:rank] = sub&[:rank] || 0 + o[:popularity] = @analytics[ont.id.split('/').last.to_s] || 0 + o[:rank] = sub ? sub[:rank] : 0 - OpenStruct.new(o) + o end def add_submission_attributes(ont_hash, sub) @@ -346,10 +350,12 @@ def check_id(name_value, objects, name_key) def object_filter(objects, object_name, name_key = 'acronym') checks = params[object_name]&.split(',') || [] - checks = checks.map { |x| check_id(x, objects, name_key) }.compact + checks = checks.map { |x| helpers.link_last_part(check_id(x, objects, name_key)) }.compact - ids = objects.map { |x| x['id'] } + objects.uniq! { |x| helpers.link_last_part(x['id']) } + ids = objects.map { |x| helpers.link_last_part(x['id']) } count = ids.count { |x| checks.include?(x) } + [objects, checks, count] end @@ -370,7 +376,7 @@ def count_objects(ontologies) object_names.each do |name| values = Array(ontology[name]) values.each do |v| - v.gsub!('http://data.bioontology.org', rest_url) + v = helpers.link_last_part(v) objects_count[name] = {} unless objects_count[name] objects_count[name][v] = (objects_count[name][v] || 0) + 1 diff --git a/app/controllers/ontologies_controller.rb b/app/controllers/ontologies_controller.rb index a8c744fe4a..11d0ae2ca5 100644 --- a/app/controllers/ontologies_controller.rb +++ b/app/controllers/ontologies_controller.rb @@ -26,6 +26,8 @@ class OntologiesController < ApplicationController before_action :authorize_and_redirect, :only => [:edit, :update, :create, :new] before_action :submission_metadata, only: [:show] + before_action :set_federated_portals, only: [:index, :ontologies_filter] + KNOWN_PAGES = Set.new(["terms", "classes", "mappings", "notes", "widgets", "summary", "properties", "instances", "schemes", "collections", "sparql"]) EXTERNAL_MAPPINGS_GRAPH = "http://data.bioontology.org/metadata/ExternalMappings" INTERPORTAL_MAPPINGS_GRAPH = "http://data.bioontology.org/metadata/InterportalMappings" @@ -49,8 +51,8 @@ def ontologies_filter streams = [prepend("ontologies_list_view-page-#{@page.page}", partial: 'ontologies/browser/ontologies')] streams += @count_objects.map do |section, values_count| values_count.map do |value, count| - replace("count_#{section}_#{value}") do - helpers.turbo_frame_tag("count_#{section}_#{value}") do + replace("count_#{section}_#{link_last_part(value)}") do + helpers.turbo_frame_tag("count_#{section}_#{link_last_part(value)}") do helpers.content_tag(:span, count.to_s, class: "hide-if-loading #{count.zero? ? 'disabled' : ''}") end end @@ -568,4 +570,7 @@ def search_first_instance_id return !results.blank? ? results.first[:name] : nil end + def set_federated_portals + RequestStore.store[:federated_portals] = params[:portals]&.split(',') + end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index c9fb873758..c134d5d96c 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -41,7 +41,7 @@ def resolve_namespaces def ontologies_analytics begin data = LinkedData::Client::Analytics.last_month.onts - data.map{|x| [x[:ont].to_s, x[:views]]}.to_h + data.map{|x| [x[:ont].split('/').last.to_s, x[:views]]}.to_h rescue StandardError {} end diff --git a/app/helpers/federation_helper.rb b/app/helpers/federation_helper.rb new file mode 100644 index 0000000000..afd60dfa00 --- /dev/null +++ b/app/helpers/federation_helper.rb @@ -0,0 +1,97 @@ +module FederationHelper + + def federated_portals + $FEDERATED_PORTALS ||= LinkedData::Client.settings.federated_portals + end + + def internal_portal_config(id) + return unless internal_ontology?(id) + + { + name: portal_name, + api: rest_url, + apikey: $API_KEY, + ui: $UI_URL, + color: "var(--primary-color)", + 'light-color': 'var(--light-color)', + } + end + + def federated_portal_config(name_key) + federated_portals[name_key.to_sym] + end + + def federated_portal_name(key) + config = federated_portal_config(key) + config ? config[:name] : key + end + + def federated_portal_color(key) + config = federated_portal_config(key) + config[:color] if config + end + + def federated_portal_light_color(key) + config = federated_portal_config(key) + config[:'light-color'] if config + end + + + def ontology_portal_config(id) + rest_url = id.split('/')[0..-3].join('/') + federated_portals.select{|_, config| config[:api].start_with?(rest_url)}.first + end + + def ontology_portal_name(id) + portal_key, _ = ontology_portal_config(id) + portal_key ? federated_portal_name(portal_key) : 'not found' + end + + + def ontology_portal_color(id) + portal_key, _ = ontology_portal_config(id) + federated_portal_color(portal_key) if portal_key + end + + + def ontoportal_ui_link(id) + portal_key, config = ontology_portal_config(id) + return nil unless portal_key + + ui_link = config[:ui] + api_link = config[:api] + + id.gsub(api_link, "#{ui_link}/") rescue id + end + + def internal_ontology?(id) + id.start_with?(rest_url) + end + + def federated_ontology?(id) + !internal_ontology?(id) + end + + def request_portals + portals = RequestStore.store[:federated_portals] || [] + [portal_name] + portals + end + + def request_portals_names + request_portals.map do |x| + config = federated_portal_config(x) + + if config + name = config[:name] + color = config[:color] + elsif portal_name.downcase.eql?(x.downcase) + name = portal_name + color = nil + else + next nil + end + + content_tag(:span, federated_portal_name(name), style: color ? "color: #{color}" : "", class: color ? "" : "text-primary") + end.compact + end +end diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index 83fc781581..be0075bfe3 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -214,10 +214,10 @@ .home-support-items - $PORTALS_INSTANCES&.each do |portal| %div.text-center - = link_to portal[:link], target: '_blank', class: 'home-logo-instances', style: "background-color: #{portal[:color]}" do + = link_to portal[:ui], target: '_blank', class: 'home-logo-instances', style: "background-color: #{portal[:color]}" do = inline_svg 'logo-white.svg', width: "35", height: "26" %p{style: "color: #{portal[:color]}"} - = portal[:portal] + = portal[:name] .home-section .home-section-title diff --git a/app/views/home/tools.html.haml b/app/views/home/tools.html.haml index 941d60ec74..d4f0455b69 100644 --- a/app/views/home/tools.html.haml +++ b/app/views/home/tools.html.haml @@ -3,10 +3,9 @@ = link_to tool[:link], class: "mx-2 d-block", style: "width: 35%" do = render Layout::CardComponent.new do |c| - c.header do |h| - - h.text do - %div.d-flex.flex-column.align-items-center.text-primary.pt-4 - = inline_svg_tag(tool[:icon], height: "50px", width: "50px") - %h3.text-primary.mt-2 - = tool[:title] + %div.d-flex.flex-column.align-items-center.text-primary.pt-4 + = inline_svg_tag(tool[:icon], height: "50px", width: "50px") + %h3.text-primary.mt-2 + = tool[:title] %p.px-4.tool-description = tool[:description] diff --git a/app/views/ontologies/browser/_ontologies.html.haml b/app/views/ontologies/browser/_ontologies.html.haml index bd3aed0cf5..3ca8685bc0 100644 --- a/app/views/ontologies/browser/_ontologies.html.haml +++ b/app/views/ontologies/browser/_ontologies.html.haml @@ -4,11 +4,23 @@ current_page: @page.page, next_page: @page.nextPage) do |c| - if @page.page.eql?(1) - = content_tag(:p, class: "browse-desc-text", style: "margin-bottom: 12px !important;") { "#{t("ontologies.showing_ontologies_size", ontologies_size: @count, analytics_size: @total_ontologies)} (#{sprintf("%.2f", @time)}s)" } - + = content_tag(:p, class: "browse-desc-text", style: "margin-bottom: 12px !important;") do + #{t("ontologies.showing_ontologies_size", ontologies_size: @count, analytics_size: @total_ontologies, portals: request_portals_names.join(', ').html_safe).html_safe} (#{sprintf("%.2f", @time)}s) + - unless @errors.blank? + %div.my-1 + = render Display::AlertComponent.new(type: 'danger') do + - @errors.each do |e| + %div + = e.errors || e - ontologies = c.collection - ontologies.each do |ontology| - = render OntologyBrowseCardComponent.new(ontology: ontology) + - config = ontology_portal_config(ontology[:id])&.last || {} + + = render OntologyBrowseCardComponent.new(ontology: ontology, + portal_name: config[:name], + onto_link: ontoportal_ui_link(ontology[:id]), + text_color: config[:color], + bg_light_color: config[:'light-color']) - c.loader do - ontologies_browse_skeleton - c.error do diff --git a/app/views/ontologies/browser/browse.html.haml b/app/views/ontologies/browser/browse.html.haml index d1979fd359..fd9777e3a8 100644 --- a/app/views/ontologies/browser/browse.html.haml +++ b/app/views/ontologies/browser/browse.html.haml @@ -65,12 +65,17 @@ .browse-filter-checks-container - values.first.each do |object| - title = (key.eql?(:categories) || key.eql?(:groups)) ? nil : '' - = group_chip_component(name: key, object: object, checked: values[1]&.include?(object["id"]) || values[1]&.include?(object["value"]) , title: title) do |c| + = group_chip_component(name: key, object: object, checked: values[1].any?(link_last_part(object["id"])) || values[1].any?(link_last_part(object["value"])) , title: title) do |c| - c.count do %span.badge.badge-light.ml-1 - = turbo_frame_tag "count_#{key}_#{object["id"]}", busy: true + = turbo_frame_tag "count_#{key}_#{link_last_part(object["id"])}", busy: true %span.show-if-loading = render LoaderComponent.new(small:true) + %div{data:{controller: "show-filter-count browse-filters", action: "change->show-filter-count#updateCount change->browse-filters#dispatchFilterEvent"}, id: "portals_filter_container"} + = render DropdownContainerComponent.new(id: "browse-portal-filter", is_open: !request_portals.empty?, title: "Results from external portals") do + .browse-filter-checks-container.px-1 + - federated_portals.each do |key, config| + = group_chip_component(name: "portals", object: { "acronym" => config[:name], "value" => key }, checked: request_portals.include?(key.to_s), title: '') .browse-second-row .browse-search-bar diff --git a/app/views/ontologies/sections/_metadata.html.haml b/app/views/ontologies/sections/_metadata.html.haml index 69aadbc570..be2083e5cb 100755 --- a/app/views/ontologies/sections/_metadata.html.haml +++ b/app/views/ontologies/sections/_metadata.html.haml @@ -22,7 +22,7 @@ = properties_dropdown('person_and_organization',t("ontologies.sections.person_and_organization"),'', @agents_properties) do |values| = horizontal_list_container(values) do |v| = agent_chip_component(v) - + = properties_dropdown('link',t("ontologies.sections.other_links"), t("ontologies.sections.info_tooltip_links") , @links_properties) do |values| = horizontal_list_container(values) do |v| = render LinkFieldComponent.new(value: v, raw: true) @@ -51,11 +51,10 @@ = Array(@content_properties['metadataVoc']).map{|x| metadata_vocabulary_display(x)}.join.html_safe = render Layout::CardComponent.new do |c| - c.header do |h| - - h.text do - = t("ontologies.sections.visits") - - if visits_data(@ontology) - = link_to(@ontology.links["analytics"] + "?apikey=#{get_apikey}&format=csv", title: t("ontologies.sections.download_as_csv")) do - = inline_svg("summary/download.svg", width: '30px', height: '20px') + = t("ontologies.sections.visits") + - if visits_data(@ontology) + = link_to(@ontology.links["analytics"] + "?apikey=#{get_apikey}&format=csv", title: t("ontologies.sections.download_as_csv")) do + = inline_svg("summary/download.svg", width: '30px', height: '20px') = render Layout::ListComponent.new do |l| - l.row do @@ -64,9 +63,8 @@ - unless @ontology.view? = render Layout::CardComponent.new do |d| - d.header do |h| - - h.text do - = t("ontologies.sections.views", acronym: @ontology.acronym) - = new_element_link(t("ontologies.sections.create_new_view"), new_view_path(@ontology.id)) + = t("ontologies.sections.views", acronym: @ontology.acronym) + = new_element_link(t("ontologies.sections.create_new_view"), new_view_path(@ontology.id)) = render Layout::ListComponent.new do |l| - l.row do = render partial: 'ontology_views' diff --git a/config/bioportal_config_env.rb.sample b/config/bioportal_config_env.rb.sample index 277b972015..352805b196 100644 --- a/config/bioportal_config_env.rb.sample +++ b/config/bioportal_config_env.rb.sample @@ -1,3 +1,6 @@ +$DEBUG_API_CLIENT = false +$API_CLIENT_INVALIDATE_CACHE = false + # Organization info $ORG = ENV['ORG'] $ORG_URL = ENV['ORG_URL'] @@ -37,9 +40,6 @@ $NCBO_API_KEY = ENV['NCBO_API_KEY'] $FAIRNESS_DISABLED = ENV['FAIRNESS_DISABLED'] $FAIRNESS_URL = ENV['FAIRNESS_URL'] - - - # Used to define other bioportal that can be mapped to # Example to map to ncbo bioportal : {"ncbo" => {"api" => "http://data.bioontology.org", "ui" => "http://bioportal.bioontology.org", "apikey" => ""} # Then create the mapping using the following class in JSON : "http://purl.bioontology.org/ontology/MESH/C585345": "ncbo:MESH" @@ -211,53 +211,71 @@ $HOME_PAGE_LOGOS = [ } ] + +# Federation configuration $PORTALS_INSTANCES = [ { - color: '#31b403', - portal: 'AgroPortal', - link: 'https://agroportal.lirmm.fr/' + name: 'AgroPortal', + api: 'https://data.agroportal.lirmm.fr', + ui: 'https://agroportal.lirmm.fr/', + color: '#349696', + apikey: '1de0a270-29c5-4dda-b043-7c3580628cd5', + 'light-color': '#F1F6FA', }, { + name: 'BioPortal', + ui: 'https://bioportal.bioontology.org/', + api: 'https://data.bioontology.org/', + apikey: '8b5b7825-538d-40e0-9e9e-5ab9274a9aeb', color: '#234979', - portal: 'BioPortal', - link: 'https://bioportal.bioontology.org/' + 'light-color': '#E9F2FA', }, { + name: 'SIFR BioPortal', + ui: 'https://bioportal.lirmm.fr/', color: '#74a9cb', - portal: 'SIFR BioPortal', - link: 'https://bioportal.lirmm.fr/' }, { + name: 'EcoPortal', + ui: 'https://ecoportal.lifewatch.eu/', + api: 'https://data.ecoportal.lifewatch.eu/', + apikey: "43a437ba-a437-4bf0-affd-ab520e584719", color: '#0d508a', - portal: 'EcoPortal', - link: 'https://ecoportal.lifewatch.eu/' + 'light-color': '#E9F2FA', }, { + name: 'MedPortal', + ui: 'http://medportal.bmicc.cn/', color: '#234979', - portal: 'MedPortal', - link: 'http://medportal.bmicc.cn/' }, { + name: 'MatPortal', + ui: 'https://matportal.org/', color: '#009574', - portal: 'MatPortal', - link: 'https://matportal.org/' }, { + name: 'IndustryPortal', + ui: 'http://industryportal.enit.fr', color: '#1c0f5d', - portal: 'IndustryPortal', - link: 'http://industryportal.enit.fr' }, { + name: 'EarthPortal', + ui: 'https://earthportal.eu/', + api: 'https://data.earthportal.eu/', + apikey: "c9147279-954f-41bd-b068-da9b0c441288", color: '#1e2251', - portal: 'EarthPortal', - link: 'https://earthportal.eu/' + 'light-color': '#F0F5F6' }, { + name: 'BiodivPortal', + ui: 'https://biodivportal.gfbio.org/', + api: 'https://data.biodivportal.gfbio.org/', + apikey: "47a57aa3-7b54-4f34-b695-dbb5f5b7363e", color: '#33691B', - portal: 'BiodivPortal', - link: 'https://biodivportal.gfbio.org/' + 'light-color': '#EBF5F5', } ] + $ONTOPORTAL_WEBSITE_LINK = "https://ontoportal.org/" $ONTOPORTAL_GITHUB_REPO = "https://github.com/ontoportal" diff --git a/config/initializers/ontologies_api_client.rb b/config/initializers/ontologies_api_client.rb index ca8f07e9eb..068da53829 100644 --- a/config/initializers/ontologies_api_client.rb +++ b/config/initializers/ontologies_api_client.rb @@ -7,4 +7,5 @@ config.debug_client = $DEBUG_RUBY_CLIENT || false config.debug_client_keys = $DEBUG_RUBY_CLIENT_KEYS || [] config.apikey = $API_KEY + config.federated_portals = $PORTALS_INSTANCES ? $PORTALS_INSTANCES.map{|x| x[:api] && x[:apikey] ? [x[:name].downcase.to_sym, x] : nil }.compact.to_h : {} end diff --git a/config/locales/en.yml b/config/locales/en.yml index fd04ba863c..79ae1583e5 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1162,7 +1162,7 @@ en: update_the_current_displayed_content: "will update the current displayed content to all the following submissions:" save: Save ontologies: - showing_ontologies_size: "Showing %{ontologies_size} of %{analytics_size}" + showing_ontologies_size: "Showing %{ontologies_size} of %{analytics_size} from %{portals}" filters: Filters no_license: No license view_license: View license diff --git a/config/locales/fr.yml b/config/locales/fr.yml index f537fc23ad..f044708cb8 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -1188,7 +1188,7 @@ fr: save: Sauvegarder ontologies: - showing_ontologies_size: "Affichage de %{ontologies_size} sur %{analytics_size}" + showing_ontologies_size: "Affichage de %{ontologies_size} sur %{analytics_size} de %{portals}" filters: Filtres no_license: Pas de licence view_license: Voir la licence From 1ac463340dfa26c703de0695c99308273b3ca19d Mon Sep 17 00:00:00 2001 From: Syphax bouazzouni Date: Thu, 19 Sep 2024 13:56:07 +0200 Subject: [PATCH 02/28] Feature: Add portals configuration tooltip in homepage (#744) * add portal configuration action and view to home controller * create portal_config_tooltip helper and use in homepage portals section * enhance portal config home section style --------- Co-authored-by: Bilel KIHAL --- Gemfile.lock | 6 +- .../stylesheets/application.css.scss.erb | 1 + .../stylesheets/portal_configuration.scss | 68 +++++++++++++++++++ app/controllers/home_controller.rb | 9 +++ app/helpers/home_helper.rb | 7 ++ app/views/home/index.html.haml | 33 ++++----- app/views/home/portal_config.html.haml | 42 ++++++++++++ config/routes.rb | 1 + 8 files changed, 148 insertions(+), 19 deletions(-) create mode 100644 app/assets/stylesheets/portal_configuration.scss create mode 100644 app/views/home/portal_config.html.haml diff --git a/Gemfile.lock b/Gemfile.lock index 2c0afc74bc..31fd0e01d0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/ontoportal-lirmm/ontologies_api_ruby_client.git - revision: 15ad7d41d9a414ed3570c073c2b75a3fb4cc19cf + revision: 670ce6adfe77aeda97974414ab0ed4b6c5dc9469 branch: development specs: ontologies_api_client (2.2.0) @@ -302,7 +302,7 @@ GEM time net-http (0.3.2) uri - net-imap (0.4.16) + net-imap (0.4.15) date net-protocol net-pop (0.1.2) @@ -458,7 +458,7 @@ GEM rubocop-ast (>= 1.32.2, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.32.3) + rubocop-ast (1.32.2) parser (>= 3.3.1.0) ruby-progressbar (1.13.0) ruby2_keywords (0.0.5) diff --git a/app/assets/stylesheets/application.css.scss.erb b/app/assets/stylesheets/application.css.scss.erb index ac5160afc2..c5b6723fb3 100755 --- a/app/assets/stylesheets/application.css.scss.erb +++ b/app/assets/stylesheets/application.css.scss.erb @@ -58,6 +58,7 @@ @import "agent_tooltip"; @import "content_finder"; @import "tools"; +@import "portal_configuration"; @import "taxonomy"; /* Bootstrap and Font Awesome */ diff --git a/app/assets/stylesheets/portal_configuration.scss b/app/assets/stylesheets/portal_configuration.scss new file mode 100644 index 0000000000..0f72b14730 --- /dev/null +++ b/app/assets/stylesheets/portal_configuration.scss @@ -0,0 +1,68 @@ + +.portal-configuration{ + padding: 10px; + .portal-configuration-title{ + h3{ + font-size: 20px; + font-weight: 600; + margin-bottom: 0; + } + span{ + font-size: 16px; + } + } + + h4, .home-section-title .text{ + font-size: 15px !important; + } + + p { + font-size: 14px; + } + .home-support-items div { + font-size: 12px; + } + .home-support-items a img { + height: 48px; + width: 48px; + } + .home-logo-instances-small{ + padding: 7px; + margin: 2px 5px !important; + border-radius: 50%; + display: inline-block; + width: 38px; + height: 38px; + display: flex; + justify-content: center; + align-items: center; + } + .portal-config-ontologies{ + display: flex; + align-items: center; + margin-top: 2px; + } + .portal-config-ontologies svg{ + width: 14px; + height: 14px; + margin-right: 4px; + } + .portal-config-ontologies svg path{ + fill: var(--primary-color); + } + .portal-description{ + font-size: 15px; + padding: 11px 0; + } + .portal-config-federated-with{ + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + margin-right: 15px; + } + .portal-config-title-text{ + font-weight: 500; + font-size: 15px !important; + } +} diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 3f75bdd3f6..d7e4beeb31 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -51,6 +51,15 @@ def set_cookies render 'cookies', layout: nil end + def portal_config + @config = $PORTALS_INSTANCES.select { |x| x[:name].downcase.eql?((params[:portal] || helpers.portal_name).downcase) }.first + if @config && @config[:api] + @portal_config = LinkedData::Client::Models::Ontology.top_level_links(@config[:api]).to_h + else + @portal_config = {} + end + end + def tools @tools = { search: { diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb index 5df6609674..0a5498b01d 100644 --- a/app/helpers/home_helper.rb +++ b/app/helpers/home_helper.rb @@ -22,6 +22,13 @@ def format_number_abbreviated(number) end end + def portal_config_tooltip(portal_name, &block) + title = render TurboFrameComponent.new(id: "portal_config_tooltip_#{portal_name&.downcase}", src: "/config?portal=#{portal_name&.downcase}", style: "width: 600px !important; max-height: 300px; overflow: scroll") + render Display::InfoTooltipComponent.new(text: title, interactive: true) do + capture(&block) + end + end + def discover_ontologies_button render Buttons::RegularButtonComponent.new(id: 'discover-ontologies-button', value: t('home.discover_ontologies_button'), variant: "secondary", state: "regular", href: "/ontologies") do |btn| btn.icon_right do diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index be0075bfe3..fd32acc016 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -22,17 +22,17 @@ .home-bubble.home-bubble-one %a.h5{href:"/ontologies/#{@anal_ont_names[0]}", style: "color: white !important"} = @anal_ont_names[0] - %p + %p = @anal_ont_numbers[0].to_s + " " + t('visits.visits') .home-bubble.home-bubble-two %a.h5{href:"/ontologies/#{@anal_ont_names[1]}", style: "color: white !important"} = @anal_ont_names[1] - %p + %p = @anal_ont_numbers[1].to_s + " " + t('visits.visits') .home-bubble.home-bubble-three %a.h5{href:"/ontologies/#{@anal_ont_names[2]}", style: "color: white !important"} = @anal_ont_names[2] - %p + %p = @anal_ont_numbers[2].to_s + " " + t('visits.visits') %a.home-bubble.home-bubble-four{:href => "/visits"} .h5 ... @@ -44,7 +44,7 @@ %p = t('home.index.tagline') = ontologies_content_autocomplete - + .home-body-container .home-section %h4= t('home.ontology_upload') @@ -115,13 +115,13 @@ %a{:href => "/landscape#fairness_assessment"} %div.home-fair-details %p= t('home.fair_details') - + .home-sub-section-right %h4= t('home.twitter_news') %hr.home-section-line .home-card.home-twitter-news %a.twitter-timeline{"data-height" => "360", :href => "https://twitter.com/lagroportal?ref_src=twsrc%5Etfw"} - .home-twitter-loader + .home-twitter-loader = render LoaderComponent.new(type: 'pulsing') %script{:async => "", :charset => "utf-8", :src => "https://platform.twitter.com/widgets.js"} @@ -182,7 +182,7 @@ .text = "#{portal_name} slices" %hr.home-section-line/ - .home-section-description + .home-section-description .div = t('home.slices_description') .home-slices-container @@ -194,8 +194,8 @@ .home-slice-ontologies = slice.ontologies.length = inline_svg 'icons/slices.svg', width: "70", height: "70" - - .home-slice-name + + .home-slice-name = "#{slice.name} (#{slice.acronym})" = render Buttons::RegularButtonComponent.new(id:'regular-button', value: t('home.suggest_slice'), variant: "secondary", state: "regular", href: '/feedback') do |btn| - btn.icon_right do @@ -208,17 +208,18 @@ = t('home.ontoportal_instances') %hr.home-section-line/ - .home-section-description + .home-section-description .div = home_ontoportal_description .home-support-items - $PORTALS_INSTANCES&.each do |portal| - %div.text-center - = link_to portal[:ui], target: '_blank', class: 'home-logo-instances', style: "background-color: #{portal[:color]}" do - = inline_svg 'logo-white.svg', width: "35", height: "26" - %p{style: "color: #{portal[:color]}"} - = portal[:name] - + = portal_config_tooltip(portal[:name]) do + %div.text-center + = link_to portal[:ui], target: '_blank', class: 'home-logo-instances', style: "background-color: #{portal[:color]}" do + = inline_svg('logo-white.svg', width: "35", height: "26") + %p{style: "color: #{portal[:color]}"} + = portal[:name] + .home-section .home-section-title .text diff --git a/app/views/home/portal_config.html.haml b/app/views/home/portal_config.html.haml new file mode 100644 index 0000000000..b484e9431c --- /dev/null +++ b/app/views/home/portal_config.html.haml @@ -0,0 +1,42 @@ += turbo_frame_tag "portal_config_tooltip_#{params[:portal]}" do + .portal-configuration + .home-section-description.mb-1 + .div.d-flex.align-items-center + %div + %div.text-center + = link_to @portal_config[:ui] || @config[:ui], target: '_blank', class: 'home-logo-instances mr-1 m-0', style: "background-color: #{@portal_config[:color] || @config[:color]}" do + = inline_svg 'logo-white.svg', width: "35", height: "26" + %div + %div.portal-configuration-title{style: "color: #{@portal_config[:color] || @config[:color]}"} + %h3 + = @portal_config[:title] || @config[:name] + - if @portal_config[:numberOfArtefacts] + .portal-config-ontologies + = inline_svg_tag 'icons/ontology.svg' + %span + = "#{@portal_config[:numberOfArtefacts]} ontologies" + - if @portal_config[:description] + .portal-description + = @portal_config[:description] + - if @portal_config[:federated_portals] + %div.mb-1 + .home-section-title + .text + Federated with + .d-flex.flex-wrap.my-1 + - @portal_config[:federated_portals].to_h.values.compact.each do |portal| + .portal-config-federated-with + = link_to portal[:ui], target: '_blank', class: 'home-logo-instances-small', style: "background-color: #{portal[:color]};" do + = inline_svg 'logo-white.svg', width: "18", height: "13" + %p{style: "color: #{portal[:color]}"} + = portal[:name] + + - if @portal_config[:fundedBy] + %div.mb-1 + .home-section-title + .portal-config-title-text + = t('home.support_and_collaborations') + .home-support-items.d-flex.flex-wrap.my-1 + - @portal_config[:fundedBy]&.each do |logo| + %a.mx-2.my-1{href:logo[:url], target: "_blanc"} + %img{src: asset_path(logo[:img_src]), width: "18", height: "13"} diff --git a/config/routes.rb b/config/routes.rb index f31620d09a..b28c26ec02 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -8,6 +8,7 @@ get 'auth/:provider/callback', to: 'login#create_omniauth' get 'locale/:language', to: 'language#set_locale_language' get 'metadata_export/index' + get '/config', to: 'home#portal_config' get '/notes/new_comment', to: 'notes#new_comment' get '/notes/new_proposal', to: 'notes#new_proposal' From ff624719973abf21f7466ba89ecc5052b3b44a63 Mon Sep 17 00:00:00 2001 From: MUH <58882014+muhammedBkf@users.noreply.github.com> Date: Mon, 23 Sep 2024 22:17:54 +0100 Subject: [PATCH 03/28] Fix: localize a forgoten text in the annotator page (#757) --- app/views/annotator/index.html.haml | 2 +- config/locales/en.yml | 1 + config/locales/fr.yml | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/views/annotator/index.html.haml b/app/views/annotator/index.html.haml index 8b83bab08c..8fcdaf4bcd 100644 --- a/app/views/annotator/index.html.haml +++ b/app/views/annotator/index.html.haml @@ -27,7 +27,7 @@ = render(ChipsComponent.new(name: 'exclude_synonyms', label: t('annotator.exclude_synonyms'), checked: params[:exclude_synonyms])) .select-ontologies - = ontologies_selector(id:'annotator_page_ontologies', label: 'Select ontologies' ,name: 'ontologies[]', selected: params[:ontologies]&.split(',')) + = ontologies_selector(id:'annotator_page_ontologies', label: t('annotator.select_ontologies') ,name: 'ontologies[]', selected: params[:ontologies]&.split(',')) = show_advanced_options_button(text: t('show_advanced_options'), init: @advanced_options_open) = hide_advanced_options_button(text: t('hide_advanced_options'), init: @advanced_options_open) .more-advanced-options{'data-reveal-component-target': 'item', class: "#{@advanced_options_open ? '' : 'd-none'}"} diff --git a/config/locales/en.yml b/config/locales/en.yml index 79ae1583e5..12d5bd6b51 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -303,6 +303,7 @@ en: and fungi, in an attempt to replace existing methods of chemical control and avoid extensive use of fungicides, which often lead to resistance in plant pathogens. In agriculture, plant growth-promoting and biocontrol microorganisms have emerged as safe alternatives to chemical pesticides. Streptomyces spp. and their metabolites may have great potential as excellent agents for controlling various fungal and bacterial phytopathogens. + select_ontologies: Select ontologies concepts: error_valid_concept: "Error: You must provide a valid concept id" missing_roots: Missing roots diff --git a/config/locales/fr.yml b/config/locales/fr.yml index f044708cb8..a9f0c20135 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -305,7 +305,7 @@ fr: et aériennes, dans le but de remplacer les méthodes actuelles de contrôle chimique et d'éviter l'utilisation extensive de fongicides, qui conduisent souvent à une résistance chez les pathogènes des plantes. En agriculture, les microorganismes promoteurs de croissance des plantes et de biocontrôle ont émergé comme des alternatives sûres aux pesticides chimiques. Les espèces de Streptomyces et leurs métabolites peuvent avoir un grand potentiel en tant qu'agents excellents pour contrôler divers phytopathogènes fongiques et bactériens. - + select_ontologies: Sélectionner des ontologies concepts: error_valid_concept: "Erreur : Vous devez fournir un identifiant de concept valide" missing_roots: Racines manquantes From 3ec8d1c5faba8ffd53e8c2575ecb699b871fc16f Mon Sep 17 00:00:00 2001 From: MUH <58882014+muhammedBkf@users.noreply.github.com> Date: Sun, 29 Sep 2024 19:36:13 +0100 Subject: [PATCH 04/28] Fix : Add language-specific anchors to footer section links and the cookie banner (#760) * Add language-specific anchors to footer section links * Add language-specific anchors to the cookie banner * Refactor language-specific anchors to use I18n default option * Remove anchors from bioportal_config_env.rb.sample; now handled by footer view --- app/views/home/cookies.html.haml | 2 +- app/views/layouts/_footer.html.haml | 4 +++- config/bioportal_config_env.rb.sample | 4 ++-- config/locales/en.yml | 4 ++++ config/locales/fr.yml | 4 ++++ 5 files changed, 14 insertions(+), 4 deletions(-) diff --git a/app/views/home/cookies.html.haml b/app/views/home/cookies.html.haml index d02f687a9b..513df903f7 100644 --- a/app/views/home/cookies.html.haml +++ b/app/views/home/cookies.html.haml @@ -10,4 +10,4 @@ %div = link_button_component(id:'accept-cookie-selector', value: t('cookies_modal.accept_button'), href: cookies_path(cookies: true), size: "slim", variant: 'primary') %div.cookie-privacy-link - = link_to t('cookies_modal.privacy_link'), $FOOTER_LINKS.dig(:sections, :agreements, :privacy_policy) + = link_to t('cookies_modal.privacy_link'), "#{$FOOTER_LINKS.dig(:sections, :agreements, :privacy_policy)}#{t('cookies_modal.privacy_policy_anchor')}" diff --git a/app/views/layouts/_footer.html.haml b/app/views/layouts/_footer.html.haml index 9d89089615..68575a0c2b 100644 --- a/app/views/layouts/_footer.html.haml +++ b/app/views/layouts/_footer.html.haml @@ -20,8 +20,10 @@ %h2 = t("layout.footer."+key.to_s) %div + - section_links.each do |section , link| - %a{:href => link, :target => "_blank"} + - anchor = I18n.t("layout.footer.#{section}_anchor", default: "") + %a{:href => "#{link}#{anchor}", :target => "_blank"} = t("layout.footer."+section.to_s) diff --git a/config/bioportal_config_env.rb.sample b/config/bioportal_config_env.rb.sample index 352805b196..1b46a0ebbf 100644 --- a/config/bioportal_config_env.rb.sample +++ b/config/bioportal_config_env.rb.sample @@ -305,8 +305,8 @@ $FOOTER_LINKS = { }, agreements: { terms: $TERMS_AND_CONDITIONS_LINK, - privacy_policy: "https://doc.jonquetlab.lirmm.fr/share/e6158eda-c109-4385-852c-51a42de9a412/doc/terms-conditions-naDsDo2Zxq#h-privacy-policy", - legal_notices: "https://doc.jonquetlab.lirmm.fr/share/e6158eda-c109-4385-852c-51a42de9a412/doc/terms-conditions-naDsDo2Zxq#h-legal-notice" + privacy_policy: "https://doc.jonquetlab.lirmm.fr/share/e6158eda-c109-4385-852c-51a42de9a412/doc/terms-conditions-naDsDo2Zxq", + legal_notices: "https://doc.jonquetlab.lirmm.fr/share/e6158eda-c109-4385-852c-51a42de9a412/doc/terms-conditions-naDsDo2Zxq" }, about: { about_us: "https://github.com/agroportal/project-management", diff --git a/config/locales/en.yml b/config/locales/en.yml index 12d5bd6b51..fe6724cc51 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -519,8 +519,11 @@ en: issues_and_requests: Issues and Requests agreements: Legal terms: Terms and Conditions + terms_anchor: "#h-agroportal-terms-and-conditions-english" privacy_policy: Privacy Policy + privacy_policy_anchor: "#h-privacy-policy" legal_notices: Legal Notices + legal_notices_anchor: "#h-legal-notice" cite_us: Cite Us acknowledgments: Acknowledgments about: About @@ -1494,6 +1497,7 @@ en: paragraph2: "The cookies are functional and non-optional. By staying on %{portal}, you acknowledge the information was delivered to you." accept_button: "Accept" privacy_link: "Privacy policy" + privacy_policy_anchor: "#h-privacy-policy" taxonomy: groups_and_categories: Groups and Categories description: In AgroPortal, ontologies are organized in groups and tagged with categories. Typically, groups associate ontologies from the same project or organization for better identification of the provenance. Whereas categories are about subjects/topics and enable to classify ontologies. As of 2016, AgroPortal's categories were established in cooperation with FAO AIMS. In 2024, we moved to UNESCO nomenclature for fields of science and technology. Groups and categories, along with other metadata, can be used on the “Browse” page of AgroPortal to filter out the list of ontologies. diff --git a/config/locales/fr.yml b/config/locales/fr.yml index a9f0c20135..9eebb36316 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -524,8 +524,11 @@ fr: issues_and_requests: Problèmes et demandes agreements: Légal terms: Termes et conditions + terms_anchor: "#h-conditions-generales-dutilisation-dagroportal-francais" privacy_policy: Politique de confidentialité + privacy_policy_anchor: "#h-vie-privee" legal_notices: Mentions légales + legal_notices_anchor: "#h-mentions-legales" cite_us: Citez-nous acknowledgments: Remerciements about: À propos @@ -1532,6 +1535,7 @@ fr: paragraph2: "Les cookies sont fonctionnels et non facultatifs. En restant sur %{portal}, vous reconnaissez que les informations vous ont été transmises." accept_button: "Accepter" privacy_link: "Vie privé" + privacy_policy_anchor: "#h-vie-privee" taxonomy: groups_and_categories: Groupes et Catégories description: Dans AgroPortal, les ontologies sont organisées en groupes et étiquetées avec des catégories. Typiquement, les groupes associent des ontologies provenant du même projet ou de la même organisation pour une meilleure identification de la provenance. Tandis que les catégories concernent des sujets/thématiques et permettent de classifier les ontologies. En 2016, les catégories d'AgroPortal ont été établies en coopération avec FAO AIMS. En 2024, nous sommes passés à la nomenclature de l'UNESCO pour les domaines des sciences et des technologies. Les groupes et les catégories, ainsi que d'autres métadonnées, peuvent être utilisés sur la page “Parcourir” d'AgroPortal pour filtrer la liste des ontologies. From 5ade60046cad9ccb5289905437af09595939a81f Mon Sep 17 00:00:00 2001 From: MUH <58882014+muhammedBkf@users.noreply.github.com> Date: Mon, 30 Sep 2024 17:41:41 +0100 Subject: [PATCH 05/28] Feature: Add `Details` sub-tab and API cotextual button in Properties, Instances, Schemes, and Collections (#754) * limite taxonomy card ontologies chips number and add a see all button * Properties layout harmonization * Instances layout harmonization * fix: resolve issues related to properties raised in code review * fix: resolve issues related to instances raised in code review * adding subtab "Details" and API cotextual button to `Schemes` * adding subtab "Details" and API cotextual button to `Collections` * Using unless instead of if in sections controllers * Adding new section tab component and using it in Properties, Instances, Schemes and Collections * display first scheme if none are selected * move the created tabs_section component to rails helpers * fix schemes and collections shows not selecting the right object * remove details partials moving the content directly to the show files * remove unnecessary call in the properties show to find the ontology * use the correct partial in the schemes controller show action * put the tab item helper in the component helpers as generic * use the correct collection partial in the show action * remove unnecessary local in the instances partial * clean instance show partial code * remove unnecessary locals in the properties partial call * remove unnecessary locals in the collections show partial call * remove unnecessary locals in the schemes show partial call * Add presence check (not nil and not empty) for ontology details component before rendering --------- Co-authored-by: Syphax bouazzouni Co-authored-by: Bilel KIHAL --- .../taxonomy_card_component.html.haml | 7 ++- app/components/tab_item_component.rb | 8 +-- app/controllers/collections_controller.rb | 5 +- app/controllers/instances_controller.rb | 2 +- app/controllers/ontologies_controller.rb | 3 +- app/controllers/schemes_controller.rb | 4 +- app/helpers/collections_helper.rb | 2 +- app/helpers/components_helper.rb | 9 +++ app/helpers/ontologies_helper.rb | 45 +++++++++++++++ app/views/collections/_collection.html.haml | 13 ----- app/views/collections/_show.html.haml | 14 +++++ app/views/collections/show.html.haml | 1 - app/views/concepts/_show.html.haml | 57 +++++-------------- app/views/instances/_details.html.haml | 27 --------- app/views/instances/_instances.html.haml | 2 +- app/views/instances/_show.html.haml | 22 +++++++ .../sections/_collections.html.haml | 18 +++--- .../ontologies/sections/_schemes.html.haml | 2 +- app/views/properties/_show.html.haml | 31 +++++----- app/views/schemes/_scheme.html.haml | 13 ----- app/views/schemes/_show.html.haml | 11 ++++ app/views/schemes/show.html.haml | 1 - 22 files changed, 155 insertions(+), 142 deletions(-) delete mode 100644 app/views/collections/_collection.html.haml create mode 100644 app/views/collections/_show.html.haml delete mode 100644 app/views/collections/show.html.haml delete mode 100644 app/views/instances/_details.html.haml create mode 100644 app/views/instances/_show.html.haml delete mode 100644 app/views/schemes/_scheme.html.haml create mode 100644 app/views/schemes/_show.html.haml delete mode 100644 app/views/schemes/show.html.haml diff --git a/app/components/display/taxonomy_card_component/taxonomy_card_component.html.haml b/app/components/display/taxonomy_card_component/taxonomy_card_component.html.haml index 05903f4f4d..ab55aa41de 100644 --- a/app/components/display/taxonomy_card_component/taxonomy_card_component.html.haml +++ b/app/components/display/taxonomy_card_component/taxonomy_card_component.html.haml @@ -11,9 +11,12 @@ .description = render TextAreaFieldComponent.new(value: @taxonomy.description) .ontologies-cards - - @taxonomy.ontologies.each do |ontology| + - @taxonomy.ontologies.each_with_index do |ontology, index| + - if index>10 + = render ChipButtonComponent.new(url: "/ontologies?#{@taxonomy.id.split('/')[-2]}=#{@taxonomy.acronym}", text: "...", tooltip:"See all ontologies ...", type: "clickable") + - break = render ChipButtonComponent.new(url: "/ontologies/#{ontology.split('/').last}", text: ontology.split('/').last, tooltip: @ontologies_names[ontology], type: "clickable") - + - if @taxonomy.children .taxonomy-children-reveal{'data-action': "click->reveal-component#toggle", 'data-id': reveal_id} .text diff --git a/app/components/tab_item_component.rb b/app/components/tab_item_component.rb index 88f11f890a..8d5f25c8e4 100644 --- a/app/components/tab_item_component.rb +++ b/app/components/tab_item_component.rb @@ -44,12 +44,8 @@ def page_name end def call - if title && !title.empty? - link_to(title, @path, id: "#{item_id}_tab", class: "#{active_class} tab-link", 'data-json-link': @json_link) - else - link_to(@path, id: "#{item_id}_tab", class: "#{active_class} tab-link", 'data-json-link': @json_link) do - content - end + link_to(@path, id: "#{item_id}_tab", class: "#{active_class} tab-link", 'data-json-link': @json_link) do + (title && !title.empty?) ? title.html_safe : content end end diff --git a/app/controllers/collections_controller.rb b/app/controllers/collections_controller.rb index 31b5db7786..c01c0cf879 100644 --- a/app/controllers/collections_controller.rb +++ b/app/controllers/collections_controller.rb @@ -37,9 +37,12 @@ def index end def show - redirect_to(ontology_path(id: params[:ontology_id], p: 'collections', collectionid: params[:id], lang: request_lang)) and return unless turbo_frame_request? + + redirect_to(ontology_path(id: params[:ontology], p: 'collections', collectionid: params[:id], lang: request_lang)) and return unless turbo_frame_request? @collection = get_request_collection + + render partial: "collections/show" end def show_label diff --git a/app/controllers/instances_controller.rb b/app/controllers/instances_controller.rb index 70db773446..acaa005a1f 100644 --- a/app/controllers/instances_controller.rb +++ b/app/controllers/instances_controller.rb @@ -41,7 +41,7 @@ def show redirect_to(ontology_path(id: params[:ontology], p: 'instances', instanceid: params[:id] || params[:instanceid], lang: request_lang)) and return unless turbo_frame_request? - render partial: 'instances/details', layout: nil + render partial: 'show' end private diff --git a/app/controllers/ontologies_controller.rb b/app/controllers/ontologies_controller.rb index 11d0ae2ca5..5a2af639fa 100644 --- a/app/controllers/ontologies_controller.rb +++ b/app/controllers/ontologies_controller.rb @@ -192,7 +192,8 @@ def instances def schemes @schemes = get_schemes(@ontology) scheme_id = params[:schemeid] || @submission_latest.URI || nil - @scheme = get_scheme(@ontology, scheme_id) if scheme_id + @scheme = scheme_id ? get_scheme(@ontology, scheme_id) : @schemes.first + render partial: 'ontologies/sections/schemes', layout: 'ontology_viewer' end diff --git a/app/controllers/schemes_controller.rb b/app/controllers/schemes_controller.rb index 7fbbcc973e..8c5c97f1f0 100644 --- a/app/controllers/schemes_controller.rb +++ b/app/controllers/schemes_controller.rb @@ -32,9 +32,11 @@ def index end def show - redirect_to(ontology_path(id: params[:ontology_id], p: 'schemes', schemeid: params[:id],lang: request_lang)) and return unless turbo_frame_request? + redirect_to(ontology_path(id: params[:ontology], p: 'schemes', schemeid: params[:id],lang: request_lang)) and return unless turbo_frame_request? @scheme = get_request_scheme + + render partial: "schemes/show" end def show_label diff --git a/app/helpers/collections_helper.rb b/app/helpers/collections_helper.rb index bb81e39cef..351401a467 100644 --- a/app/helpers/collections_helper.rb +++ b/app/helpers/collections_helper.rb @@ -4,7 +4,7 @@ module CollectionsHelper def get_collections(ontology, add_colors: false) collections = ontology.explore.collections(language: request_lang) generate_collections_colors(collections) if add_colors - collections + collections.sort_by{ |x| x.prefLabel } end def get_collection(ontology, collection_uri) diff --git a/app/helpers/components_helper.rb b/app/helpers/components_helper.rb index 210e4ab57c..43e84004d3 100644 --- a/app/helpers/components_helper.rb +++ b/app/helpers/components_helper.rb @@ -1,6 +1,15 @@ module ComponentsHelper include TermsReuses + def tab_item_component(container_tabs:, title:, path:, selected: false, json_link: "", &content) + container_tabs.item(title: title.html_safe, path: path, selected: selected, json_link: json_link) + container_tabs.item_content { capture(&content) } + end + + def alert_component(message, type: "info") + render Display::AlertComponent.new(type: type, message: message) + end + def chips_component(id: , name: , label: , value: , checked: false , tooltip: nil, &block) content_tag(:div, data: { controller: 'tooltip' }, title: tooltip) do check_input(id: id, name: name, value: value, label: label, checked: checked, &block) diff --git a/app/helpers/ontologies_helper.rb b/app/helpers/ontologies_helper.rb index 33adc9efd5..f37836e9fb 100644 --- a/app/helpers/ontologies_helper.rb +++ b/app/helpers/ontologies_helper.rb @@ -530,6 +530,51 @@ def language_selector_hidden_tag(section) data: { controller: "language-change", 'language-change-section-value': section, action: "change->language-change#dispatchLangChangeEvent" } end + def ontology_object_json_link(ontology_acronym, object_type, id) + "#{rest_url}/ontologies/#{ontology_acronym}/#{object_type}/#{escape(id)}?display=all&apikey=#{get_apikey}" + end + + def render_permalink_link + content_tag(:div, class: 'mx-1') do + link_to("#classPermalinkModal", class: "class-permalink nav-link", title: t('concepts.permanent_link_class'), aria: { label: t('concepts.permanent_link_class') }, data: { toggle: "modal", current_purl: @current_purl }) do + content_tag(:i, '', class: "fas fa-link", aria: { hidden: "true" }) + end + end + end + + def render_concepts_json_button(link) + content_tag(:div, class: 'concepts_json_button') do + render RoundedButtonComponent.new(link: link, target: '_blank') + end + end + + + def ontology_object_details_component(frame_id: , ontology_id:, objects_title:, object:, &block) + render TurboFrameComponent.new(id: frame_id, data: {"turbo-frame-target": "frame"}) do + return if !object.present? + return alert_component(object.errors.join) if object.errors + + ontology_object_tabs_component(ontology_id: ontology_id, objects_title: objects_title, object_id: object["@id"]) do |tabs| + tab_item_component(container_tabs: tabs, title: t('concepts.details'), path: '#details', selected: true) do + capture(&block) + end + end + end + end + + def ontology_object_tabs_component(ontology_id:, objects_title:, object_id:, &block) + resource_url = ontology_object_json_link(ontology_id, objects_title, object_id) + render TabsContainerComponent.new(type: 'outline') do |c| + concat(c.pinned_right do + content_tag(:div, '', 'data-concepts-json-target': 'button') do + concat(render_permalink_link) if $PURL_ENABLED + concat(render_concepts_json_button(resource_url)) + end + end) + + capture(c, &block) + end + end def display_complex_text(definitions) diff --git a/app/views/collections/_collection.html.haml b/app/views/collections/_collection.html.haml deleted file mode 100644 index 16503de745..0000000000 --- a/app/views/collections/_collection.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -= turbo_frame_tag 'collection' do - = render ConceptDetailsComponent.new(id:'collection-label', acronym: @ontology.acronym, concept_id: collection.id, - properties: collection.properties, - top_keys: %w[created modified comment note], - bottom_keys: [], - exclude_keys: %w[member]) do |c| - - c.header(stripped: true) do |t| - - t.add_row({th: t("collections.id")}, {td: link_to_with_actions(collection["@id"], acronym: @ontology.acronym)}) - - t.add_row({th: t("collections.preferred_name")}, {td: display_in_multiple_languages(get_collection_label(collection))}) - - t.add_row({th: t("collections.members_count")}) do |r| - - r.td do - = link_to collection["memberCount"], "/ontologies/" + @ontology.acronym + "/?p=classes&sub_menu=list&concept_collections=" + collection["@id"], 'data-turbo-frame':'_top' - - t.add_row({th: t("collections.type")}, {td: collection["@type"]}) diff --git a/app/views/collections/_show.html.haml b/app/views/collections/_show.html.haml new file mode 100644 index 0000000000..d79a3e47c8 --- /dev/null +++ b/app/views/collections/_show.html.haml @@ -0,0 +1,14 @@ += ontology_object_details_component(frame_id: "collection", ontology_id: @ontology.acronym, objects_title: "collections", object: @collection) do + = render ConceptDetailsComponent.new(id:'collection-label', acronym: @ontology.acronym, concept_id: @collection.id, + properties: @collection.properties, + top_keys: %w[created modified comment note], + bottom_keys: [], + exclude_keys: %w[member]) do |c| + - c.header(stripped: true) do |t| + - t.add_row({th: t("collections.id")}, {td: link_to_with_actions(@collection["@id"], acronym: @ontology.acronym)}) + - t.add_row({th: t("collections.preferred_name")}, {td: display_in_multiple_languages(get_collection_label(@collection))}) + - t.add_row({th: t("collections.members_count")}) do |r| + - r.td do + = link_to @collection["memberCount"], "/ontologies/" + @ontology.acronym + "/?p=classes&sub_menu=list&concept_collections=" + @collection["@id"], 'data-turbo-frame':'_top' + - t.add_row({th: t("collections.type")}, {td: @collection["@type"]}) + diff --git a/app/views/collections/show.html.haml b/app/views/collections/show.html.haml deleted file mode 100644 index 4ef79429cf..0000000000 --- a/app/views/collections/show.html.haml +++ /dev/null @@ -1 +0,0 @@ -= render partial: 'collection', locals: {collection: @collection} \ No newline at end of file diff --git a/app/views/concepts/_show.html.haml b/app/views/concepts/_show.html.haml index a30f061fdf..8d690b65fd 100644 --- a/app/views/concepts/_show.html.haml +++ b/app/views/concepts/_show.html.haml @@ -4,63 +4,32 @@ = t('concepts.use_jump_to') - else %div{'data-controller': 'concepts-json', 'data-action': 'click->concepts-json#update'} - = render TabsContainerComponent.new(type:'outline') do |c| - - c.pinned_right do - - if $PURL_ENABLED - %div.mx-1 - = link_to("#classPermalinkModal", class: "class-permalink nav-link", title: t('concepts.permanent_link_class'), aria: {label: t('concepts.permanent_link_class')}, data: {toggle: "modal", current_purl: "#{@current_purl}"}) do - %i{class: "fas fa-link", aria: {hidden: "true"}} - %div{'data-concepts-json-target': 'button'} - .concepts_json_button - = render RoundedButtonComponent.new(link: "#{@ontology.id}/classes/#{escape(@concept.id)}?display=all&apikey=#{get_apikey}", target:'_blank') + = ontology_object_tabs_component(ontology_id: @ontology.acronym, objects_title: "classes", object_id: @concept.id) do |c| - apikey = "apikey=#{get_apikey}" - baseClassUrl = "#{@ontology.id}/classes/#{escape(@concept.id)}" - - c.item(title: t('concepts.details'), path: '#details', selected: true, json_link: "#{baseClassUrl}?#{apikey}&display=all") - - - unless skos? - - c.item(id: 'instances', path: '#instances', json_link: "#{baseClassUrl}/instances?#{apikey}") do - = t('concepts.instances') - ( - %span#concept_instances_sorted_list_count - ) - - - c.item(title: t('concepts.visualization'), path: '#visualization') - - - c.item(id: 'notes', path: '#notes', json_link: "#{baseClassUrl}/notes?#{apikey}") do - = t('concepts.notes') - %span#note_count_wrapper - ( - %span#note_count= @notes.length - ) - - c.item(id: 'mappings', path: '#mappings', json_link: "#{baseClassUrl}/mappings?#{apikey}") do - .d-flex - #{t('concepts.mappings')} - ( - = concept_mappings_loader(ontology_acronym: @ontology.acronym, concept_id: @concept.id) - ) - - - if @enable_ontolobridge - - c.item(title: t('concepts.new_term_requests'), path: '#request_term') - - - c.item_content do + - tab_item_component(container_tabs: c, title: t('concepts.details'), path: '#details', selected: true, json_link: "#{baseClassUrl}?#{apikey}&display=all") do = render :partial =>'/concepts/details' - unless skos? - - c.item_content do + - count_span = content_tag(:span, "#{t('concepts.instances')} (#{content_tag(:span, "", id: 'concept_instances_sorted_list_count')})".html_safe) + - tab_item_component(container_tabs: c, title: count_span, path: '#instances', json_link: "#{baseClassUrl}/instances?#{apikey}") do = render :partial =>'instances/instances' , locals: {id: "class-instances-data-table"} - - c.item_content do + + + - tab_item_component(container_tabs: c, title: t('concepts.visualization'), path: '#visualization') do = render :partial =>'/concepts/biomixer' - - c.item_content do + + - count_span = content_tag(:span, "#{t('concepts.notes')} (#{content_tag(:span, @notes.length, id: 'note_count')})".html_safe) + - tab_item_component(container_tabs: c, title: count_span, path: '#notes', json_link: "#{baseClassUrl}/notes?#{apikey}") do = render :partial =>'/notes/list' - - c.item_content do + + - count_span = content_tag(:span, "#{t('concepts.mappings')} (#{content_tag(:span, concept_mappings_loader(ontology_acronym: @ontology.acronym, concept_id: @concept.id))})".html_safe, class: "d-flex") + - tab_item_component(container_tabs: c, title: count_span, path: '#mappings', json_link: "#{baseClassUrl}/mappings?#{apikey}") do = render TurboFrameComponent.new(id:'concept_mappings', src:"/ajax/mappings/get_concept_table?ontologyid=#{@ontology.acronym}&conceptid=#{CGI.escape(@concept.id)}") - - if @enable_ontolobridge - - c.item_content do - = render :partial =>'/concepts/request_term' :javascript jQuery(document).ready(function(){ diff --git a/app/views/instances/_details.html.haml b/app/views/instances/_details.html.haml deleted file mode 100644 index 146dcd9464..0000000000 --- a/app/views/instances/_details.html.haml +++ /dev/null @@ -1,27 +0,0 @@ -= render TurboFrameComponent.new(id: params[:modal]&.to_s.eql?('true') ? modal_frame_id : 'instance_show') do - - if @instance && @instance["@id"] - %div - - ontology_acronym = params[:ontology_id] || @ontology.acronym - - filter_properties = ["http://www.w3.org/1999/02/22-rdf-syntax-ns#type", "http://www.w3.org/2000/01/rdf-schema#label", "http://www.w3.org/2004/02/skos/core#prefLabel"] - - = render ConceptDetailsComponent.new(id:'instance-details', acronym: ontology_acronym, concept_id: @instance["@id"]) do |c| - - c.header(stripped: true) do |t| - - t.add_row({th: t("instances.id")}, {td: link_to_with_actions(@instance["@id"], acronym: @ontology.acronym) }) - - - label = @instance['label'] || @instance['prefLabel'] - - unless label.blank? - - t.add_row({th: t('instances.label') }, {td: label.join(',').html_safe}) - - - types = @instance.types.reject{|x| x['NamedIndividual']} - - unless types.empty? - - t.add_row({th: t('instances.type') }) do |r| - - r.td do - = types.reject{|x| x['NamedIndividual']}.map {|cls| link_to_class(ontology_acronym,cls)}.join(', ').html_safe - - properties = @instance[:properties].to_h.select{|k,v| !filter_properties.include? k.to_s} - - properties.each do |prop| - - if !prop[1].nil? - - t.add_row({th: link_to_property(prop[0], ontology_acronym)}, {td: prop[1].map { |value| instance_property_value(value , ontology_acronym) }.join(', ').html_safe}) - - - - diff --git a/app/views/instances/_instances.html.haml b/app/views/instances/_instances.html.haml index 78b8ce0c29..9e5ea8eb33 100644 --- a/app/views/instances/_instances.html.haml +++ b/app/views/instances/_instances.html.haml @@ -9,7 +9,7 @@ - if params[:p].eql?('instances') %div#prop_contents{data: {'container-splitter-target': 'container'}} - = render partial: 'instances/details' + = render partial: 'instances/show' diff --git a/app/views/instances/_show.html.haml b/app/views/instances/_show.html.haml new file mode 100644 index 0000000000..71fbc857ca --- /dev/null +++ b/app/views/instances/_show.html.haml @@ -0,0 +1,22 @@ += ontology_object_details_component(frame_id: params['modal'].eql?('true') ? modal_frame_id : "instance_show", ontology_id: @ontology.acronym, objects_title: "instances", object: @instance) do + = render ConceptDetailsComponent.new(id: 'instance-details', acronym: @ontology.acronym, concept_id: @instance["@id"]) do |c| + - c.header(stripped: true) do |t| + - t.add_row({ th: t("instances.id") }, { td: link_to_with_actions(@instance["@id"], acronym: @ontology.acronym) }) + + - label = @instance['label'] || @instance['prefLabel'] + - t.add_row({ th: t('instances.label') }, { td: label.join(', ').html_safe }) unless label.blank? + + - types = @instance.types.reject { |x| x['NamedIndividual'] } + - unless types.empty? + - t.add_row({ th: t('instances.type') }) do |r| + - r.td do + = types.map { |cls| link_to_class(@ontology.acronym, cls) }.join(', ').html_safe + + - filter_properties = %w[http://www.w3.org/1999/02/22-rdf-syntax-ns#type http://www.w3.org/2000/01/rdf-schema#label http://www.w3.org/2004/02/skos/core#prefLabel] + - properties = @instance[:properties].to_h.reject { |k, _| filter_properties.include?(k.to_s) } + - properties.each do |prop, values| + - if values.present? + - t.add_row({ th: link_to_property(prop, @ontology.acronym) }, { td: values.map { |value| instance_property_value(value, @ontology.acronym) }.join(', ').html_safe }) + + + diff --git a/app/views/ontologies/sections/_collections.html.haml b/app/views/ontologies/sections/_collections.html.haml index 280c647603..9463602928 100644 --- a/app/views/ontologies/sections/_collections.html.haml +++ b/app/views/ontologies/sections/_collections.html.haml @@ -1,17 +1,15 @@ = render TurboFrameComponent.new(id: "collections", data: {"turbo-frame-target": "frame"} ) do - - if no_collections? - = no_collections_alert - - else - %div.ont-collections{data:{controller: 'container-splitter'}} - %div#collectionsTree.card.sidebar{data:{'container-splitter-target': 'container'}} + %div.ont-collections{data:{controller: 'container-splitter'}} + %div#collectionsTree.card.sidebar{data:{'container-splitter-target': 'container'}} + - if no_collections? + = no_collections_alert + - else = tree_container_component(id: "collections_sorted_list_view-page-1", placeholder: t('ontologies.sections.collections_search_placeholder', acronym: @ontology.acronym), frame_url: "/ontologies/#{@ontology.acronym}/collections", tree_url: "/ontologies/#{@ontology.acronym}/collections?#{request.original_url.split('?')[1]}") - - %div#collection_contents{data:{'container-splitter-target': 'container'}} - = render TurboFrameComponent.new(id: 'collection') do - - if @collection - = render partial: 'collections/collection', locals: {collection: @collection} + %div#collection_contents{data:{'container-splitter-target': 'container'}} + = render TurboFrameComponent.new(id: 'collection') do + = render partial: 'collections/show' diff --git a/app/views/ontologies/sections/_schemes.html.haml b/app/views/ontologies/sections/_schemes.html.haml index aa01c54aa4..fc91ee21e7 100644 --- a/app/views/ontologies/sections/_schemes.html.haml +++ b/app/views/ontologies/sections/_schemes.html.haml @@ -11,7 +11,7 @@ %div#scheme_contents{data:{'container-splitter-target': 'container'}} = render TurboFrameComponent.new(id:'scheme') do - = render partial: 'schemes/scheme', locals: {scheme: @scheme} + = render partial: 'schemes/show' diff --git a/app/views/properties/_show.html.haml b/app/views/properties/_show.html.haml index f74964f85e..d6ea4ce531 100644 --- a/app/views/properties/_show.html.haml +++ b/app/views/properties/_show.html.haml @@ -1,18 +1,13 @@ -= render TurboFrameComponent.new(id: 'property_show', data: {"turbo-frame-target": "frame"}) do - - if @property - - if @property.errors - = render Display::AlertComponent.new(type:'info', message: @property.errors.join) - - else - - properties = LinkedData::Client::Models::Property.properties_to_hash(@property).first - = render ConceptDetailsComponent.new(id:'property-details', acronym: @acronym, concept_id: @property.id, - properties: @property.properties, - top_keys: [], - bottom_keys: properties.keys.map(&:to_s), - exclude_keys: []) do |c| - - c.header(stripped: true) do |t| - - t.add_row({th: t('properties.id')}, {td: link_to_with_actions(@property.id, acronym: @acronym)}) - - t.add_row({th: t('properties.type')}, {td: @property.type }) - - t.add_row({th: t('properties.preferred_name')}, {td: display_in_multiple_languages(@property.label)}) unless @property.label.blank? - - t.add_row({th: t('properties.definitions')}, {td: display_in_multiple_languages(@property.definition)}) unless @property.definition.blank? - - t.add_row({th: t('properties.domain')}, {td: get_link_for_cls_ajax(@property.domain, @acronym, '_top')}) unless @property.domain.blank? - - t.add_row({th: t('properties.range')}, {td: get_link_for_cls_ajax(@property.range, @acronym, '_top')}) unless @property.range.blank? += ontology_object_details_component(frame_id: "property_show", ontology_id: @acronym, objects_title: "properties", object: @property) do + = render ConceptDetailsComponent.new(id:'property-details', acronym: @acronym, concept_id: @property.id, + properties: @property.properties, + top_keys: [], + bottom_keys: LinkedData::Client::Models::Property.properties_to_hash(@property).first.keys.map(&:to_s), + exclude_keys: []) do |c| + - c.header(stripped: true) do |t| + - t.add_row({th: t('properties.id')}, {td: link_to_with_actions(@property.id, acronym: @acronym)}) + - t.add_row({th: t('properties.type')}, {td: @property.type }) + - t.add_row({th: t('properties.preferred_name')}, {td: display_in_multiple_languages(@property.label)}) unless @property.label.blank? + - t.add_row({th: t('properties.definitions')}, {td: display_in_multiple_languages(@property.definition)}) unless @property.definition.blank? + - t.add_row({th: t('properties.domain')}, {td: get_link_for_cls_ajax(@property.domain, @acronym, '_top')}) unless @property.domain.blank? + - t.add_row({th: t('properties.range')}, {td: get_link_for_cls_ajax(@property.range, @acronym, '_top')}) unless @property.range.blank? \ No newline at end of file diff --git a/app/views/schemes/_scheme.html.haml b/app/views/schemes/_scheme.html.haml deleted file mode 100644 index 5cfac8c316..0000000000 --- a/app/views/schemes/_scheme.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -= turbo_frame_tag 'scheme' do - - if @scheme && !@scheme.empty? - = render ConceptDetailsComponent.new(id:'scheme-label', acronym: @ontology.acronym, concept_id: @scheme.id, - properties: scheme.properties, - top_keys: %w[description comment], - bottom_keys: %w[disjoint subclass is_a has_part], - exclude_keys: []) do |c| - - c.header(stripped: true) do |t| - - t.add_row({th: t('schemes.id')} , {td: link_to_with_actions(scheme["@id"], acronym: @ontology.acronym)}) - - t.add_row({th: t('schemes.preferred_name')} , {td: display_in_multiple_languages(get_scheme_label(scheme))}) - - t.add_row({th: t('schemes.type')} , {td: scheme["@type"]}) - - diff --git a/app/views/schemes/_show.html.haml b/app/views/schemes/_show.html.haml new file mode 100644 index 0000000000..b0af055f4b --- /dev/null +++ b/app/views/schemes/_show.html.haml @@ -0,0 +1,11 @@ += ontology_object_details_component(frame_id: "scheme", ontology_id: @ontology.acronym, objects_title: "schemes", object: @scheme) do + = render ConceptDetailsComponent.new(id:'scheme-label', acronym: @ontology.acronym, concept_id: @scheme.id, + properties: @scheme.properties, + top_keys: %w[description comment], + bottom_keys: %w[disjoint subclass is_a has_part], + exclude_keys: []) do |c| + - c.header(stripped: true) do |t| + - t.add_row({th: t('schemes.id')} , {td: link_to_with_actions(@scheme["@id"], acronym: @ontology.acronym)}) + - t.add_row({th: t('schemes.preferred_name')} , {td: display_in_multiple_languages(get_scheme_label(@scheme))}) + - t.add_row({th: t('schemes.type')} , {td: @scheme["@type"]}) + diff --git a/app/views/schemes/show.html.haml b/app/views/schemes/show.html.haml deleted file mode 100644 index 79eb63ac8f..0000000000 --- a/app/views/schemes/show.html.haml +++ /dev/null @@ -1 +0,0 @@ -= render partial: 'scheme', locals: {scheme: @scheme} \ No newline at end of file From 47c88488a405053193c5c7f603f9b0d953c1d40c Mon Sep 17 00:00:00 2001 From: Bilel Kihal <61744974+Bilelkihal@users.noreply.github.com> Date: Fri, 11 Oct 2024 15:39:05 +0200 Subject: [PATCH 06/28] Feature: Allow multiple parentCategories (#772) * update categories selector in admin page to allow multiple parentCategories * handle category multiple parents in category page * add null safety in nest categories children function --- app/controllers/admin/categories_controller.rb | 2 +- app/controllers/taxonomy_controller.rb | 7 +++---- app/helpers/application_helper.rb | 2 +- app/views/admin/categories/_form.html.haml | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/app/controllers/admin/categories_controller.rb b/app/controllers/admin/categories_controller.rb index f2c0d3ea00..1e3ba2ed8b 100644 --- a/app/controllers/admin/categories_controller.rb +++ b/app/controllers/admin/categories_controller.rb @@ -122,7 +122,7 @@ def unescape_id end def category_params - params.require(:category).permit(:acronym, :name, :description, :parentCategory, {ontologies:[]}).to_h + params.require(:category).permit(:acronym, :name, :description, {parentCategory: []}, {ontologies:[]}).to_h end def _categories diff --git a/app/controllers/taxonomy_controller.rb b/app/controllers/taxonomy_controller.rb index 06832abe8b..b3afa2542a 100644 --- a/app/controllers/taxonomy_controller.rb +++ b/app/controllers/taxonomy_controller.rb @@ -8,7 +8,6 @@ def index end private - def initialize_taxonomy @groups = LinkedData::Client::Models::Group.all slices = LinkedData::Client::Models::Slice.all @@ -33,13 +32,13 @@ def nest_categories_children(categories) category_index[category[:id]] = category end categories.each do |category| - if category.parentCategory - parent = category_index[category.parentCategory] + category[:parentCategory].each do |parent_id| + parent = category_index[parent_id] parent[:children] ||= [] parent[:children] << category end end - categories.reject! { |category| category.parentCategory } + categories.reject! { |category| category[:parentCategory]&.any? } categories end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index c134d5d96c..a349b0c6ce 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -546,6 +546,6 @@ def cancel_button_component(class_name: nil, id: , value:, data: nil) def categories_select(id: nil, name: nil, selected: 'None') categories_for_select = LinkedData::Client::Models::Category.all.map{|x| ["#{x.name} (#{x.acronym})", x.id]}.unshift(["None", '']) - render Input::SelectComponent.new(id: id, name: name, value: categories_for_select, selected: selected) + render Input::SelectComponent.new(id: id, name: name, value: categories_for_select, selected: selected, multiple: true) end end diff --git a/app/views/admin/categories/_form.html.haml b/app/views/admin/categories/_form.html.haml index 75974b5a3a..fff9d1a6cf 100644 --- a/app/views/admin/categories/_form.html.haml +++ b/app/views/admin/categories/_form.html.haml @@ -34,7 +34,7 @@ %th = t('admin.categories.form.parent_category') %td.top - = categories_select(id: 'category_parent_select', name: 'category[parentCategory]', selected: @category&.parentCategory) + = categories_select(id: 'category_parent_select', name: 'category[parentCategory][]', selected: @category&.parentCategory) - unless new_record %tr %th From bbe9fd70bf3c80befd0845a75d47fcf480d0ed49 Mon Sep 17 00:00:00 2001 From: Syphax bouazzouni Date: Mon, 14 Oct 2024 14:41:40 +0200 Subject: [PATCH 07/28] Fix: browse page white page issue (#774) * ensure that the custom ontology viewer context fix is only applied to it * handle collections sort if all languages mode raising an exception --- app/helpers/collections_helper.rb | 2 +- app/javascript/controllers/turbo_frame_controller.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/helpers/collections_helper.rb b/app/helpers/collections_helper.rb index 351401a467..72945b3682 100644 --- a/app/helpers/collections_helper.rb +++ b/app/helpers/collections_helper.rb @@ -4,7 +4,7 @@ module CollectionsHelper def get_collections(ontology, add_colors: false) collections = ontology.explore.collections(language: request_lang) generate_collections_colors(collections) if add_colors - collections.sort_by{ |x| x.prefLabel } + collections.sort_by{ |x| main_language_label(x.prefLabel) || '' } if collections end def get_collection(ontology, collection_uri) diff --git a/app/javascript/controllers/turbo_frame_controller.js b/app/javascript/controllers/turbo_frame_controller.js index 6520acb64a..aacc99eca1 100644 --- a/app/javascript/controllers/turbo_frame_controller.js +++ b/app/javascript/controllers/turbo_frame_controller.js @@ -32,7 +32,6 @@ export default class extends Controller { this.urlValue = this.#updatedPageUrl(data) this.frame.src = this.urlValue - } } @@ -44,7 +43,7 @@ export default class extends Controller { if (currentDisplayedUrl.toString().includes(this.urlValue)){ return true - } else if (currentDisplayedUrl.searchParams.get('p') === initUrl.searchParams.get('p')){ + } else if (currentDisplayedUrl.searchParams.has('p') && currentDisplayedUrl.searchParams.get('p') === initUrl.searchParams.get('p')){ // this is a custom fix for only the ontology viewer page, // that use the parameter ?p=section to tell which section is displayed return true From ef836ce19c065d078a747c0721b287b89ea061a9 Mon Sep 17 00:00:00 2001 From: Bilel Kihal <61744974+Bilelkihal@users.noreply.github.com> Date: Mon, 14 Oct 2024 17:03:54 +0200 Subject: [PATCH 08/28] Feature: Implement federation for the search page (#739) * simplify the header component to use content instead of a new section * update dropdown component to a custom title section instead of text * update browse page to use Dropdown component not bootsrap one * add color option to square badge component * add text and bg colors options to ontology browse card component * add federation helper code and config sample * add portals filters in the browse page * use federation helpers to get federation ontologies information * remove the filter using index code from submission filter as no used * refactor the filter_using_data function to be faster by using an hash * update the browse analytics cache to change depending on portals * add categories and groups ids on hover to know its origin when federated * use the last part of ids for browse counts independently of its origin * remove binding.pry from final federation code * add an error message if one of the external portal is down * display federated results in search page * display federated search results in different colors * add portals param in federated search * add federation checks in search page * merge federated search results * show portals names in federated search result chips in the form 'AgroPortal' instead of 'agroportal' * sort federated search results by string similarity * fix merge development to federated search issues * clean search result component * internationalize: results from other portals in search and browse page * fix performance issue in federated search * merge results using class id and ontology acronym * extract federation enabled into a helper in federated search * refactor search result elem function * add realtime benchmark for search federated search * add portal names and colors in top of federated search results * make sort by string similarity not case sensitive * remove duplicated federation configuration * extract portal button in home page to federation helper, fix the style of it and make it clickable * fix icons colors in federation * display a message in the federated search results when a portal is not responding * disable input chips in federated search page for the portals that are not responding * show tooltip for disabled input chips in the federated search * clean search controller federation code * update ontologies api ruby client to the latest version of federated search * fix federated browse icons colors * add tooltip for portal button in federated search * open external ontologies links in new tab in federated browse * use portal button helper in federated browse * cache federation status call * update chip helpers to support disabled state * use turbo frame for federation input chips, and use federation portals status to disable non working portals * add skelton loading animation for federation input chips * cache federation input chips separately * use cached federation input chips in browse page * fix browse page federation inputs section title style * initialize federation portals input chips in the home page (asynch in the background) * clean federated search aggregator code * move chip skelton to components helper * internationalize portal is not responding message * clean federation portal status cache method * clean federation stimulus controller code * display federated browse errors as warning instead of danger (orange instead of red) * remove related code to fedrated portals status to put it in another PR * use the key "collection" instead of "results" in federated search result hash to maintain the endpoint expected schema * Fix issue when enabling federation and ontologies selector * rename the federation color setter stimulus controller * extract federation errors helpers and a fix a typo in a function name * clean search aggregator by extracting some federation helpers * use again the development branch of the api client * fix some imports and functions calls after the refactor * extract and create federated portal button component * put again custom api documentation line * remove no more used data controller calls * clean the search result component federated code --------- Co-authored-by: Syphax bouazzouni --- Gemfile | 9 ++- Gemfile.lock | 60 +++++++++-------- .../stylesheets/application.css.scss.erb | 1 + app/assets/stylesheets/components/chips.scss | 4 +- .../stylesheets/components/search_result.scss | 3 +- app/assets/stylesheets/federation.scss | 31 +++++++++ app/components/chips_component.rb | 2 +- app/components/display/header_component.rb | 1 + .../display/search_result_component.rb | 27 ++++++-- .../search_result_component.html.haml | 19 ++++-- .../federated_portal_button_component.rb | 13 ++++ ...ederated_portal_button_component.html.haml | 7 ++ ...ated_portal_button_component_controller.js | 18 +++++ .../ontology_browse_card_component.rb | 2 +- .../ontology_browse_card_component.html.haml | 17 ++--- app/controllers/application_controller.rb | 5 ++ app/controllers/concerns/search_aggregator.rb | 66 +++++++++++++++---- app/controllers/home_controller.rb | 2 +- app/controllers/ontologies_controller.rb | 3 - app/controllers/search_controller.rb | 19 ++++-- app/helpers/application_helper.rb | 15 +++-- app/helpers/components_helper.rb | 17 +++-- app/helpers/federation_helper.rb | 36 +++++++++- app/helpers/inputs_helper.rb | 2 +- app/javascript/component_controllers/index.js | 7 +- app/javascript/controllers/index.js | 2 +- .../ontologies/browser/_ontologies.html.haml | 2 +- app/views/ontologies/browser/browse.html.haml | 5 +- app/views/search/index.html.haml | 12 +++- config/bioportal_config_env.rb.sample | 24 +++++-- config/locales/en.yml | 7 +- config/locales/fr.yml | 7 +- 32 files changed, 329 insertions(+), 116 deletions(-) create mode 100644 app/assets/stylesheets/federation.scss create mode 100644 app/components/federated_portal_button_component.rb create mode 100644 app/components/federated_portal_button_component/federated_portal_button_component.html.haml create mode 100644 app/components/federated_portal_button_component/federated_portal_button_component_controller.js diff --git a/Gemfile b/Gemfile index 781e6379e2..b2a3f58053 100644 --- a/Gemfile +++ b/Gemfile @@ -88,12 +88,15 @@ gem 'view_component', '~> 2.72' # Pagination library for Rails gem 'will_paginate', '~> 3.0' +# String similarity, used by federated search to rank results +gem 'string-similarity' + # Render SVG files in Rails views gem 'inline_svg' # ISO language codes and flags -gem 'flag-icons-rails', '~> 3.4' -gem 'iso-639', '~> 0.3.6' +gem "iso-639", "~> 0.3.6" +gem "flag-icons-rails", "~> 3.4" # Custom API client gem 'ontologies_api_client', git: 'https://github.com/ontoportal-lirmm/ontologies_api_ruby_client.git', branch: 'development' @@ -179,4 +182,4 @@ group :test do # Testing framework for Rails gem 'rspec-rails' -end \ No newline at end of file +end diff --git a/Gemfile.lock b/Gemfile.lock index 31fd0e01d0..949528bfb7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/ontoportal-lirmm/ontologies_api_ruby_client.git - revision: 670ce6adfe77aeda97974414ab0ed4b6c5dc9469 + revision: 0598e0068e545a5be9bbb501b0155c8bb2d9afb5 branch: development specs: ontologies_api_client (2.2.0) @@ -145,19 +145,20 @@ GEM crass (1.0.6) css_parser (1.17.1) addressable + csv (3.3.0) dalli (3.2.8) date (3.3.4) debug (1.9.2) irb (~> 1.10) reline (>= 0.3.8) - deepl-rb (2.5.3) + deepl-rb (3.0.2) diff-lcs (1.5.1) docile (1.4.1) domain_name (0.6.20240107) ed25519 (1.3.0) erubi (1.13.0) erubis (2.7.0) - excon (0.111.0) + excon (0.112.0) execjs (2.9.1) faraday (2.0.1) faraday-net_http (~> 2.0) @@ -176,7 +177,7 @@ GEM sass-rails globalid (1.2.1) activesupport (>= 6.1) - graphql (2.3.14) + graphql (2.3.18) base64 fiber-storage graphql-client (0.23.0) @@ -203,7 +204,7 @@ GEM http-accept (1.7.0) http-cookie (1.0.7) domain_name (~> 0.5) - i18n (1.14.5) + i18n (1.14.6) concurrent-ruby (~> 1.0) i18n-tasks (0.9.37) activesupport (>= 4.0.2) @@ -217,7 +218,7 @@ GEM terminal-table (>= 1.5.1) i18n-tasks-csv (1.1) i18n-tasks (~> 0.9) - importmap-rails (2.0.1) + importmap-rails (2.0.3) actionpack (>= 6.0.0) activesupport (>= 6.0.0) railties (>= 6.0.0) @@ -225,10 +226,11 @@ GEM activesupport (>= 3.0) nokogiri (>= 1.6) io-console (0.7.2) - irb (1.14.0) + irb (1.14.1) rdoc (>= 4.0.0) reline (>= 0.4.2) - iso-639 (0.3.6) + iso-639 (0.3.8) + csv jquery-rails (4.6.0) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) @@ -245,7 +247,7 @@ GEM bindata faraday (~> 2.0) faraday-follow_redirects - jwt (2.8.2) + jwt (2.9.3) base64 language_server-protocol (3.17.0.3) launchy (3.0.1) @@ -287,12 +289,13 @@ GEM marcel (1.0.4) matrix (0.4.2) method_source (1.1.0) - mime-types (3.5.2) + mime-types (3.6.0) + logger mime-types-data (~> 3.2015) - mime-types-data (3.2024.0903) + mime-types-data (3.2024.1001) mini_mime (1.1.5) minitest (5.25.1) - msgpack (1.7.2) + msgpack (1.7.3) multi_json (1.15.0) multi_xml (0.6.0) multipart-post (2.4.1) @@ -302,7 +305,7 @@ GEM time net-http (0.3.2) uri - net-imap (0.4.15) + net-imap (0.4.16) date net-protocol net-pop (0.1.2) @@ -315,9 +318,9 @@ GEM net-ssh (>= 5.0.0, < 8.0.0) net-smtp (0.5.0) net-protocol - net-ssh (7.2.3) + net-ssh (7.3.0) netrc (0.11.0) - newrelic_rpm (9.13.0) + newrelic_rpm (9.14.0) nio4r (2.7.3) nokogiri (1.15.6-x86_64-linux) racc (~> 1.4) @@ -328,7 +331,7 @@ GEM rack (>= 1.2, < 4) snaky_hash (~> 2.0) version_gem (~> 1.1) - oj (3.16.5) + oj (3.16.6) bigdecimal (>= 3.0) ostruct (>= 0.2) omniauth (2.1.2) @@ -338,8 +341,8 @@ GEM omniauth-github (2.0.1) omniauth (~> 2.0) omniauth-oauth2 (~> 1.8) - omniauth-google-oauth2 (1.1.3) - jwt (>= 2.0) + omniauth-google-oauth2 (1.2.0) + jwt (>= 2.9) oauth2 (~> 2.0) omniauth (~> 2.0) omniauth-oauth2 (~> 1.8) @@ -369,7 +372,7 @@ GEM psych (5.1.2) stringio public_suffix (5.1.1) - puma (5.6.8) + puma (5.6.9) nio4r (~> 2.0) racc (1.8.1) rack (2.2.9) @@ -429,14 +432,14 @@ GEM http-cookie (>= 1.0.2, < 2.0) mime-types (>= 1.16, < 4.0) netrc (~> 0.8) - rexml (3.3.7) - rouge (4.3.0) + rexml (3.3.8) + rouge (4.4.0) rspec-core (3.13.1) rspec-support (~> 3.13.0) rspec-expectations (3.13.3) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-mocks (3.13.1) + rspec-mocks (3.13.2) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-rails (7.0.1) @@ -458,7 +461,7 @@ GEM rubocop-ast (>= 1.32.2, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.32.2) + rubocop-ast (1.32.3) parser (>= 3.3.1.0) ruby-progressbar (1.13.0) ruby2_keywords (0.0.5) @@ -510,24 +513,24 @@ GEM ostruct stimulus-rails (1.3.4) railties (>= 6.0.0) + string-similarity (2.1.0) stringio (3.1.1) temple (0.10.3) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) - terser (1.2.3) + terser (1.2.4) execjs (>= 0.3.0, < 3) thor (1.3.2) tilt (2.4.0) time (0.4.0) date timeout (0.4.1) - turbo-rails (2.0.6) + turbo-rails (2.0.10) actionpack (>= 6.0.0) - activejob (>= 6.0.0) railties (>= 6.0.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - unicode-display_width (2.5.0) + unicode-display_width (2.6.0) uri (0.13.1) version_gem (1.1.4) view_component (2.83.0) @@ -539,7 +542,7 @@ GEM activemodel (>= 6.0.0) bindex (>= 0.4.0) railties (>= 6.0.0) - webmock (3.23.1) + webmock (3.24.0) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) @@ -620,6 +623,7 @@ DEPENDENCIES simplecov-cobertura sprockets-rails stimulus-rails + string-similarity terser turbo-rails tzinfo-data diff --git a/app/assets/stylesheets/application.css.scss.erb b/app/assets/stylesheets/application.css.scss.erb index c5b6723fb3..52b88a0a6d 100755 --- a/app/assets/stylesheets/application.css.scss.erb +++ b/app/assets/stylesheets/application.css.scss.erb @@ -60,6 +60,7 @@ @import "tools"; @import "portal_configuration"; @import "taxonomy"; +@import "federation"; /* Bootstrap and Font Awesome */ @import "bootstrap"; diff --git a/app/assets/stylesheets/components/chips.scss b/app/assets/stylesheets/components/chips.scss index ac1a2a939f..267fbb00db 100644 --- a/app/assets/stylesheets/components/chips.scss +++ b/app/assets/stylesheets/components/chips.scss @@ -32,7 +32,7 @@ } .chips-container.disabled div label > span, .chips-container div label span:has(span.disabled){ - background-color: #f8f9fa !important; + background-color: #f8f9fa !important; } .chips-container div label input[type="checkbox"]:checked ~ span{ @@ -42,4 +42,4 @@ .chips-container div label input[type="checkbox"]:checked ~ span .chips-check-icon{ display:unset; -} \ No newline at end of file +} diff --git a/app/assets/stylesheets/components/search_result.scss b/app/assets/stylesheets/components/search_result.scss index fc99288618..f5d8510458 100644 --- a/app/assets/stylesheets/components/search_result.scss +++ b/app/assets/stylesheets/components/search_result.scss @@ -41,7 +41,7 @@ display: flex; justify-content: center; align-items: center; - border-radius: 4px; + border-radius: 5px; background-color: var(--light-color); padding: 5px 13px; margin-right: 10px; @@ -52,6 +52,7 @@ } .search-result-component .actions .button svg{ width: 16px; + height: 16px; } .search-result-component .actions .button svg path{ diff --git a/app/assets/stylesheets/federation.scss b/app/assets/stylesheets/federation.scss new file mode 100644 index 0000000000..6bb893e251 --- /dev/null +++ b/app/assets/stylesheets/federation.scss @@ -0,0 +1,31 @@ +.federation-portal-button{ + display: flex; + justify-content: center; + align-items: center; + border-radius: 5px; + background-color: var(--light-color); + padding: 5px 13px; + margin-right: 0px; + height: 100%; +} + + +.federation-portal-button:hover{ + cursor: pointer; +} + +.federation-portal-button svg{ + width: 16px; + height: 16px; +} +.federation-portal-button svg path{ + fill: var(--primary-color); +} + +.federation-portal-button .text{ + color: var(--primary-color); + margin-left: 8px; +} +.federation-portal-button.icon-right .text{ + margin:0 8px; +} \ No newline at end of file diff --git a/app/components/chips_component.rb b/app/components/chips_component.rb index 624681f812..b92080d79c 100644 --- a/app/components/chips_component.rb +++ b/app/components/chips_component.rb @@ -13,4 +13,4 @@ def initialize(id:nil, name:, label: nil, value: nil, checked: false, tooltip: def checked? @checked end -end \ No newline at end of file +end diff --git a/app/components/display/header_component.rb b/app/components/display/header_component.rb index d8b8ecebd0..48ae752754 100644 --- a/app/components/display/header_component.rb +++ b/app/components/display/header_component.rb @@ -5,6 +5,7 @@ class Display::HeaderComponent < ViewComponent::Base include ComponentsHelper + def initialize(text: nil, tooltip: nil) super @text = text diff --git a/app/components/display/search_result_component.rb b/app/components/display/search_result_component.rb index 18d56a288e..e5be88256b 100644 --- a/app/components/display/search_result_component.rb +++ b/app/components/display/search_result_component.rb @@ -2,10 +2,13 @@ class Display::SearchResultComponent < ViewComponent::Base include UrlsHelper include ModalHelper include MultiLanguagesHelper + include FederationHelper + include ComponentsHelper renders_many :subresults, Display::SearchResultComponent renders_many :reuses, Display::SearchResultComponent - def initialize(number: 0,title: nil, ontology_acronym: nil ,uri: nil, definition: nil, link: nil, is_sub_component: false) + + def initialize(number: 0,title: nil, ontology_acronym: nil ,uri: nil, definition: nil, link: nil, is_sub_component: false, portal_name: nil, portal_color: nil, portal_light_color: nil, other_portals: []) @title = title @uri = uri @definition = definition @@ -13,6 +16,10 @@ def initialize(number: 0,title: nil, ontology_acronym: nil ,uri: nil, definition @is_sub_component = is_sub_component @ontology_acronym = ontology_acronym @number = number.to_s + @portal_name = portal_name + @portal_color = portal_color + @portal_light_color = portal_light_color + @other_portals = other_portals end def sub_component_class @@ -61,12 +68,22 @@ def visualize_button end def reveal_ontologies_button(text,id,icon) - content_tag(:div, class: 'button icon-right', 'data-action': "click->reveal-component#toggle", 'data-id': id) do - inline_svg_tag(icon) + - content_tag(:div, class: 'text') do + content_tag(:div, class: 'button icon-right', 'data-action': "click->reveal-component#toggle", 'data-id': id, style: @portal_color ? "background-color: #{@portal_light_color} !important" : '') do + inline_svg_tag(icon, class: "federated-icon-#{@portal_name}") + + content_tag(:div, class: 'text', style: @portal_color ? "color: #{@portal_color} !important" : '') do text end + - inline_svg_tag("icons/arrow-down.svg") + inline_svg_tag("icons/arrow-down.svg", class: "federated-icon-#{@portal_name}") end end + + def external_class? + !@portal_name.nil? + end + + def all_federated_portals + out = Array(@other_portals) + out.prepend({name: @portal_name, color: @portal_color, light_color: @portal_light_color, link: @link}) if external_class? + out + end end diff --git a/app/components/display/search_result_component/search_result_component.html.haml b/app/components/display/search_result_component/search_result_component.html.haml index 9d4d48bc08..4771756fd2 100644 --- a/app/components/display/search_result_component/search_result_component.html.haml +++ b/app/components/display/search_result_component/search_result_component.html.haml @@ -1,20 +1,25 @@ .search-result-component{class: sub_component_class, 'data-controller': 'reveal-component'} - %a.title{href: @link} - = @title + %a.title{href: @link, style: @portal_color ? "color: #{@portal_color} !important" : '', target: @portal_color ? "_blank" : ''} + .d-flex.align-items-center + = @title + = inline_svg_tag 'icons/external-link.svg', class: "ml-1 federated-icon-#{@portal_name} #{@portal_color ? '' : 'd-none'}" + - if @uri .uri = @uri - if @definition - = display_in_multiple_languages(@definition) .actions - = details_button - = visualize_button - = mappings_button + - unless external_class? + = details_button + = visualize_button + = mappings_button - if subresults? = reveal_ontologies_button("#{subresults.size} #{t('search.result_component.more_from_ontology')}", sub_ontologies_id, 'icons/three-dots.svg') - if reuses? = reveal_ontologies_button("#{t('search.result_component.reuses_in')} #{reuses.size} ontologies", reuses_id, 'icons/reuses.svg') + - all_federated_portals.each do |p| + = portal_button(name: p[:name], color: p[:color], light_color: p[:light_color], link: p[:link], tooltip: "Source #{p[:name].humanize.gsub("portal", "Portal")}") - if subresults? .more-from-ontology.d-none{id: sub_ontologies_id} .vertical-line @@ -28,4 +33,4 @@ .search-result-sub-components - reuses.each do |reuse| .search-result-sub-component - = reuse \ No newline at end of file + = reuse diff --git a/app/components/federated_portal_button_component.rb b/app/components/federated_portal_button_component.rb new file mode 100644 index 0000000000..4d8a1e886c --- /dev/null +++ b/app/components/federated_portal_button_component.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class FederatedPortalButtonComponent < ViewComponent::Base + attr_reader :name, :tooltip, :link, :color, :light_color + + def initialize(name:, link:, color:, tooltip:, light_color:) + @name = name + @tooltip = tooltip + @link = link + @color = color + @light_color = light_color + end +end diff --git a/app/components/federated_portal_button_component/federated_portal_button_component.html.haml b/app/components/federated_portal_button_component/federated_portal_button_component.html.haml new file mode 100644 index 0000000000..e45a6f0520 --- /dev/null +++ b/app/components/federated_portal_button_component/federated_portal_button_component.html.haml @@ -0,0 +1,7 @@ +%span{'data-controller': 'federation-portals-colors', + 'data-federation-portals-colors-color-value': color, + 'data-federation-portals-colors-portal-name-value': name.downcase} +%a{ href: link, target: '_blank', 'data-controller' => 'tooltip', title: tooltip, class: 'federation-portal-button button icon-right', style: color ? "background-color: #{light_color} !important" : '' } + = inline_svg_tag('logos/ontoportal.svg', class: "federated-icon-#{name.downcase}") + %div{ class: 'text', style: color ? "color: #{color} !important" : '' } + = name.humanize.gsub("portal", "Portal") diff --git a/app/components/federated_portal_button_component/federated_portal_button_component_controller.js b/app/components/federated_portal_button_component/federated_portal_button_component_controller.js new file mode 100644 index 0000000000..dd6d4a708d --- /dev/null +++ b/app/components/federated_portal_button_component/federated_portal_button_component_controller.js @@ -0,0 +1,18 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static values = { + color: String, + portalName: String, + } + connect() { + this.#initIconsStyle() + } + + #initIconsStyle(){ + const style = document.createElement('style'); + style.innerHTML = `.federated-icon-${this.portalNameValue} path { fill: ${this.colorValue} !important; }\n`; + document.head.appendChild(style); + } + + } diff --git a/app/components/ontology_browse_card_component.rb b/app/components/ontology_browse_card_component.rb index dfc2c05d0e..24d15ab661 100644 --- a/app/components/ontology_browse_card_component.rb +++ b/app/components/ontology_browse_card_component.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class OntologyBrowseCardComponent < ViewComponent::Base - include ApplicationHelper, OntologiesHelper, FederationHelper + include ApplicationHelper, OntologiesHelper, FederationHelper, ComponentsHelper def initialize(ontology: nil, onto_link: nil, text_color: nil, bg_light_color: nil, portal_name: nil) super diff --git a/app/components/ontology_browse_card_component/ontology_browse_card_component.html.haml b/app/components/ontology_browse_card_component/ontology_browse_card_component.html.haml index 6a08ccc836..51f7986ea0 100644 --- a/app/components/ontology_browse_card_component/ontology_browse_card_component.html.haml +++ b/app/components/ontology_browse_card_component/ontology_browse_card_component.html.haml @@ -3,11 +3,12 @@ .d-flex .browse-ontology-description .browse-ontology-title-bar - %a.browse-ontology-title{:href => onto_link, data: {'turbo': 'false'} , style: style_text} + %a.browse-ontology-title{:href => onto_link, data: {'turbo': 'false'} , style: style_text, target: external_ontology? ? '_blank' : ''} = ontology[:name]+" ("+ontology[:acronym]+")" = private_ontology_icon(ontology[:private]) - if external_ontology? - = render Display::InfoTooltipComponent.new(text: "Federated ontology from #{ontology[:sources].map{|x| link_to(x,x)}.join(', ')}", icon: 'external-link.svg') + %div{class: "federated-icon-#{@portal_name&.downcase} d-inline"} + = render Display::InfoTooltipComponent.new(text: "Federated ontology from #{ontology[:sources].map{|x| link_to(x,x)}.join(', ')}", icon: 'external-link.svg') - if session[:user]&.admin? - ontology_status = status_string(ontology) = render Display::InfoTooltipComponent.new(text: ontology_status, icon: submission_status_icons(ontology_status)) @@ -77,14 +78,8 @@ - ontology[:sources].each do |id| - config = ontology_portal_config(id)&.last || internal_portal_config(id) || {} - unless config.blank? - %div.mx-1{title: content_tag(:div, "Source #{config[:name]}"), data:{controller: 'tooltip', 'tooltip-interactive-value': 'true'}} - = render ChipButtonComponent.new(type: "clickable" , style: style_bg) do - = link_to ontoportal_ui_link(id), target: '_top' do - %span.d-inline - %span.mr-1 - = inline_svg 'logo-white.svg', width: "20", height: "20" - %span - = config[:name] + %span{style: "padding: 3px 0; margin-right: 0.25rem;"} + = portal_button(name: config[:name], color: @text_color, light_color: @bg_light_color, link: ontoportal_ui_link(id), tooltip: "Source #{config[:name]}") - if session[:user]&.admin? %div.mx-1{title: content_tag(:div, debug(ontology), style: 'height: 300px; overflow: scroll'), data:{controller: 'tooltip', 'tooltip-interactive-value': 'true'}} @@ -102,4 +97,4 @@ .two .browse-sket-column-three .one - .two \ No newline at end of file + .two diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 291e2ae33d..b9f6f31d72 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -175,6 +175,7 @@ def bp_config_json def rest_url helpers.rest_url end + def request_portals helpers.request_portals end @@ -440,6 +441,10 @@ def json_link(url, optional_params) optional_params_str = filtered_params.map { |param, value| "#{param}=#{value}" }.join("&") return base_url + optional_params_str + "&apikey=#{$API_KEY}" end + + def set_federated_portals + RequestStore.store[:federated_portals] = params[:portals]&.split(',') + end private def not_found_record(exception) diff --git a/app/controllers/concerns/search_aggregator.rb b/app/controllers/concerns/search_aggregator.rb index d50d369a8f..a45f0237ee 100644 --- a/app/controllers/concerns/search_aggregator.rb +++ b/app/controllers/concerns/search_aggregator.rb @@ -1,5 +1,6 @@ module SearchAggregator - include UrlsHelper, MultiLanguagesHelper + include UrlsHelper, MultiLanguagesHelper, FederationHelper + require 'string-similarity' extend ActiveSupport::Concern BLACKLIST_FIX_STR = [ "https://", @@ -27,11 +28,16 @@ module SearchAggregator def aggregate_results(query, results) ontologies = aggregate_by_ontology(results) grouped_results = add_subordinate_ontologies(query, ontologies) + all_ontologies = LinkedData::Client::Models::Ontology.all(include: 'acronym,name', include_views: true, display_links: false, display_context: false) - grouped_results.map do |group| + search_results = grouped_results.map do |group| format_search_result(group, all_ontologies) end + + search_results = merge_sort_federated_results(query, search_results) if federation_enabled? + + search_results end def format_search_result(result, ontologies) @@ -40,16 +46,21 @@ def format_search_result(result, ontologies) result = same_ont.shift ontology = result.links['ontology'].split('/').last { - root: search_result_elem(result, ontology, ontology_name_acronym(ontologies, ontology)), - descendants: same_ont.map { |x| search_result_elem(x, ontology, '') }, - reuses: same_cls.map do |x| - format_search_result(x, ontologies) - end + root: search_result_elem(result, ontology, ontology_name_acronym(ontologies, ontology)), + descendants: same_ont.map { |x| search_result_elem(x, ontology, '') }, + reuses: same_cls.map do |x| + format_search_result(x, ontologies) + end } end private + def merge_sort_federated_results(query, search_results) + search_results = merge_federated_results(search_results) + sort_results_by_string_similarity(query, search_results) + end + def search_concept_label(label) label = language_hash(label) @@ -59,20 +70,25 @@ def search_concept_label(label) pref_lab.downcase.include?(@search_query.downcase) || @search_query.downcase.include?(pref_lab.downcase) end.first || label.first end - + label end - def search_result_elem(class_object, ontology_acronym, title) + def search_result_elem(class_object, ontology_acronym, title) label = search_concept_label(class_object.prefLabel) + request_lang = helpers.request_lang&.eql?("ALL") ? '' : "&language=#{helpers.request_lang}" - { + result = { uri: class_object.id.to_s, - title: title.nil? || title.empty? ? "#{label} - #{ontology_acronym}" : "#{label} - #{title}", + title: title.to_s.empty? ? "#{label} - #{ontology_acronym}" : "#{label} - #{title}", ontology_acronym: ontology_acronym, - link: "/ontologies/#{ontology_acronym}?p=classes&conceptid=#{escape(class_object.id)}#{helpers.request_lang&.eql?("ALL") ? '' : "&language="+helpers.request_lang.to_s}", - definition: class_object.definition + link: "/ontologies/#{ontology_acronym}?p=classes&conceptid=#{escape(class_object.id)}#{request_lang}", + definition: class_object.definition, } + + result.merge!(class_federation_configuration(class_object)) if federation_enabled? + + result end @@ -228,5 +244,27 @@ def blacklist_cls_id_components(cls_id, blacklist_words) stripped_id end -end + def merge_federated_results(search_results) + search_results.each do |element| + element[:root][:other_portals] = [] + element[:reuses].reject! do |reuse| + if (element[:root][:ontology_acronym] == reuse[:root][:ontology_acronym]) && (element[:root][:uri] == reuse[:root][:uri]) + portal_name = reuse[:root][:portal_name] + link = reuse[:root][:link] + element[:root][:other_portals] << {name: portal_name, color: federated_portal_color(portal_name), light_color: federated_portal_light_color(portal_name), link: link} + true + else + false + end + end + end + end + + def sort_results_by_string_similarity(query, search_results) + search_results = search_results.sort_by do |entry| + root_similarity = String::Similarity.cosine(query.downcase, entry[:root][:title].split('-').first.gsub(" ", "").downcase) + -root_similarity + end + end +end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index d7e4beeb31..bc9531e562 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -4,7 +4,7 @@ class HomeController < ApplicationController layout :determine_layout - include FairScoreHelper + include FairScoreHelper, FederationHelper def index @analytics = helpers.ontologies_analytics diff --git a/app/controllers/ontologies_controller.rb b/app/controllers/ontologies_controller.rb index 5a2af639fa..4b3008a37e 100644 --- a/app/controllers/ontologies_controller.rb +++ b/app/controllers/ontologies_controller.rb @@ -571,7 +571,4 @@ def search_first_instance_id return !results.blank? ? results.first[:name] : nil end - def set_federated_portals - RequestStore.store[:federated_portals] = params[:portals]&.split(',') - end end diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 841f0f948a..b6a7c5ede4 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -1,7 +1,7 @@ require 'uri' class SearchController < ApplicationController - include SearchAggregator, SearchContent + include SearchAggregator, SearchContent, FederationHelper skip_before_action :verify_authenticity_token @@ -13,14 +13,25 @@ def index @advanced_options_open = false @search_results = [] @json_url = json_link("#{rest_url}/search", {}) + params[:portals] = params[:portals]&.join(',') return if @search_query.empty? params[:pagesize] = "150" - results = LinkedData::Client::Models::Class.search(@search_query, params).collection + set_federated_portals + + params[:ontologies] = nil if federation_enabled? + + @time = Benchmark.realtime do + results = LinkedData::Client::Models::Class.search(@search_query, params) + @federation_errors = federation_error(results) if federation_error?(results) + results = results[:collection] + + + @search_results = aggregate_results(@search_query, results) + end @advanced_options_open = !search_params_empty? - @search_results = aggregate_results(@search_query, results) @json_url = json_link("#{rest_url}/search", params.permit!.to_h) end @@ -141,7 +152,7 @@ def search_params [ :ontologies, :categories, :also_search_properties, :also_search_obsolete, :also_search_views, - :require_exact_match, :require_definition + :require_exact_match, :require_definition, :portals ] end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index a349b0c6ce..e061a0a05e 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -11,6 +11,11 @@ module ApplicationHelper include ModalHelper, MultiLanguagesHelper, UrlsHelper + def url_to_endpoint(url) + uri = URI.parse(url) + endpoint = uri.path.sub(/^\//, '') + endpoint + end RESOLVE_NAMESPACE = {:omv => "http://omv.ontoware.org/2005/05/ontology#", :skos => "http://www.w3.org/2004/02/skos/core#", :owl => "http://www.w3.org/2002/07/owl#", :rdf => "http://www.w3.org/1999/02/22-rdf-syntax-ns#", :rdfs => "http://www.w3.org/2000/01/rdf-schema#", :metadata => "http://data.bioontology.org/metadata/", :metadata_def => "http://data.bioontology.org/metadata/def/", :dc => "http://purl.org/dc/elements/1.1/", :xsd => "http://www.w3.org/2001/XMLSchema#", @@ -23,17 +28,16 @@ module ApplicationHelper :oboInOwl => "http://www.geneontology.org/formats/oboInOwl#", :idot => "http://identifiers.org/idot/", :sd => "http://www.w3.org/ns/sparql-service-description#", :cclicense => "http://creativecommons.org/licenses/", 'skos-xl' => "http://www.w3.org/2008/05/skos-xl#"} - def url_to_endpoint(url) - uri = URI.parse(url) - endpoint = uri.path.sub(/^\//, '') - endpoint - end def search_json_link(link = @json_url, style: '') custom_style = "font-size: 50px; line-height: 0.5; margin-left: 6px; #{style}".strip render IconWithTooltipComponent.new(icon: "json.svg",link: link, target: '_blank', title: t('fair_score.go_to_api'), size:'small', style: custom_style) end + def portal_name_from_uri(uri) + URI.parse(uri).hostname.split('.').first + end + def resolve_namespaces RESOLVE_NAMESPACE end @@ -155,7 +159,6 @@ def at_slice? !@subdomain_filter.nil? && !@subdomain_filter[:active].nil? && @subdomain_filter[:active] == true end - def add_comment_button(parent_id, parent_type) if session[:user].nil? link_to t('application.add_comment'), login_index_path(redirect: request.url), class: "secondary-button regular-button slim" diff --git a/app/helpers/components_helper.rb b/app/helpers/components_helper.rb index 43e84004d3..e43d546db5 100644 --- a/app/helpers/components_helper.rb +++ b/app/helpers/components_helper.rb @@ -1,6 +1,10 @@ module ComponentsHelper include TermsReuses + def portal_button(name: nil , color: nil , light_color: nil, link: nil, tooltip: nil) + render FederatedPortalButtonComponent.new(name: name, color: color, link: link, tooltip: tooltip, light_color: light_color) + end + def tab_item_component(container_tabs:, title:, path:, selected: false, json_link: "", &content) container_tabs.item(title: title.html_safe, path: path, selected: selected, json_link: json_link) container_tabs.item_content { capture(&content) } @@ -38,7 +42,7 @@ def check_resolvability_container(url) end end end - + def search_page_input_component(name:, value: nil, placeholder: , button_icon: 'icons/search.svg', type: 'text', &block) content_tag :div, class: 'search-page-input-container', data: { controller: 'reveal' } do search_input = content_tag :div, class: 'search-page-input' do @@ -72,7 +76,7 @@ def paginated_list_component(id:, results:, next_page_url:, child_url:, child_tu end end) end - + concepts = c.collection if concepts && !concepts.empty? concepts.each do |concept| @@ -113,7 +117,7 @@ def copy_link_to_clipboard(url, show_content: false) end - def generated_link_to_clipboard(url, acronym) + def generated_link_to_clipboard(url, acronym) url = "#{$UI_URL}/ontologies/#{acronym}/#{link_last_part(url)}" content_tag(:span, id: "generate_portal_link", style: 'display: inline-block;') do render ClipboardComponent.new(icon: 'icons/copy_link.svg', title: "#{t("components.copy_portal_uri", portal_name: portal_name)} #{link_to(url)}", message: url, show_content: false) @@ -132,7 +136,7 @@ def htaccess_tag(acronym) def link_to_with_actions(link_to_tag, acronym: nil, url: nil, copy: true, check_resolvability: true, generate_link: true, generate_htaccess: false) tag = link_to_tag url = link_to_tag if url.nil? - + tag += content_tag(:span, class: 'mx-1') do concat copy_link_to_clipboard(url) if copy concat generated_link_to_clipboard(url, acronym) if generate_link @@ -145,11 +149,11 @@ def link_to_with_actions(link_to_tag, acronym: nil, url: nil, copy: true, check_ def tree_component(root, selected, target_frame:, sub_tree: false, id: nil, auto_click: false, submission: nil, &child_data_generator) root.children.sort! { |a, b| (a.prefLabel || a.id).downcase <=> (b.prefLabel || b.id).downcase } - + render TreeViewComponent.new(id: id, sub_tree: sub_tree, auto_click: auto_click) do |tree_child| root.children.each do |child| children_link, data, href = child_data_generator.call(child) - + if children_link.nil? || data.nil? || href.nil? raise ArgumentError, t('components.error_block') end @@ -288,5 +292,4 @@ def form_cancel_button end end - end diff --git a/app/helpers/federation_helper.rb b/app/helpers/federation_helper.rb index afd60dfa00..572e26db3e 100644 --- a/app/helpers/federation_helper.rb +++ b/app/helpers/federation_helper.rb @@ -44,16 +44,14 @@ def ontology_portal_config(id) def ontology_portal_name(id) portal_key, _ = ontology_portal_config(id) - portal_key ? federated_portal_name(portal_key) : 'not found' + portal_key ? federated_portal_name(portal_key) : nil end - def ontology_portal_color(id) portal_key, _ = ontology_portal_config(id) federated_portal_color(portal_key) if portal_key end - def ontoportal_ui_link(id) portal_key, config = ontology_portal_config(id) return nil unless portal_key @@ -94,4 +92,36 @@ def request_portals_names content_tag(:span, federated_portal_name(name), style: color ? "color: #{color}" : "", class: color ? "" : "text-primary") end.compact end + + def federation_enabled? + params[:portals] + end + + def federation_error?(response) + !response[:errors].blank? + end + + def federation_error(response) + federation_errors = response[:errors].map{|e| ontology_portal_name(e.split(' ').last.gsub('search', ''))} + federation_errors.map{ |p| "#{p} #{t('federation.not_responding')} " }.join(' ') + end + + def class_federation_configuration(class_object) + is_external = federation_external_class?(class_object) + portal_name = is_external ? helpers.portal_name_from_uri(class_object.links['ui']) : nil + + result = { + portal_name: portal_name, + portal_color: is_external ? federated_portal_color(portal_name) : nil, + portal_light_color: is_external ? federated_portal_light_color(portal_name) : nil + } + result[:link] = class_object.links['ui'] if is_external + result + end + + def federation_external_class?(class_object) + !class_object.links['self'].include?($REST_URL) + end + + end diff --git a/app/helpers/inputs_helper.rb b/app/helpers/inputs_helper.rb index cd4782b6f3..94b677ccd2 100644 --- a/app/helpers/inputs_helper.rb +++ b/app/helpers/inputs_helper.rb @@ -82,4 +82,4 @@ def attribute_error(attr) def input_error_message(name) attribute_error(method_name(name)) end -end \ No newline at end of file +end diff --git a/app/javascript/component_controllers/index.js b/app/javascript/component_controllers/index.js index c0d3bcd20f..bd6df841a2 100644 --- a/app/javascript/component_controllers/index.js +++ b/app/javascript/component_controllers/index.js @@ -25,6 +25,7 @@ import Table_component_controller from '../../components/table_component/table_c import clipboard_component_controller from '../../components/clipboard_component/clipboard_component_controller' import range_slider_component_controller from '../../components/input/range_slider_component/range_slider_component_controller' import RDFHighlighter from '../../components/display/rdf_highlighter_component/rdf_highlighter_component_controller' +import FederationController from "../../components/federated_portal_button_component/federated_portal_button_component_controller" application.register("rdf-highlighter", RDFHighlighter) application.register('turbo-modal', TurboModalController) @@ -35,10 +36,12 @@ application.register('subscribe-notes', Ontology_subscribe_button_component_cont application.register('search-input', Search_input_component_controller) application.register('tabs-container', Tabs_container_component_controller) application.register('circle-progress-bar', CircleProgressBarComponentController) -application.register('alert-component', alert_component_controller) +application.register('alert-component', alert_component_controller) application.register('progress-pages', Progress_pages_component_controller) application.register('reveal-component', Reveal_component_controller) application.register('table-component', Table_component_controller) application.register('clipboard', clipboard_component_controller) -application.register('range-slider', range_slider_component_controller) \ No newline at end of file + +application.register('range-slider', range_slider_component_controller) +application.register("federation-portals-colors", FederationController) diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js index 24f39bf453..fa09b85416 100644 --- a/app/javascript/controllers/index.js +++ b/app/javascript/controllers/index.js @@ -105,4 +105,4 @@ import MappingsController from "./mappings_visualization_controller" application.register('mappings', MappingsController) import ConceptsJsonButtonController from "./concepts_json_button_controller.js" -application.register('concepts-json', ConceptsJsonButtonController) \ No newline at end of file +application.register('concepts-json', ConceptsJsonButtonController) diff --git a/app/views/ontologies/browser/_ontologies.html.haml b/app/views/ontologies/browser/_ontologies.html.haml index 3ca8685bc0..cadbb85168 100644 --- a/app/views/ontologies/browser/_ontologies.html.haml +++ b/app/views/ontologies/browser/_ontologies.html.haml @@ -8,7 +8,7 @@ #{t("ontologies.showing_ontologies_size", ontologies_size: @count, analytics_size: @total_ontologies, portals: request_portals_names.join(', ').html_safe).html_safe} (#{sprintf("%.2f", @time)}s) - unless @errors.blank? %div.my-1 - = render Display::AlertComponent.new(type: 'danger') do + = render Display::AlertComponent.new(type: 'warning') do - @errors.each do |e| %div = e.errors || e diff --git a/app/views/ontologies/browser/browse.html.haml b/app/views/ontologies/browser/browse.html.haml index fd9777e3a8..34d195e55a 100644 --- a/app/views/ontologies/browser/browse.html.haml +++ b/app/views/ontologies/browser/browse.html.haml @@ -71,8 +71,8 @@ = turbo_frame_tag "count_#{key}_#{link_last_part(object["id"])}", busy: true %span.show-if-loading = render LoaderComponent.new(small:true) - %div{data:{controller: "show-filter-count browse-filters", action: "change->show-filter-count#updateCount change->browse-filters#dispatchFilterEvent"}, id: "portals_filter_container"} - = render DropdownContainerComponent.new(id: "browse-portal-filter", is_open: !request_portals.empty?, title: "Results from external portals") do + .browse-federation-input-chip-container + = render DropdownContainerComponent.new(id: "browse-portal-filter", is_open: !request_portals.empty?, title: t('federation.results_from_external_portals')) do .browse-filter-checks-container.px-1 - federated_portals.each do |key, config| = group_chip_component(name: "portals", object: { "acronym" => config[:name], "value" => key }, checked: request_portals.include?(key.to_s), title: '') @@ -91,4 +91,3 @@ - list.loader do = browser_counter_loader - ontologies_browse_skeleton - diff --git a/app/views/search/index.html.haml b/app/views/search/index.html.haml index 28ecbf2eed..d5f117cf13 100644 --- a/app/views/search/index.html.haml +++ b/app/views/search/index.html.haml @@ -15,7 +15,12 @@ = t("search.advanced_options.ontologies") .field = ontologies_selector(id:'search_page_ontologies' ,name: 'ontologies[]', selected: params[:ontologies]&.split(',')) - + .filter-container + .title + = t('federation.results_from_external_portals') + .field.d-flex + - federated_portals.each do |key, config| + = group_chip_component(name: "portals[]", object: { "acronym" => config[:name], "value" => key }, checked: request_portals.include?(key.to_s), title: '') .right .filter-container .title @@ -35,7 +40,7 @@ .search-page-options{class: @search_results.empty? ? 'justify-content-end': ''} - unless @search_results.empty? .search-page-number-of-results - = "#{t('search.match_in')} #{@search_results.length} #{t('search.ontologies')}" + = "#{t('search.match_in')} #{@search_results.length} #{t('search.ontologies')} #{t('federation.from')} #{request_portals_names.join(', ').html_safe} (#{sprintf("%.2f", @time)}s)".html_safe %div.d-flex .search-page-json.mx-4.mt-1 = search_json_link @@ -49,6 +54,8 @@ =inline_svg_tag 'icons/hide.svg' .text = t('search.hide_advanced_options') + - unless @federation_errors.blank? + = render Display::AlertComponent.new(message: @federation_errors, type: "warning") - if @search_results .search-page-results-container - number = 0 @@ -69,4 +76,3 @@ .browse-empty-illustration %img{:src => "#{asset_path("empty-box.svg")}"} %p No result was found - \ No newline at end of file diff --git a/config/bioportal_config_env.rb.sample b/config/bioportal_config_env.rb.sample index 1b46a0ebbf..90dad17f3c 100644 --- a/config/bioportal_config_env.rb.sample +++ b/config/bioportal_config_env.rb.sample @@ -211,14 +211,12 @@ $HOME_PAGE_LOGOS = [ } ] - -# Federation configuration $PORTALS_INSTANCES = [ { name: 'AgroPortal', api: 'https://data.agroportal.lirmm.fr', ui: 'https://agroportal.lirmm.fr/', - color: '#349696', + color: '#3CB371', apikey: '1de0a270-29c5-4dda-b043-7c3580628cd5', 'light-color': '#F1F6FA', }, @@ -233,14 +231,17 @@ $PORTALS_INSTANCES = [ { name: 'SIFR BioPortal', ui: 'https://bioportal.lirmm.fr/', + api: 'https://data.bioportal.lirmm.fr/', + apikey: '1de0a270-29c5-4dda-b043-7c3580628cd5', color: '#74a9cb', + 'light-color': '#E9F2FA', }, { name: 'EcoPortal', ui: 'https://ecoportal.lifewatch.eu/', api: 'https://data.ecoportal.lifewatch.eu/', apikey: "43a437ba-a437-4bf0-affd-ab520e584719", - color: '#0d508a', + color: '#2076C9', 'light-color': '#E9F2FA', }, { @@ -256,26 +257,37 @@ $PORTALS_INSTANCES = [ { name: 'IndustryPortal', ui: 'http://industryportal.enit.fr', + api: 'https://data.industryportal.enit.fr/', + apikey: '019adb70-1d64-41b7-8f6e-8f7e5eb54942', color: '#1c0f5d', + 'light-color': '#F0F5F6', }, { name: 'EarthPortal', ui: 'https://earthportal.eu/', api: 'https://data.earthportal.eu/', apikey: "c9147279-954f-41bd-b068-da9b0c441288", - color: '#1e2251', + color: '#404696', 'light-color': '#F0F5F6' }, + { + name: 'TestPortal', + ui: 'https://testportal.lirmm.fr/', + api: 'https://data.testportal.lirmm.fr/', + color: '#74a9cb', + apikey: '1de0a270-29c5-4dda-b043-7c3580628cd5', + }, { name: 'BiodivPortal', ui: 'https://biodivportal.gfbio.org/', api: 'https://data.biodivportal.gfbio.org/', apikey: "47a57aa3-7b54-4f34-b695-dbb5f5b7363e", - color: '#33691B', + color: '#349696', 'light-color': '#EBF5F5', } ] + $ONTOPORTAL_WEBSITE_LINK = "https://ontoportal.org/" $ONTOPORTAL_GITHUB_REPO = "https://github.com/ontoportal" diff --git a/config/locales/en.yml b/config/locales/en.yml index fe6724cc51..e99e3420c6 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1501,4 +1501,9 @@ en: taxonomy: groups_and_categories: Groups and Categories description: In AgroPortal, ontologies are organized in groups and tagged with categories. Typically, groups associate ontologies from the same project or organization for better identification of the provenance. Whereas categories are about subjects/topics and enable to classify ontologies. As of 2016, AgroPortal's categories were established in cooperation with FAO AIMS. In 2024, we moved to UNESCO nomenclature for fields of science and technology. Groups and categories, along with other metadata, can be used on the “Browse” page of AgroPortal to filter out the list of ontologies. - show_sub_categories: Show sub categories \ No newline at end of file + show_sub_categories: Show sub categories + + federation: + results_from_external_portals: Results from external portals + from: from + not_responding: is not responding. \ No newline at end of file diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 9eebb36316..d5b33a5d7c 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -1539,4 +1539,9 @@ fr: taxonomy: groups_and_categories: Groupes et Catégories description: Dans AgroPortal, les ontologies sont organisées en groupes et étiquetées avec des catégories. Typiquement, les groupes associent des ontologies provenant du même projet ou de la même organisation pour une meilleure identification de la provenance. Tandis que les catégories concernent des sujets/thématiques et permettent de classifier les ontologies. En 2016, les catégories d'AgroPortal ont été établies en coopération avec FAO AIMS. En 2024, nous sommes passés à la nomenclature de l'UNESCO pour les domaines des sciences et des technologies. Les groupes et les catégories, ainsi que d'autres métadonnées, peuvent être utilisés sur la page “Parcourir” d'AgroPortal pour filtrer la liste des ontologies. - show_sub_categories: Afficher les sous-catégories \ No newline at end of file + show_sub_categories: Afficher les sous-catégories + federation: + results_from_external_portals: Résultats provenant de portails externes + from: de + not_responding: ne répond pas. + From 7eb8d766bf5b32337b036ae2b5b88035ef4f9463 Mon Sep 17 00:00:00 2001 From: Bilel Kihal <61744974+Bilelkihal@users.noreply.github.com> Date: Wed, 23 Oct 2024 01:24:30 +0200 Subject: [PATCH 09/28] Feature: Add federation portals status check (#769) * cache federation status call * update chip helpers to support disabled state * use turbo frame for federation input chips, and use federation portals status to disable non working portals * add skelton loading animation for federation input chips * cache federation input chips separately * use cached federation input chips in browse page * fix browse page federation inputs section title style * initialize federation portals input chips in the home page (asynch in the background) * clean federated search aggregator code * move chip skelton to components helper * internationalize portal is not responding message * clean federation portal status cache method * clean federation stimulus controller code * display federated browse errors as warning instead of danger (orange instead of red) * use federation portal status in home page portals configuration * fix issue in federation portal status method * clean portal_config_tooltip method to make it more readable * consider the portal as down if the api of it is not present in the config file * put federation portal status vue in a helper, and remove the html file of it * internationalize portals status message * extract federation_input_chips to a helper * put init portals status in home page logic in a helper for clarity * change home/federation_portals_status root to status/:portal_name * use dig in federation_portal_status to ensure to not raise an exception * move init portal status helper to federation file and fix class name * move federation helpers from application helper to federation file and fix variables calls * add loading state to the chips component and use it in federation status * rename federation chip component helper name to prevent conflicts --------- Co-authored-by: Syphax bouazzouni --- app/assets/stylesheets/browse.scss | 19 +++++++ app/assets/stylesheets/components/chips.scss | 37 +++++++++++++ app/components/chips_component.rb | 12 +++- .../chips_component/chips_component.html.haml | 20 ++++--- app/controllers/concerns/search_aggregator.rb | 1 - app/controllers/home_controller.rb | 9 +++ app/helpers/application_helper.rb | 1 + app/helpers/components_helper.rb | 18 ++++-- app/helpers/federation_helper.rb | 55 +++++++++++++++++++ app/helpers/home_helper.rb | 14 ++++- app/helpers/inputs_helper.rb | 4 +- app/views/home/index.html.haml | 3 +- app/views/ontologies/browser/browse.html.haml | 5 +- app/views/search/index.html.haml | 3 +- config/locales/en.yml | 3 +- config/locales/fr.yml | 2 +- config/routes.rb | 1 + 17 files changed, 181 insertions(+), 26 deletions(-) diff --git a/app/assets/stylesheets/browse.scss b/app/assets/stylesheets/browse.scss index 2dc96db265..a55292bde0 100644 --- a/app/assets/stylesheets/browse.scss +++ b/app/assets/stylesheets/browse.scss @@ -451,6 +451,25 @@ margin-top: 10px; } +.browse-federation-input-chips{ + display: flex; + flex-wrap: wrap; + margin: 0 15px 15px 15px; + div { + flex-grow: 1; + } + + label{ + display: grid; + } +} +.browse-federation-input-chip-container{ + p{ + font-size: 14px; + font-weight: 400; + color: #666666; + } +} @media only screen and (max-width: 1250px) { .browse-first-row { diff --git a/app/assets/stylesheets/components/chips.scss b/app/assets/stylesheets/components/chips.scss index 267fbb00db..13c15630d2 100644 --- a/app/assets/stylesheets/components/chips.scss +++ b/app/assets/stylesheets/components/chips.scss @@ -32,6 +32,7 @@ } .chips-container.disabled div label > span, .chips-container div label span:has(span.disabled){ + opacity: 60%; background-color: #f8f9fa !important; } @@ -43,3 +44,39 @@ .chips-container div label input[type="checkbox"]:checked ~ span .chips-check-icon{ display:unset; } + +.chips-container.loading div { + cursor: default; + opacity: 0.6; +} + +.chips-container .skeleton { + width: 80px; + height: 36px; + background-color: #e0e0e0; + border-radius: 5px; + animation: shimmer 4s infinite; + position: relative; + overflow: hidden; +} + +@keyframes shimmer { + 0% { + background-position: -200px 0; + } + 100% { + background-position: 200px 0; + } +} + +.chips-container .skeleton::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient(90deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0.6) 50%, rgba(255, 255, 255, 0.2) 100%); + animation: shimmer 4s infinite; + border-radius: 5px; +} diff --git a/app/components/chips_component.rb b/app/components/chips_component.rb index b92080d79c..101643a865 100644 --- a/app/components/chips_component.rb +++ b/app/components/chips_component.rb @@ -1,16 +1,26 @@ class ChipsComponent < ViewComponent::Base renders_one :count - def initialize(id:nil, name:, label: nil, value: nil, checked: false, tooltip: nil) + def initialize(id:nil, name:, label: nil, value: nil, checked: false, tooltip: nil, disabled: false, loading: false) @id = id || name @name = name @value = value || 'true' @checked = checked @label = label || @value @tooltip = tooltip + @disabled = disabled + @loading = loading end def checked? @checked end + + def disabled_class_name + @disabled ? 'disabled' : '' + end + + def loading_class_name + @loading ? 'loading' : '' + end end diff --git a/app/components/chips_component/chips_component.html.haml b/app/components/chips_component/chips_component.html.haml index f08d5a33dd..74acecd425 100644 --- a/app/components/chips_component/chips_component.html.haml +++ b/app/components/chips_component/chips_component.html.haml @@ -1,9 +1,13 @@ -.chips-container{class: @disabled ? 'disabled' : '', 'data-controller': 'tooltip', title: @tooltip} +.chips-container{class: "#{disabled_class_name} #{loading_class_name}", 'data-controller': 'tooltip', title: @tooltip} %div - %label{:for => "chips-#{@id}-check"} - %input{:id => "chips-#{@id}-check", :name => @name, :type => "checkbox", :value => @value, checked: checked?, disabled: @disabled} - %span - = inline_svg_tag 'check.svg', class: 'chips-check-icon' - %div - = @label - = count + - if @loading + %label + %span.skeleton + - else + %label{:for => "chips-#{@id}-check"} + %input{:id => "chips-#{@id}-check", :name => @name, :type => "checkbox", :value => @value, checked: checked?, disabled: @disabled} + %span + = inline_svg_tag 'check.svg', class: 'chips-check-icon' + %div + = @label + = count diff --git a/app/controllers/concerns/search_aggregator.rb b/app/controllers/concerns/search_aggregator.rb index a45f0237ee..e8a72a74ed 100644 --- a/app/controllers/concerns/search_aggregator.rb +++ b/app/controllers/concerns/search_aggregator.rb @@ -91,7 +91,6 @@ def search_result_elem(class_object, ontology_acronym, title) result end - def ontology_name_acronym(ontologies, selected_acronym) ontology = ontologies.select { |x| x.acronym.eql?(selected_acronym.split('/').last) }.first "#{ontology.name} (#{ontology.acronym})" if ontology diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index bc9531e562..772d6e52fc 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -166,6 +166,15 @@ def annotator_recommender_form end end + def federation_portals_status + @name = params[:name] + @acronym = params[:acronym] + @key = params[:portal_name] + @checked = params[:checked].eql?('true') + @portal_up = federation_portal_status(portal_name: @key.downcase.to_sym) + render inline: helpers.federation_chip_component(@key, @name, @acronym, @checked, @portal_up) + end + private # Dr. Musen wants 5 specific groups to appear first, sorted by order of importance. diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index e061a0a05e..81a97e01fc 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -551,4 +551,5 @@ def categories_select(id: nil, name: nil, selected: 'None') categories_for_select = LinkedData::Client::Models::Category.all.map{|x| ["#{x.name} (#{x.acronym})", x.id]}.unshift(["None", '']) render Input::SelectComponent.new(id: id, name: name, value: categories_for_select, selected: selected, multiple: true) end + end diff --git a/app/helpers/components_helper.rb b/app/helpers/components_helper.rb index e43d546db5..6d69ad2ad3 100644 --- a/app/helpers/components_helper.rb +++ b/app/helpers/components_helper.rb @@ -14,19 +14,19 @@ def alert_component(message, type: "info") render Display::AlertComponent.new(type: type, message: message) end - def chips_component(id: , name: , label: , value: , checked: false , tooltip: nil, &block) + def chips_component(id: , name: , label: , value: , checked: false , tooltip: nil, disabled: false, &block) content_tag(:div, data: { controller: 'tooltip' }, title: tooltip) do - check_input(id: id, name: name, value: value, label: label, checked: checked, &block) + check_input(id: id, name: name, value: value, label: label, checked: checked, disabled: disabled, &block) end end - def group_chip_component(id: nil, name: , object: , checked: , value: nil, title: nil, &block) + def group_chip_component(id: nil, name: , object: , checked: , value: nil, title: nil, disabled: false, &block) title ||= object["name"] value ||= (object["value"] || object["acronym"] || object["id"]) chips_component(id: id || value, name: name, label: object["acronym"], checked: checked, - value: value, tooltip: title, &block) + value: value, tooltip: title, disabled: disabled, &block) end alias :category_chip_component :group_chip_component @@ -292,4 +292,14 @@ def form_cancel_button end end + def chips_skelton + content_tag(:div, class: 'chips-container loading') do + content_tag(:div) do + content_tag(:label) do + content_tag(:span, '', class: 'skeleton') + end + end + end + end + end diff --git a/app/helpers/federation_helper.rb b/app/helpers/federation_helper.rb index 572e26db3e..d1e9e9f755 100644 --- a/app/helpers/federation_helper.rb +++ b/app/helpers/federation_helper.rb @@ -124,4 +124,59 @@ def federation_external_class?(class_object) end + def federation_portal_status(portal_name: nil) + Rails.cache.fetch("federation_portal_up_#{portal_name}", expires_in: 2.hours) do + portal_api = federated_portals&.dig(portal_name,:api) + return false unless portal_api + portal_up = false + begin + response = Faraday.new(url: portal_api) do |f| + f.adapter Faraday.default_adapter + f.request :url_encoded + f.options.timeout = 20 + f.options.open_timeout = 20 + end.head + portal_up = response.success? + rescue StandardError => e + Rails.logger.error("Error checking portal status for #{portal_name}: #{e.message}") + end + portal_up + end + end + + def federation_chip_component(key, name, acronym, checked, portal_up) + render TurboFrameComponent.new(id:"federation_portals_status_#{key}") do + content_tag(:div, style: "cursor: default;") do + title = "#{!portal_up ? "#{key.humanize.gsub('portal', 'Portal')} #{t('federation.not_responding')}" : ''}" + group_chip_component(name: name, + object: { "acronym" => acronym, "value" => key }, + checked: checked, + title: title , + disabled: !portal_up) + end + end + end + + def federation_input_chips(name: nil) + federated_portals.map do |key, config| + turbo_frame_component = TurboFrameComponent.new( + id: "federation_portals_status_#{key}", + src: "status/#{key}?name=#{name}&acronym=#{config[:name]}&checked=#{request_portals.include?(key.to_s)}" + ) + + content_tag :div do + render(turbo_frame_component) do |container| + container.loader do + render ChipsComponent.new(name: '', loading: true, tooltip: t('federation.check_status', portal: key.to_s.humanize.gsub('portal', 'Portal'))) + end + end + end + end.join.html_safe + end + + def init_federation_portals_status + content_tag(:div, class: 'd-none') do + federation_input_chips + end + end end diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb index 0a5498b01d..2ddcdc5275 100644 --- a/app/helpers/home_helper.rb +++ b/app/helpers/home_helper.rb @@ -23,12 +23,21 @@ def format_number_abbreviated(number) end def portal_config_tooltip(portal_name, &block) - title = render TurboFrameComponent.new(id: "portal_config_tooltip_#{portal_name&.downcase}", src: "/config?portal=#{portal_name&.downcase}", style: "width: 600px !important; max-height: 300px; overflow: scroll") + portal_id = portal_name&.downcase + title = if federation_portal_status(portal_name: portal_id) + render( + TurboFrameComponent.new( + id: "portal_config_tooltip_#{portal_id}", + src: "/config?portal=#{portal_id}", + style: "width: 600px !important; max-height: 300px; overflow: scroll" + ) + ) + end render Display::InfoTooltipComponent.new(text: title, interactive: true) do capture(&block) end end - + def discover_ontologies_button render Buttons::RegularButtonComponent.new(id: 'discover-ontologies-button', value: t('home.discover_ontologies_button'), variant: "secondary", state: "regular", href: "/ontologies") do |btn| btn.icon_right do @@ -43,4 +52,5 @@ def home_ontoportal_description content_tag(:div, t('home.ontoportal_description', ontoportal_link: ontoportal_link, github_link: github_link).html_safe, style: "margin-bottom: 20px") end + end diff --git a/app/helpers/inputs_helper.rb b/app/helpers/inputs_helper.rb index 94b677ccd2..242de76990 100644 --- a/app/helpers/inputs_helper.rb +++ b/app/helpers/inputs_helper.rb @@ -28,8 +28,8 @@ def number_input(name: , label: '', value: ) value: value) end - def check_input(id:, name:, value:, label: '', checked: false, &block) - render ChipsComponent.new(name: name, id: id, label: label, value: value, checked: checked) do |c| + def check_input(id:, name:, value:, label: '', checked: false, disabled: false, &block) + render ChipsComponent.new(name: name, id: id, label: label, value: value, checked: checked, disabled: disabled) do |c| if block_given? capture(c, &block) end diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index fd32acc016..28a0a25504 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -230,6 +230,8 @@ %a{href:logo[:url], target: "_blanc"} %img{src: asset_path(logo[:img_src])} += init_federation_portals_status + :javascript function submitAnnotator(){ document.getElementById("annotator_submit").click() @@ -237,4 +239,3 @@ function submitRecommender(){ document.getElementById("recommender_submit").click() } - diff --git a/app/views/ontologies/browser/browse.html.haml b/app/views/ontologies/browser/browse.html.haml index 34d195e55a..baca3ce7b8 100644 --- a/app/views/ontologies/browser/browse.html.haml +++ b/app/views/ontologies/browser/browse.html.haml @@ -73,9 +73,8 @@ = render LoaderComponent.new(small:true) .browse-federation-input-chip-container = render DropdownContainerComponent.new(id: "browse-portal-filter", is_open: !request_portals.empty?, title: t('federation.results_from_external_portals')) do - .browse-filter-checks-container.px-1 - - federated_portals.each do |key, config| - = group_chip_component(name: "portals", object: { "acronym" => config[:name], "value" => key }, checked: request_portals.include?(key.to_s), title: '') + .px-1.browse-federation-input-chips + = federation_input_chips(name: "portals") .browse-second-row .browse-search-bar diff --git a/app/views/search/index.html.haml b/app/views/search/index.html.haml index d5f117cf13..1fe45b7ae8 100644 --- a/app/views/search/index.html.haml +++ b/app/views/search/index.html.haml @@ -19,8 +19,7 @@ .title = t('federation.results_from_external_portals') .field.d-flex - - federated_portals.each do |key, config| - = group_chip_component(name: "portals[]", object: { "acronym" => config[:name], "value" => key }, checked: request_portals.include?(key.to_s), title: '') + = federation_input_chips(name: "portals[]") .right .filter-container .title diff --git a/config/locales/en.yml b/config/locales/en.yml index e99e3420c6..cc3d73bb4e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1506,4 +1506,5 @@ en: federation: results_from_external_portals: Results from external portals from: from - not_responding: is not responding. \ No newline at end of file + not_responding: is not responding. + check_status: Checking %{portal} availability diff --git a/config/locales/fr.yml b/config/locales/fr.yml index d5b33a5d7c..b54f2d4430 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -1544,4 +1544,4 @@ fr: results_from_external_portals: Résultats provenant de portails externes from: de not_responding: ne répond pas. - + check_status: Vérification de la disponibilité de %{portal} diff --git a/config/routes.rb b/config/routes.rb index b28c26ec02..b3760280fb 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -131,6 +131,7 @@ end get '' => 'home#index' + get 'status/:portal_name', to: 'home#federation_portals_status' match 'sparql_proxy', to: 'admin#sparql_endpoint', via: [:get, :post] From e8df1678acfb6a1061d822edc1c81abf79244946 Mon Sep 17 00:00:00 2001 From: Bilel Kihal <61744974+Bilelkihal@users.noreply.github.com> Date: Wed, 23 Oct 2024 02:49:42 +0200 Subject: [PATCH 10/28] Fix: federated browse page not updating the categories when portals changed (#779) * sort by name when we do federated browse * extract categories and groups inputs sections into a turbo frame * use switch input helper in the browse page for more clarity * create dropdown component helper and add small argument to loader helper * use current_user_admin? helper instead of session[:user]&.admin? * move some of the browse page code to helpers * fix the bug of mixing the section filters because this.element was the container not the filters section blocks * update the categories section in browse page if request_portal changed --------- Co-authored-by: Syphax --- app/controllers/ontologies_controller.rb | 15 ++- app/helpers/components_helper.rb | 10 +- app/helpers/ontologies_helper.rb | 108 +++++++++++++----- .../controllers/browse_filters_controller.js | 15 ++- app/views/ontologies/browser/browse.html.haml | 54 +++------ 5 files changed, 131 insertions(+), 71 deletions(-) diff --git a/app/controllers/ontologies_controller.rb b/app/controllers/ontologies_controller.rb index 4b3008a37e..81dfe0bc9a 100644 --- a/app/controllers/ontologies_controller.rb +++ b/app/controllers/ontologies_controller.rb @@ -49,15 +49,28 @@ def ontologies_filter if @page.page.eql?(1) streams = [prepend("ontologies_list_view-page-#{@page.page}", partial: 'ontologies/browser/ontologies')] + streams += @count_objects.map do |section, values_count| values_count.map do |value, count| replace("count_#{section}_#{link_last_part(value)}") do helpers.turbo_frame_tag("count_#{section}_#{link_last_part(value)}") do - helpers.content_tag(:span, count.to_s, class: "hide-if-loading #{count.zero? ? 'disabled' : ''}") + helpers.content_tag(:span, count.to_s, class: "hide-if-loading #{count.zero? ? 'disabled' : ''}") end end end end.flatten + + unless request_portals.empty? + streams += [ + replace('categories_refresh_for_federation') do + key = "categories" + objects, checked_values, _ = @filters[key.to_sym] + helpers.browse_filter_section_body(checked_values: checked_values, + key: key, objects: objects, + counts: @count_objects[key.to_sym]) + end + ] + end else streams = [replace("ontologies_list_view-page-#{@page.page}", partial: 'ontologies/browser/ontologies')] end diff --git a/app/helpers/components_helper.rb b/app/helpers/components_helper.rb index 6d69ad2ad3..5c43b24d8a 100644 --- a/app/helpers/components_helper.rb +++ b/app/helpers/components_helper.rb @@ -1,6 +1,12 @@ module ComponentsHelper include TermsReuses + def dropdown_component(id: ,title: nil, tooltip:nil , is_open: false, &block) + render DropdownContainerComponent.new(id: id, title: title, tooltip: tooltip, is_open: is_open) do |d| + capture(d, &block) if block_given? + end + end + def portal_button(name: nil , color: nil , light_color: nil, link: nil, tooltip: nil) render FederatedPortalButtonComponent.new(name: name, color: color, link: link, tooltip: tooltip, light_color: light_color) end @@ -183,8 +189,8 @@ def chart_component(title: '', type:, labels:, datasets:, index_axis: 'x', show_ content_tag(:canvas, nil, data: data) end - def loader_component(type = 'pulsing') - render LoaderComponent.new(type: type) + def loader_component(type:'pulsing', small: false ) + render LoaderComponent.new(type: type, small: small) end def info_tooltip(text, interactive: true) diff --git a/app/helpers/ontologies_helper.rb b/app/helpers/ontologies_helper.rb index f37836e9fb..f3a7ae05f7 100644 --- a/app/helpers/ontologies_helper.rb +++ b/app/helpers/ontologies_helper.rb @@ -71,32 +71,10 @@ def ontology_alternative_names(submission = @submission_latest) end) end end + def private_ontology_icon(is_private) raw(content_tag(:i, '', class: 'fas fa-key', title: t('ontologies.private_ontology'))) if is_private end - def browse_filter_section_label(key) - labels = { - categories: t('ontologies.categories'), - groups: t('ontologies.groups'), - hasFormalityLevel: t('ontologies.formality_levels'), - isOfType: t('ontologies.ontology_types'), - naturalLanguage: t('ontologies.natural_languages') - } - - labels[key] || key.to_s.underscore.humanize.capitalize - end - - def browser_counter_loader - content_tag(:div, class: "browse-desc-text", style: "margin-bottom: 15px;") do - content_tag(:div, class: "d-flex align-items-center") do - str = content_tag(:span, t('ontologies.showing')) - str += content_tag(:span, "", class: "p-1 p-2", style: "color: #a7a7a7;") do - render LoaderComponent.new(small: true) - end - str - end - end - end def ontologies_browse_skeleton(pagesize = 5) pagesize.times do @@ -815,7 +793,6 @@ def n_triples_to_table(n_triples_string) end end - private def submission_languages(submission = @submission) Array(submission&.naturalLanguage).map { |natural_language| natural_language["iso639"] && natural_language.split('/').last }.compact @@ -825,12 +802,87 @@ def id_to_acronym(id) id.split('/').last end - def browse_taxonomy_tooltip(texonomy) - content_tag(:div, class: 'd-flex') do - content_tag(:div, "See more information about #{texonomy} in ", class: 'mr-1') + - content_tag(:a, 'here', href: "/#{texonomy}", target: '_blank') + def browse_taxonomy_tooltip(taxonomy_type) + return nil unless taxonomy_type.eql?("categories") || taxonomy_type.eql?("groups") + + content_tag(:div, class: '') do + content_tag(:span, "See more information about #{taxonomy_type} in ", class: 'mr-1') + + content_tag(:a, 'here', href: "/#{taxonomy_type}", target: '_blank') + end + end + + def browse_chip_filter(key:, object:, values:, countable: true, count: nil) + title = (key.to_s.eql?("categories") || key.to_s.eql?("groups")) ? nil : '' + checked = values.any? { |obj| [link_last_part(object["id"]), link_last_part(object["value"])].include?(obj) } + + group_chip_component(name: key, object: object, checked: checked, title: title) do |c| + c.count { browse_chip_count_badge(key: key, id: object["id"], count: count) } if countable end end + def browse_chip_count_badge(id:, key:, count: nil) + content_tag :span, class: 'badge badge-light ml-1' do + turbo_frame_tag("count_#{key}_#{link_last_part(id)}", busy: true) + + if count || count == 0 + content_tag(:span, count.to_s, class: "hide-if-loading #{count.zero? ? 'disabled' : ''}") + else + content_tag(:span, class: 'show-if-loading') do + loader_component(small: true, type: nil) + end + end + end + end + + def browse_filter_section_label(key) + labels = { + categories: t('ontologies.categories'), + groups: t('ontologies.groups'), + hasFormalityLevel: t('ontologies.formality_levels'), + isOfType: t('ontologies.ontology_types'), + naturalLanguage: t('ontologies.natural_languages') + } + + labels[key] || key.to_s.underscore.humanize.capitalize + end + + def browse_filter_section_header(key: nil, count: nil, title: nil) + render Display::HeaderComponent.new(tooltip: key ? browse_taxonomy_tooltip(key.to_s) : nil) do + content_tag(:span, class: "browse-filter-title-bar") do + concat title || browse_filter_section_label(key) + + concat content_tag(:span, count, class: "badge badge-primary mx-1", + "data-show-filter-count-target": "countSpan", + style: "#{count&.positive? ? '' : 'display: none;'}") + end + + end + end + + def browse_filter_section_body(checked_values: , key:, objects:, countable: true, counts: nil) + output = content_tag(:div, class: "browse-filter-checks-container px-3") do + Array(objects).map do |object| + count = counts ? counts[link_last_part(object["id"])] || 0 : nil + concat browse_chip_filter(key: key, object: object, values: checked_values, countable: countable, count: count) + end + end + + if key.to_s.include?("categories") + turbo_frame_tag('categories_refresh_for_federation') { output.html_safe } + else + output + end + end + + def browser_counter_loader + content_tag(:div, class: "browse-desc-text", style: "margin-bottom: 15px;") do + content_tag(:div, class: "d-flex align-items-center") do + str = content_tag(:span, t('ontologies.showing')) + str += content_tag(:span, "", class: "p-1 p-2", style: "color: #a7a7a7;") do + render LoaderComponent.new(small: true) + end + str + end + end + end end diff --git a/app/javascript/controllers/browse_filters_controller.js b/app/javascript/controllers/browse_filters_controller.js index 74272dc26a..51eff387bb 100644 --- a/app/javascript/controllers/browse_filters_controller.js +++ b/app/javascript/controllers/browse_filters_controller.js @@ -2,6 +2,7 @@ import {Controller} from "@hotwired/stimulus" import debounce from "debounce" // Connects to data-controller="browse-filters" export default class extends Controller { + static targets = ['sort'] initialize() { this.dispatchInputEvent = debounce(this.dispatchInputEvent.bind(this), 700); @@ -46,11 +47,16 @@ export default class extends Controller { filter = "private_only" break; default: - checks = this.#getSelectedChecks().map(x => x.value) + checks = this.#getSelectedChecks(event).map(x => x.value) filter = event.target.name } - this.#dispatchEvent(filter, checks) + event.stopPropagation() + } + + federationChange(event){ + this.sortTarget.value = "ontology_name" + this.sortTarget.dispatchEvent(new Event('change', { bubbles: true })) } @@ -63,11 +69,10 @@ export default class extends Controller { data: data }, bubbles: true }); - this.element.dispatchEvent(customEvent); } - #getSelectedChecks() { - return Array.from(this.element.querySelectorAll('input:checked')) + #getSelectedChecks(event) { + return Array.from(event.currentTarget.querySelectorAll('input:checked')) } } diff --git a/app/views/ontologies/browser/browse.html.haml b/app/views/ontologies/browser/browse.html.haml index baca3ce7b8..5a5a122080 100644 --- a/app/views/ontologies/browser/browse.html.haml +++ b/app/views/ontologies/browser/browse.html.haml @@ -33,48 +33,32 @@ %div{data: { controller: "turbo-frame history browse-filters" , "turbo-frame-url-value": "/ontologies_filter?page=1&#{request.original_url.split('?').last}", action: "change->browse-filters#dispatchFilterEvent changed->history#updateURL changed->turbo-frame#updateFrame"}} .browse-sub-container - .browse-first-row{data:{controller: "browse-filters", action: "change->browse-filters#dispatchFilterEvent changed->history#updateURL"}} + .browse-first-row %div.pt-1 = upload_ontology_button %div{style:'margin-top: 30px'} %p.browse-filters-title= t("ontologies.filters") - - if session[:user]&.admin? + - if current_user_admin? %div.browse-filter.admin-border - = render SwitchInputComponent.new(id:'filter-private', name:'private_only', checked: @show_private_only) do - = t("ontologies.browser.show_private_ontology") + = switch_input(id:'filter-private', name:'private_only', checked: @show_private_only, label: t("ontologies.browser.show_private_ontology")) %div.browse-filter - = render SwitchInputComponent.new(id:'filter-views', name:'views', checked: @show_views) do - = t("ontologies.browser.show_ontology_views") - = render SwitchInputComponent.new(id:'filter-retired', name:'retired',checked: @show_retired) do - = t("ontologies.browser.show_retired_ontologies") + = switch_input(id:'filter-views', name:'views', checked: @show_views, label: t("ontologies.browser.show_ontology_views")) + = switch_input(id:'filter-retired', name:'retired',checked: @show_retired , label: t("ontologies.browser.show_retired_ontologies")) - @filters.each do |key, values| - - if session[:user]&.admin? || key != :missingStatus - .browse-filter{data:{controller: "show-filter-count browse-filters", action: "change->show-filter-count#updateCount change->browse-filters#dispatchFilterEvent"}, id: "#{key}_filter_container", style: "#{"border-color: var(--admin-color);" if key == :missingStatus}"} - .browse-filter-title-bar{"data-target" => "#browse-#{key}-filter", "data-toggle" => "collapse"} - %p - = browse_filter_section_label(key) - %span.badge.badge-primary{"data-show-filter-count-target":"countSpan", style: "#{values[2] && values[2].positive? ? '' : 'display: none;'}"} - = values[2] - .d-flex.align-items-center - - if key.eql?(:categories) || key.eql?(:groups) - .mr-2 - = render Display::InfoTooltipComponent.new(text: browse_taxonomy_tooltip(key.to_s)) - = inline_svg_tag 'arrow-down.svg' - .collapse{id: "browse-#{key}-filter", class: "#{values[2].positive? ? 'show': ''}"} - .browse-filter-checks-container - - values.first.each do |object| - - title = (key.eql?(:categories) || key.eql?(:groups)) ? nil : '' - = group_chip_component(name: key, object: object, checked: values[1].any?(link_last_part(object["id"])) || values[1].any?(link_last_part(object["value"])) , title: title) do |c| - - c.count do - %span.badge.badge-light.ml-1 - = turbo_frame_tag "count_#{key}_#{link_last_part(object["id"])}", busy: true - %span.show-if-loading - = render LoaderComponent.new(small:true) - .browse-federation-input-chip-container - = render DropdownContainerComponent.new(id: "browse-portal-filter", is_open: !request_portals.empty?, title: t('federation.results_from_external_portals')) do - .px-1.browse-federation-input-chips - = federation_input_chips(name: "portals") + %div{ id: "#{key}_filter_container", data:{controller: "browse_filters show-filter-count", + action: "change->show-filter-count#updateCount + change->browse-filters#dispatchFilterEvent"}} + - objects, checked_values, count = values + = dropdown_component(id: "browse-#{key}-filter", is_open: count.positive?) do |d| + - d.title { browse_filter_section_header(key: key, count: count)} + = browse_filter_section_body(key: key, checked_values: checked_values, objects: objects) + + %div{ data:{action: "change->browse-filters#federationChange"}} + = dropdown_component(id: "browse-portal-filter", is_open: !request_portals.empty?) do |d| + - d.title { browse_filter_section_header(title: t('federation.results_from_external_portals'))} + .px-1.browse-federation-input-chips + = federation_input_chips(name: "portals") .browse-second-row .browse-search-bar @@ -83,7 +67,7 @@ .browse-search-filters %select#format.browse-format-filter{:name => "format"} = options_for_select(@formats, @selected_format) - %select#Sort_by.browse-sort-by-filter{:name => "Sort_by"} + %select#Sort_by.browse-sort-by-filter{name: "Sort_by", 'data-browse-filters-target': "sort"} = options_for_select(@sorts_options, @sort_by) .browse-ontologies = render TurboFrameComponent.new(id: "ontologies_list_view-page-1" , src: "/ontologies_filter?page=1&#{request.original_url.split('?').last}", data:{"turbo-frame-target":"frame", "turbo-frame-url-value": "/ontologies_filter"}) do |list| From ea3526a69dc8d930e075ec56db088d22789bf889 Mon Sep 17 00:00:00 2001 From: Bilel Kihal <61744974+Bilelkihal@users.noreply.github.com> Date: Wed, 23 Oct 2024 04:38:58 +0200 Subject: [PATCH 11/28] Feature: Add federation results counts grouped by portal (#778) * add text for api json button in search page, and show it only when the search is performed * count federated search results count by portal * show separate portals federated search result counts * show separate federated browse ontologies counts for each portal * Fix color issue in browse page portal name button * fix federated browse counts function to handle all cases * limit search page results count sentence width * fix api json button non clickable in search page * Fix external ontology icon when the ontology is not external in the browse page * display ontology sources buttons in browse page for internal ontologies that are present in other portals * use helpers for re-used ui components in the search UI page * extract same helper for the error message alert UI component * harmonize federation result text code * fix text_with_icon helper style * remove no more used :from local text key * complete match_in local text * move urls related helpers from application_helper to urls_helpers file * update search aggregator and result component to save ontology id * extract common logic to count ontology id from search and browse results * clean url helpers usages --------- Co-authored-by: Syphax --- app/assets/stylesheets/search.scss | 2 + .../display/search_result_component.rb | 4 +- .../ontology_browse_card_component.rb | 2 +- .../ontology_browse_card_component.html.haml | 12 ++-- app/controllers/concerns/search_aggregator.rb | 8 +-- app/controllers/concerns/submission_filter.rb | 4 +- app/controllers/ontologies_controller.rb | 4 +- app/controllers/search_controller.rb | 1 + app/helpers/application_helper.rb | 49 +-------------- app/helpers/components_helper.rb | 18 +----- app/helpers/federation_helper.rb | 63 +++++++++++++++---- app/helpers/instances_helper.rb | 2 +- app/helpers/mappings_helper.rb | 2 +- app/helpers/urls_helper.rb | 40 ++++++++++++ .../ontologies/browser/_ontologies.html.haml | 18 +++--- app/views/ontologies/browser/browse.html.haml | 2 +- app/views/recommender/index.html.haml | 4 +- app/views/search/index.html.haml | 60 +++++++++--------- config/locales/en.yml | 4 +- config/locales/fr.yml | 4 +- 20 files changed, 160 insertions(+), 143 deletions(-) diff --git a/app/assets/stylesheets/search.scss b/app/assets/stylesheets/search.scss index e66312f7e2..5dbea30de7 100644 --- a/app/assets/stylesheets/search.scss +++ b/app/assets/stylesheets/search.scss @@ -69,6 +69,7 @@ .search-page-advanced-button .text{ margin-left: 10px; color: var(--primary-color); + margin-top: 2px; } .search-page-advanced-button .icon svg path{ @@ -76,6 +77,7 @@ } .search-page-number-of-results{ color: #888888; + max-width: 800px; } .search-page-result-element{ diff --git a/app/components/display/search_result_component.rb b/app/components/display/search_result_component.rb index e5be88256b..4b112b60f8 100644 --- a/app/components/display/search_result_component.rb +++ b/app/components/display/search_result_component.rb @@ -8,13 +8,13 @@ class Display::SearchResultComponent < ViewComponent::Base renders_many :subresults, Display::SearchResultComponent renders_many :reuses, Display::SearchResultComponent - def initialize(number: 0,title: nil, ontology_acronym: nil ,uri: nil, definition: nil, link: nil, is_sub_component: false, portal_name: nil, portal_color: nil, portal_light_color: nil, other_portals: []) + def initialize(number: 0,title: nil, ontology_id: nil ,uri: nil, definition: nil, link: nil, is_sub_component: false, portal_name: nil, portal_color: nil, portal_light_color: nil, other_portals: []) @title = title @uri = uri @definition = definition @link = link @is_sub_component = is_sub_component - @ontology_acronym = ontology_acronym + @ontology_acronym = ontology_id&.split('/')&.last @number = number.to_s @portal_name = portal_name @portal_color = portal_color diff --git a/app/components/ontology_browse_card_component.rb b/app/components/ontology_browse_card_component.rb index 24d15ab661..cb40202109 100644 --- a/app/components/ontology_browse_card_component.rb +++ b/app/components/ontology_browse_card_component.rb @@ -17,7 +17,7 @@ def ontology end def external_ontology? - !internal_ontology?(@ontology[:id]) || (Array(@ontology[:sources]).size > 1) + !internal_ontology?(@ontology[:id]) end def onto_link diff --git a/app/components/ontology_browse_card_component/ontology_browse_card_component.html.haml b/app/components/ontology_browse_card_component/ontology_browse_card_component.html.haml index 51f7986ea0..4f02e889f4 100644 --- a/app/components/ontology_browse_card_component/ontology_browse_card_component.html.haml +++ b/app/components/ontology_browse_card_component/ontology_browse_card_component.html.haml @@ -74,12 +74,12 @@ %span.mx-1{data:{controller:'tooltip'}, title: t('components.view_of_the_ontology', ontology: ontology[:viewOfOnt].split('/').last )} = render ChipButtonComponent.new(type: "clickable", text: t('components.view'), style: style_bg) - - if external_ontology? - - ontology[:sources].each do |id| - - config = ontology_portal_config(id)&.last || internal_portal_config(id) || {} - - unless config.blank? - %span{style: "padding: 3px 0; margin-right: 0.25rem;"} - = portal_button(name: config[:name], color: @text_color, light_color: @bg_light_color, link: ontoportal_ui_link(id), tooltip: "Source #{config[:name]}") + + - ontology[:sources]&.each do |id| + - config = ontology_portal_config(id)&.last || internal_portal_config(id) || {} + - unless config.blank? + %span{style: "padding: 3px 0; margin-right: 0.25rem;"} + = portal_button(name: config[:name], color: config[:color], light_color: config[:"light-color"], link: ontoportal_ui_link(id), tooltip: "Source #{config[:name]}") - if session[:user]&.admin? %div.mx-1{title: content_tag(:div, debug(ontology), style: 'height: 300px; overflow: scroll'), data:{controller: 'tooltip', 'tooltip-interactive-value': 'true'}} diff --git a/app/controllers/concerns/search_aggregator.rb b/app/controllers/concerns/search_aggregator.rb index e8a72a74ed..71f22ad423 100644 --- a/app/controllers/concerns/search_aggregator.rb +++ b/app/controllers/concerns/search_aggregator.rb @@ -44,7 +44,7 @@ def format_search_result(result, ontologies) same_ont = result[:same_ont] same_cls = result[:sub_ont] result = same_ont.shift - ontology = result.links['ontology'].split('/').last + ontology = result.links['ontology'] { root: search_result_elem(result, ontology, ontology_name_acronym(ontologies, ontology)), descendants: same_ont.map { |x| search_result_elem(x, ontology, '') }, @@ -74,14 +74,14 @@ def search_concept_label(label) label end - def search_result_elem(class_object, ontology_acronym, title) + def search_result_elem(class_object, ontology_id, title) label = search_concept_label(class_object.prefLabel) request_lang = helpers.request_lang&.eql?("ALL") ? '' : "&language=#{helpers.request_lang}" - + ontology_acronym = link_last_part(ontology_id) result = { uri: class_object.id.to_s, title: title.to_s.empty? ? "#{label} - #{ontology_acronym}" : "#{label} - #{title}", - ontology_acronym: ontology_acronym, + ontology_id: ontology_id, link: "/ontologies/#{ontology_acronym}?p=classes&conceptid=#{escape(class_object.id)}#{request_lang}", definition: class_object.definition, } diff --git a/app/controllers/concerns/submission_filter.rb b/app/controllers/concerns/submission_filter.rb index b37beeb8d4..2097fbb7e7 100644 --- a/app/controllers/concerns/submission_filter.rb +++ b/app/controllers/concerns/submission_filter.rb @@ -56,7 +56,9 @@ def submissions_paginate_filter(params) count = @page.page.eql?(1) ? count_objects(submissions) : {} - [@page.collection, @page.totalCount, count, filter_params] + federation_counts = federated_browse_counts(submissions) + + [@page.collection, @page.totalCount, count, filter_params, federation_counts] end def ontologies_with_filters_url(filters, page: 1, count: false) diff --git a/app/controllers/ontologies_controller.rb b/app/controllers/ontologies_controller.rb index 81dfe0bc9a..041c1e6361 100644 --- a/app/controllers/ontologies_controller.rb +++ b/app/controllers/ontologies_controller.rb @@ -44,7 +44,7 @@ def index def ontologies_filter @time = Benchmark.realtime do - @ontologies, @count, @count_objects, @request_params = submissions_paginate_filter(params) + @ontologies, @count, @count_objects, @request_params, @federation_counts = submissions_paginate_filter(params) end if @page.page.eql?(1) @@ -206,7 +206,7 @@ def schemes @schemes = get_schemes(@ontology) scheme_id = params[:schemeid] || @submission_latest.URI || nil @scheme = scheme_id ? get_scheme(@ontology, scheme_id) : @schemes.first - + render partial: 'ontologies/sections/schemes', layout: 'ontology_viewer' end diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index b6a7c5ede4..680d58cce8 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -30,6 +30,7 @@ def index @search_results = aggregate_results(@search_query, results) + @federation_counts = federated_search_counts(@search_results) end @advanced_options_open = !search_params_empty? @json_url = json_link("#{rest_url}/search", params.permit!.to_h) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 81a97e01fc..0150c3b6a3 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -11,11 +11,7 @@ module ApplicationHelper include ModalHelper, MultiLanguagesHelper, UrlsHelper - def url_to_endpoint(url) - uri = URI.parse(url) - endpoint = uri.path.sub(/^\//, '') - endpoint - end + RESOLVE_NAMESPACE = {:omv => "http://omv.ontoware.org/2005/05/ontology#", :skos => "http://www.w3.org/2004/02/skos/core#", :owl => "http://www.w3.org/2002/07/owl#", :rdf => "http://www.w3.org/1999/02/22-rdf-syntax-ns#", :rdfs => "http://www.w3.org/2000/01/rdf-schema#", :metadata => "http://data.bioontology.org/metadata/", :metadata_def => "http://data.bioontology.org/metadata/def/", :dc => "http://purl.org/dc/elements/1.1/", :xsd => "http://www.w3.org/2001/XMLSchema#", @@ -59,18 +55,6 @@ def get_apikey end end - def rest_hostname - extract_hostname(REST_URI) - end - - def extract_hostname(url) - begin - uri = URI.parse(url) - uri.hostname - rescue URI::InvalidURIError - url - end - end def omniauth_providers_info $OMNIAUTH_PROVIDERS @@ -84,11 +68,6 @@ def omniauth_token_provider(strategy) omniauth_provider_info(strategy.to_sym).keys.first end - def encode_param(string) - CGI.escape(string) - end - - def current_user session[:user] end @@ -102,9 +81,6 @@ def child_id(child) child.id.to_s.split('/').last end - - - # Create a popup button with a ? inside to display help when hovered def help_tooltip(content, html_attribs = {}, icon = 'fas fa-question-circle', css_class = nil, text = nil) html_attribs["title"] = content attribs = [] @@ -145,15 +121,6 @@ def onts_for_select onts_for_select end - def link_last_part(url) - return "" if url.nil? - - if url.include?('#') - url.split('#').last - else - url.split('/').last - end - end def at_slice? !@subdomain_filter.nil? && !@subdomain_filter[:active].nil? && @subdomain_filter[:active] == true @@ -186,14 +153,6 @@ def add_proposal_button(parent_id, parent_type) end - def link?(str) - # Regular expression to match strings starting with "http://" or "https://" - link_pattern = /\Ahttps?:\/\// - str = str&.strip - # Check if the string matches the pattern - !!(str =~ link_pattern) - end - def subscribe_button(ontology_id) return if ontology_id.nil? render TurboFrameComponent.new(id: 'subscribe_button', src: ontology_subscriptions_path(ontology_id: ontology_id.split('/').last), class: 'ml-1') do |t| @@ -363,10 +322,6 @@ def help_path(anchor: nil) "#{Rails.configuration.settings.links[:help]}##{anchor}" end - def uri?(url) - url =~ /\A#{URI::DEFAULT_PARSER.make_regexp(%w[http https])}\z/ - end - def extract_label_from(uri) label = uri.to_s.chomp('/').chomp('#') index = label.index('#') @@ -506,7 +461,7 @@ def insert_sample_text_button(text) end end - def empty_state(text) + def empty_state(text = t('no_result_was_found')) content_tag(:div, class:'browse-empty-illustration') do inline_svg_tag('empty-box.svg') + content_tag(:p, text) diff --git a/app/helpers/components_helper.rb b/app/helpers/components_helper.rb index 5c43b24d8a..c135804790 100644 --- a/app/helpers/components_helper.rb +++ b/app/helpers/components_helper.rb @@ -34,8 +34,6 @@ def group_chip_component(id: nil, name: , object: , checked: , value: nil, title checked: checked, value: value, tooltip: title, disabled: disabled, &block) end - alias :category_chip_component :group_chip_component - def rdf_highlighter_container(format, content) render Display::RdfHighlighterComponent.new(format: format, text: content) @@ -107,7 +105,6 @@ def paginated_list_component(id:, results:, next_page_url:, child_url:, child_tu end end - def resolvability_check_tag(url) content_tag(:span, check_resolvability_container(url), style: 'display: inline-block;', onClick: "window.open('#{check_resolvability_url(url: url)}', '_blank');") end @@ -122,7 +119,6 @@ def copy_link_to_clipboard(url, show_content: false) end end - def generated_link_to_clipboard(url, acronym) url = "#{$UI_URL}/ontologies/#{acronym}/#{link_last_part(url)}" content_tag(:span, id: "generate_portal_link", style: 'display: inline-block;') do @@ -138,7 +134,6 @@ def htaccess_tag(acronym) end end - def link_to_with_actions(link_to_tag, acronym: nil, url: nil, copy: true, check_resolvability: true, generate_link: true, generate_htaccess: false) tag = link_to_tag url = link_to_tag if url.nil? @@ -274,14 +269,12 @@ def properties_dropdown(id, title, tooltip, properties, is_open: false, &block) end end - def regular_button(id, value, variant: "secondary", state: "regular", size: "slim", &block) render Buttons::RegularButtonComponent.new(id:id, value: value, variant: variant, state: state, size: size) do |btn| capture(btn, &block) if block_given? end end - def form_save_button render Buttons::RegularButtonComponent.new(id: 'save-button', value: t('components.save_button'), variant: "primary", size: "slim", type: "submit") do |btn| btn.icon_left do @@ -298,14 +291,9 @@ def form_cancel_button end end - def chips_skelton - content_tag(:div, class: 'chips-container loading') do - content_tag(:div) do - content_tag(:label) do - content_tag(:span, '', class: 'skeleton') - end - end + def text_with_icon(text:, icon:) + content_tag(:div, class: 'd-flex align-items-center icon') do + inline_svg_tag(icon, height: '18', weight: '18') + content_tag(:div, class: 'text') {text} end end - end diff --git a/app/helpers/federation_helper.rb b/app/helpers/federation_helper.rb index d1e9e9f755..6104d89456 100644 --- a/app/helpers/federation_helper.rb +++ b/app/helpers/federation_helper.rb @@ -36,25 +36,24 @@ def federated_portal_light_color(key) config[:'light-color'] if config end - def ontology_portal_config(id) rest_url = id.split('/')[0..-3].join('/') - federated_portals.select{|_, config| config[:api].start_with?(rest_url)}.first + federated_portals.select { |_, config| config[:api].start_with?(rest_url) }.first end def ontology_portal_name(id) - portal_key, _ = ontology_portal_config(id) + portal_key, _ = ontology_portal_config(id) portal_key ? federated_portal_name(portal_key) : nil end def ontology_portal_color(id) - portal_key, _ = ontology_portal_config(id) + portal_key, _ = ontology_portal_config(id) federated_portal_color(portal_key) if portal_key end def ontoportal_ui_link(id) - portal_key, config = ontology_portal_config(id) - return nil unless portal_key + portal_key, config = ontology_portal_config(id) + return nil unless portal_key ui_link = config[:ui] api_link = config[:api] @@ -75,8 +74,8 @@ def request_portals [portal_name] + portals end - def request_portals_names - request_portals.map do |x| + def request_portals_names(counts, time) + output = request_portals.map do |x| config = federated_portal_config(x) if config @@ -89,8 +88,10 @@ def request_portals_names next nil end - content_tag(:span, federated_portal_name(name), style: color ? "color: #{color}" : "", class: color ? "" : "text-primary") - end.compact + content_tag(:span, "#{federated_portal_name(name)} (#{counts[federated_portal_name(name).downcase]})", style: color ? "color: #{color}" : "", class: color ? "" : "text-primary") + end.compact.join(", ") + + "#{output} in #{sprintf("%.2f", time)}s" end def federation_enabled? @@ -102,8 +103,18 @@ def federation_error?(response) end def federation_error(response) - federation_errors = response[:errors].map{|e| ontology_portal_name(e.split(' ').last.gsub('search', ''))} - federation_errors.map{ |p| "#{p} #{t('federation.not_responding')} " }.join(' ') + federation_errors = response[:errors].map { |e| ontology_portal_name(e.split(' ').last.gsub('search', '')) } + federation_errors.map { |p| "#{p} #{t('federation.not_responding')} " }.join(' ') + end + + def alert_message_if_federation_error(errors, &block) + return if errors.blank? + + content_tag(:div, class: 'my-1') do + render Display::AlertComponent.new(type: 'warning') do + capture(&block) + end + end end def class_federation_configuration(class_object) @@ -179,4 +190,32 @@ def init_federation_portals_status federation_input_chips end end + def federated_search_counts(search_results) + ids = search_results.map do |result| + result.dig(:root, :ontology_id) || rest_url + end + counts_ontology_ids_by_portal_name(ids) + end + + def federated_browse_counts(ontologies) + ids = ontologies.map { |ontology| ontology[:id] } + counts_ontology_ids_by_portal_name(ids) + end + + private + + def counts_ontology_ids_by_portal_name(portals_ids) + counts = Hash.new(0) + current_portal, *federation_portals = request_portals + portals_ids.each do |id| + counts[current_portal.downcase] += 1 if id.include?(current_portal.to_s.downcase) + + federation_portals.each do |portal| + portal_api = federated_portals[portal.downcase.to_sym][:api] + counts[portal.downcase] += 1 if id.include?(portal_api) + end + end + + counts + end end diff --git a/app/helpers/instances_helper.rb b/app/helpers/instances_helper.rb index db5170b233..79e5b17586 100644 --- a/app/helpers/instances_helper.rb +++ b/app/helpers/instances_helper.rb @@ -57,7 +57,7 @@ def link_to_property(property, ontology_acronym) end def instance_property_value(property, ontology_acronym) - if uri?(property) + if link?(property) instance, types = get_instance_and_type(property, ontology_acronym) return link_to_instance(instance, ontology_acronym) unless instance.empty? end diff --git a/app/helpers/mappings_helper.rb b/app/helpers/mappings_helper.rb index afc85907a8..bfe583a41b 100644 --- a/app/helpers/mappings_helper.rb +++ b/app/helpers/mappings_helper.rb @@ -141,7 +141,7 @@ def get_mappings_target target_ontology = ontology_to target = concept_to_id else - if helpers.uri?(ontology_to) + if helpers.link?(ontology_to) target_ontology = LinkedData::Client::Models::Ontology.find(ontology_to) else target_ontology = LinkedData::Client::Models::Ontology.find_by_acronym(ontology_to).first diff --git a/app/helpers/urls_helper.rb b/app/helpers/urls_helper.rb index 5e26db110b..b193f0a084 100644 --- a/app/helpers/urls_helper.rb +++ b/app/helpers/urls_helper.rb @@ -1,4 +1,40 @@ module UrlsHelper + def url_to_endpoint(url) + uri = URI.parse(url) + endpoint = uri.path.sub(/^\//, '') + endpoint + end + def rest_hostname + extract_hostname(REST_URI) + end + + def extract_hostname(url) + begin + uri = URI.parse(url) + uri.hostname + rescue URI::InvalidURIError + url + end + end + + def link?(str) + # Regular expression to match strings starting with "http://" or "https://" + link_pattern = /\Ahttps?:\/\// + str = str&.strip + # Check if the string matches the pattern + !!(str =~ link_pattern) + end + + def link_last_part(url) + return "" if url.nil? + + if url.include?('#') + url.split('#').last + else + url.split('/').last + end + end + def escape(string) CGI.escape(string) if string end @@ -6,4 +42,8 @@ def escape(string) def unescape(string) CGI.unescape(string) if string end + + def encode_param(string) + escape(string) + end end diff --git a/app/views/ontologies/browser/_ontologies.html.haml b/app/views/ontologies/browser/_ontologies.html.haml index cadbb85168..b9ba32c32e 100644 --- a/app/views/ontologies/browser/_ontologies.html.haml +++ b/app/views/ontologies/browser/_ontologies.html.haml @@ -5,13 +5,13 @@ - if @page.page.eql?(1) = content_tag(:p, class: "browse-desc-text", style: "margin-bottom: 12px !important;") do - #{t("ontologies.showing_ontologies_size", ontologies_size: @count, analytics_size: @total_ontologies, portals: request_portals_names.join(', ').html_safe).html_safe} (#{sprintf("%.2f", @time)}s) - - unless @errors.blank? - %div.my-1 - = render Display::AlertComponent.new(type: 'warning') do - - @errors.each do |e| - %div - = e.errors || e + = t("ontologies.showing_ontologies_size", ontologies_size: @count, analytics_size: @total_ontologies, portals: request_portals_names(@federation_counts, @time)).html_safe + + = alert_message_if_federation_error(@errors) do + - @errors.each do |e| + %div + = e.errors || e + - ontologies = c.collection - ontologies.each do |ontology| - config = ontology_portal_config(ontology[:id])&.last || {} @@ -24,6 +24,4 @@ - c.loader do - ontologies_browse_skeleton - c.error do - .browse-empty-illustration - %img{:src => "#{asset_path("empty-box.svg")}"} - %p No result was found \ No newline at end of file + = empty_state diff --git a/app/views/ontologies/browser/browse.html.haml b/app/views/ontologies/browser/browse.html.haml index 5a5a122080..e912fe3234 100644 --- a/app/views/ontologies/browser/browse.html.haml +++ b/app/views/ontologies/browser/browse.html.haml @@ -1,7 +1,7 @@ .browse-center .browse-container .container.align-alert - - if session[:user]&.admin? + - if current_user_admin? %div{style:'width: 70%;'} = render Display::AlertComponent.new(type: 'info') do %span.d-flex.align-items-center diff --git a/app/views/recommender/index.html.haml b/app/views/recommender/index.html.haml index 5ac33ba53c..57df61d9db 100644 --- a/app/views/recommender/index.html.haml +++ b/app/views/recommender/index.html.haml @@ -78,7 +78,7 @@ - btn.icon_left do = inline_svg_tag "edit.svg" - if @results && @results.empty? - = empty_state(t('no_result_was_found')) + = empty_state - unless @results.nil? || @results.empty? .recommender-page-results .title @@ -122,5 +122,3 @@ = render Buttons::RegularButtonComponent.new(id:'recommender_go_annotator', value: t('recommender.call_annotator'), variant: "secondary", href: "/annotator?text=#{params[:input]}&ontologies=#{params[:ontologies]}", size: "slim", target: '_blank', state: "regular") do |btn| - btn.icon_right do = inline_svg_tag "arrow-right-outlined.svg" - - diff --git a/app/views/search/index.html.haml b/app/views/search/index.html.haml index 1fe45b7ae8..e109c8f180 100644 --- a/app/views/search/index.html.haml +++ b/app/views/search/index.html.haml @@ -39,39 +39,37 @@ .search-page-options{class: @search_results.empty? ? 'justify-content-end': ''} - unless @search_results.empty? .search-page-number-of-results - = "#{t('search.match_in')} #{@search_results.length} #{t('search.ontologies')} #{t('federation.from')} #{request_portals_names.join(', ').html_safe} (#{sprintf("%.2f", @time)}s)".html_safe + = t('search.match_in', results_size: @search_results.length, portals: request_portals_names(@federation_counts, @time)).html_safe %div.d-flex - .search-page-json.mx-4.mt-1 - = search_json_link + .search-page-advanced-button.mr-4{class: @search_results.blank? ? 'd-none' : ''} + %a.d-flex{href: @json_url, target: '_blank'} + = text_with_icon(text: "API JSON", icon: 'json.svg') + .search-page-advanced-button.show-options{class: "#{@advanced_options_open ? 'd-none' : ''}",'data': {'action': 'click->reveal-component#show', 'reveal-component-target': 'showButton'}} - .icon - =inline_svg_tag 'icons/settings.svg' - .text - = t('search.show_advanced_options') + = text_with_icon(text: t('search.show_advanced_options'), icon: 'icons/settings.svg') + .search-page-advanced-button.hide-options{class: "#{@advanced_options_open ? '' : 'd-none'}", 'data': {'action': 'click->reveal-component#hide', 'reveal-component-target': 'hideButton'}} - .icon - =inline_svg_tag 'icons/hide.svg' - .text - = t('search.hide_advanced_options') - - unless @federation_errors.blank? - = render Display::AlertComponent.new(message: @federation_errors, type: "warning") - - if @search_results - .search-page-results-container - - number = 0 - - @search_results.each do |result| - .search-page-result-element - - number = number + 1 - - descendants = result[:descendants] - - reuses = result[:reuses] - - result[:root][:number] = number - = render Display::SearchResultComponent.new(result[:root]) do |c| - - descendants.each { |d| c.subresult(d.merge(is_sub_component: true))} - - reuses.each do |r| - - number = number + 1 - - c.reuse(r[:root].merge(is_sub_component: true, number: number)) do |b| - - r[:descendants].each { |dd| b.subresult(dd.merge(is_sub_component: true))} + = text_with_icon(text: t('search.hide_advanced_options'), icon: 'icons/hide.svg') + + + + + = alert_message_if_federation_error(@federation_errors) {@federation_errors} + + .search-page-results-container + - number = 0 + - @search_results.each do |result| + .search-page-result-element + - number = number + 1 + - descendants = result[:descendants] + - reuses = result[:reuses] + - result[:root][:number] = number + = render Display::SearchResultComponent.new(result[:root]) do |c| + - descendants.each { |d| c.subresult(d.merge(is_sub_component: true))} + - reuses.each do |r| + - number = number + 1 + - c.reuse(r[:root].merge(is_sub_component: true, number: number)) do |b| + - r[:descendants].each { |dd| b.subresult(dd.merge(is_sub_component: true))} - if @search_results.empty? && !@search_query.empty? - .browse-empty-illustration - %img{:src => "#{asset_path("empty-box.svg")}"} - %p No result was found + = empty_state diff --git a/config/locales/en.yml b/config/locales/en.yml index cc3d73bb4e..4f9ee4c48b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -921,8 +921,7 @@ en: classes_with_definitions: Classes with definitions show_advanced_options: Show options hide_advanced_options: Hide options - match_in: Match in - ontologies: ontologies + match_in: Match in %{results_size} ontologies from %{portals} result_component: details: Details visualize: Vizualize @@ -1505,6 +1504,5 @@ en: federation: results_from_external_portals: Results from external portals - from: from not_responding: is not responding. check_status: Checking %{portal} availability diff --git a/config/locales/fr.yml b/config/locales/fr.yml index b54f2d4430..0b319d391e 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -937,8 +937,7 @@ fr: classes_with_definitions: Classes avec définitions show_advanced_options: Afficher les options hide_advanced_options: Cacher les options - match_in: Correspondance dans - ontologies: ontologies + match_in: Correspondance dans %{results_size} ontologies de %{portals} result_component: details: Détails visualize: Visualiser @@ -1542,6 +1541,5 @@ fr: show_sub_categories: Afficher les sous-catégories federation: results_from_external_portals: Résultats provenant de portails externes - from: de not_responding: ne répond pas. check_status: Vérification de la disponibilité de %{portal} From dc448c982ca6e1b58291d7fac2cb641646aeeaa7 Mon Sep 17 00:00:00 2001 From: Bilel KIHAL Date: Wed, 23 Oct 2024 19:03:16 +0200 Subject: [PATCH 12/28] fix issue after merging federation PRs --- app/helpers/federation_helper.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/helpers/federation_helper.rb b/app/helpers/federation_helper.rb index 6104d89456..1cb1a2279d 100644 --- a/app/helpers/federation_helper.rb +++ b/app/helpers/federation_helper.rb @@ -1,4 +1,5 @@ module FederationHelper + include ApplicationHelper def federated_portals $FEDERATED_PORTALS ||= LinkedData::Client.settings.federated_portals From c52b5c8f5a75d7c8893da8ce1aba45d32062a912 Mon Sep 17 00:00:00 2001 From: Bilel Kihal <61744974+Bilelkihal@users.noreply.github.com> Date: Thu, 24 Oct 2024 02:59:36 +0200 Subject: [PATCH 13/28] Feature: Add canonical portal for duplicate ontologies (#770) * method to get the canonical portal for an ontology using pullLocation * display federated ontologies in browse page based on the canonical portal * clean canonical portal function in federation helper * extract canonical portal ontology choice in a function for clarity * display search page results based on the canonical portal of the ontology * move federation canonical to federation helper * clean swap portal attributes function in federation helper * fix and clean apply canonical portal function * extract count portals into a separate function from external_canonical_ontology_portal function + apply_canonical_portal function * put again search canonical logic the search_aggregator file and refactor the code * do the search canonical logic only if enabled * fix rest_hostname raising an exception that REST_URI does not exist --------- Co-authored-by: Syphax bouazzouni --- Gemfile | 2 +- app/controllers/concerns/search_aggregator.rb | 44 ++++++++++++++++++- app/controllers/concerns/submission_filter.rb | 8 +--- app/helpers/federation_helper.rb | 29 ++++++++++++ app/helpers/urls_helper.rb | 2 +- 5 files changed, 75 insertions(+), 10 deletions(-) diff --git a/Gemfile b/Gemfile index b2a3f58053..38db8f1d81 100644 --- a/Gemfile +++ b/Gemfile @@ -100,8 +100,8 @@ gem "flag-icons-rails", "~> 3.4" # Custom API client gem 'ontologies_api_client', git: 'https://github.com/ontoportal-lirmm/ontologies_api_ruby_client.git', branch: 'development' - # Ruby 2.7.8 pinned gems (to remove when migrating to Ruby >= 3.0) + gem 'ffi', '~> 1.16.3' gem 'net-ftp', '~> 0.2.0', require: false gem 'net-http', '~> 0.3.2' diff --git a/app/controllers/concerns/search_aggregator.rb b/app/controllers/concerns/search_aggregator.rb index 71f22ad423..24cc2b292c 100644 --- a/app/controllers/concerns/search_aggregator.rb +++ b/app/controllers/concerns/search_aggregator.rb @@ -31,11 +31,15 @@ def aggregate_results(query, results) all_ontologies = LinkedData::Client::Models::Ontology.all(include: 'acronym,name', include_views: true, display_links: false, display_context: false) + search_results = grouped_results.map do |group| format_search_result(group, all_ontologies) end - search_results = merge_sort_federated_results(query, search_results) if federation_enabled? + if federation_enabled? + search_results = merge_sort_federated_results(query, search_results) if federation_enabled? + search_results = swap_canonical_portal_results_first(search_results) + end search_results end @@ -251,7 +255,13 @@ def merge_federated_results(search_results) if (element[:root][:ontology_acronym] == reuse[:root][:ontology_acronym]) && (element[:root][:uri] == reuse[:root][:uri]) portal_name = reuse[:root][:portal_name] link = reuse[:root][:link] - element[:root][:other_portals] << {name: portal_name, color: federated_portal_color(portal_name), light_color: federated_portal_light_color(portal_name), link: link} + element[:root][:other_portals] << { + name: portal_name, + color: federated_portal_color(portal_name), + light_color: federated_portal_light_color(portal_name), + link: link, + ontology_id: reuse[:root][:ontology_id] + } true else false @@ -260,6 +270,36 @@ def merge_federated_results(search_results) end end + def swap_canonical_portal_results_first(search_results) + all_submissions = LinkedData::Client::Models::OntologySubmission.all(include: 'pullLocation', include_views: true, display_links: false, display_context: false) + + search_results.each do |result| + next if result[:root][:portal_name].nil? || result[:root][:other_portals].blank? + + result_ontology_ids = [result[:root][:ontology_id]] + result[:root][:other_portals].map { |p| p[:ontology_id] } + + result_submissions = all_submissions.select do |submission| + result_ontology_ids.any? { |ontology_id| submission.id.include?(ontology_id) } + end + + canonical_portal = most_referred_portal(result_submissions) + is_internal_ontology = result[:root][:portal_name].eql?(canonical_portal.to_s) + + next if canonical_portal.nil? || is_internal_ontology + + canonical_portal_result = result[:root][:other_portals].find { |r| r[:name] == canonical_portal.to_s } + swap_portal_attributes(result[:root], canonical_portal_result) if canonical_portal_result + end + search_results + end + + + def swap_portal_attributes(root_portal, new_portal) + [:link, :portal_name, :portal_color, :portal_light_color].each do |attribute| + root_portal[attribute], new_portal[attribute] = new_portal[attribute], root_portal[attribute] + end + end + def sort_results_by_string_similarity(query, search_results) search_results = search_results.sort_by do |entry| root_similarity = String::Similarity.cosine(query.downcase, entry[:root][:title].split('-').first.gsub(" ", "").downcase) diff --git a/app/controllers/concerns/submission_filter.rb b/app/controllers/concerns/submission_filter.rb index 2097fbb7e7..34ec133480 100644 --- a/app/controllers/concerns/submission_filter.rb +++ b/app/controllers/concerns/submission_filter.rb @@ -70,18 +70,14 @@ def ontologies_with_filters_url(filters, page: 1, count: false) def merge_by_acronym(submissions) merged_submissions = [] submissions.group_by { |x| x[:ontology]&.acronym }.each do |acronym, ontologies| - if ontologies.size.eql?(1) - ontology = ontologies.first - else - ontology = ontologies.select { |x| helpers.internal_ontology?(x[:id]) }.first || ontologies.first - end - + ontology = canonical_ontology(ontologies) ontology[:sources] = ontologies.map { |x| x[:id] } merged_submissions << ontology end merged_submissions end + def filter_submissions(ontologies, query:, status:, show_views:, private_only:, languages:, page_size:, formality_level:, is_of_type:, groups:, categories:, formats:) submissions = LinkedData::Client::Models::OntologySubmission.all(include: BROWSE_ATTRIBUTES.join(','), also_include_views: true, display_links: false, display_context: false) diff --git a/app/helpers/federation_helper.rb b/app/helpers/federation_helper.rb index 1cb1a2279d..135cae2883 100644 --- a/app/helpers/federation_helper.rb +++ b/app/helpers/federation_helper.rb @@ -135,6 +135,18 @@ def federation_external_class?(class_object) !class_object.links['self'].include?($REST_URL) end + def canonical_ontology(ontologies) + if ontologies.size.eql?(1) + ontologies.first + else + internal_ontology = ontologies.select { |x| helpers.internal_ontology?(x[:id]) }.first + if internal_ontology + internal_ontology + else + external_canonical_ontology_portal(ontologies) + end + end + end def federation_portal_status(portal_name: nil) Rails.cache.fetch("federation_portal_up_#{portal_name}", expires_in: 2.hours) do @@ -191,6 +203,7 @@ def init_federation_portals_status federation_input_chips end end + def federated_search_counts(search_results) ids = search_results.map do |result| result.dig(:root, :ontology_id) || rest_url @@ -219,4 +232,20 @@ def counts_ontology_ids_by_portal_name(portals_ids) counts end + + def external_canonical_ontology_portal(ontologies) + canonical_portal = most_referred_portal(ontologies) + ontologies.select{|o| o[:id].include?(canonical_portal.to_s)}.first + end + + def most_referred_portal(ontology_submissions) + portal_counts = Hash.new(0) + ontology_submissions.each do |submission| + federated_portals.keys.each do |portal| + portal_counts[portal] += 1 if submission[:pullLocation]&.include?(portal.to_s) + end + end + portal_counts.max_by { |_, count| count }&.first + end + end diff --git a/app/helpers/urls_helper.rb b/app/helpers/urls_helper.rb index b193f0a084..2c149e29d4 100644 --- a/app/helpers/urls_helper.rb +++ b/app/helpers/urls_helper.rb @@ -5,7 +5,7 @@ def url_to_endpoint(url) endpoint end def rest_hostname - extract_hostname(REST_URI) + extract_hostname($REST_URL) end def extract_hostname(url) From 18a4cac1b817a524012c5e936e918525b4de887c Mon Sep 17 00:00:00 2001 From: Syphax bouazzouni Date: Thu, 24 Oct 2024 11:04:29 +0200 Subject: [PATCH 14/28] Fix: slices homepage metrics count (#775) * create metrics helper module and implement portal_metrics that handle slices also * update home controller and home index to use the portal_metrics helper * add mappings sliced count * add projects sliced count * use the slice name in figures section title if enabled * fix mappings metrics count when not sliced to includes interportal and external mappings --- app/controllers/application_controller.rb | 17 ------ app/controllers/home_controller.rb | 23 ++------ app/helpers/application_helper.rb | 5 ++ app/helpers/metrics_helper.rb | 69 +++++++++++++++++++++++ app/views/home/index.html.haml | 16 +++--- 5 files changed, 86 insertions(+), 44 deletions(-) create mode 100644 app/helpers/metrics_helper.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index b9f6f31d72..7af57ee341 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -387,23 +387,6 @@ def get_apikey() return apikey end - def total_mapping_count - total_count = 0 - - begin - stats = LinkedData::Client::HTTP.get("#{REST_URI}/mappings/statistics/ontologies") - unless stats.blank? - stats = stats.to_h.compact - # Some of the mapping counts are erroneously stored as strings - stats.transform_values!(&:to_i) - total_count = stats.values.sum - end - rescue - LOG.add :error, e.message - end - - return total_count - end def determine_layout if Rails.env.appliance? diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 772d6e52fc..2d20328b74 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -3,31 +3,15 @@ class HomeController < ApplicationController layout :determine_layout - - include FairScoreHelper, FederationHelper + include FairScoreHelper, FederationHelper,MetricsHelper def index @analytics = helpers.ontologies_analytics - # Calculate BioPortal summary statistics - - @ont_count = if @analytics.empty? - LinkedData::Client::Models::Ontology.all.size - else - @analytics.keys.size - end - metrics = LinkedData::Client::Models::Metrics.all - metrics = metrics.each_with_object(Hash.new(0)) do |h, sum| - h.to_hash.slice(:classes, :properties, :individuals).each { |k, v| sum[k] += v } - end @slices = LinkedData::Client::Models::Slice.all - @cls_count = metrics[:classes] - @individuals_count = metrics[:individuals] - @prop_count = metrics[:properties] - @map_count = total_mapping_count - @projects_count = LinkedData::Client::Models::Project.all.length - @users_count = LinkedData::Client::Models::User.all.length + @metrics = portal_metrics(@analytics) + @upload_benefits = [ t('home.benefit1'), @@ -166,6 +150,7 @@ def annotator_recommender_form end end + def federation_portals_status @name = params[:name] @acronym = params[:acronym] diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 0150c3b6a3..1ad824206b 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -372,6 +372,11 @@ def portal_name $SITE end + def current_slice_name + name = @subdomain_filter[:name] + name.blank? ? nil : name + end + def navitems items = [["/ontologies", t('layout.header.browse')], ["/mappings", t('layout.header.mappings')], diff --git a/app/helpers/metrics_helper.rb b/app/helpers/metrics_helper.rb new file mode 100644 index 0000000000..5eb39444ce --- /dev/null +++ b/app/helpers/metrics_helper.rb @@ -0,0 +1,69 @@ +module MetricsHelper + + def portal_metrics(analytics) + ontologies_acronym = if analytics.empty? + LinkedData::Client::Models::Ontology.all.map { |x| x.acronym } + else + analytics.keys + end + + metrics = ontologies_metrics(ontologies_acronym) + + ont_count = ontologies_acronym.size + cls_count = metrics[:classes] + individuals_count = metrics[:individuals] + prop_count = metrics[:properties] + map_count = total_mapping_count(ontologies_acronym) + projects_count = projects_count(ontologies_acronym) + users_count = LinkedData::Client::Models::User.all.length + + { + ontologies_count: ont_count, + class_count: cls_count, + individuals_count: individuals_count, + properties_count: prop_count, + mappings_count: map_count, + projects_count: projects_count, + users_count: users_count + } + end + + def ontologies_metrics(ontologies_acronym = []) + metrics = LinkedData::Client::Models::Metrics.all + + metrics.each_with_object(Hash.new(0)) do |h, sum| + acronym = h.submission&.first&.split('/')&.dig(-3) + next nil if acronym.nil? + next nil unless ontologies_acronym.blank? || ontologies_acronym.include?(acronym) + + h.to_hash.slice(:classes, :properties, :individuals).each { |k, v| sum[k] += v } + end + end + + private + + def projects_count(ontologies_acronym = []) + projects = LinkedData::Client::Models::Project.all + projects.select! { |p| ontologies_acronym.intersection(p.ontologyUsed.map{|x| x.split('/').last}).any? } unless ontologies_acronym.empty? + projects.size + end + + def total_mapping_count(ontologies_acronym = []) + total_count = 0 + begin + stats = LinkedData::Client::HTTP.get(MappingStatistics::MAPPING_STATISTICS_URL) + unless stats.blank? + stats = stats.to_h.compact + # Some of the mapping counts are erroneously stored as strings + stats.select!{ |acronym, count| ontologies_acronym.include?(acronym.to_s) } if helpers.at_slice? + stats.transform_values!(&:to_i) + total_count = stats.values.sum + end + rescue StandardError => e + LOG.add :error, e.message + end + + total_count + end + +end diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index 28a0a25504..253e0546ad 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -127,7 +127,7 @@ .home-section %h4 - = t('home.agroportal_figures', site: portal_name) + = t('home.agroportal_figures', site: current_slice_name || portal_name) %hr.home-section-line/ .home-statistics-container .home-statistics @@ -135,43 +135,43 @@ %hr/ %div %h4 - = format_number_abbreviated(@ont_count) + = format_number_abbreviated(@metrics[:ontologies_count]) %p= t("home.ontologies") .home-statistics-item %hr/ %div %h4 - = format_number_abbreviated(@cls_count) + = format_number_abbreviated(@metrics[:class_count]) %p= t("home.classes") .home-statistics-item %hr/ %div %h4 - = format_number_abbreviated(@individuals_count) + = format_number_abbreviated(@metrics[:individuals_count]) %p= t("home.individuals") .home-statistics-item %hr/ %div %h4 - = format_number_abbreviated(@prop_count) + = format_number_abbreviated(@metrics[:properties_count]) %p= t("home.properties") .home-statistics-item %hr/ %div %h4 - = format_number_abbreviated(@projects_count) + = format_number_abbreviated(@metrics[:projects_count]) %p= t("home.projects") .home-statistics-item %hr/ %div %h4 - = format_number_abbreviated(@map_count) + = format_number_abbreviated(@metrics[:mappings_count]) %p= t("home.mappings") .home-statistics-item %hr/ %div %h4 - = format_number_abbreviated(@users_count) + = format_number_abbreviated(@metrics[:users_count]) %p= t("home.users") .home-statistics-link.justify-content-end{style: @analytics.empty? && "visibility: hidden"} = link_to t("home.see_details"),'/statistics', target: "_blank" From 227f5633dc38954153bc00b256dfc3fc54a4d814 Mon Sep 17 00:00:00 2001 From: Bilel Kihal <61744974+Bilelkihal@users.noreply.github.com> Date: Fri, 25 Oct 2024 07:37:35 +0200 Subject: [PATCH 15/28] Fix: put back category chip component alias to group chip component (#783) --- app/helpers/components_helper.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/helpers/components_helper.rb b/app/helpers/components_helper.rb index c135804790..a63ba8edb5 100644 --- a/app/helpers/components_helper.rb +++ b/app/helpers/components_helper.rb @@ -34,6 +34,7 @@ def group_chip_component(id: nil, name: , object: , checked: , value: nil, title checked: checked, value: value, tooltip: title, disabled: disabled, &block) end + alias :category_chip_component :group_chip_component def rdf_highlighter_container(format, content) render Display::RdfHighlighterComponent.new(format: format, text: content) From 817c1f5f1d9cdd97862ca14e9caca292908a011f Mon Sep 17 00:00:00 2001 From: Bilel Kihal <61744974+Bilelkihal@users.noreply.github.com> Date: Fri, 25 Oct 2024 14:56:24 +0200 Subject: [PATCH 16/28] Fix: Summary page scroll up auto multiple times (#781) * fix summary page scroll up auto multiple times * clean center tree view code and use it for: 'classes', 'properties', 'schemes', 'collections', 'instances' * revert adding useVisibility, and extract tree_view_pages to a constant --- .../controllers/simple_tree_controller.js | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/app/javascript/controllers/simple_tree_controller.js b/app/javascript/controllers/simple_tree_controller.js index 858bfde2c8..4cece829a7 100644 --- a/app/javascript/controllers/simple_tree_controller.js +++ b/app/javascript/controllers/simple_tree_controller.js @@ -1,4 +1,7 @@ import { Controller } from '@hotwired/stimulus' + +const TREE_VIEW_PAGES = ['classes', 'properties', 'schemes', 'collections', 'instances'] + // Connects to data-controller="simple-tree" export default class extends Controller { @@ -7,17 +10,7 @@ export default class extends Controller { } connect () { - setTimeout(() => { - let activeElem = this.element.querySelector('.tree-link.active'); - if (activeElem) { - activeElem.scrollIntoView({ block: 'center' }); - window.scrollTo({top: 0,}); - if (this.autoClickValue) { - activeElem.click(); - } - } - this.#onClickTooManyChildrenInit(); - }, 0); + this.#centerTreeView() } select (event) { @@ -33,6 +26,29 @@ export default class extends Controller { event.target.nextElementSibling.nextElementSibling.classList.toggle('hidden') } + #centerTreeView() { + setTimeout(() => { + const location = window.location.href; + + const isTreeViewPage = TREE_VIEW_PAGES.some(param => location.includes(`p=${param}`)); + + if (isTreeViewPage) { + const activeElem = this.element.querySelector('.tree-link.active'); + + if (activeElem) { + activeElem.scrollIntoView({ block: 'center' }); + window.scrollTo({ top: 0 }); + + if (this.autoClickValue) { + activeElem.click(); + } + } + + this.#onClickTooManyChildrenInit(); + } + }, 0); + } + #onClickTooManyChildrenInit () { jQuery('.too_many_children_override').live('click', (event) => { event.preventDefault() From a69658dadf13116adaa3c76dcc65977fc38a4240 Mon Sep 17 00:00:00 2001 From: Bilel Kihal <61744974+Bilelkihal@users.noreply.github.com> Date: Sat, 26 Oct 2024 10:31:23 +0200 Subject: [PATCH 17/28] Feature: Add enable/disable federation option (#780) * add enable/disable federation * hide federation inputs in browse page when federation is disabled * hide federation inputs in search page when federation is disabled * fix federation enabled function name in ontologies controller * put @federation_enabled variable in the right place in search controller index * rename federation_portals_enabled? to federated_request? * remove the usage of "@federation_enabled" to use directly the helper * fix a bug of the url of federation status, to add "/" to sure to always go to the correct route --------- Co-authored-by: Syphax --- app/controllers/concerns/search_aggregator.rb | 6 +++--- app/controllers/search_controller.rb | 2 +- app/helpers/federation_helper.rb | 9 +++++++-- app/views/ontologies/browser/browse.html.haml | 11 ++++++----- app/views/search/index.html.haml | 11 ++++++----- 5 files changed, 23 insertions(+), 16 deletions(-) diff --git a/app/controllers/concerns/search_aggregator.rb b/app/controllers/concerns/search_aggregator.rb index 24cc2b292c..078c89c5c9 100644 --- a/app/controllers/concerns/search_aggregator.rb +++ b/app/controllers/concerns/search_aggregator.rb @@ -36,8 +36,8 @@ def aggregate_results(query, results) format_search_result(group, all_ontologies) end - if federation_enabled? - search_results = merge_sort_federated_results(query, search_results) if federation_enabled? + if federated_request? + search_results = merge_sort_federated_results(query, search_results) search_results = swap_canonical_portal_results_first(search_results) end @@ -90,7 +90,7 @@ def search_result_elem(class_object, ontology_id, title) definition: class_object.definition, } - result.merge!(class_federation_configuration(class_object)) if federation_enabled? + result.merge!(class_federation_configuration(class_object)) if federated_request? result end diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 680d58cce8..9a7efa1514 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -21,7 +21,7 @@ def index set_federated_portals - params[:ontologies] = nil if federation_enabled? + params[:ontologies] = nil if federated_request? @time = Benchmark.realtime do results = LinkedData::Client::Models::Class.search(@search_query, params) diff --git a/app/helpers/federation_helper.rb b/app/helpers/federation_helper.rb index 135cae2883..d961712bc0 100644 --- a/app/helpers/federation_helper.rb +++ b/app/helpers/federation_helper.rb @@ -95,10 +95,15 @@ def request_portals_names(counts, time) "#{output} in #{sprintf("%.2f", time)}s" end - def federation_enabled? + def federated_request? params[:portals] end + def federation_enabled? + !federated_portals.blank? + end + + def federation_error?(response) !response[:errors].blank? end @@ -185,7 +190,7 @@ def federation_input_chips(name: nil) federated_portals.map do |key, config| turbo_frame_component = TurboFrameComponent.new( id: "federation_portals_status_#{key}", - src: "status/#{key}?name=#{name}&acronym=#{config[:name]}&checked=#{request_portals.include?(key.to_s)}" + src: "/status/#{key}?name=#{name}&acronym=#{config[:name]}&checked=#{request_portals.include?(key.to_s)}" ) content_tag :div do diff --git a/app/views/ontologies/browser/browse.html.haml b/app/views/ontologies/browser/browse.html.haml index e912fe3234..d6632fbc27 100644 --- a/app/views/ontologies/browser/browse.html.haml +++ b/app/views/ontologies/browser/browse.html.haml @@ -54,11 +54,12 @@ - d.title { browse_filter_section_header(key: key, count: count)} = browse_filter_section_body(key: key, checked_values: checked_values, objects: objects) - %div{ data:{action: "change->browse-filters#federationChange"}} - = dropdown_component(id: "browse-portal-filter", is_open: !request_portals.empty?) do |d| - - d.title { browse_filter_section_header(title: t('federation.results_from_external_portals'))} - .px-1.browse-federation-input-chips - = federation_input_chips(name: "portals") + - if federation_enabled? + %div{ data:{action: "change->browse-filters#federationChange"}} + = dropdown_component(id: "browse-portal-filter", is_open: !request_portals.empty?) do |d| + - d.title { browse_filter_section_header(title: t('federation.results_from_external_portals'))} + .px-1.browse-federation-input-chips + = federation_input_chips(name: "portals") .browse-second-row .browse-search-bar diff --git a/app/views/search/index.html.haml b/app/views/search/index.html.haml index e109c8f180..a53fccdbea 100644 --- a/app/views/search/index.html.haml +++ b/app/views/search/index.html.haml @@ -15,11 +15,12 @@ = t("search.advanced_options.ontologies") .field = ontologies_selector(id:'search_page_ontologies' ,name: 'ontologies[]', selected: params[:ontologies]&.split(',')) - .filter-container - .title - = t('federation.results_from_external_portals') - .field.d-flex - = federation_input_chips(name: "portals[]") + - if federation_enabled? + .filter-container + .title + = t('federation.results_from_external_portals') + .field.d-flex + = federation_input_chips(name: "portals[]") .right .filter-container .title From d83e3049aeeaf66bcac042b5522c7076afc5a8b0 Mon Sep 17 00:00:00 2001 From: Bilel Kihal <61744974+Bilelkihal@users.noreply.github.com> Date: Sat, 26 Oct 2024 10:32:36 +0200 Subject: [PATCH 18/28] Fix: Jump to infinit loading after search federation work (#782) * fix jump to infinit loading * search result as an object instead of hash in search controller --- app/controllers/search_controller.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 9a7efa1514..e0d15048da 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -26,7 +26,7 @@ def index @time = Benchmark.realtime do results = LinkedData::Client::Models::Class.search(@search_query, params) @federation_errors = federation_error(results) if federation_error?(results) - results = results[:collection] + results = results.collection @search_results = aggregate_results(@search_query, results) @@ -48,6 +48,7 @@ def json_search params.delete("ontologies") end search_page = LinkedData::Client::Models::Class.search(params[:q], params) + @results = search_page.collection response = "" From fea06125af71bfa1a918cb2c7e61f3b798bebae2 Mon Sep 17 00:00:00 2001 From: Bilel Kihal <61744974+Bilelkihal@users.noreply.github.com> Date: Sat, 26 Oct 2024 11:01:39 +0200 Subject: [PATCH 19/28] Fix: Include views in ontologies selector of search page (#784) * include views in ontologies selector * refactored onts_for_select to have the option to include views and by default no --------- Co-authored-by: Syphax --- app/helpers/application_helper.rb | 4 ++-- app/views/search/index.html.haml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 1ad824206b..3f69732afd 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -106,8 +106,8 @@ def error_message_alert end end - def onts_for_select - ontologies ||= LinkedData::Client::Models::Ontology.all(include: "acronym,name") + def onts_for_select(include_views: false) + ontologies ||= LinkedData::Client::Models::Ontology.all({include: "acronym,name", include_views: include_views}) onts_for_select = [['', '']] ontologies.each do |ont| next if ( ont.acronym.nil? or ont.acronym.empty? ) diff --git a/app/views/search/index.html.haml b/app/views/search/index.html.haml index a53fccdbea..c193b6cd0e 100644 --- a/app/views/search/index.html.haml +++ b/app/views/search/index.html.haml @@ -14,8 +14,8 @@ .title = t("search.advanced_options.ontologies") .field - = ontologies_selector(id:'search_page_ontologies' ,name: 'ontologies[]', selected: params[:ontologies]&.split(',')) - - if federation_enabled? + = ontologies_selector(id:'search_page_ontologies' ,name: 'ontologies[]', selected: params[:ontologies]&.split(','), ontologies: onts_for_select(include_views: true)) + - if federation_enabled? .filter-container .title = t('federation.results_from_external_portals') From e3a2ba2fdd0c9b615f946ae4815786f05c09c009 Mon Sep 17 00:00:00 2001 From: Syphax Date: Mon, 28 Oct 2024 11:42:14 +0100 Subject: [PATCH 20/28] Update Gemfile.lock --- Gemfile.lock | 30 +++++++++++++++--------------- app/views/search/index.html.haml | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 03c7f75383..3bb8c68812 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/ontoportal-lirmm/ontologies_api_ruby_client.git - revision: 0598e0068e545a5be9bbb501b0155c8bb2d9afb5 + revision: 7423b46ff6fa7e5ef0f1d36548f7c04466939f71 branch: development specs: ontologies_api_client (2.2.0) @@ -160,8 +160,8 @@ GEM ed25519 (1.3.0) erubi (1.13.0) erubis (2.7.0) - excon (0.112.0) - execjs (2.9.1) + excon (1.0.0) + execjs (2.10.0) faraday (2.0.1) faraday-net_http (~> 2.0) ruby2_keywords (>= 0.0.4) @@ -179,7 +179,7 @@ GEM sass-rails globalid (1.2.1) activesupport (>= 6.1) - graphql (2.3.18) + graphql (2.3.19) base64 fiber-storage graphql-client (0.23.0) @@ -241,8 +241,8 @@ GEM railties (>= 3.2.16) jsbundling-rails (1.3.1) railties (>= 6.0.0) - json (2.7.2) - json-jwt (1.16.6) + json (2.7.4) + json-jwt (1.16.7) activesupport (>= 4.2) aes_key_wrap base64 @@ -266,7 +266,7 @@ GEM rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) logger (1.6.1) - loofah (2.22.0) + loofah (2.23.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) lookbook (1.5.5) @@ -307,7 +307,7 @@ GEM time net-http (0.3.2) uri - net-imap (0.4.16) + net-imap (0.4.17) date net-protocol net-pop (0.1.2) @@ -377,7 +377,7 @@ GEM puma (5.6.9) nio4r (~> 2.0) racc (1.8.1) - rack (2.2.9) + rack (2.2.10) rack-protection (3.2.0) base64 (>= 0.1.0) rack (~> 2.2, >= 2.2.4) @@ -434,9 +434,9 @@ GEM http-cookie (>= 1.0.2, < 2.0) mime-types (>= 1.16, < 4.0) netrc (~> 0.8) - rexml (3.3.8) + rexml (3.3.9) rouge (4.4.0) - rspec-core (3.13.1) + rspec-core (3.13.2) rspec-support (~> 3.13.0) rspec-expectations (3.13.3) diff-lcs (>= 1.2.0, < 2.0) @@ -453,7 +453,7 @@ GEM rspec-mocks (~> 3.13) rspec-support (~> 3.13) rspec-support (3.13.1) - rubocop (1.66.1) + rubocop (1.67.0) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) @@ -507,7 +507,7 @@ GEM actionpack (>= 6.1) activesupport (>= 6.1) sprockets (>= 3.0.0) - sshkit (1.23.1) + sshkit (1.23.2) base64 net-scp (>= 1.1.2) net-sftp (>= 2.1.2) @@ -527,13 +527,13 @@ GEM time (0.4.0) date timeout (0.4.1) - turbo-rails (2.0.10) + turbo-rails (2.0.11) actionpack (>= 6.0.0) railties (>= 6.0.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unaccent (0.4.0) - unicode-display_width (2.5.0) + unicode-display_width (2.6.0) uri (0.13.1) version_gem (1.1.4) view_component (2.83.0) diff --git a/app/views/search/index.html.haml b/app/views/search/index.html.haml index c193b6cd0e..a87850f86e 100644 --- a/app/views/search/index.html.haml +++ b/app/views/search/index.html.haml @@ -15,7 +15,7 @@ = t("search.advanced_options.ontologies") .field = ontologies_selector(id:'search_page_ontologies' ,name: 'ontologies[]', selected: params[:ontologies]&.split(','), ontologies: onts_for_select(include_views: true)) - - if federation_enabled? + - if federation_enabled? .filter-container .title = t('federation.results_from_external_portals') From 8bf8a9d8d3bafb3080c85fa54fce7ec8a1415f04 Mon Sep 17 00:00:00 2001 From: Syphax Date: Mon, 28 Oct 2024 11:46:50 +0100 Subject: [PATCH 21/28] Fix: disabled federation browse page state --- app/controllers/concerns/submission_filter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/concerns/submission_filter.rb b/app/controllers/concerns/submission_filter.rb index 34ec133480..c039147280 100644 --- a/app/controllers/concerns/submission_filter.rb +++ b/app/controllers/concerns/submission_filter.rb @@ -47,7 +47,7 @@ def submissions_paginate_filter(params) submissions = filter_submissions(@ontologies, **params) - submissions = merge_by_acronym(submissions) + submissions = merge_by_acronym(submissions) if federation_enabled? submissions = sort_submission_by(submissions, @sort_by, @search) From e5387e5f902f896b29dcfe80d0711100ac29473b Mon Sep 17 00:00:00 2001 From: Bilel Kihal <61744974+Bilelkihal@users.noreply.github.com> Date: Wed, 30 Oct 2024 10:38:39 +0100 Subject: [PATCH 22/28] Fix: project page ontologies not selected (#790) * fix the issue in the projects page when the ontologies are private * fix ontologies selector name in projects from * pass the correct project ontologies value in project ontologies selector * pass an empty array if used ontologies of a group is nil --- app/controllers/projects_controller.rb | 14 +++++++------- app/views/projects/_form.html.haml | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 490072b4bd..5816730466 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -26,13 +26,13 @@ def show redirect_to projects_path return end - + @project = projects.first @ontologies_used = [] onts_used = @project.ontologyUsed onts_used.each do |ont_used| ont = LinkedData::Client::Models::Ontology.find(ont_used) - unless ont.nil? + unless ont.nil? || ont.errors @ontologies_used << Hash["name", ont.name, "acronym", ont.acronym] end end @@ -62,7 +62,7 @@ def edit @project = projects.first @user_select_list = LinkedData::Client::Models::User.all.map {|u| [u.username, u.id]} @user_select_list.sort! {|a,b| a[1].downcase <=> b[1].downcase} - @usedOntologies = @project.ontologyUsed || [] + @usedOntologies = @project.ontologyUsed&.map{|o| o.split('/').last} @ontologies = LinkedData::Client::Models::Ontology.all end @@ -76,7 +76,7 @@ def create @project = LinkedData::Client::Models::Project.new(values: project_params) @project_saved = @project.save - + # Project successfully created. if response_success?(@project_saved) flash[:notice] = t('projects.project_successfully_created') @@ -160,10 +160,10 @@ def destroy def project_params p = params.require(:project).permit(:name, :acronym, :institution, :contacts, { creator:[] }, :homePage, :description, { ontologyUsed:[] }) - + p[:creator]&.reject!(&:blank?) - p[:ontologyUsed]&.reject!(&:blank?) - p.to_h + p[:ontologyUsed] ||= [] + p = p.to_h end def flash_error(msg) diff --git a/app/views/projects/_form.html.haml b/app/views/projects/_form.html.haml index cd3eea4235..55bdb61edc 100644 --- a/app/views/projects/_form.html.haml +++ b/app/views/projects/_form.html.haml @@ -53,5 +53,5 @@ %div#ontology_picker_project{style: "padding-top: 2em;"} - selected_ontologies = @project.ontologyUsed && @project.ontologyUsed.map {|id| id.split('/').last } || [] - locals = { sel_text: t('projects.form.select_ontologies'), selected_ontologies: selected_ontologies, form_object: :project, form_attribute: "ontologyUsed" } - = ontologies_selector(id:'projects_page_ontologies_selector' ,name: 'ontologies') + = ontologies_selector(id:'projects_page_ontologies_selector' ,name: 'project[ontologyUsed][]', selected: @usedOntologies) From 1aa8d3850eb5f87b39d84b13f78edaa14f8abfa2 Mon Sep 17 00:00:00 2001 From: Bilel Kihal <61744974+Bilelkihal@users.noreply.github.com> Date: Wed, 30 Oct 2024 10:40:04 +0100 Subject: [PATCH 23/28] fix collection tree issue by including MultiLanguagesHelper to make sure main_language_label is defined in collections helper (#793) --- app/helpers/collections_helper.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/helpers/collections_helper.rb b/app/helpers/collections_helper.rb index b366a1401c..9a80d24111 100644 --- a/app/helpers/collections_helper.rb +++ b/app/helpers/collections_helper.rb @@ -1,4 +1,5 @@ module CollectionsHelper + include MultiLanguagesHelper def get_collections(ontology, add_colors: false) From 70f0e4daa445de6006baa15f420c5874b40d1bd7 Mon Sep 17 00:00:00 2001 From: Bilel Kihal <61744974+Bilelkihal@users.noreply.github.com> Date: Wed, 30 Oct 2024 10:45:32 +0100 Subject: [PATCH 24/28] Fix: ontologies selector localization text and add "[view]" of the views in the list (#791) * fix ontologies selector weird results sentence * show [view] beside ontology views in ontologies selector --- app/helpers/application_helper.rb | 4 ++-- .../ontologies_selector/ontologies_selector_results.html.haml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 3f69732afd..acf5335c1c 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -107,14 +107,14 @@ def error_message_alert end def onts_for_select(include_views: false) - ontologies ||= LinkedData::Client::Models::Ontology.all({include: "acronym,name", include_views: include_views}) + ontologies ||= LinkedData::Client::Models::Ontology.all({include: "acronym,name,viewOf", include_views: include_views}) onts_for_select = [['', '']] ontologies.each do |ont| next if ( ont.acronym.nil? or ont.acronym.empty? ) acronym = ont.acronym name = ont.name abbreviation = acronym.empty? ? "" : "(#{acronym})" - ont_label = "#{name.strip} #{abbreviation}" + ont_label = "#{name.strip} #{abbreviation}#{ont.viewOf ? ' [view]' : ''}" onts_for_select << [ont_label, acronym] end onts_for_select.sort! { |a,b| a[0].downcase <=> b[0].downcase } diff --git a/app/views/ontologies/ontologies_selector/ontologies_selector_results.html.haml b/app/views/ontologies/ontologies_selector/ontologies_selector_results.html.haml index 2aeafa23df..9bd25a770a 100644 --- a/app/views/ontologies/ontologies_selector/ontologies_selector_results.html.haml +++ b/app/views/ontologies/ontologies_selector/ontologies_selector_results.html.haml @@ -2,7 +2,7 @@ .ontologies-selector-results .horizontal-line .results-number - = t("ontologies.showing_ontologies_size", ontologies_size: @ontologies.length, analytics_size: @total_ontologies_number) + = t("ontologies.showing_ontologies_size", ontologies_size: @ontologies.length, analytics_size: @total_ontologies_number, portals: portal_name) %span.select-all{'data-action': 'click->ontologies-selector#selectall'} = t('ontologies_selector.select_all') .ontologies From 1e4b80996ec39f9f70191d9f45043ac7c6596887 Mon Sep 17 00:00:00 2001 From: Syphax bouazzouni Date: Wed, 30 Oct 2024 15:57:12 +0100 Subject: [PATCH 25/28] Fix: update select_language_label helper to select f no english found any language remaining over the not tagged (#795) * update select_language_label helper to select f no english found any language remaining over the not tagged * fix tests by fixing a dependency change * fix login system tests after securing the user delete action in the API --- Gemfile.lock | 2 +- app/components/tree_link_component.rb | 46 ++++++++++++++-------- app/helpers/multi_languages_helper.rb | 2 +- test/helpers/application_test_helpers.rb | 50 +++++++++++++++--------- test/system/login_flows_test.rb | 2 - test/system/submission_flows_test.rb | 16 +++----- 6 files changed, 69 insertions(+), 49 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 3bb8c68812..73328d8412 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -160,7 +160,7 @@ GEM ed25519 (1.3.0) erubi (1.13.0) erubis (2.7.0) - excon (1.0.0) + excon (0.112.0) execjs (2.10.0) faraday (2.0.1) faraday-net_http (~> 2.0) diff --git a/app/components/tree_link_component.rb b/app/components/tree_link_component.rb index 73da902f20..fd23706d96 100644 --- a/app/components/tree_link_component.rb +++ b/app/components/tree_link_component.rb @@ -2,26 +2,19 @@ class TreeLinkComponent < ViewComponent::Base include MultiLanguagesHelper, ModalHelper, ApplicationHelper - def initialize(child:, href:, children_href: , selected: false , data: {}, muted: false, target_frame: nil, open_in_modal: false, is_reused: nil) + + def initialize(child:, href:, children_href:, selected: false, data: {}, muted: false, target_frame: nil, open_in_modal: false, is_reused: nil) + super + @child = child @active_style = selected ? 'active' : '' - #@icons = child.relation_icon(node) @muted_style = muted ? 'text-muted' : '' @href = href @children_link = children_href - label = (@child.prefLabel || @child.label) rescue @child.id - if label.nil? - @pref_label_html = link_last_part(child.id) - else - pref_label_lang, @pref_label_html = select_language_label(label) - pref_label_lang = pref_label_lang.to_s.upcase - @tooltip = pref_label_lang.eql?("@NONE") ? "" : pref_label_lang - if child.obsolete? - @pref_label_html = "#{@pref_label_html}".html_safe - end - end - @data ||= { controller: 'tooltip', 'tooltip-position-value': 'right', turbo: true, 'turbo-frame': target_frame, action: 'click->simple-tree#select'} + @pref_label_html, @tooltip = node_label(child) + + @data ||= { controller: 'tooltip', 'tooltip-position-value': 'right', turbo: true, 'turbo-frame': target_frame, action: 'click->simple-tree#select' } @data.merge!(data) do |_, old, new| "#{old} #{new}" @@ -32,7 +25,6 @@ def initialize(child:, href:, children_href: , selected: false , data: {}, muted @is_reused = is_reused end - # This gives a very hacky short code to use to uniquely represent a class # based on its parent in a tree. Used for unique ids in HTML for the tree view def short_uuid @@ -49,7 +41,7 @@ def open? end def border_left - !@child.hasChildren ? 'pl-3 tree-border-left' : '' + !@child.hasChildren ? 'pl-3 tree-border-left' : '' end def li_id @@ -75,4 +67,26 @@ def open_children_link end + private + + def node_label(child) + label = begin + child.prefLabel || child.label + rescue + child.id + end + + if label.nil? + pref_label_html = link_last_part(child.id) + else + pref_label_lang, pref_label_html = select_language_label(label) + pref_label_lang = pref_label_lang.to_s.upcase + tooltip = pref_label_lang.eql?("@NONE") ? "" : pref_label_lang + + pref_label_html = "#{pref_label_html}".html_safe if child.obsolete? + end + + [pref_label_html, tooltip] + end + end diff --git a/app/helpers/multi_languages_helper.rb b/app/helpers/multi_languages_helper.rb index 9e21f2e0ed..6e127b94e0 100644 --- a/app/helpers/multi_languages_helper.rb +++ b/app/helpers/multi_languages_helper.rb @@ -153,7 +153,7 @@ def select_language_label(concept_label, platform_languages = %i[en fr]) end end - concept_value || concept.to_a.first + concept_value || concept.reject { |k| k.to_s.eql?('@none') }.first || concept.first end def main_language_label(label) diff --git a/test/helpers/application_test_helpers.rb b/test/helpers/application_test_helpers.rb index 975c220eac..28530711f7 100644 --- a/test/helpers/application_test_helpers.rb +++ b/test/helpers/application_test_helpers.rb @@ -16,17 +16,27 @@ module Users def sign_in_as(username) user = fixtures(:users)[username] logged_in_user = LinkedData::Client::Models::User.authenticate(user.username, user.password) - if logged_in_user && !logged_in_user.errors - logged_in_user = create_user(user) - end + logged_in_user = create_user(user) if logged_in_user && !logged_in_user.errors logged_in_user end def create_user(user, admin: false) - admin_user = LinkedData::Client::Models::User.authenticate('admin', 'password') if admin + admin_user = LinkedData::Client::Models::User.authenticate('admin', 'password') existent_user = LinkedData::Client::Models::User.find_by_username(user.username).first - existent_user.delete if existent_user + conn = Faraday.new(url: LinkedData::Client.settings.rest_url) do |faraday| + faraday.request :url_encoded + faraday.response :logger + faraday.adapter Faraday.default_adapter + faraday.headers = { + "Accept" => "application/json", + "Authorization" => "apikey token=#{admin_user.apikey}", + "User-Agent" => "NCBO API Ruby Client v0.1.0" + } + + end + + conn.delete("/users/#{user.username}") if existent_user values = user.to_h values[:role] = ["ADMINISTRATOR"] if admin @@ -34,17 +44,6 @@ def create_user(user, admin: false) if admin # Overwrite the normal ".save" to accept creating admin user - conn = Faraday.new(url: LinkedData::Client.settings.rest_url) do |faraday| - faraday.request :url_encoded - faraday.response :logger - faraday.adapter Faraday.default_adapter - faraday.headers = { - "Accept" => "application/json", - "Authorization" => "apikey token=#{admin_user.apikey}", - "User-Agent" => "NCBO API Ruby Client v0.1.0" - } - - end conn.post(existent_user.class.collection_path, existent_user.to_hash.to_json, 'Content-Type' => 'application/json') else existent_user.save @@ -61,7 +60,22 @@ def delete_users(users = LinkedData::Client::Models::User.all) end def delete_user(user) - LinkedData::Client::Models::User.find_by_username(user.username).first&.delete + admin_user = LinkedData::Client::Models::User.authenticate('admin', 'password') + existent_user = LinkedData::Client::Models::User.find_by_username(user.username).first + + conn = Faraday.new(url: LinkedData::Client.settings.rest_url) do |faraday| + faraday.request :url_encoded + faraday.response :logger + faraday.adapter Faraday.default_adapter + faraday.headers = { + "Accept" => "application/json", + "Authorization" => "apikey token=#{admin_user.apikey}", + "User-Agent" => "NCBO API Ruby Client v0.1.0" + } + + end + + conn.delete("/users/#{user.username}") if existent_user end end @@ -133,4 +147,4 @@ def delete_agents(agents = LinkedData::Client::Models::Agent.all) Array(agents).each { |g| g.delete } end end -end \ No newline at end of file +end diff --git a/test/system/login_flows_test.rb b/test/system/login_flows_test.rb index d6ff0deb49..8bed283d20 100644 --- a/test/system/login_flows_test.rb +++ b/test/system/login_flows_test.rb @@ -22,8 +22,6 @@ class LoginFlowsTest < ApplicationSystemTestCase new_user = @user_john delete_user(new_user) - LinkedData::Client::Models::User.find_by_username(new_user.username).first&.delete - fill_in 'user_firstName', with: new_user.firstName fill_in 'user_lastName', with: new_user.lastName fill_in 'user_username', with: new_user.username diff --git a/test/system/submission_flows_test.rb b/test/system/submission_flows_test.rb index 6045308d04..a0c06a4f47 100644 --- a/test/system/submission_flows_test.rb +++ b/test/system/submission_flows_test.rb @@ -46,7 +46,6 @@ class SubmissionFlowsTest < ApplicationSystemTestCase assert_text cat.acronym.titleize end - assert_text @new_submission.URI assert_text @new_submission.description assert_text @new_submission.pullLocation @@ -103,7 +102,6 @@ class SubmissionFlowsTest < ApplicationSystemTestCase click_on "Licensing" submission_licensing_edit_fill(ontology_2, submission_2) - # Persons and organizations tab click_on "Persons and organizations" submission_agent_edit_fill(submission_2) @@ -148,7 +146,10 @@ class SubmissionFlowsTest < ApplicationSystemTestCase assert_text submission_2.URI assert_text submission_2.versionIRI + + wait_for '.submission-status' assert_selector '.submission-status', text: submission_2.version + assert_selector ".flag-icon-fr" # todo fix this submission_2.identifier.each do |id| assert_text id @@ -258,7 +259,6 @@ class SubmissionFlowsTest < ApplicationSystemTestCase assert_text "rdfs" assert_text "dct" - open_dropdown "#configuration" submission_2.keyClasses.each do |key| @@ -341,10 +341,8 @@ class SubmissionFlowsTest < ApplicationSystemTestCase sleep 0.5 click_button 'Back' - fill_ontology(ontology_2, submission_2, add_submission: true) - assert_selector 'h2', text: 'Ontology submitted successfully!' click_on current_url.gsub("/ontologies/success/#{existent_ontology.acronym}", '') + ontology_path(existent_ontology.acronym) @@ -362,7 +360,6 @@ class SubmissionFlowsTest < ApplicationSystemTestCase assert_text submission_2.description assert_text submission_2.pullLocation - # check assert_selector '.fas.fa-key' if submission_2.status.eql?('private') @@ -380,7 +377,6 @@ class SubmissionFlowsTest < ApplicationSystemTestCase assert_text group.name end - open_dropdown "#dates" assert_date submission_2.modificationDate assert_date existent_submission.released @@ -483,7 +479,6 @@ def submission_agent_edit_fill(submission) list_inputs "#submissioncontact_from_group_input", "submission[contact]", submission.contact - agent1 = fixtures(:agents)[:agent1] agent2 = fixtures(:agents)[:agent2] @@ -624,7 +619,6 @@ def fill_ontology(new_ontology, new_submission, add_submission: false) list_checks new_ontology.hasDomain.map(&:acronym), @categories.map(&:acronym) list_checks new_ontology.group.map(&:acronym), @groups.map(&:acronym) - click_button 'Next' # Page 2 @@ -644,9 +638,9 @@ def fill_ontology(new_ontology, new_submission, add_submission: false) # Page 3 if add_submission - date_picker_fill_in 'submission[modificationDate]', new_submission.modificationDate + date_picker_fill_in 'submission[modificationDate]', new_submission.modificationDate else - date_picker_fill_in 'submission[released]', new_submission.released + date_picker_fill_in 'submission[released]', new_submission.released end list_inputs "#submissioncontact_from_group_input", "submission[contact]", new_submission.contact From d67ba52615725d5768353877ca29a535090c2dbb Mon Sep 17 00:00:00 2001 From: Bilel Kihal <61744974+Bilelkihal@users.noreply.github.com> Date: Wed, 30 Oct 2024 16:00:49 +0100 Subject: [PATCH 26/28] Fix: clean concepts controller code to improve tree view performance (#794) --- app/controllers/concepts_controller.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/controllers/concepts_controller.rb b/app/controllers/concepts_controller.rb index 196a61f97d..1063c953b4 100644 --- a/app/controllers/concepts_controller.rb +++ b/app/controllers/concepts_controller.rb @@ -45,14 +45,15 @@ def index @ontology = LinkedData::Client::Models::Ontology.find_by_acronym(params[:ontology]).first @ob_instructions = helpers.ontolobridge_instructions_template(@ontology) - @submission = @ontology.explore.latest_submission(include: 'all') + @submission = @ontology.explore.latest_submission(include:'uriRegexPattern,preferredNamespaceUri') + + @concept = @ontology.explore.single_class({dispay: 'prefLabel'}, params[:id]) - @concept = @ontology.explore.single_class({full: true}, params[:id]) concept_not_found(params[:id]) if @concept.nil? @schemes = params[:concept_schemes].split(',') @concept.children = @concept.explore.children(pagesize: 750, concept_schemes: Array(@schemes).join(','), language: request_lang, display: 'prefLabel,obsolete,hasChildren').collection || [] - @concept.children.sort! { |x, y| (x.prefLabel || "").downcase <=> (y.prefLabel || "").downcase } unless @concept.children.empty? + render turbo_stream: [ replace(helpers.child_id(@concept) + '_open_link') { TreeLinkComponent.tree_close_icon }, replace(helpers.child_id(@concept) + '_childs') do From e616842389ae436b47d09f95fe7cc5bfbf2911e2 Mon Sep 17 00:00:00 2001 From: Syphax bouazzouni Date: Wed, 30 Oct 2024 17:38:56 +0100 Subject: [PATCH 27/28] Fix: Disable slices not hiding the slices section in the home page (#797) * update select_language_label helper to select f no english found any language remaining over the not tagged * hide the slices section in the home page if slices disabled --- app/helpers/application_helper.rb | 3 +++ app/views/home/index.html.haml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index acf5335c1c..6d565bdb22 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -121,6 +121,9 @@ def onts_for_select(include_views: false) onts_for_select end + def slices_enabled? + $ENABLE_SLICES.eql?(true) + end def at_slice? !@subdomain_filter.nil? && !@subdomain_filter[:active].nil? && @subdomain_filter[:active] == true diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index 253e0546ad..8bc39eba97 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -176,7 +176,7 @@ .home-statistics-link.justify-content-end{style: @analytics.empty? && "visibility: hidden"} = link_to t("home.see_details"),'/statistics', target: "_blank" - - if @slices + - if slices_enabled? .home-section .home-section-title .text From f1773c1c09e3014d488906c047215c75bdd3c961 Mon Sep 17 00:00:00 2001 From: Bilel Kihal <61744974+Bilelkihal@users.noreply.github.com> Date: Thu, 31 Oct 2024 12:54:09 +0100 Subject: [PATCH 28/28] Feature: update concept mappings table UI (#798) * fix mappings table style * hide relations column when it's completely empty * use icons and Abbreviations types and sources * hide actions column if no mapping is of type REST * update create new mapping button style * remove concept mapping table padding * remove relation column and remove Action title * make the column width for the mapping to and ontology larger * move some mappings helper to the mapping controller as used only there * move the logic from the mapping table view to helpers * fix link text component icon size * change mappings action icons colors to primary color * pass button component in create new mapping instead of using only the css of it * internationalize create new mapping button text * internationalize mapping types description * fix interportal mapping issue when the interportal_hash is empty * limit mapping type tooltip by 300px width --------- Co-authored-by: Syphax --- app/assets/stylesheets/mappings.scss | 25 +++ app/components/link_text_component.rb | 2 +- app/controllers/mappings_controller.rb | 54 +++++- app/helpers/mappings_helper.rb | 174 +++++++++--------- .../mappings/_concept_mappings.html.haml | 17 +- app/views/mappings/_mapping_table.html.haml | 12 +- app/views/mappings/_show_line.html.haml | 57 ++---- config/locales/en.yml | 7 + config/locales/fr.yml | 8 + 9 files changed, 202 insertions(+), 154 deletions(-) diff --git a/app/assets/stylesheets/mappings.scss b/app/assets/stylesheets/mappings.scss index 89a7ebffbe..20ddbb6940 100644 --- a/app/assets/stylesheets/mappings.scss +++ b/app/assets/stylesheets/mappings.scss @@ -320,6 +320,28 @@ div#map_from_concept_details_table, div#map_to_concept_details_table { #concept_mappings_table { width: 100%; + word-break: break-word; + font-size: 13px; +} +.mappings-table-mapping-to{ + width: 45%; +} +.mappings-table-mapping-to .chip-button-component-container{ + text-wrap: wrap !important; +} +.mappings-table-mapping-to a{ + background-color: unset; + line-height: unset; + padding: unset; + font-weight: unset; + font-size: unset; +} +.mappings-table-icon{ + width: 18px; + height: 18px; +} +.mappings-table-icon path{ + fill: var(--gray-color); } .summary-mappings-tab-table { @@ -341,4 +363,7 @@ div#map_from_concept_details_table, div#map_to_concept_details_table { padding: 10px; outline: none; margin-left: 0 !important; +} +.mappings-table-actions svg path{ + fill: var(--primary-color); } \ No newline at end of file diff --git a/app/components/link_text_component.rb b/app/components/link_text_component.rb index 47ea0f6dfc..36dc79ba33 100644 --- a/app/components/link_text_component.rb +++ b/app/components/link_text_component.rb @@ -10,7 +10,7 @@ def initialize(text:, icon: nil, target: nil) end def call - svg_icon = !@icon&.empty? ? inline_svg(@icon) : '' + svg_icon = !@icon&.empty? ? inline_svg(@icon, width: '14px', height: '14px') : '' extra_span = @text == t('mappings.upload_mappings') ? '' : "#{svg_icon}" "#{@text}#{extra_span}".html_safe end diff --git a/app/controllers/mappings_controller.rb b/app/controllers/mappings_controller.rb index b1bcfc8c32..df582acb4e 100644 --- a/app/controllers/mappings_controller.rb +++ b/app/controllers/mappings_controller.rb @@ -342,4 +342,56 @@ def valid_values?(values) end errors end -end \ No newline at end of file + + def set_mapping_target(concept_to_id:, ontology_to:, mapping_type: ) + case mapping_type + when 'interportal' + @map_to_interportal, @map_to_interportal_ontology = ontology_to.match(%r{(.*)/ontologies/(.*)}).to_a[1..] + @map_to_interportal_class = concept_to_id + when 'external' + @map_to_external_ontology = ontology_to + @map_to_external_class = concept_to_id + else + @map_to_bioportal_ontology_id = ontology_to + @map_to_bioportal_full_id = concept_to_id + end + end + + def get_mappings_target_params + mapping_type = Array(params[:mapping_type]).first + external = true + case mapping_type + when 'interportal' + ontology_to = "#{params[:map_to_interportal]}/ontologies/#{params[:map_to_interportal_ontology]}" + concept_to_id = params[:map_to_interportal_class] + when 'external' + ontology_to = params[:map_to_external_ontology] + concept_to_id = params[:map_to_external_class] + else + ontology_to = params[:map_to_bioportal_ontology_id] + concept_to_id = params[:map_to_bioportal_full_id] + external = false + end + [ontology_to, concept_to_id, external] + end + + def get_mappings_target + ontology_to, concept_to_id, external_mapping = get_mappings_target_params + target = '' + if external_mapping + target_ontology = ontology_to + target = concept_to_id + else + if helpers.link?(ontology_to) + target_ontology = LinkedData::Client::Models::Ontology.find(ontology_to) + else + target_ontology = LinkedData::Client::Models::Ontology.find_by_acronym(ontology_to).first + end + if target_ontology + target = target_ontology.explore.single_class(concept_to_id).id + target_ontology = target_ontology.id + end + end + [target_ontology, target, external_mapping] + end +end diff --git a/app/helpers/mappings_helper.rb b/app/helpers/mappings_helper.rb index bfe583a41b..c1e503b547 100644 --- a/app/helpers/mappings_helper.rb +++ b/app/helpers/mappings_helper.rb @@ -2,43 +2,85 @@ module MappingsHelper # Used to replace the full URI by the prefixed URI RELATIONSHIP_PREFIX = { - "http://www.w3.org/2004/02/skos/core#" => "skos:", - "http://www.w3.org/2000/01/rdf-schema#" => "rdfs:", - "http://www.w3.org/2002/07/owl#" => "owl:", - "http://www.w3.org/1999/02/22-rdf-syntax-ns#" => "rdf:", - "http://purl.org/linguistics/gold/" => "gold:", - "http://lemon-model.net/lemon#" => "lemon:" + 'http://www.w3.org/2004/02/skos/core#' => 'skos:', + 'http://www.w3.org/2000/01/rdf-schema#' => 'rdfs:', + 'http://www.w3.org/2002/07/owl#' => 'owl:', + 'http://www.w3.org/1999/02/22-rdf-syntax-ns#' => 'rdf:', + 'http://purl.org/linguistics/gold/' => 'gold:', + 'http://lemon-model.net/lemon#' => 'lemon:' } INTERPORTAL_HASH = $INTERPORTAL_HASH - - # a little method that returns true if the URIs array contain a gold:translation or gold:freeTranslation - def translation?(relation_array) - if relation_array.kind_of?(Array) - relation_array.map!(&:downcase) - if relation_array.include? "http://purl.org/linguistics/gold/translation" - true - elsif relation_array.include? "http://purl.org/linguistics/gold/freetranslation" - true - else - false + def mapping_links(mapping, concept) + target_concept = mapping.classes.select do |c| + c.id != concept.id && c.links['ontology'] != concept.links['ontology'] + end.first + target_concept ||= mapping.classes.last + process = mapping.process || {} + + if inter_portal_mapping?(target_concept) + cls_link = ajax_to_inter_portal_cls(target_concept) + ont_name = target_concept.links['ontology'] + ont_link = link_to ont_name, get_inter_portal_ui_link(ont_name, process['name']), target: '_blank' + source_tooltip = 'Internal-portal' + elsif internal_mapping?(target_concept) + begin + ont = target_concept.explore.ontology + ont_name = ont.acronym + ont_link = link_to ont_name, ontology_path(ont_name), 'data-turbo-frame': '_top' + rescue + ont_name = target_concept.links['ontology'] || target_concept.id + ont_link = ont_name end + cls_link = raw(get_link_for_cls_ajax(target_concept.id, ont_name, '_top')) + source_tooltip = 'Internal' else - false + cls_label = ExternalLinkTextComponent.new(text: target_concept.links['self']).call + cls_link = raw("#{cls_label}") + ont_name = target_concept.links['ontology'] + ont_link = link_to ExternalLinkTextComponent.new(text: ont_name).call, target_concept.links['ontology'], + target: '_blank' + source_tooltip = 'External' end + + [cls_link, ont_link, source_tooltip] + end + + def mapping_prefixed_relations(mapping) + process = mapping.process || {} + Array(process[:relation]).each { |relation| get_prefixed_uri(relation) } + end + + def mapping_type_tooltip(map) + relations = mapping_prefixed_relations(map) + process = map.process || {} + type = if map.source.to_s.include? 'SKOS' + 'SKOS' + else + map.source + end + types_description = { + 'CUI' => t('mappings.types_description.cui'), + 'LOOM' => t('mappings.types_description.loom'), + 'REST' => t('mappings.types_description.rest'), + 'SAME_URI' => t('mappings.types_description.same_uri'), + 'SKOS' => t('mappings.types_description.skos') + } + type_tooltip = content_tag(:div, "#{map.source} #{relations.join(', ')} : #{types_description[type]} #{process[:source_name]}".strip, style: 'width: 300px') + [type, type_tooltip] end # a little method that returns the uri with a prefix : http://purl.org/linguistics/gold/translation become gold:translation def get_prefixed_uri(uri) RELATIONSHIP_PREFIX.each { |k, v| uri.sub!(k, v) } - return uri + uri end # method to get (using http) prefLabel for interportal classes # Using bp_ajax_controller.ajax_process_interportal_cls will try to resolve class labels. def ajax_to_inter_portal_cls(cls) - inter_portal_acronym = get_inter_portal_acronym(cls.links["ui"]) + inter_portal_acronym = get_inter_portal_acronym(cls.links['ui']) href_cls = " href='#{cls.links["ui"]}' " if inter_portal_acronym data_cls = " data-cls='#{cls.links["self"]}?apikey=' " @@ -52,7 +94,7 @@ def ajax_to_inter_portal_cls(cls) def ajax_to_internal_cls(cls) link_to("#{cls.id}".html_safe, - ontology_path(cls.explore.ontology.acronym, p: 'classes', conceptid: cls.id), target: "_blank") + ontology_path(cls.explore.ontology.acronym, p: 'classes', conceptid: cls.id), target: '_blank') end # to get the apikey from the interportal instance of the interportal class. @@ -60,7 +102,7 @@ def ajax_to_internal_cls(cls) def get_inter_portal_acronym(class_ui_url) if !INTERPORTAL_HASH.nil? INTERPORTAL_HASH.each do |key, value| - if class_ui_url.start_with?(value["ui"]) + if class_ui_url.start_with?(value['ui']) return key else return nil @@ -71,11 +113,11 @@ def get_inter_portal_acronym(class_ui_url) # method to extract the prefLabel from the external class URI def get_label_for_external_cls(class_uri) - if class_uri.include? "#" - prefLabel = class_uri.split("#")[-1] - else - prefLabel = class_uri.split("/")[-1] - end + prefLabel = if class_uri.include? '#' + class_uri.split('#')[-1] + else + class_uri.split('/')[-1] + end return prefLabel end @@ -86,11 +128,11 @@ def ajax_to_external_cls(cls) # Replace the inter_portal mapping ontology URI (that link to the API) by the link to the ontology in the UI def get_inter_portal_ui_link(uri, process_name) process_name = '' if process_name.nil? - interportal_acronym = process_name.split(" ")[2] - if interportal_acronym.nil? || interportal_acronym.empty? + interportal_acronym = process_name.split(' ')[2] + if interportal_acronym.nil? || interportal_acronym.empty? || INTERPORTAL_HASH[interportal_acronym].nil? uri else - uri.sub!(INTERPORTAL_HASH[interportal_acronym]["api"], INTERPORTAL_HASH[interportal_acronym]["ui"]) + uri.sub!(INTERPORTAL_HASH[interportal_acronym]['api'], INTERPORTAL_HASH[interportal_acronym]['ui']) end end @@ -99,66 +141,14 @@ def internal_mapping?(cls) end def inter_portal_mapping?(cls) - !internal_mapping?(cls) && cls.links.has_key?("ui") - end - - def get_mappings_target_params - mapping_type = Array(params[:mapping_type]).first - external = true - case mapping_type - when 'interportal' - ontology_to = "#{params[:map_to_interportal]}/ontologies/#{params[:map_to_interportal_ontology]}" - concept_to_id = params[:map_to_interportal_class] - when 'external' - ontology_to = params[:map_to_external_ontology] - concept_to_id = params[:map_to_external_class] - else - ontology_to = params[:map_to_bioportal_ontology_id] - concept_to_id = params[:map_to_bioportal_full_id] - external = false - end - [ontology_to, concept_to_id, external] - end - - def set_mapping_target(concept_to_id:, ontology_to:, mapping_type: ) - case mapping_type - when 'interportal' - @map_to_interportal, @map_to_interportal_ontology = ontology_to.match(%r{(.*)/ontologies/(.*)}).to_a[1..] - @map_to_interportal_class = concept_to_id - when 'external' - @map_to_external_ontology = ontology_to - @map_to_external_class = concept_to_id - else - @map_to_bioportal_ontology_id = ontology_to - @map_to_bioportal_full_id = concept_to_id - end - end - - def get_mappings_target - ontology_to, concept_to_id, external_mapping = get_mappings_target_params - target = '' - if external_mapping - target_ontology = ontology_to - target = concept_to_id - else - if helpers.link?(ontology_to) - target_ontology = LinkedData::Client::Models::Ontology.find(ontology_to) - else - target_ontology = LinkedData::Client::Models::Ontology.find_by_acronym(ontology_to).first - end - if target_ontology - target = target_ontology.explore.single_class(concept_to_id).id - target_ontology = target_ontology.id - end - end - [target_ontology, target, external_mapping] + !internal_mapping?(cls) && cls.links.has_key?('ui') end def type?(type) @mapping_type.nil? && type.eql?('internal') || @mapping_type.eql?(type) end - def concept_mappings_loader(ontology_acronym: ,concept_id: ) + def concept_mappings_loader(ontology_acronym:, concept_id:) content_tag(:span, id: 'mapping_count') do concat(content_tag(:div, class: 'concepts-mapping-count ml-1 mr-1') do render(TurboFrameComponent.new( @@ -173,21 +163,23 @@ def concept_mappings_loader(ontology_acronym: ,concept_id: ) end def client_filled_modal - link_to_modal "", "" + link_to_modal '', '' end def mappings_bubble_view_legend content_tag(:div, class: 'mappings-bubble-view-legend') do - mappings_legend_section(t('mappings.bubble_view_legend.bubble_size'), t('mappings.bubble_view_legend.bubble_size_desc'), 'mappings-bubble-size-legend') + + mappings_legend_section(t('mappings.bubble_view_legend.bubble_size'), + t('mappings.bubble_view_legend.bubble_size_desc'), 'mappings-bubble-size-legend') + mappings_legend_section( - t('mappings.bubble_view_legend.color_degree'),t('mappings.bubble_view_legend.color_degree_desc'),'mappings-bubble-color-legend') + + t('mappings.bubble_view_legend.color_degree'), t('mappings.bubble_view_legend.color_degree_desc'), 'mappings-bubble-color-legend') + content_tag(:div, class: 'content-container') do content_tag(:div, class: 'bubble-view-legend-item') do content_tag(:div, class: 'title') do - content_tag(:div, t('mappings.bubble_view_legend.yellow_bubble'), class: 'd-inline') + content_tag(:span, t('mappings.bubble_view_legend.selected_bubble')) + content_tag(:div, t('mappings.bubble_view_legend.yellow_bubble'), + class: 'd-inline') + content_tag(:span, t('mappings.bubble_view_legend.selected_bubble')) end + - content_tag(:div, class: "mappings-bubble-size-legend d-flex justify-content-center") do - content_tag(:div, '', class: "bubble yellow") + content_tag(:div, class: 'mappings-bubble-size-legend d-flex justify-content-center') do + content_tag(:div, '', class: 'bubble yellow') end end end @@ -209,7 +201,7 @@ def mappings_legend_section(title_text, description_text, css_class) def mappings_legend(css_class) content_tag(:div, class: css_class) do content_tag(:div, t('mappings.bubble_view_legend.less_mappings'), class: 'mappings-legend-text') + - (1..6).map { |i| content_tag(:div, "", class: "bubble bubble#{i}") }.join.html_safe + + (1..6).map { |i| content_tag(:div, '', class: "bubble bubble#{i}") }.join.html_safe + content_tag(:div, t('mappings.bubble_view_legend.more_mappings'), class: 'mappings-legend-text') end end diff --git a/app/views/mappings/_concept_mappings.html.haml b/app/views/mappings/_concept_mappings.html.haml index c71fe9066b..2a6b56b29e 100644 --- a/app/views/mappings/_concept_mappings.html.haml +++ b/app/views/mappings/_concept_mappings.html.haml @@ -2,17 +2,16 @@ = "#{@mappings.size}" = turbo_frame_tag @type.eql?('modal') ? 'application_modal_content' : 'concept_mappings' do - %div{:style => "padding: 1%; width: 98%"} + %div.p-1 - if session[:user].nil? = link_to "Create New Mapping", "/login?redirect=/ontologies/#{@ontology.acronym}/?p=classes&t=mappings&conceptid=#{escape(@concept.id)}", :method => :get, :class => "btn btn-default mb-3" - else - = link_to_modal("Create New Mapping", - new_mapping_path(ontology_from: "#{@ontology.id}", conceptid_from: "#{@concept.id}"), - id: "new_mapping_btn", - role: "button", - class: "btn btn-default mb-3", - data: { show_modal_title_value: "Create a new mapping for #{@concept.prefLabel}" }, - ) + %div{style: 'width: 250px; margin: 0 0 10px 0;'} + = link_to_modal(nil, + new_mapping_path(ontology_from: "#{@ontology.id}", conceptid_from: "#{@concept.id}"), + data: { show_modal_title_value: "Create a new mapping for #{@concept.prefLabel}" }, + ) do + = render Buttons::RegularButtonComponent.new(id:'new_mapping_btn' , value:t('mappings.create_new_mapping'), variant: 'secondary', size: 'slim', state: 'no-anim') #mapping_details - = render :partial => '/mappings/mapping_table' \ No newline at end of file + = render :partial => '/mappings/mapping_table' diff --git a/app/views/mappings/_mapping_table.html.haml b/app/views/mappings/_mapping_table.html.haml index 1d91b3b5bb..20efcd92d5 100644 --- a/app/views/mappings/_mapping_table.html.haml +++ b/app/views/mappings/_mapping_table.html.haml @@ -1,16 +1,14 @@ = check_box_tag "delete_mappings_permission", @delete_mapping_permission, @delete_mapping_permission, style: "display: none;" %div#concept_mappings_tables_div = render_alerts_container(MappingsController) - %table#concept_mappings_table.table-content-stripped.table-content{width: "100%", style:'word-break: break-word'} + %table#concept_mappings_table.table-content-stripped.table-content.table-mini %thead %tr %th= t("mappings.mapping_table.mapping_to") - %th{width: "30%"}= t("mappings.count.ontology") - %th= t("mappings.mapping_table.relations") - %th= t("mappings.mapping_table.source") + %th= t("mappings.count.ontology") %th= t("mappings.mapping_table.type") - - if current_user_admin? - %th{:class => 'delete_mappings_column'}= t("mappings.mapping_table.actions") + - if current_user_admin? && !@mappings.all? { |obj| !obj.source.eql?('REST') } + %th{:class => 'delete_mappings_column'} %tbody#concept_mappings_table_content - @mappings.each do |map| = render partial: 'mappings/show_line' , locals: {map: map, concept: @concept} @@ -19,4 +17,4 @@ :javascript jQuery(document).ready(function(){ ajax_process_init(); - }) \ No newline at end of file + }) diff --git a/app/views/mappings/_show_line.html.haml b/app/views/mappings/_show_line.html.haml index bbfab2b78e..4c94555c4d 100644 --- a/app/views/mappings/_show_line.html.haml +++ b/app/views/mappings/_show_line.html.haml @@ -1,56 +1,23 @@ -- process = map.process || {} -- source = "#{map.source} #{process[:source_name]}" -- relations = process[:relation]&.each { |relation| get_prefixed_uri(relation)} +- type, type_tooltip = mapping_type_tooltip(map) +- cls_link, ont_link, source_tooltip = mapping_links(map, concept) - map_id = map.id.to_s.split("/").last -- target_concept = map.classes.select {|target_concept| target_concept.id != concept.id && target_concept.links['ontology'] != concept.links['ontology']}.first || map.classes.last -- if inter_portal_mapping?(target_concept) - - cls_link = ajax_to_inter_portal_cls(target_concept) - - ont_name = target_concept.links['ontology'] - - ont_acronym = ont_name - - ont_link = link_to ont_acronym , get_inter_portal_ui_link(target_concept.links['ontology'], process["name"]), target: '_blank' - - type = 'Inter-portal' -- elsif internal_mapping?(target_concept) - - begin - - ont = target_concept.explore.ontology - - ont_name = ont.acronym - - ont_link = link_to ont_name, ontology_path(ont_name), 'data-turbo-frame':'_top' - - rescue - - ont_name = target_concept.links['ontology'] || target_concept.id - - ont_link = ont_name - - cls_link = raw(get_link_for_cls_ajax(target_concept.id, ont_name, '_top')) - - type = 'Internal' -- else - - cls_label = get_label_for_external_cls(target_concept.links["self"]) - - cls_link = raw("#{cls_label}") - - ont_name = target_concept.links['ontology'] - - ont_link = link_to ont_name, target_concept.links['ontology'], target: "_blank" - - type = 'External' - %tr.human{:id => map_id} - %td + %td.mappings-table-mapping-to = cls_link - %td + %td.mappings-table-mapping-to = ont_link %td - - relations&.each do |r| - = r - %br/ - %td - #{source} - - if !process.nil? - - if translation?(process["relation"]) - %img{:src => asset_path('sifr/english_language_flag.png'), :style => "padding: 5px", :align => "right", :title => "Traduction"} - %td - = type + = render ChipButtonComponent.new(class: 'chip_button_small mr-1', text: type, tooltip:"#{source_tooltip} mapping of type #{type_tooltip}") + - if current_user_admin? %td{:class => 'delete_mappings_column'} - if map.id && !map.id.empty? && session[:user] && (session[:user].id.to_i == map.creator || session[:user].admin?) && map.source.eql?('REST') - %div.d-flex - = link_to_modal(t("mappings.show_line.edit_modal"), + %div.d-flex.mappings-table-actions + = link_to_modal(nil, mapping_path(map_id, {conceptid_from: @concept.id}), - role: "button", - class: "btn btn-link", + class: 'btn btn-link p-0 mr-1', data: { show_modal_title_value: t("mappings.show_line.edit_mapping", preflabel: @concept.prefLabel)}, - ) - = button_to t("mappings.show_line.delete_button"), CGI.unescape(mapping_path(map.id)), method: :delete, class:'btn btn-link', form: {data: { turbo: true, turbo_confirm: t("mappings.show_line.turbo_confirm"), turbo_frame: '_top'}} \ No newline at end of file + ) do + = inline_svg_tag "edit.svg", width: '15px', height: '15px' + = button_to inline_svg_tag('icons/delete.svg', width: '16px', heigth: '16px'), CGI.unescape(mapping_path(map.id)), class: 'btn btn-link p-0', method: :delete, form: {data: { turbo: true, turbo_confirm: t("mappings.show_line.turbo_confirm"), turbo_frame: '_top'}} diff --git a/config/locales/en.yml b/config/locales/en.yml index 4f9ee4c48b..a63e3500eb 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -572,12 +572,19 @@ en: date: Date ontology_visits: Ontology visits mappings: + create_new_mapping: Create new mapping all: All description: Dive into an overview of the mappings in the bubble view, efficiently locate a specific ontology in the table view or upload your own mappings. tabs: bubble_view: Bubbles view table_view: Table view upload_mappings: Upload mappings + types_description: + cui: Created between 2 concepts that have the same CUI (Concept Unique Identifiers) + loom: Lexical mappings created between 2 concepts with very similar labels (preferred name) + rest: A mapping added by a user using the REST API (or the UI, which is calling the API to create it) + same_uri: Created between 2 concepts with the same URI. + skos: Mappings based on SKOS relationships, (e.g. skos:exactMatch or skos:closeMatch) filter_ontologies: Filter ontologies in the bubble view filter_bubbles: Filter bubbles external_mappings: "External Mappings (%{number_with_delimiter})" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 0b319d391e..d0969e5795 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -580,12 +580,20 @@ fr: ontology_visits: Visites d'ontologies mappings: + create_new_mapping: Créer un nouveau mapping all: Tous description: Plongez dans une vue d'ensemble des mappings dans la vue en bulles, localisez efficacement une ontologie spécifique dans la vue en tableau ou téléchargez vos propres mappings. tabs: bubble_view: Vue en bulles table_view: Vue en tableau upload_mappings: Télécharger des mappings + types_description: + cui: Créé entre 2 concepts ayant le même CUI (Identifiants Uniques de Concepts) + loom: Mappings lexicaux créés entre 2 concepts avec des libellés très similaires (nom préféré) + rest: Un mapping ajouté par un utilisateur via l'API REST (ou l'interface utilisateur, qui appelle l'API pour le créer) + same_uri: Créé entre 2 concepts ayant la même URI. + skos: Mapping basés sur des relations SKOS (par exemple, skos:exactMatch ou skos:closeMatch) + filter_ontologies: Filtrer les ontologies dans la vue en bulles filter_bubbles: Filtrer les bulles external_mappings: "Mappings externes (%{number_with_delimiter})"