diff --git a/lib/pact_broker/api/pact_broker_urls.rb b/lib/pact_broker/api/pact_broker_urls.rb index 1a71d2ab0..f5b2db774 100644 --- a/lib/pact_broker/api/pact_broker_urls.rb +++ b/lib/pact_broker/api/pact_broker_urls.rb @@ -73,7 +73,7 @@ def latest_pacts_url base_url "#{base_url}/pacts/latest" end - def pact_versions_url consumer_name, provider_name, base_url + def pact_versions_url consumer_name, provider_name, base_url = "" "#{base_url}/pacts/provider/#{url_encode(provider_name)}/consumer/#{url_encode(consumer_name)}/versions" end diff --git a/lib/pact_broker/ui/view_models/index_item.rb b/lib/pact_broker/ui/view_models/index_item.rb index 5bdb9d21e..b0298b1bb 100644 --- a/lib/pact_broker/ui/view_models/index_item.rb +++ b/lib/pact_broker/ui/view_models/index_item.rb @@ -2,6 +2,7 @@ require 'pact_broker/ui/helpers/url_helper' require 'pact_broker/date_helper' require 'pact_broker/versions/abbreviate_number' +require 'pact_broker/configuration' module PactBroker module UI @@ -66,6 +67,10 @@ def any_webhooks? @relationship.any_webhooks? end + def pact_versions_url + PactBroker::Api::PactBrokerUrls.pact_versions_url(consumer_name, provider_name, PactBroker.configuration.base_url) + end + def webhook_label return "" unless show_webhook_status? case @relationship.webhook_status @@ -90,6 +95,10 @@ def show_webhook_status? @relationship.latest? end + def show_settings? + @relationship.latest? + end + def webhook_last_execution_date PactBroker::DateHelper.distance_of_time_in_words(@relationship.last_webhook_execution_date, DateTime.now) + " ago" end diff --git a/lib/pact_broker/ui/views/index/_css_and_js.haml b/lib/pact_broker/ui/views/index/_css_and_js.haml new file mode 100644 index 000000000..8cd08f70b --- /dev/null +++ b/lib/pact_broker/ui/views/index/_css_and_js.haml @@ -0,0 +1,6 @@ +%link{rel: 'stylesheet', href: '/stylesheets/index.css'} +%link{rel: 'stylesheet', href: '/stylesheets/material-menu.css'} +%link{rel: 'stylesheet', href: '/stylesheets/material-icon.css'} +%script{type: 'text/javascript', src:'/javascripts/jquery.tablesorter.min.js'} +%script{type: 'text/javascript', src:'/javascripts/material-menu.js'} +%script{type: 'text/javascript', src:'/javascripts/index.js'} 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 b8e0b43bb..5b58ba44d 100644 --- a/lib/pact_broker/ui/views/index/show-with-tags.haml +++ b/lib/pact_broker/ui/views/index/show-with-tags.haml @@ -1,6 +1,5 @@ %body - %link{rel: 'stylesheet', href: '/stylesheets/index.css'} - %script{type: 'text/javascript', src:'/javascripts/jquery.tablesorter.min.js'} + = render :haml, :'index/_css_and_js', :layout => false .container = render :haml, :'index/_navbar', :layout => false, locals: {tag_toggle: false} - if index_items.empty? @@ -31,6 +30,7 @@ %th Last
verified %span.glyphicon.glyphicon-sort.relationships-sort + %th %tbody - index_items.each do | index_item | @@ -73,6 +73,9 @@ = index_item.last_verified_date.gsub("about ", "") - if index_item.warning? %span.glyphicon.glyphicon-warning-sign{ 'aria-hidden': true } + %td + - if index_item.show_settings? + %span.integration-settings.glyphicon.glyphicon-cog{ 'aria-hidden': true, 'data-pact-versions-url': index_item.pact_versions_url, 'data-consumer-name': index_item.consumer_name, 'data-provider-name': index_item.provider_name} %div.relationships-size = index_items.size_label diff --git a/lib/pact_broker/ui/views/index/show.haml b/lib/pact_broker/ui/views/index/show.haml index a9a0c2fba..7fbc8161a 100644 --- a/lib/pact_broker/ui/views/index/show.haml +++ b/lib/pact_broker/ui/views/index/show.haml @@ -1,6 +1,5 @@ %body - %link{rel: 'stylesheet', href: '/stylesheets/index.css'} - %script{type: 'text/javascript', src:'/javascripts/jquery.tablesorter.min.js'} + = render :haml, :'index/_css_and_js', :layout => false .container = render :haml, :'index/_navbar', :layout => false, locals: {tag_toggle: true} - if index_items.empty? @@ -26,6 +25,7 @@ Webhook
status %th Last
verified + %th %tbody - index_items.each do | index_item | @@ -54,6 +54,8 @@ = index_item.last_verified_date - if index_item.warning? %span.glyphicon.glyphicon-warning-sign{ 'aria-hidden': true } + %td + %span.integration-settings.glyphicon.glyphicon-cog{ 'aria-hidden': true, 'data-pact-versions-url': index_item.pact_versions_url, 'data-consumer-name': index_item.consumer_name, 'data-provider-name': index_item.provider_name} %div.relationships-size = index_items.size_label diff --git a/public/fonts/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2 b/public/fonts/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2 new file mode 100644 index 000000000..34cdd2afb Binary files /dev/null and b/public/fonts/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2 differ diff --git a/public/javascripts/index.js b/public/javascripts/index.js new file mode 100644 index 000000000..8b6082d28 --- /dev/null +++ b/public/javascripts/index.js @@ -0,0 +1,85 @@ +$(document).ready(function() { + $(".integration-settings") + .materialMenu("init", { + position: "overlay", + animationSpeed: 1, + items: [ + { + type: "normal", + text: "Delete all pact versions...", + click: function(e) { + promptToDeletePactVersions($(e).data(), $(e).closest("tr")); + } + } + ] + }) + .click(function() { + $(this).materialMenu("open"); + }); +}); + +function promptToDeletePactVersions(rowData, row) { + const agree = confirm( + `This will delete all versions of the pact between ${ + rowData.consumerName + } and ${rowData.providerName}. It will keep ${rowData.consumerName} and ${ + rowData.providerName + }, and all other data related to them (webhooks, application versions, and tags). Do you wish to continue?` + ); + if (agree) { + deletePactVersions( + rowData.pactVersionsUrl, + function() { + handleDeletionSuccess(row); + }, + handleDeletionFailure + ); + } +} + +function handleDeletionSuccess(row) { + row + .children("td, th") + .animate({ padding: 0 }) + .wrapInner("
") + .children() + .slideUp(function() { + $(this) + .closest("tr") + .remove(); + }); +} + +function handleDeletionFailure(response) { + let errorMessage = null; + + if (response.error && response.error.message && response.error.reference) { + errorMessage = + "Could not delete resources due to error: " + + response.error.message + + "\nError reference: " + + response.error.reference; + } else { + errorMessage = + "Could not delete resources due to error: " + JSON.stringify(response); + } + + alert(errorMessage); +} + +function deletePactVersions(url, successCallback, errorCallback) { + $.ajax({ + url: url, + dataType: "json", + type: "delete", + accepts: { + text: "application/hal+json" + }, + success: function(data, textStatus, jQxhr) { + successCallback(); + }, + error: function(jqXhr, textStatus, errorThrown) { + errorCallback(jqXhr.responseJSON); + } + }); +} diff --git a/public/javascripts/material-menu.js b/public/javascripts/material-menu.js new file mode 100755 index 000000000..e8883e364 --- /dev/null +++ b/public/javascripts/material-menu.js @@ -0,0 +1,241 @@ +(function ($) { + var id = 0; + var menus = {}; + + $.fn.materialMenu = function (action, settings) { + var settings = $.extend({ + animationSpeed: 250, + position: "", + items: [] + }, settings); + + return this.each(function (item) { + var parent = $(this); + if (action === "init") { + parent.attr('id', getNextId()); + var menu = getMenuForParent(parent); + menu.element = $('
'); + menu.parent = parent; + menu.settings = settings; + menu.items = []; + + settings.items.forEach(function (item) { + var item = $.extend({ + text: "", + type: "normal", + radioGroup: "default", + click: function () { } + }, item); + if (item.type === 'toggle') { + var elStr = "
  • "; + var itemElement = $(elStr) + .append("check") + .append("" + item.text + "") + .click(function () { + if (item.checked) { + item.element.addClass('unchecked'); + } else { + item.element.removeClass('unchecked'); + } + item.checked = !item.checked; + item.click(menu.parent, item.checked); + closeMenu(menu); + }); + } else if (item.type === 'radio') { + var elStr = "
  • "; + var itemElement = $(elStr) + .append("check") + .append("" + item.text + "") + .click(function () { + menu.items.forEach(function (otherItem) { + if (otherItem.radioGroup === item.radioGroup) { + if (otherItem == item) { + item.element.removeClass('unchecked'); + otherItem.checked = false; + } else { + otherItem.element.addClass('unchecked'); + otherItem.checked = false; + } + } + }); + item.click(menu.parent, item.checked); + closeMenu(menu); + }); + } else if (item.type === 'divider') { + var itemElement = $("
  • "); + } else if (item.type === 'label') { + var itemElement = $("
  • ") + .html(item.text); + } else if (item.type === 'submenu') { + var itemElement = $("
  • ") + .append("" + item.text + "") + .append('
    arrow_drop_up
    ') + .click(function () { + item.click(menu.parent); + closeMenu(menu); + }); + } else if (item.type === 'normal') { + var itemElement = $("
  • ") + .html(item.text) + .click(function () { + item.click(menu.parent); + closeMenu(menu); + }); + } else { + console.log("Menu item with invalid type, type was: " + item.type); + return; + } + itemElement.attr('id', getNextId()); + item.element = itemElement; + menu.element.children('ul').append(itemElement); + menu.items.push(item); + }); + menu.element.hide(); + $('body').append(menu.element); + + return this; + } + + if (action === 'open') { + var menu = getMenuForParent(parent); + if (menu.open) { + return; + } + openMenu(menu); + return this; + } + + if (action === 'close') { + var menu = getMenuForParent(parent); + if (!menu.open) { + return; + } + closeMenu(menu); + return this; + } + }); + }; + + function openMenu(menu) { + menu.open = true; + updatePos(menu); + + menu.element.css('opacity', 0) + .slideDown(menu.settings.animationSpeed) + .animate( + { opacity: 1 }, + { queue: false, duration: 'fast' } + ); + + $(document).on('mousedown', function (event) { + if (!$(event.target).closest(menu.element).length) { + closeMenu(menu); + } + }); + } + + function closeMenu(menu) { + menu.element.fadeOut(menu.settings.animationSpeed, function () { + menu.open = false; + }); + } + + function updatePos(menu) { + // position the div, according to it's parent using the worlds most hacky thing ever + var offset = $("#" + menu.parent.attr('id')).offset(); + var left = offset.left; + var top = offset.top + menu.parent.outerHeight(); + + // If the menu is greater than 75% of the screen size, it should scroll + menu.element.height('auto'); // so the height calculation works correctly + var menuHeight = menu.element.outerHeight(); + var windowHeight = $(window).height(); + if (menuHeight > windowHeight * 0.75) { + menu.element.height(windowHeight * 0.75); + menuHeight = menu.element.outerHeight(); + } + + // Offset top, if the menu would appear below the screen (with 5px margin) + var distanceFromBottom = windowHeight - menuHeight - top - 5; + if (distanceFromBottom < 0) { + // Need to adjust the menu, to make it fit the screen bounds + if (distanceFromBottom > -menuHeight / 2) { + menu.element.height(menu.element.height() + distanceFromBottom); + + // If doing overlay positioning, subtract height + if (menu.settings.position.indexOf('overlay') >= 0) { + top -= menu.parent.outerHeight(); + } + } else { + top -= menuHeight; + // If NOT doing overlay positioning, subtract height + if (menu.settings.position.indexOf('overlay') == -1) { + top -= menu.parent.outerHeight(); + } + } + } + + // Calculate width so we can ensure the menu is not displayed off of the right hand side of the screen + var menuWidth = menu.element.outerWidth() + var windowWidth = $(window).width(); + var distanceFromRight = windowWidth - menuWidth - left - 5; + if (distanceFromRight < 0) { + left -= menu.element.outerWidth() - menu.parent.outerWidth(); + } + + menu.element.css({ top: top, left: left }); + } + + function getMenuForParent(parent) { + var id = parent.attr('id'); + if (menus[id] == undefined) { + menus[id] = {}; + } + return menus[id]; + } + + function getNextId() { + return 'sm-' + id++; + } + + // Should rethink how this works, but will do for now + $(document).ready(function () { + var items = $('sm-title'); + for (var i=0; i') + .text(element.text()) + .addClass('sm-text sm-font-title') + .attr('id', getNextId()); + element.replaceWith(newElement); + } + + items = $('sm-toolbar'); + for (var i=0; i
    ') + .html(element.html()) + .addClass('sm-toolbar sm-primary-500') + .attr('id', getNextId()); + + var toolbarItems = newElement.find('sm-item'); + console.log(toolbarItems); + for (var j=0; j') + .append($('') + .text(item.text()) + .addClass('sm-icon material-icons md-24')) + .attr('id', getNextId()); + if (item.attr('left') != undefined) { + newItem.addClass('sm-left'); + } else if (item.attr('right') != undefined) { + newItem.addClass('sm-right'); + } + item.replaceWith(newItem); + } + + element.replaceWith(newElement); + } + }); +}(jQuery)); \ No newline at end of file diff --git a/public/stylesheets/material-icon.css b/public/stylesheets/material-icon.css new file mode 100644 index 000000000..55d2aa0f6 --- /dev/null +++ b/public/stylesheets/material-icon.css @@ -0,0 +1,23 @@ +/* fallback */ +@font-face { + font-family: 'Material Icons'; + font-style: normal; + font-weight: 400; + src: url(/fonts/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2) format('woff2'); +} + +.material-icons { + font-family: 'Material Icons'; + font-weight: normal; + font-style: normal; + font-size: 24px; + line-height: 1; + letter-spacing: normal; + text-transform: none; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + direction: ltr; + -webkit-font-feature-settings: 'liga'; + -webkit-font-smoothing: antialiased; +} diff --git a/public/stylesheets/material-menu.css b/public/stylesheets/material-menu.css new file mode 100755 index 000000000..50a85af93 --- /dev/null +++ b/public/stylesheets/material-menu.css @@ -0,0 +1,108 @@ +.sm-rotate-90 { + -webkit-transform: rotate(90deg); + -moz-transform: rotate(90deg); + -o-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg); +} + +.sm-right { + float: right; +} + +/* Material menu */ +.material-menu { + z-index: 1000; + position: absolute; + overflow-y: auto; + max-width: 320px; + + font-family: 'Roboto', sans-serif; + font-size: 15px; + color: rgba(0, 0, 0, 0.87); + + padding: 16px 0; + background-color: #FFFFFF; + border-radius: 2px; + box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); + + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -o-user-select: none; + user-select: none; +} + +.material-menu ul { + list-style-type: none; + margin: 0; + padding: 0; +} + +.material-menu li { + padding: 0 24px; + min-height: 32px; + line-height: 32px; + cursor: pointer; +} + +.material-menu li * { + display: inline-block; + vertical-align: middle; +} + +.material-menu li span { + margin-left: 16px; +} + +.material-menu li.check { + padding: 0 24px; + height: 32px; + line-height: 32px; + cursor: pointer; +} + +.material-menu li.check.unchecked i { + color: transparent; +} + +.material-menu li.divider { + margin: 10px 0; + min-height: 1px; + height: 1px; + background-color: #EAEAEA; + cursor: default; +} + +.material-menu li.divider:hover { + background-color: #EAEAEA; +} + +.material-menu li.label { + font-weight: bold; + cursor: default; +} + +.material-menu li.label:hover { + background-color: inherit; +} + +.material-menu li:hover { + background-color: #EEEEEE; +} + +.material-menu .icon-wrapper { + height: inherit; + line-height: inherit; +} + +.material-icons { + color: rgba(0, 0, 0, 0.54); +} + +.material-icons.md-18 { font-size: 18px; } +.material-icons.md-24 { font-size: 24px; } +.material-icons.md-36 { font-size: 36px; } +.material-icons.md-48 { font-size: 48px; }