Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update Importer Index and Show Entries With Search, Filtering, Sort and More #914

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
89a8ebb
Thats it, Ive had enough. we need status_message as a column on entry…
orangewolf Feb 8, 2024
3a88627
call the cops
orangewolf Feb 8, 2024
7fb851e
Add datatable to the importer show page. search, filter, sort and pag…
orangewolf Feb 8, 2024
72426e6
fix filters
orangewolf Feb 8, 2024
9c9f3e3
call the cops
orangewolf Feb 8, 2024
fc8c3c1
call the cops
orangewolf Feb 8, 2024
58b5cf2
call the cops
orangewolf Feb 8, 2024
a45b6b1
Merge branch 'main' into one_table_to_rule_them_all_one_table_to_find…
jeremyf Feb 8, 2024
f2b153c
Change datatables search query to Arel
kirkkwang Feb 9, 2024
458638a
Add specs for DatatablesBehavior
kirkkwang Feb 9, 2024
70d9e33
rubocoppers
kirkkwang Feb 9, 2024
415a7e2
Merge branch 'denomalize_status_message' into one_table_to_rule_them_…
orangewolf Feb 9, 2024
783adc1
Merge branch 'one_table_to_rule_them_all_one_table_to_find_them_one_t…
orangewolf Feb 9, 2024
7964466
Found another place we need to include the monad lib. This time so th…
orangewolf Feb 8, 2024
c488439
Merge branch 'main' of github.com:samvera/bulkrax
orangewolf Feb 9, 2024
c5d9308
Merge branch 'main' into one_table_to_rule_them_all_one_table_to_find…
orangewolf Feb 9, 2024
cdf3845
implement importers index page
orangewolf Feb 9, 2024
ac18fbb
select other statuses we use
orangewolf Feb 9, 2024
870fb1b
Merge branch 'main' of github.com:samvera/bulkrax
orangewolf Feb 9, 2024
8da78bb
Merge branch 'main' into one_table_to_rule_them_all_one_table_to_find…
orangewolf Feb 9, 2024
f7dd4aa
spec fixes
orangewolf Feb 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions app/assets/javascripts/bulkrax/datatables.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
Blacklight.onLoad(function() {
if($('#importer-show-table').length) {
$('#importer-show-table').DataTable( {
'processing': true,
'serverSide': true,
"ajax": window.location.href.replace(/(\/importers\/\d+)/, "$1/entry_table.json"),
"pageLength": 30,
"lengthMenu": [[30, 100, 200], [30, 100, 200]],
"columns": [
{ "data": "identifier" },
{ "data": "id" },
{ "data": "status_message" },
{ "data": "type" },
{ "data": "updated_at" },
{ "data": "errors", "orderable": false },
{ "data": "actions", "orderable": false }
],
initComplete: function () {
// Add entry class filter
entrySelect.bind(this)()
// Add status filter
statusSelect.bind(this)()
// Add refresh link
refreshLink.bind(this)()
}
} );
}

if($('#importers-table').length) {
$('#importers-table').DataTable( {
'processing': true,
'serverSide': true,
"ajax": window.location.href.replace(/(\/importers)/, "$1/importer_table.json"),
"pageLength": 30,
"lengthMenu": [[30, 100, 200], [30, 100, 200]],
"columns": [
{ "data": "name" },
{ "data": "status_message" },
{ "data": "last_imported_at" },
{ "data": "next_import_at" },
{ "data": "enqueued_records", "orderable": false },
{ "data": "processed_records", "orderable": false },
{ "data": "failed_records", "orderable": false },
{ "data": "deleted_records", "orderable": false },
{ "data": "total_collection_entries", "orderable": false },
{ "data": "total_work_entries", "orderable": false },
{ "data": "total_file_set_entries", "orderable": false },
{ "data": "actions", "orderable": false }
],
initComplete: function () {
// Add status filter
statusSelect.bind(this)()
// Add refresh link
refreshLink.bind(this)()
}
} );
}

})

function entrySelect() {
let entrySelect = document.createElement('select')
entrySelect.id = 'entry-filter'
entrySelect.classList.value = 'form-control input-sm'
entrySelect.style.marginRight = '10px'

entrySelect.add(new Option('Filter by Entry Class', ''))
// Read the options from the footer and add them to the entrySelect
$('#importer-entry-classes').text().split('|').forEach(function (col, i) {
entrySelect.add(new Option(col.trim()))
})
document.querySelector('div#importer-show-table_filter').firstChild.prepend(entrySelect)

// Apply listener for user change in value
entrySelect.addEventListener('change', function () {
var val = entrySelect.value;
this.api()
.search(val ? val : '', false, false)
.draw();
}.bind(this));
}

