diff --git a/app/controllers/concerns/user_authorization.rb b/app/controllers/concerns/user_authorization.rb index 3ef1a62e..0dfa4436 100644 --- a/app/controllers/concerns/user_authorization.rb +++ b/app/controllers/concerns/user_authorization.rb @@ -14,6 +14,7 @@ def setup_two_factor_authentication excluded_paths = ['/users/sign_in', '/users/sign_out', '/users/password/new', '/users/invitation', '/users/invitation/accept'] + return if current_user.blank? return if excluded_paths.include?(request.path) return unless current_user.enforce_two_factor? return if current_user.two_factor_setup? diff --git a/app/controllers/pipelines_controller.rb b/app/controllers/pipelines_controller.rb index 6a8eb518..462b480f 100644 --- a/app/controllers/pipelines_controller.rb +++ b/app/controllers/pipelines_controller.rb @@ -3,11 +3,10 @@ class PipelinesController < ApplicationController include LastEditedBy - before_action :assign_sort_by, only: %w[index create] before_action :find_pipeline, only: %w[show destroy edit update clone] def index - @pipelines = Pipeline.order(@sort_by).page(params[:page]) + @pipelines = pipelines @pipeline = Pipeline.new end @@ -33,7 +32,7 @@ def create redirect_to pipeline_path(@pipeline), notice: t('.success') else flash.alert = t('.failure') - @pipelines = Pipeline.order(@sort_by).page(params[:page]) + @pipelines = pipelines render :index end end @@ -78,9 +77,12 @@ def find_pipeline @pipeline = Pipeline.find(params[:id]) end - def assign_sort_by - @sort_by = { name: :asc } - @sort_by = { updated_at: :desc } if params['sort_by'] == 'updated_at' + def pipelines + PipelineSearchQuery.new(params).call.order(sort_by).page(params[:page]) + end + + def sort_by + @sort_by ||= params['sort_by'] == 'name' ? { name: :asc } : { updated_at: :desc } end def pipeline_params diff --git a/app/frontend/entrypoints/application.js b/app/frontend/entrypoints/application.js index b306a800..35c48ae7 100644 --- a/app/frontend/entrypoints/application.js +++ b/app/frontend/entrypoints/application.js @@ -31,13 +31,14 @@ console.log( // import '~/index.css' import * as bootstrap from "bootstrap"; +import "/js/ClearField"; import "/js/TestRecordExtraction"; import "/js/TestEnrichmentExtraction"; import "/js/TestTransformationRecordSelector"; import "/js/TestDestination"; import "/js/Tooltips"; import "/js/CollapseScroll"; -import "/js/ContentSourceFilter"; +import "/js/SubmittingSelect"; import "/js/CreateModal"; import "/js/AutoComplete"; diff --git a/app/frontend/entrypoints/application.scss b/app/frontend/entrypoints/application.scss index 891622f3..6147ac61 100644 --- a/app/frontend/entrypoints/application.scss +++ b/app/frontend/entrypoints/application.scss @@ -1,16 +1,13 @@ // Modules - @import "../stylesheets/modules/colors"; @import "../stylesheets/modules/fonts"; // Libraries - @import "~bootstrap/scss/bootstrap"; @import "~bootstrap-icons/font/bootstrap-icons.min"; @import "~autoComplete/dist/css/autoComplete.css"; // Mixins - @import "../stylesheets/mixins/bem"; // Bootstrap overrides @@ -25,19 +22,17 @@ @import "../stylesheets/bootstrap/accordion"; // Library overrides - @import "../stylesheets/autocomplete/autocomplete"; - // Blocks - +@import "../stylesheets/blocks/card-link"; +@import "../stylesheets/blocks/definition-group"; @import "../stylesheets/blocks/field-nav"; @import "../stylesheets/blocks/field-nav-panel"; @import "../stylesheets/blocks/form-in-dropdown"; +@import "../stylesheets/blocks/harvest-card"; @import "../stylesheets/blocks/header"; @import "../stylesheets/blocks/jump-to"; @import "../stylesheets/blocks/record-view"; -@import "../stylesheets/blocks/definition-group"; +@import "../stylesheets/blocks/search"; @import "../stylesheets/blocks/table"; - -@import "../stylesheets/base"; diff --git a/app/frontend/js/ClearField.js b/app/frontend/js/ClearField.js new file mode 100644 index 00000000..1ce25295 --- /dev/null +++ b/app/frontend/js/ClearField.js @@ -0,0 +1,14 @@ +const clearButtons = document.querySelectorAll("[data-clear-field]"); + +clearButtons.forEach(function (clearButton) { + clearButton.addEventListener("click", (event) => { + const form = event.target.closest("form"); + const fieldNameToClear = event.target.dataset.clearField; + const fieldToClear = form.querySelector( + `input[name="${fieldNameToClear}"]` + ); + + fieldToClear.value = ""; + form.submit(); + }); +}); diff --git a/app/frontend/js/ContentSourceFilter.js b/app/frontend/js/ContentSourceFilter.js deleted file mode 100644 index 83addaa4..00000000 --- a/app/frontend/js/ContentSourceFilter.js +++ /dev/null @@ -1,9 +0,0 @@ -const contentSourceFilter = document.getElementById("js-content-source-filter"); - -if (contentSourceFilter) { - const form = contentSourceFilter.closest("form"); - - contentSourceFilter.addEventListener("change", (event) => { - form.submit(); - }); -} diff --git a/app/frontend/js/SubmittingSelect.js b/app/frontend/js/SubmittingSelect.js new file mode 100644 index 00000000..954c28c0 --- /dev/null +++ b/app/frontend/js/SubmittingSelect.js @@ -0,0 +1,11 @@ +const selectElements = document.querySelectorAll( + '[data-submitting-select="true"]' +); + +selectElements.forEach(function (selectElement) { + const form = selectElement.closest("form"); + + selectElement.addEventListener("change", () => { + form.submit(); + }); +}); diff --git a/app/frontend/js/components/SharedDefinitionsView.jsx b/app/frontend/js/components/SharedDefinitionsView.jsx index ec48a09f..e3cc730c 100644 --- a/app/frontend/js/components/SharedDefinitionsView.jsx +++ b/app/frontend/js/components/SharedDefinitionsView.jsx @@ -16,7 +16,7 @@ const SharedDefinitionsView = ({ definitionType }) => { return (
diff --git a/app/frontend/stylesheets/_base.scss b/app/frontend/stylesheets/_base.scss deleted file mode 100644 index 38a171c4..00000000 --- a/app/frontend/stylesheets/_base.scss +++ /dev/null @@ -1,5 +0,0 @@ -body { - background-color: $bleached-silk; - color: $antarctic-deep; - font-family: 'Lato', sans-serif; -} diff --git a/app/frontend/stylesheets/blocks/_card-link.scss b/app/frontend/stylesheets/blocks/_card-link.scss new file mode 100644 index 00000000..2bfc91d4 --- /dev/null +++ b/app/frontend/stylesheets/blocks/_card-link.scss @@ -0,0 +1,22 @@ +// this is used to make a card which has interactive elements +// clickable without breaking a11y +// See https://inclusive-components.design/cards/ +.card--clickable { + &:hover { + border-color: $primary; + } + + .card__link { + color: var(--bs-card-color); + text-decoration: none; + + &::after { + content: ""; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + } + } +} diff --git a/app/frontend/stylesheets/blocks/_harvest-card.scss b/app/frontend/stylesheets/blocks/_harvest-card.scss new file mode 100644 index 00000000..3879f3ff --- /dev/null +++ b/app/frontend/stylesheets/blocks/_harvest-card.scss @@ -0,0 +1,33 @@ +.harvest-card { + @include element('actions') { + position: absolute; + right: 0rem; + top: 0; + bottom: 0; + margin: auto; + height: 2.25rem; + + &:hover { + cursor: pointer; + } + } + + @include element('actions-toggle') { + border: none; + } + + @include element('action') { + padding: .625rem 1rem; + } + + @include element('right-arrow') { + position: absolute; + right: -1.5rem; + top: 0; + bottom: 0; + width: 1.25rem; + height: 1.25rem; + margin: auto; + color: $primary; + } +} diff --git a/app/frontend/stylesheets/blocks/_search.scss b/app/frontend/stylesheets/blocks/_search.scss new file mode 100644 index 00000000..616e463b --- /dev/null +++ b/app/frontend/stylesheets/blocks/_search.scss @@ -0,0 +1,51 @@ +.search { + position: relative; + + @include element('label') { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + padding: .4rem $form-floating-padding-x; + overflow: hidden; + text-align: start; + text-overflow: ellipsis; + white-space: nowrap; + pointer-events: none; + border: 1px solid transparent; + } + + &__input { + width: 22rem; + + &::placeholder { + color: transparent; + } + + &:focus ~ .search__label, &:not(:placeholder-shown) ~ .search__label { + display: none; + } + } + + @include element('clear') { + position: absolute; + top: 0; + right: .5rem; + margin-top: .3rem; + + --bs-btn-padding-y: .25rem; + --bs-btn-padding-x: .5rem; + --bs-btn-font-size: .75rem; + --bs-btn-border-color: #{$lynx-white}; + --bs-btn-bg: #{$lynx-white}; + --bs-btn-color: #{$primary}; + --bs-btn-hover-color: #{$primary}; + --bs-btn-hover-bg: #{shade-color($lynx-white, 10%)}; + --bs-btn-hover-border-color: #{shade-color($lynx-white, 10%)}; + + > .bi-plus::before { + transform: rotate(45deg); + } + } +} diff --git a/app/frontend/stylesheets/bootstrap/_card.scss b/app/frontend/stylesheets/bootstrap/_card.scss index 99ee27a2..ff242997 100644 --- a/app/frontend/stylesheets/bootstrap/_card.scss +++ b/app/frontend/stylesheets/bootstrap/_card.scss @@ -32,40 +32,6 @@ cursor: default } } - - @include element('control-action') { - padding: 10px 16px 9px 16px; - } - - @include element('control') { - position: absolute; - right: .5rem; - top: 0; - bottom: 0; - margin: auto; - height: 1rem; - - &:hover { - cursor: pointer; - } - } - - @include element('right-arrow') { - position: absolute; - right: -1.5rem; - top: 0; - bottom: 0; - width: 1.25rem; - height: 1.25rem; - margin: auto; - color: $primary; - } -} - -a.card { - &:hover { - border-color: $primary; - } } form { diff --git a/app/frontend/stylesheets/bootstrap/_form.scss b/app/frontend/stylesheets/bootstrap/_form.scss index af6e9b2e..2a2b09f0 100644 --- a/app/frontend/stylesheets/bootstrap/_form.scss +++ b/app/frontend/stylesheets/bootstrap/_form.scss @@ -1,3 +1,3 @@ -.form-label { +.form-label, .col-form-label { font-weight: 600; } diff --git a/app/frontend/stylesheets/modules/_colors.scss b/app/frontend/stylesheets/modules/_colors.scss index a2070f3d..37a0132d 100644 --- a/app/frontend/stylesheets/modules/_colors.scss +++ b/app/frontend/stylesheets/modules/_colors.scss @@ -1,12 +1,13 @@ -// https://color-name-generator.com/ +// https://color-name-generator.com/ $doctor: #F8F9FA; $bleached-silk: #F2F2F2; $vital-green: #198754; $antarctic-deep: #343A40; -$bright-star: #dee2e6; -$pompeii-ash: #6c757d; -$satin-white: #ced4da; +$bright-star: #DEE2E6; +$pompeii-ash: #6C757D; +$satin-white: #CED4DA; +$lynx-white: #F7F7F7; $primary: $vital-green; $dark: $antarctic-deep; diff --git a/app/models/extraction_definition.rb b/app/models/extraction_definition.rb index 5163e191..b12ebc75 100644 --- a/app/models/extraction_definition.rb +++ b/app/models/extraction_definition.rb @@ -3,6 +3,8 @@ # Used to store the information for running an extraction # class ExtractionDefinition < ApplicationRecord + FORMATS = %w[JSON XML HTML].freeze + # The destination is used for Enrichment Extractions # To know where to pull the records that are to be enriched from belongs_to :destination, optional: true @@ -39,7 +41,7 @@ class ExtractionDefinition < ApplicationRecord # Harvest related validation with_options if: :harvest? do - validates :format, presence: true, inclusion: { in: %w[JSON XML HTML] } + validates :format, presence: true, inclusion: { in: FORMATS } validates :base_url, presence: true, format: { with: URI::DEFAULT_PARSER.make_regexp } validate :total_selector_format validates :total_selector, presence: true diff --git a/app/models/pipeline.rb b/app/models/pipeline.rb index 21cebe2d..9b554f2e 100644 --- a/app/models/pipeline.rb +++ b/app/models/pipeline.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Pipeline < ApplicationRecord + paginates_per 19 # not 20 because of the "Create new pipeline" button + has_many :harvest_definitions, dependent: :destroy has_many :harvest_jobs, through: :harvest_definitions belongs_to :last_edited_by, class_name: 'User', optional: true @@ -10,6 +12,19 @@ class Pipeline < ApplicationRecord validates :name, presence: true, uniqueness: true + def self.search(words, format) + words = sanitized_words(words) + return self if words.blank? && format.blank? + + query = where('name LIKE ?', words) + .or(where('description LIKE ?', words)) + .or(where(last_edited_by_id: search_user_ids(words))) + .or(where(id: search_source_ids(words))) + + query = query.and(where(id: search_format_ids(format))) if format.present? + query + end + def harvest harvest_definitions.find_by(kind: 'harvest') end diff --git a/app/queries/pipeline_search_query.rb b/app/queries/pipeline_search_query.rb new file mode 100644 index 00000000..d984dc5f --- /dev/null +++ b/app/queries/pipeline_search_query.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +class PipelineSearchQuery + def initialize(params) + @words = sanitized_words(params[:search]) + @format = params[:format] + @query = Pipeline + end + + def call + return @query if @words.blank? && @format.blank? + return @query.where(id: search_format_ids) if @words.blank? && @format.present? + + @query = or_words_filters + @query = and_format_filter if @format.present? + + @query + end + + private + + def sanitized_words(words) + words = Pipeline.sanitize_sql_like(words || '') + return nil if words.empty? + + "%#{words}%" + end + + def or_words_filters + @query.where('name LIKE ?', @words) + .or(Pipeline.where('description LIKE ?', @words)) + .or(Pipeline.where(last_edited_by_id: search_user_ids)) + .or(Pipeline.where(id: search_source_ids)) + end + + def and_format_filter + @query.and(Pipeline.where(id: search_format_ids)) + end + + def search_user_ids + User.where('username LIKE ?', @words).pluck(:id) + end + + def search_source_ids + HarvestDefinition.where('source_id LIKE ?', @words).pluck(:pipeline_id) + end + + def search_format_ids + ExtractionDefinition.where(format: @format).pluck(:pipeline_id) + end +end diff --git a/app/views/destinations/index.html.erb b/app/views/destinations/index.html.erb index aa7ec1b4..28223ec2 100644 --- a/app/views/destinations/index.html.erb +++ b/app/views/destinations/index.html.erb @@ -18,9 +18,9 @@
<%- @destinations.each do |destination| %>
- <%= link_to destination, class: 'card mb-3' do %> + <%= link_to destination, class: 'card card--clickable mb-3' do %>
-
<%= destination.name %>
+
<%= destination.name %>
<% end %>
diff --git a/app/views/jobs/_jobs.html.erb b/app/views/jobs/_jobs.html.erb index f6e642c4..f590d053 100644 --- a/app/views/jobs/_jobs.html.erb +++ b/app/views/jobs/_jobs.html.erb @@ -5,7 +5,7 @@ <%= link_to pipeline_harvest_definition_extraction_definition_extraction_job_path( pipeline, harvest_definition, extraction_definition, job ), - class: 'card mb-3' do %> + class: 'card card--clickable mb-3' do %>
<%= job.name %>
<%= job.updated_at.to_fs(:light) %>
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 85282521..eee56d85 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -1,5 +1,5 @@ - + <%= content_for?(:title) ? "#{yield(:title)} | " : '' %>Harvester diff --git a/app/views/pipelines/_card.html.erb b/app/views/pipelines/_card.html.erb index 68a8508e..c8326172 100644 --- a/app/views/pipelines/_card.html.erb +++ b/app/views/pipelines/_card.html.erb @@ -5,87 +5,100 @@ delete_path ||= '' position = type == 'extraction' ? 'first' : 'last' %> -
+
-
<%= definition.name %>
-
- <%= send("#{type}_card_subtitle", definition) %> -
- - <% if definition.shared? %> - Shared (<%= "#{definition.harvest_definitions.count} pipelines" %>) - <% end %> - -
- - +
+
+
<% if position == 'first' %> - + <% end %>
diff --git a/app/views/pipelines/index.html.erb b/app/views/pipelines/index.html.erb index 54808a7a..4cd04d92 100644 --- a/app/views/pipelines/index.html.erb +++ b/app/views/pipelines/index.html.erb @@ -6,73 +6,97 @@

Pipelines

- <% end %> -
-
    - -
  • - - Sort by: - -
  • - -
  • - <%= form_with url: '/pipelines', method: :get do |form| %> - <%= select_tag( - :sort_by, - options_for_select([['Alphabetical', 'name'], ['Last Edited', 'updated_at']], @sort_by.first), - class: 'form-select', id: 'js-content-source-filter' - ) %> - <% end %> -
  • - -
+<%= form_with url: '/pipelines', method: :get, class: 'row justify-content-end mb-5' do |form| %> +
+ +
+
+
-
+
+ <%= form.label :format, 'Format:', class: 'col-form-label' %> +
+
+ <%= form.select( + :format, + [['All formats', '']] + ExtractionDefinition::FORMATS.map { |format| [format, format] }, + { selected: params[:format].in?([nil, '', 'All formats']) ? 'All formats' : params[:format] }, + class: 'form-select', 'data-submitting-select': true + ) %> +
-
+
+ <%= form.label :sort_by, 'Sort by:', class: 'col-form-label' %> +
+
+ <%= form.select( + :sort_by, + [['Last Edited', 'updated_at'], ['Alphabetical', 'name']], + { selected: @sort_by.keys.first }, + class: 'form-select', 'data-submitting-select': true + ) %> +
+<% end %> -
+
-
- -
+
+ +
- <%- @pipelines.each do |pipeline| %> -
- <%= link_to pipeline, class: 'card mb-3 d-flex' do %> -
-
<%= pipeline.name %>
-
- <%= last_edited_by(pipeline) %> -
-
- - <%= pipeline.harvest_definitions.harvest.count %> Harvest - - - <%= pipeline.harvest_definitions.enrichment.count %> Enrichments + <%- @pipelines.each do |pipeline| %> +
+ <%= link_to pipeline, class: 'card card--clickable mb-3 d-flex' do %> +
+

<%= pipeline.name %>

+

+ <%= last_edited_by(pipeline) %> +

+
+ + <%= pipeline.harvest_definitions.harvest.count %> Harvest + + + <%= pipeline.harvest_definitions.enrichment.count %> Enrichments + + + <% if pipeline.schedules.any? %> + + Scheduled - - <% if pipeline.schedules.any? %> - - Scheduled - - <% end %> -
+ <% end %>
- <% end %> -
- <%- end %> +
+ <% end %> +
+ <%- end %> -
+
<%= render 'shared/pagination_below_table', items: @pipelines %> diff --git a/app/views/schedules/index.html.erb b/app/views/schedules/index.html.erb index 9205dbb6..0b7c3279 100644 --- a/app/views/schedules/index.html.erb +++ b/app/views/schedules/index.html.erb @@ -31,7 +31,7 @@
- <%= link_to new_pipeline_schedule_path(@pipeline), class: 'card card--create-cta mb-3 d-flex' do %> + <%= link_to new_pipeline_schedule_path(@pipeline), class: 'card card--clickable card--create-cta mb-3 d-flex' do %>
+ Create new schedule
@@ -40,7 +40,7 @@ <%- @schedules.each do |schedule| %>
- <%= link_to pipeline_schedule_path(@pipeline, schedule), class: 'card mb-3 d-flex' do %> + <%= link_to pipeline_schedule_path(@pipeline, schedule), class: 'card card--clickable mb-3 d-flex' do %>
<%= schedule.name %>
diff --git a/app/views/users/index.html.erb b/app/views/users/index.html.erb index 9764a51b..2e2e3387 100644 --- a/app/views/users/index.html.erb +++ b/app/views/users/index.html.erb @@ -22,7 +22,7 @@
<% @users.each do |user| %>
- <%= link_to user, class: 'card mb-3' do %> + <%= link_to user, class: 'card card--clickable mb-3' do %>
<%= user.username %>

<%= user.email %>

diff --git a/config/application.rb b/config/application.rb index f7d443e1..3e037e45 100644 --- a/config/application.rb +++ b/config/application.rb @@ -34,6 +34,7 @@ class Application < Rails::Application # config.eager_load_paths << Rails.root.join("extras") config.eager_load_paths << Rails.root.join('supplejack') config.eager_load_paths << Rails.root.join('slices') + config.eager_load_paths << Rails.root.join('queries') config.time_zone = ENV.fetch('TIME_ZONE', 'Auckland') diff --git a/extractions/staging/.keep b/extractions/staging/.keep deleted file mode 100644 index e69de29b..00000000