Webhook responses

stable
Xavier Julián 2015-01-21 15:22:46 +01:00 committed by David Barragán Merino
parent cdca5a2553
commit 7585ad3450
9 changed files with 432 additions and 73 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

@ -24,9 +24,239 @@ 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) =>
webhooklogs = webhooklogs.reverse()
for log in webhooklogs
statusText = String(log.status)
log.validStatus = statusText.length==3 and statusText[0]="2"
log.prettySentData = JSON.stringify(log.request_data.data, undefined, 2)
log.prettySentHeaders = JSON.stringify(log.request_headers, undefined, 2)
log.prettyDate = moment(log.created).format("DD MMM YYYY [at] hh:mm:ss")
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)")
else
textElement.text("(Show history)")
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")
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", () ->
$rs.webhooks.test(webhook.id).then =>
updateLogs()
$el.find(".webhooks-history").addClass("open")
updateShowHideHistoryText()
$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: i18in
message = "Webhook '#{webhook.name}'" #TODO: i18in
$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

@ -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

@ -2,19 +2,19 @@ block head
title Taiga Your agile, free, and open source project management tool
block content
div.wrapper.roles(tg-github-webhooks, ng-controller="GithubController as ctrl",
div.wrapper.roles(ng-controller="WebhooksController as ctrl",
ng-init="section='admin'")
sidebar.menu-secondary.sidebar(tg-admin-navigation="Webhooks")
include ../admin-menu
sidebar.menu-tertiary.sidebar(tg-admin-navigation="third-parties-webhooks")
include ../admin-submenu-third-parties
section.main.admin-common.admin-webhooks
section.main.admin-common.admin-webhooks(tg-new-webhook)
include ../../components/mainTitle
p.admin-subtitle Webhooks notify external services about events in Taiga, like comments, user stories....
div.webhooks-options
a.button.button-green.add-webhook(href="",title="Add a New Webhook") Add Webhook
a.button.button-green.hidden.add-webhook(href="",title="Add a New Webhook") Add Webhook
section.webhooks-table.basic-table
div.table-header
@ -23,72 +23,77 @@ block content
div.webhook-url URL
div.webhook-options
div.table-body
form.row
div.webhook-service
input(type="text", name="service-name", placeholder="Type the service name")
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="validStatus ? 'history-success' : 'history-error'")
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
input(type="text", name="service-sexret-key", placeholder="Type the service secret key")
input(type="text", name="service-payload-url", placeholder="Type the service payload url")
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.icon.icon-floppy(href="", title="Save Webhook")
a.icon.icon-delete(href="", title="Cancel Webhook")
div.single-webhook-wrapper
div.row
div.webhook-service
span Github
div.webhook-url
span http://github.kjrw3543m/nwdlkw4m535/ffm
a(href="", title="Test history") (See test history)
div.webhook-options
div.webhook-options-wrapper
a.icon.icon-floppy(href="", title="Save Webhook")
a.icon.icon-edit(href="", title="Edit Webhook")
a.icon.icon-delete(href="", title="Delete Webhook")
div.single-webhook-wrapper
div.row
div.webhook-service
span Slack
div.webhook-url
span http://slack.kjrw3543m/nwdlkw4m535/ffm
a(href="", title="Test history") (See test history)
div.webhook-options
div.webhook-options-wrapper
a.icon.icon-floppy(href="", title="Save Webhook")
a.icon.icon-edit(href="", title="Edit Webhook")
a.icon.icon-delete(href="", title="Delete Webhook")
div.webhooks-history
div.history-single
div
span.history-response.history-success
span.history-date Just now
span.icon.icon-arrow-bottom
div.history-single
div
span.history-response.history-error
span.history-date Just now
span.icon.icon-arrow-bottom
a.add-new.icon.icon-floppy(href="", title="Save Webhook")
a.cancel-new.icon.icon-delete(href="", title="Cancel Webhook")
//
form
fieldset
label(for="secret-key") Secret key
input(type="text", name="secret-key", ng-model="github.secret", placeholder="Secret key", id="secret-key")
fieldset
.select-input-text(tg-select-input-text)
div
label(for="payload-url") Payload URL
.field-with-option
input(type="text", ng-model="github.webhooks_url", name="payload-url", readonly="readonly", placeholder="Payload URL", id="payload-url")
.option-wrapper.select-input-content
.icon.icon-copy
.help-copy Copy to clipboard: Ctrl+C
button(type="submit", class="hidden")
a.button.button-green.submit-button(href="", title="Save") Save
a.help-button(href="https://taiga.io/support/github-integration/", target="_blank")
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

@ -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

@ -78,32 +78,53 @@
.webhook-url-inputs {
display: flex;
flex-direction: row;
input {
flex-basis: 1;
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;
border-bottom: 1px solid $whitish;
cursor: pointer;
display: flex;
justify-content: space-between;
margin-left: 22%;
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 {
.history-response-icon {
background: $gray;
border-radius: 25%;
display: inline-block;
height: .8rem;
margin-right: .5rem;
vertical-align: middle;
width: .8rem;
&.history-success {
background: $fresh-taiga;
@ -112,4 +133,64 @@
background: $red;
}
}
.history-single-response {
@include slide(1000px, hidden, $min: 0);
&.open {
margin-top: 1rem;
}
}
.history-single-request-header,
.history-single-response-header {
display: flex;
justify-content: space-between;
padding: .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;
}
}
}