diff --git a/lib/db.rb b/lib/db.rb index 9070c3e82..fc0ffd1b5 100644 --- a/lib/db.rb +++ b/lib/db.rb @@ -29,6 +29,7 @@ def self.connect db_credentials # logger = Logger.new($stdout) con = Sequel.connect(db_credentials.merge(:logger => logger, :pool_class => Sequel::ThreadedConnectionPool, :encoding => 'utf8')) con.extension(:connection_validator) + con.extension(:pagination) con.pool.connection_validation_timeout = -1 #Check the connection on every request con.timezone = :utc con.run("SET sql_mode='STRICT_TRANS_TABLES';") if db_credentials[:adapter].to_s =~ /mysql/ diff --git a/lib/pact_broker/app.rb b/lib/pact_broker/app.rb index 7dcf58645..fc2de3e8c 100644 --- a/lib/pact_broker/app.rb +++ b/lib/pact_broker/app.rb @@ -110,6 +110,7 @@ def configure_database_connection PactBroker::DB.connection.timezone = :utc PactBroker::DB.validate_connection_config if configuration.validate_database_connection_config PactBroker::DB.set_mysql_strict_mode_if_mysql + PactBroker::DB.connection.extension(:pagination) Sequel.datetime_class = DateTime Sequel.database_timezone = :utc # Store all dates in UTC, assume any date without a TZ is UTC Sequel.application_timezone = :local # Convert dates to localtime when retrieving from database diff --git a/lib/pact_broker/index/page.rb b/lib/pact_broker/index/page.rb new file mode 100644 index 000000000..b78254100 --- /dev/null +++ b/lib/pact_broker/index/page.rb @@ -0,0 +1,12 @@ +module PactBroker + module Index + class Page < Array + attr_reader :pagination_record_count + + def initialize(array, pagination_record_count) + super(array) + @pagination_record_count = pagination_record_count + end + end + end +end diff --git a/lib/pact_broker/index/service.rb b/lib/pact_broker/index/service.rb index 07bce256d..c4ef5b2f7 100644 --- a/lib/pact_broker/index/service.rb +++ b/lib/pact_broker/index/service.rb @@ -4,6 +4,7 @@ require 'pact_broker/matrix/head_row' require 'pact_broker/matrix/aggregated_row' require 'pact_broker/repositories/helpers' +require 'pact_broker/index/page' module PactBroker module Index @@ -20,6 +21,8 @@ class Service Sequel.desc(:consumer_version_order), Sequel.asc(Sequel.function(:lower, :provider_name)) ].freeze + DEFAULT_PAGE_SIZE = 30 + DEFAULT_PAGE_NUMBER = 1 # This method provides data for both the OSS server side rendered index (with and without tags) # and the Pactflow UI. It really needs to be broken into to separate methods, as it's getting too messy @@ -77,6 +80,7 @@ def self.find_index_items_optimised options = {} webhooks = PactBroker::Webhooks::Webhook.select(:consumer_id, :provider_id).distinct.all pact_publication_ids = head_pact_publication_ids(options) + pagination_record_count = pact_publication_ids.pagination_record_count pact_publications = PactBroker::Pacts::PactPublication .where(id: pact_publication_ids) @@ -89,7 +93,7 @@ def self.find_index_items_optimised options = {} .eager(latest_verification: { provider_version: :tags_with_latest_flag }) .eager(:head_pact_tags) - pact_publications.all.collect do | pact_publication | + index_items = pact_publications.all.collect do | pact_publication | is_overall_latest_for_integration = latest_pact_publication_ids.include?(pact_publication.id) latest_verification = latest_verification_for_pseudo_branch(pact_publication, is_overall_latest_for_integration, latest_verifications_for_cv_tags, options[:tags]) webhook = webhooks.find{ |webhook| webhook.is_for?(pact_publication.integration) } @@ -106,6 +110,8 @@ def self.find_index_items_optimised options = {} options[:tags] && latest_verification ? latest_verification.provider_version.tags_with_latest_flag.select(&:latest?) : [] ) end.sort + + Page.new(index_items, pagination_record_count) end # Worst. Code. Ever. @@ -193,8 +199,7 @@ def self.head_pact_publication_ids(options = {}) end query.order(*HEAD_PP_ORDER_COLUMNS) - .limit(options[:limit] || 50) - .offset(options[:offset] || 0) + .paginate(options[:page_number] || DEFAULT_PAGE_NUMBER, options[:page_size] || DEFAULT_PAGE_SIZE) .select(:id) end diff --git a/lib/pact_broker/ui/controllers/index.rb b/lib/pact_broker/ui/controllers/index.rb index 94ca4202d..d7c737aa9 100644 --- a/lib/pact_broker/ui/controllers/index.rb +++ b/lib/pact_broker/ui/controllers/index.rb @@ -14,11 +14,27 @@ class Index < Base if params[:tags] tags = params[:tags] == 'true' ? true : [*params[:tags]].compact end - options = { tags: tags, limit: params[:limit]&.to_i, offset: params[:offset]&.to_i } + page_number = params[:page]&.to_i || 1 + page_size = params[:pageSize]&.to_i || 30 + options = { + tags: tags, + page_number: page_number, + page_size: page_size + } + options[:optimised] = true if params[:optimised] == 'true' - view_model = ViewDomain::IndexItems.new(index_service.find_index_items(options)) + index_items = ViewDomain::IndexItems.new(index_service.find_index_items(options)) + page = tags ? :'index/show-with-tags' : :'index/show' - haml page, {locals: {index_items: view_model, title: "Pacts"}, layout: :'layouts/main'} + locals = { + index_items: index_items, title: "Pacts", + page_number: page_number, + page_size: page_size, + pagination_record_count: index_items.pagination_record_count, + current_page_size: index_items.size + } + + haml page, {locals: locals, layout: :'layouts/main'} end def set_headers diff --git a/lib/pact_broker/ui/view_models/index_items.rb b/lib/pact_broker/ui/view_models/index_items.rb index 7aae3794e..459d38e58 100644 --- a/lib/pact_broker/ui/view_models/index_items.rb +++ b/lib/pact_broker/ui/view_models/index_items.rb @@ -5,26 +5,28 @@ module UI module ViewDomain class IndexItems + attr_reader :pagination_record_count + def initialize index_items + # Why are we sorting twice!? @index_items = index_items.collect{ |index_item| IndexItem.new(index_item) }.sort + # until the feature flag is turned on + @pagination_record_count = index_items.size + @pagination_record_count = index_items.pagination_record_count if index_items.respond_to?(:pagination_record_count) end def each(&block) index_items.each(&block) end - def size_label - case index_items.size - when 1 then "1 pact" - else - "#{index_items.size} pacts" - end - end - def empty? index_items.empty? end + def size + index_items.size + end + private attr_reader :index_items diff --git a/lib/pact_broker/ui/views/index/_pagination.haml b/lib/pact_broker/ui/views/index/_pagination.haml new file mode 100644 index 000000000..6d17ffbed --- /dev/null +++ b/lib/pact_broker/ui/views/index/_pagination.haml @@ -0,0 +1,31 @@ +%script{type: 'text/javascript', src:'/javascripts/pagination.js'} + +:javascript + const PAGE_NUMBER = #{page_number}; + const PAGE_SIZE = #{page_size}; + const TOTAL_NUMBER = #{pagination_record_count} + const CURRENT_PAGE_SIZE = #{current_page_size} + + $(document).ready(function(){ + function createPageLink(pageNumber, pageSize) { + const url = new URL(window.location) + url.searchParams.set('page', pageNumber) + url.searchParams.set('pageSize', pageSize) + return url.toString() + } + + function createFooter(currentPage, totalPage, totalNumber) { + return `` + } + + $('div.pagination').pagination({ + dataSource: [], + totalNumber: TOTAL_NUMBER, + pageNumber: PAGE_NUMBER, + pageSize: PAGE_SIZE, + pageRange: 2, + pageLink: createPageLink, + ulClassName: 'pagination', + footer: createFooter + }) + }); diff --git a/lib/pact_broker/ui/views/index/show-with-tags.haml b/lib/pact_broker/ui/views/index/show-with-tags.haml index 563f10146..ccf17e7d6 100644 --- a/lib/pact_broker/ui/views/index/show-with-tags.haml +++ b/lib/pact_broker/ui/views/index/show-with-tags.haml @@ -82,8 +82,11 @@ %td - if index_item.show_settings? %span.integration-settings.glyphicon.glyphicon-option-horizontal{ 'aria-hidden': true } - %div.relationships-size - = index_items.size_label + + %div.pagination + + - pagination_locals = { page_number: page_number, page_size: page_size, pagination_record_count: pagination_record_count, current_page_size: current_page_size } + = render :haml, :'index/_pagination', :layout => false, locals: pagination_locals :javascript $(function(){ diff --git a/lib/pact_broker/ui/views/index/show.haml b/lib/pact_broker/ui/views/index/show.haml index 814eac68e..6d11c77a6 100644 --- a/lib/pact_broker/ui/views/index/show.haml +++ b/lib/pact_broker/ui/views/index/show.haml @@ -55,8 +55,11 @@ %span.glyphicon.glyphicon-warning-sign{ 'aria-hidden': true } %td %span.integration-settings.glyphicon.glyphicon-option-horizontal{ 'aria-hidden': true } - %div.relationships-size - = index_items.size_label + %div.pagination + + - pagination_locals = { page_number: page_number, page_size: page_size, pagination_record_count: pagination_record_count, current_page_size: current_page_size } + = render :haml, :'index/_pagination', :layout => false, locals: pagination_locals + :javascript $(function(){ diff --git a/public/javascripts/pagination.js b/public/javascripts/pagination.js new file mode 100644 index 000000000..9f51e6f8e --- /dev/null +++ b/public/javascripts/pagination.js @@ -0,0 +1,1127 @@ +// A hacked version of pagination.js to get just the nav renderiing functionality. +// TODO Remove all the unnecessary code! +/* + * pagination.js 2.1.5 + * A jQuery plugin to provide simple yet fully customisable pagination. + * https://github.com/superRaytin/paginationjs + * + * Homepage: http://pagination.js.org + * + * Copyright 2014-2100, superRaytin + * Released under the MIT license. + */ + +(function(global, $) { + + if (typeof $ === 'undefined') { + throwError('Pagination requires jQuery.'); + } + + var pluginName = 'pagination'; + + var pluginHookMethod = 'addHook'; + + var eventPrefix = '__pagination-'; + + // Conflict, use backup + if ($.fn.pagination) { + pluginName = 'pagination2'; + } + + $.fn[pluginName] = function(options) { + + if (typeof options === 'undefined') { + return this; + } + + var container = $(this); + + var attributes = $.extend({}, $.fn[pluginName].defaults, options); + + var pagination = { + + initialize: function() { + var self = this; + + // Cache attributes of current instance + if (!container.data('pagination')) { + container.data('pagination', {}); + } + + if (self.callHook('beforeInit') === false) return; + + // Pagination has been initialized, destroy it + if (container.data('pagination').initialized) { + $('.paginationjs', container).remove(); + } + + // Whether to disable Pagination at the initialization + self.disabled = !!attributes.disabled; + + // Model will be passed to the callback function + var model = self.model = { + pageRange: attributes.pageRange, + pageSize: attributes.pageSize + }; + + // dataSource`s type is unknown, parse it to find true data + self.parseDataSource(attributes.dataSource, function(dataSource) { + + // Currently in asynchronous mode + self.isAsync = Helpers.isString(dataSource); + if (Helpers.isArray(dataSource)) { + model.totalNumber = attributes.totalNumber; // = dataSource.length; + } + + // Currently in asynchronous mode and a totalNumberLocator is specified + self.isDynamicTotalNumber = self.isAsync && attributes.totalNumberLocator; + + var el = self.render(true); + + // Add extra className to the pagination element + if (attributes.className) { + el.addClass(attributes.className); + } + + model.el = el; + + // Append/prepend pagination element to the container + container[attributes.position === 'bottom' ? 'append' : 'prepend'](el); + + // Bind events + //self.observer(); + + // Pagination is currently initialized + container.data('pagination').initialized = true; + + // Will be invoked after initialized + self.callHook('afterInit', el); + }); + }, + + render: function(isBoot) { + var self = this; + var model = self.model; + var el = model.el || $(''); + var isForced = isBoot !== true; + + self.callHook('beforeRender', isForced); + + var currentPage = model.pageNumber || attributes.pageNumber; + var pageRange = attributes.pageRange || 0; + var totalPage = self.getTotalPage(); + + var rangeStart = currentPage - pageRange; + var rangeEnd = currentPage + pageRange; + + if (rangeEnd > totalPage) { + rangeEnd = totalPage; + rangeStart = totalPage - pageRange * 2; + rangeStart = rangeStart < 1 ? 1 : rangeStart; + } + + if (rangeStart <= 1) { + rangeStart = 1; + rangeEnd = Math.min(pageRange * 2 + 1, totalPage); + } + + el.html(self.generateHTML({ + currentPage: currentPage, + pageRange: pageRange, + rangeStart: rangeStart, + rangeEnd: rangeEnd + })); + + // There is only one page + if (attributes.hideWhenLessThanOnePage) { + el[totalPage <= 1 ? 'hide' : 'show'](); + } + + self.callHook('afterRender', isForced); + + return el; + }, + + // Generate HTML of the pages + generatePageNumbersHTML: function(args) { + var self = this; + var currentPage = args.currentPage; + var totalPage = self.getTotalPage(); + var rangeStart = args.rangeStart; + var rangeEnd = args.rangeEnd; + var pageSize = attributes.pageSize; + var html = ''; + var i; + + var pageLink = attributes.pageLink; + var ellipsisText = attributes.ellipsisText; + + var classPrefix = attributes.classPrefix; + var activeClassName = attributes.activeClassName; + var disableClassName = attributes.disableClassName; + + // Disable page range, display all the pages + if (attributes.pageRange === null) { + for (i = 1; i <= totalPage; i++) { + if (i == currentPage) { + html += '
  • ' + i + '<\/a><\/li>'; + } else { + html += '
  • ' + i + '<\/a><\/li>'; + } + } + return html; + } + + if (rangeStart <= 3) { + for (i = 1; i < rangeStart; i++) { + if (i == currentPage) { + html += '
  • ' + i + '<\/a><\/li>'; + } else { + html += '
  • ' + i + '<\/a><\/li>'; + } + } + } else { + if (attributes.showFirstOnEllipsisShow) { + html += '
  • 1<\/a><\/li>'; + } + html += '
  • ' + ellipsisText + '<\/a><\/li>'; + } + + for (i = rangeStart; i <= rangeEnd; i++) { + if (i == currentPage) { + html += '
  • ' + i + '<\/a><\/li>'; + } else { + html += '
  • ' + i + '<\/a><\/li>'; + } + } + + if (rangeEnd >= totalPage - 2) { + for (i = rangeEnd + 1; i <= totalPage; i++) { + html += '
  • ' + i + '<\/a><\/li>'; + } + } else { + html += '
  • ' + ellipsisText + '<\/a><\/li>'; + + if (attributes.showLastOnEllipsisShow) { + html += '
  • ' + totalPage + '<\/a><\/li>'; + } + } + + return html; + }, + + // Generate HTML content from the template + generateHTML: function(args) { + var self = this; + var currentPage = args.currentPage; + var totalPage = self.getTotalPage(); + + var totalNumber = self.getTotalNumber(); + var pageSize = attributes.pageSize; + + var showPrevious = attributes.showPrevious; + var showNext = attributes.showNext; + var showPageNumbers = attributes.showPageNumbers; + var showNavigator = attributes.showNavigator; + var showGoInput = attributes.showGoInput; + var showGoButton = attributes.showGoButton; + + var pageLink = attributes.pageLink; + var prevText = attributes.prevText; + var nextText = attributes.nextText; + var goButtonText = attributes.goButtonText; + + var classPrefix = attributes.classPrefix; + var disableClassName = attributes.disableClassName; + var ulClassName = attributes.ulClassName; + + var html = ''; + var goInput = ''; + var goButton = ''; + var formattedString; + + var formatNavigator = $.isFunction(attributes.formatNavigator) ? attributes.formatNavigator(currentPage, totalPage, totalNumber) : attributes.formatNavigator; + var formatGoInput = $.isFunction(attributes.formatGoInput) ? attributes.formatGoInput(goInput, currentPage, totalPage, totalNumber) : attributes.formatGoInput; + var formatGoButton = $.isFunction(attributes.formatGoButton) ? attributes.formatGoButton(goButton, currentPage, totalPage, totalNumber) : attributes.formatGoButton; + + var autoHidePrevious = $.isFunction(attributes.autoHidePrevious) ? attributes.autoHidePrevious() : attributes.autoHidePrevious; + var autoHideNext = $.isFunction(attributes.autoHideNext) ? attributes.autoHideNext() : attributes.autoHideNext; + + var header = $.isFunction(attributes.header) ? attributes.header(currentPage, totalPage, totalNumber) : attributes.header; + var footer = $.isFunction(attributes.footer) ? attributes.footer(currentPage, totalPage, totalNumber) : attributes.footer; + + // Whether to display header + if (header) { + formattedString = self.replaceVariables(header, { + currentPage: currentPage, + totalPage: totalPage, + totalNumber: totalNumber + }); + html += formattedString; + } + + if (showPrevious || showPageNumbers || showNext) { + html += '
    '; + + if (ulClassName) { + html += '