Merge pull request #274 from taigaio/us/1678/webhooks

Us/1678/webhooks
stable
David Barragán Merino 2015-01-26 18:08:56 +01:00
commit a880f1bcd3
17 changed files with 585 additions and 7 deletions

View File

@ -3,6 +3,8 @@
## 1.5.0 Betula Pendula - FOSDEM 2015 (unreleased)
### Features
- Taiga webhooks
+ Created admin panel with webhook settings.
- Not showing closed milestones by default in backlog view.
- In kanban view an archived user story status doesn't show his content by default.
- Now you can export and import projects between Taiga instances.

View File

@ -101,6 +101,8 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
{templateUrl: "admin/admin-memberships.html"})
$routeProvider.when("/project/:pslug/admin/roles",
{templateUrl: "admin/admin-roles.html"})
$routeProvider.when("/project/:pslug/admin/third-parties/webhooks",
{templateUrl: "admin/admin-third-parties-webhooks.html"})
$routeProvider.when("/project/:pslug/admin/third-parties/github",
{templateUrl: "admin/admin-third-parties-github.html"})
$routeProvider.when("/project/:pslug/admin/third-parties/gitlab",

View File

@ -24,9 +24,240 @@ taiga = @.taiga
mixOf = @.taiga.mixOf
bindMethods = @.taiga.bindMethods
debounce = @.taiga.debounce
timeout = @.taiga.timeout
module = angular.module("taigaAdmin")
#############################################################################
## Webhooks
#############################################################################
class WebhooksController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.FiltersMixin)
@.$inject = [
"$scope",
"$tgRepo",
"$tgResources",
"$routeParams",
"$appTitle"
]
constructor: (@scope, @repo, @rs, @params, @appTitle) ->
bindMethods(@)
@scope.sectionName = "Webhooks" #i18n
@scope.project = {}
promise = @.loadInitialData()
promise.then () =>
@appTitle.set("Webhooks - " + @scope.project.name)
promise.then null, @.onInitialDataError.bind(@)
@scope.$on "webhooks:reload", @.loadWebhooks
loadWebhooks: ->
return @rs.webhooks.list(@scope.projectId).then (webhooks) =>
@scope.webhooks = webhooks
loadProject: ->
return @rs.projects.get(@scope.projectId).then (project) =>
@scope.project = project
@scope.$emit('project:loaded', project)
return project
loadInitialData: ->
promise = @repo.resolve({pslug: @params.pslug}).then (data) =>
@scope.projectId = data.project
return data
return promise.then(=> @.loadProject())
.then(=> @.loadWebhooks())
module.controller("WebhooksController", WebhooksController)
#############################################################################
## Webhook Directive
#############################################################################
WebhookDirective = ($rs, $repo, $confirm, $loading) ->
link = ($scope, $el, $attrs) ->
webhook = $scope.$eval($attrs.tgWebhook)
updateLogs = () ->
$rs.webhooklogs.list(webhook.id).then (webhooklogs) =>
for log in webhooklogs
log.validStatus = 200 <= log.status < 300
log.prettySentHeaders = _.map(_.pairs(log.request_headers), ([header, value]) -> "#{header}: #{value}").join("\n")
log.prettySentData = JSON.stringify(log.request_data.data, undefined, 2)
log.prettyDate = moment(log.created).format("DD MMM YYYY [at] hh:mm:ss") # TODO: i18n
webhook.logs_counter = webhooklogs.length
webhook.logs = webhooklogs
updateShowHideHistoryText()
updateShowHideHistoryText = () ->
textElement = $el.find(".toggle-history")
historyElement = textElement.parents(".single-webhook-wrapper").find(".webhooks-history")
if historyElement.hasClass("open")
textElement.text("(Hide history)") # TODO: i18n
else
textElement.text("(Show history)") # TODO: i18n
showVisualizationMode = () ->
$el.find(".edition-mode").addClass("hidden")
$el.find(".visualization-mode").removeClass("hidden")
showEditMode = () ->
$el.find(".visualization-mode").addClass("hidden")
$el.find(".edition-mode").removeClass("hidden")
openHistory = () ->
$el.find(".webhooks-history").addClass("open")
cancel = () ->
showVisualizationMode()
$scope.$apply ->
webhook.revert()
save = debounce 2000, (target) ->
form = target.parents("form").checksley()
return if not form.validate()
value = target.scope().value
promise = $repo.save(webhook)
promise.then =>
showVisualizationMode()
promise.then null, (data) ->
$confirm.notify("error")
form.setErrors(data)
$el.on "click", ".test-webhook", () ->
openHistory()
$rs.webhooks.test(webhook.id).then =>
updateLogs()
$el.on "click", ".edit-webhook", () ->
showEditMode()
$el.on "click", ".cancel-existing", () ->
cancel()
$el.on "click", ".edit-existing", (event) ->
event.preventDefault()
target = angular.element(event.currentTarget)
save(target)
$el.on "keyup", ".edition-mode input", (event) ->
if event.keyCode == 13
target = angular.element(event.currentTarget)
saveWebhook(target)
else if event.keyCode == 27
target = angular.element(event.currentTarget)
cancel(target)
$el.on "click", ".delete-webhook", () ->
title = "Delete webhook" #TODO: i18n
message = "Webhook '#{webhook.name}'" #TODO: i18n
$confirm.askOnDelete(title, message).then (finish) =>
onSucces = ->
finish()
$scope.$emit("webhooks:reload")
onError = ->
finish(false)
$confirm.notify("error")
$repo.remove(webhook).then(onSucces, onError)
$el.on "click", ".toggle-history", (event) ->
target = angular.element(event.currentTarget)
if not webhook.logs? or webhook.logs.length == 0
updateLogs().then ->
#Waiting for ng-repeat to finish
timeout 0, ->
$el.find(".webhooks-history").toggleClass("open")
updateShowHideHistoryText()
else
$el.find(".webhooks-history").toggleClass("open")
$scope.$apply () ->
updateShowHideHistoryText()
$el.on "click", ".history-single", (event) ->
target = angular.element(event.currentTarget)
target.toggleClass("history-single-open")
target.siblings(".history-single-response").toggleClass("open")
$el.on "click", ".resend-request", (event) ->
target = angular.element(event.currentTarget)
log = target.data("log")
$rs.webhooklogs.resend(log).then () =>
updateLogs()
return {link:link}
module.directive("tgWebhook", ["$tgResources", "$tgRepo", "$tgConfirm", "$tgLoading", WebhookDirective])
#############################################################################
## New webhook Directive
#############################################################################
NewWebhookDirective = ($rs, $repo, $confirm, $loading) ->
link = ($scope, $el, $attrs) ->
webhook = $scope.$eval($attrs.tgWebhook)
formDOMNode = $el.find(".new-webhook-form")
addWebhookDOMNode = $el.find(".add-webhook")
initializeNewValue = ->
$scope.newValue = {
"name": ""
"url": ""
"key": ""
}
initializeNewValue()
$scope.$watch "webhooks", (webhooks) ->
if webhooks?
if webhooks.length == 0
formDOMNode.removeClass("hidden")
addWebhookDOMNode.addClass("hidden")
formDOMNode.find("input")[0].focus()
else
formDOMNode.addClass("hidden")
addWebhookDOMNode.removeClass("hidden")
formDOMNode.on "click", ".add-new", debounce 2000, (event) ->
event.preventDefault()
form = formDOMNode.checksley()
return if not form.validate()
$scope.newValue.project = $scope.project.id
promise = $repo.create("webhooks", $scope.newValue)
promise.then =>
$scope.$emit("webhooks:reload")
initializeNewValue()
promise.then null, (data) ->
$confirm.notify("error")
form.setErrors(data)
formDOMNode.on "click", ".cancel-new", (event) ->
$scope.$apply ->
initializeNewValue()
addWebhookDOMNode.on "click", (event) ->
formDOMNode.removeClass("hidden")
formDOMNode.find("input")[0].focus()
return {link:link}
module.directive("tgNewWebhook", ["$tgResources", "$tgRepo", "$tgConfirm", "$tgLoading", NewWebhookDirective])
#############################################################################
## Github Controller