function statusSelect() {
let statusSelect = document.createElement('select');
statusSelect.id = 'status-filter'
statusSelect.classList.value = 'form-control input-sm'
statusSelect.style.marginRight = '10px'

statusSelect.add(new Option('Filter by Status', ''));
statusSelect.add(new Option('Complete'))
statusSelect.add(new Option('Pending'))
statusSelect.add(new Option('Failed'))
statusSelect.add(new Option('Deleted'))
statusSelect.add(new Option('Complete (with failures)'))

document.querySelector('div.dataTables_filter').firstChild.prepend(statusSelect)

// Apply listener for user change in value
statusSelect.addEventListener('change', function () {
var val = statusSelect.value;
this.api()
.search(val ? val : '', false, false)
.draw();
}.bind(this));
}

function refreshLink() {
let refreshLink = document.createElement('a');
refreshLink.onclick = function() {
this.api().ajax.reload(null, false)
}.bind(this)
refreshLink.classList.value = 'glyphicon glyphicon-refresh'
refreshLink.style.marginLeft = '10px'
document.querySelector('div.dataTables_filter').firstChild.append(refreshLink)
}
27 changes: 21 additions & 6 deletions app/controllers/bulkrax/importers_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,35 @@ class ImportersController < ::Bulkrax::ApplicationController
include Hyrax::ThemedLayoutController if defined?(::Hyrax)
include Bulkrax::DownloadBehavior
include Bulkrax::API
include Bulkrax::DatatablesBehavior
include Bulkrax::ValidationHelper

protect_from_forgery unless: -> { api_request? }
before_action :token_authenticate!, if: -> { api_request? }, only: [:create, :update, :delete]
before_action :authenticate_user!, unless: -> { api_request? }
before_action :check_permissions
before_action :set_importer, only: [:show, :edit, :update, :destroy]
before_action :set_importer, only: [:show, :entry_table, :edit, :update, :destroy]
with_themed_layout 'dashboard' if defined?(::Hyrax)

# GET /importers
def index
# NOTE: We're paginating this in the browser.
@importers = Importer.order(created_at: :desc).all
if api_request?
@importers = Importer.order(created_at: :desc).all
json_response('index')
elsif defined?(::Hyrax)
add_importer_breadcrumbs
end
end

def importer_table
@importers = Importer.order(table_order).page(table_page).per(table_per_page)
@importers = @importers.where(importer_table_search) if importer_table_search.present?
respond_to do |format|
format.json { render json: format_importers(@importers) }
end
end

# GET /importers/1
def show
if api_request?
Expand All @@ -34,9 +43,15 @@ def show
add_importer_breadcrumbs
add_breadcrumb @importer.name
end
@work_entries = @importer.entries.where(type: @importer.parser.entry_class.to_s).page(params[:work_entries_page]).per(30)
@collection_entries = @importer.entries.where(type: @importer.parser.collection_entry_class.to_s).page(params[:collections_entries_page]).per(30)
@file_set_entries = @importer.entries.where(type: @importer.parser.file_set_entry_class.to_s).page(params[:file_set_entries_page]).per(30)
@first_entry = @importer.entries.first
end

def entry_table
@entries = @importer.entries.order(table_order).page(table_page).per(table_per_page)
@entries = @entries.where(entry_table_search) if entry_table_search.present?
respond_to do |format|
format.json { render json: format_entries(@entries, @importer) }
end
end

# GET /importers/new
Expand Down Expand Up @@ -210,7 +225,7 @@ def files_for_import(file, cloud_files)

# Use callbacks to share common setup or constraints between actions.
def set_importer
@importer = Importer.find(params[:id])
@importer = Importer.find(params[:id] || params[:importer_id])
end

def importable_params
Expand Down
134 changes: 134 additions & 0 deletions app/controllers/concerns/bulkrax/datatables_behavior.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# frozen_string_literal: true

module Bulkrax
module DatatablesBehavior
extend ActiveSupport::Concern

def table_per_page
per_page = params[:length].to_i
per_page < 1 ? 30 : per_page
end

def order_value(column)
params['columns']&.[](column)&.[]('data')
end

def table_order
"#{order_value(params&.[]('order')&.[]('0')&.[]('column'))} #{params&.[]('order')&.[]('0')&.[]('dir')}" if params&.[]('order')&.[]('0')&.[]('column').present?
end

# convert offset to page number
def table_page
params[:start].blank? ? 1 : (params[:start].to_i / params[:length].to_i) + 1
end

