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; }