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 @@
-
<% 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.text_field(
+ :search,
+ value: params[:search],
+ placeholder: 'Search by name, description, source_id, last edited by',
+ class: 'form-control search__input'
+ ) %>
+ <%= form.label :search, class: 'search__label' do %>
+
+ Search by name, description, source_id, last edited by
+ <% end %>
+
+ <% if params[:search].present? %>
+
+ Clear
+
+
+ <% end %>
+
+
+
+ Search
-
+
+ <%= 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 %>
-
+
-
-
-
-
+ Create new pipeline
-
-
-
+
+
+
+
+ Create new pipeline
+
+
+
- <%- @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