def entry_table_search
return @entry_table_search if @entry_table_search
return @entry_table_search = false if params['search']&.[]('value').blank?

table_search_value = params['search']&.[]('value')&.downcase

['identifier', 'id', 'status_message', 'type', 'updated_at'].map do |col|
column = Bulkrax::Entry.arel_table[col]
column = Arel::Nodes::NamedFunction.new('CAST', [column.as('text')])
column = Arel::Nodes::NamedFunction.new('LOWER', [column])
@entry_table_search = if @entry_table_search
@entry_table_search.or(column.matches("%#{table_search_value}%"))
else
column.matches("%#{table_search_value}%")
end
end

@entry_table_search
end

def importer_table_search
return @importer_table_search if @importer_table_search
return @importer_table_search = false if params['search']&.[]('value').blank?

table_search_value = params['search']&.[]('value')&.downcase

['name', 'id', 'status_message', 'last_error_at', 'last_succeeded_at', 'updated_at'].map do |col|
column = Bulkrax::Importer.arel_table[col]
column = Arel::Nodes::NamedFunction.new('CAST', [column.as('text')])
column = Arel::Nodes::NamedFunction.new('LOWER', [column])
@importer_table_search = if @importer_table_search
@importer_table_search.or(column.matches("%#{table_search_value}%"))
else
column.matches("%#{table_search_value}%")
end
end

@importer_table_search
end

def format_importers(importers)
result = importers.map do |i|
{
name: view_context.link_to(i.name, view_context.importer_path(i)),
status_message: status_message_for(i),
last_imported_at: i.last_imported_at&.strftime("%b %d, %Y"),
next_import_at: i.next_import_at&.strftime("%b %d, %Y"),
enqueued_records: i.last_run&.enqueued_records,
processed_records: i.last_run&.processed_records || 0,
failed_records: i.last_run&.failed_records || 0,
deleted_records: i.last_run&.deleted_records,
total_collection_entries: i.last_run&.total_collection_entries,
total_work_entries: i.last_run&.total_work_entries,
total_file_set_entries: i.last_run&.total_file_set_entries,
actions: importer_util_links(i)
}
end
{
data: result,
recordsTotal: Bulkrax::Importer.count,
recordsFiltered: importers.size
}
end

def format_entries(entries, item)
result = entries.map do |e|
{
identifier: view_context.link_to(e.identifier, view_context.item_entry_path(item, e)),
id: e.id,
status_message: status_message_for(e),
type: e.type,
updated_at: e.updated_at,
errors: e.latest_status&.error_class&.present? ? view_context.link_to(e.latest_status.error_class, view_context.item_entry_path(item, e), title: e.latest_status.error_message) : "",
actions: entry_util_links(e, item)
}
end
{
data: result,
recordsTotal: item.entries.size,
recordsFiltered: item.entries.size
}
end

def entry_util_links(e, item)
links = []
links << view_context.link_to(view_context.raw('<span class="glyphicon glyphicon-info-sign"></span>'), view_context.item_entry_path(item, e))
links << "<a class='glyphicon glyphicon-repeat' data-toggle='modal' data-target='#bulkraxItemModal' data-entry-id='#{e.id}'></a>" if view_context.an_importer?(item)
links << view_context.link_to(view_context.raw('<span class="glyphicon glyphicon-trash"></span>'), view_context.item_entry_path(item, e), method: :delete, data: { confirm: 'This will delete the entry and any work associated with it. Are you sure?' })
links.join(" ")
end

def status_message_for(e)
if e.status_message == "Complete"
"<td><span class='glyphicon glyphicon-ok' style='color: green;'></span> #{e.status_message}</td>"
elsif e.status_message == "Pending"
"<td><span class='glyphicon glyphicon-option-horizontal' style='color: blue;'></span> #{e.status_message}</td>"
else
"<td><span class='glyphicon glyphicon-remove' style='color: #{e.status == 'Deleted' ? 'green' : 'red'};'></span> #{e.status_message}</td>"
end
end

def importer_util_links(i)
links = []
links << view_context.link_to(view_context.raw('<span class="glyphicon glyphicon-info-sign"></span>'), importer_path(i))
links << view_context.link_to(view_context.raw('<span class="glyphicon glyphicon-pencil"></span>'), edit_importer_path(i))
links << view_context.link_to(view_context.raw('<span class="glyphicon glyphicon-remove"></span>'), i, method: :delete, data: { confirm: 'Are you sure?' })
links.join(" ")
end
end
end
Loading
Loading