View File

@ -93,6 +93,7 @@ urls = {
"project-admin-project-values-issue-severities": "/project/:project/admin/project-values/issue-severities"
"project-admin-memberships": "/project/:project/admin/memberships"
"project-admin-roles": "/project/:project/admin/roles"
"project-admin-third-parties-webhooks": "/project/:project/admin/third-parties/webhooks"
"project-admin-third-parties-github": "/project/:project/admin/third-parties/github"
"project-admin-third-parties-gitlab": "/project/:project/admin/third-parties/gitlab"
"project-admin-third-parties-bitbucket": "/project/:project/admin/third-parties/bitbucket"

View File

@ -27,7 +27,6 @@ class TgLoadingService extends taiga.Service
target.data('loading-old-content', target.html())
target.addClass('loading')
target.html("<img class='loading-spinner' src='/svg/spinner-circle.svg' alt='loading...' />")
debugger
finish: (target) ->
if target.hasClass('loading')

View File

@ -85,6 +85,10 @@ urls = {
"priorities": "/priorities"
"severities": "/severities"
"project-modules": "/projects/%s/modules"
"webhooks": "/webhooks"
"webhooks-test": "/webhooks/%s/test"
"webhooklogs": "/webhooklogs"
"webhooklogs-resend": "/webhooklogs/%s/resend"
# History
"history/us": "/history/userstory"
@ -145,5 +149,7 @@ module.run([
"$tgHistoryResourcesProvider",
"$tgKanbanResourcesProvider",
"$tgModulesResourcesProvider",
"$tgWebhooksResourcesProvider",
"$tgWebhookLogsResourcesProvider",
initResources
])

View File

@ -0,0 +1,17 @@
resourceProvider = ($repo, $urls, $http) ->
service = {}
service.list = (webhookId) ->
params = {webhook: webhookId}
return $repo.queryMany("webhooklogs", params)
service.resend = (webhooklogId) ->
url = $urls.resolve("webhooklogs-resend", webhooklogId)
return $http.post(url)
return (instance) ->
instance.webhooklogs = service
module = angular.module("taigaResources")
module.factory("$tgWebhookLogsResourcesProvider", ["$tgRepo", "$tgUrls", "$tgHttp", resourceProvider])

View File

@ -0,0 +1,17 @@
resourceProvider = ($repo, $urls, $http) ->
service = {}
service.list = (projectId) ->
params = {project: projectId}
return $repo.queryMany("webhooks", params)
service.test = (webhookId) ->
url = $urls.resolve("webhooks-test", webhookId)
return $http.post(url)
return (instance) ->
instance.webhooks = service
module = angular.module("taigaResources")
module.factory("$tgWebhooksResourcesProvider", ["$tgRepo", "$tgUrls", "$tgHttp", resourceProvider])

View File

@ -0,0 +1,99 @@
block head
title Taiga Your agile, free, and open source project management tool
block content
div.wrapper.roles(ng-controller="WebhooksController as ctrl",
ng-init="section='admin'")
sidebar.menu-secondary.sidebar(tg-admin-navigation="Webhooks")
include ../includes/modules/admin-menu
sidebar.menu-tertiary.sidebar(tg-admin-navigation="third-parties-webhooks")
include ../includes/modules/admin-submenu-third-parties
section.main.admin-common.admin-webhooks(tg-new-webhook)
include ../includes/components/mainTitle
p.admin-subtitle Webhooks notify external services about events in Taiga, like comments, user stories....
div.webhooks-options
a.button.button-green.hidden.add-webhook(href="",title="Add a New Webhook") Add Webhook
section.webhooks-table.basic-table
div.table-header
div.row
div.webhook-service Name
div.webhook-url URL
div.webhook-options
div.table-body
div.single-webhook-wrapper(tg-webhook="webhook", ng-repeat="webhook in webhooks")
div.edition-mode.hidden
form.row
fieldset.webhook-service
input(type="text", name="name", placeholder="Type the service name", data-required="true", ng-model="webhook.name")
div.webhook-url
div.webhook-url-inputs
fieldset
input(type="text", name="url", data-type="url", placeholder="Type the service payload url", data-required="true", ng-model="webhook.url")
fieldset
input(type="text", name="key", placeholder="Type the service secret key", data-required="true", ng-model="webhook.key")
div.webhook-options
a.edit-existing.icon.icon-floppy(href="", title="Save Webhook")
a.cancel-existing.icon.icon-delete(href="", title="Cancel Webhook")
div.visualization-mode
div.row
div.webhook-service
span(ng-bind="webhook.name")
div.webhook-url
span(ng-bind="webhook.url")
a.show-history.toggle-history(href="", title="Toggle history", ng-show="webhook.logs_counter") (Show history)
div.webhook-options
div.webhook-options-wrapper
a.test-webhook.icon.icon-check-square(href="", title="Test Webhook")
a.edit-webhook.icon.icon-edit(href="", title="Edit Webhook")
a.delete-webhook.icon.icon-delete(href="", title="Delete Webhook")
div.webhooks-history(ng-show="webhook.logs")
div.history-single-wrapper(ng-repeat="log in webhook.logs")
div.history-single
div
span.history-response-icon(ng-class="log.validStatus ? 'history-success' : 'history-error'", title="{{log.status}}")
span.history-date(ng-bind="log.prettyDate")
span.toggle-log.icon.icon-arrow-bottom
div.history-single-response
div.history-single-request-header
span Request
a.resend-request(href="", title="Resend request", data-log="{{log.id}}")
span.icon.icon-reload
span Resend request
div.history-single-request-body
div.response-container
span.response-title Headers
textarea(name="headers", ng-bind="log.prettySentHeaders")
div.response-container
span.response-title Payload
textarea(name="payload", ng-bind="log.prettySentData")
div.history-single-response-header
span Response
div.history-single-response-body
div.response-container
textarea(name="response-data", ng-bind="log.response_data")
form.new-webhook-form.row.hidden
fieldset.webhook-service
input(type="text", name="name", placeholder="Type the service name", data-required="true", ng-model="newValue.name")
div.webhook-url
div.webhook-url-inputs
fieldset
input(type="text", name="url", data-type="url", placeholder="Type the service payload url", data-required="true", ng-model="newValue.url")
fieldset
input(type="text", name="key", placeholder="Type the service secret key", data-required="true", ng-model="newValue.key")
div.webhook-options
a.add-new.icon.icon-floppy(href="", title="Save Webhook")
a.cancel-new.icon.icon-delete(href="", title="Cancel Webhook")
a.help-button(href="https://taiga.io/support/webhooks/", target="_blank")
span.icon.icon-help
span Do you need help? Check out our support page!

View File

@ -21,8 +21,8 @@ section.admin-menu
span.title Roles & Permissions
span.icon.icon-arrow-right
li#adminmenu-third-parties
a(href="" tg-nav="project-admin-third-parties-github:project=project.slug")
span.title Third parties
a(href="" tg-nav="project-admin-third-parties-webhooks:project=project.slug")
span.title Integrations
span.icon.icon-arrow-right
li#adminmenu-contrib(ng-show="contribPlugins.length > 0")
a(href="" tg-nav="project-admin-contrib:project=project.slug,plugin=contribPlugins[0].slug")

View File

@ -1,9 +1,13 @@
section.admin-submenu
header
h1 Third parties
h1 Services
nav
ul
li#adminmenu-third-parties-webhooks.third-parties-webhooks
a(href="", tg-nav="project-admin-third-parties-webhooks:project=project.slug")
span.title Webhooks
span.icon.icon-arrow-right
li#adminmenu-third-parties-github
a(href="", tg-nav="project-admin-third-parties-github:project=project.slug")
span.title Github

View File

@ -12,6 +12,7 @@
%text {font-family: 'opensans-regular', Arial, Helvetica, sans-serif; line-height: 1.3rem;}
%bold {font-family: 'opensans-semibold', Arial, Helvetica, sans-serif;}
%taiga {font-family: 'taiga';}
%mono {font-family: 'courier new', 'monospace';}
%lightbox {
background: rgba($white, .95);

View File

@ -20,7 +20,7 @@
@mixin slide($max, $overflow, $min: 0) {
max-height: $min;
transition: max-height .5s ease-in;
#{$overflow}: hidden;
overflow: #{$overflow};
&.open {
transition: max-height .5s ease-in;
max-height: $max;

View File

@ -11,7 +11,6 @@
a {
display: block;
padding: 1rem 0 1rem 1rem;
&.active,
&:hover {
.icon {
opacity: 1;
@ -19,6 +18,12 @@
}
}
}
.active {
.icon {
opacity: 1;
transition: opacity .3s linear;
}
}
.icon {
color: $blackish;
float: right;

View File

@ -0,0 +1,193 @@
.admin-webhooks {
.webhooks-table {
.row {
border-bottom: 0;
padding: .5rem 0;
}
.row:hover {
.webhook-options-wrapper {
opacity: 1;
transition: opacity .2s linear;
}
}
}
.table-header {
@extend %bold;
border-bottom: 1px solid $gray-light;
}
.table-body {
.webhook-service {
color: $gray;
}
}
.single-webhook-wrapper {
border-bottom: 1px solid $whitish;
}
.webhooks-options {
margin-bottom: 1rem;
text-align: right;
}
.webhook-service,
.webhook-url {
margin-right: .5rem;
}
.webhook-service {
flex-basis: 20%;
flex-grow: 0;
}
.webhook-url {
flex-basis: 400px;
flex-grow: 8;
span {
@include ellipsis($width: 65%);
color: $gray-light;
display: inline-block;
vertical-align: middle;
}
a {
color: $green-taiga;
margin-left: .5rem;
&:hover {
color: $fresh-taiga;
}
}
}
.webhook-options {
flex-basis: 100px;
flex-grow: 0;
min-width: 100px;
text-align: center;
a {
color: $gray-light;
margin-right: .5rem;
transition: color .2s linear;
vertical-align: middle;
&:hover {
color: $green-taiga;
transition: color .2s linear;
}
}
}
.webhook-options-wrapper {
opacity: 0;
transition: opacity .3s linear;
}
.webhook-url-inputs {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: center;
fieldset {
flex-grow: 1;
margin-right: .3rem;
}
}
.webhooks-history{
@include slide(1000px, hidden, $min: 0);
}
.history-single-wrapper {
border-bottom: 1px solid $whitish;
margin-left: 22%;
}
.history-single {
align-items: center;
cursor: pointer;
display: flex;
justify-content: space-between;
padding: .5rem;
transition: background .2s linear;
&:hover {
background: rgba($fresh-taiga, .1);
transition: background .2s linear;
}
&.history-single-open {
&:hover {
background: none;
}
.icon-arrow-bottom {
transform: rotate(180deg);
transition: transform .3s linear;
}
}
.icon-arrow-bottom {
transform: rotate(0);
transition: transform .3s linear;
}
}
.history-response-icon {
background: $gray;
border-radius: 25%;
display: inline-block;
height: .8rem;
margin-right: .5rem;
width: .8rem;
&.history-success {
background: $fresh-taiga;
}
&.history-error {
background: $red;
}
}
.history-single-response {
@include slide(1000px, hidden, $min: 0);
}
.history-single-request-header,
.history-single-response-header {
display: flex;
justify-content: space-between;
padding: 1.5rem 0 .5rem 0;
span:first-child {
@extend %bold;
color: $gray-light;
}
a {
@extend %small;
color: $gray-light;
&:hover {
color: $fresh-taiga;
transition: color .2s linear;
}
}
.icon {
margin-right: .3rem;
vertical-align: middle;
}
}
.history-single-request-body,
.history-single-response-body {
.response-container {
@extend %mono;
align-content: center;
align-items: center;
background: $whitish;
display: flex;
flex-direction: row;
justify-content: space-around;
margin-bottom: .5rem;
}
span {
@extend %small;
color: $gray-light;
flex-basis: 20%;
flex-grow: 1;
flex-shrink: 0;
text-align: center;
}
textarea {
@extend %mono;
border: 0;
flex-grow: 2;
min-height: 7.5rem;
}
}
.history-single-response-body {
textarea {
min-height: 10rem;
}
}
}

View File

@ -19,7 +19,7 @@
textarea {
height: 10rem;
}
.button-green {
.submit-button {
color: $white;
display: block;
text-align: center;

View File

@ -125,6 +125,7 @@ exports.files = function () {
'modules/admin/default-values',
'modules/admin/project-values',
'modules/admin/third-parties',
'modules/admin/admin-third-parties-webhooks',
'modules/admin/contrib',
//Modules user Settings