From a0d93abff4d28a0de5ff940fc8ab137f770df882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xavier=20Juli=C3=A1n?= Date: Wed, 21 Jan 2015 13:31:48 +0100 Subject: [PATCH 1/8] Webhooks structure --- app/coffee/app.coffee | 2 + app/coffee/modules/base.coffee | 1 + app/partials/includes/modules/admin-menu.jade | 4 +- .../modules/admin-submenu-third-parties.jade | 6 +- .../admin/admin-third-parties-webhooks.jade | 71 +++++++++++++++++++ app/styles/modules/admin/admin-menu.scss | 7 +- .../admin/admin-third-parties-webhooks.scss | 34 +++++++++ app/styles/modules/admin/third-parties.scss | 2 +- main-sass.js | 1 + 9 files changed, 123 insertions(+), 5 deletions(-) create mode 100644 app/partials/views/modules/admin/admin-third-parties-webhooks.jade create mode 100644 app/styles/modules/admin/admin-third-parties-webhooks.scss diff --git a/app/coffee/app.coffee b/app/coffee/app.coffee index 2349b3a7..e7f49f72 100644 --- a/app/coffee/app.coffee +++ b/app/coffee/app.coffee @@ -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", diff --git a/app/coffee/modules/base.coffee b/app/coffee/modules/base.coffee index 5a98d548..3e44ac58 100644 --- a/app/coffee/modules/base.coffee +++ b/app/coffee/modules/base.coffee @@ -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" diff --git a/app/partials/includes/modules/admin-menu.jade b/app/partials/includes/modules/admin-menu.jade index 44f3d6d2..e14c5849 100644 --- a/app/partials/includes/modules/admin-menu.jade +++ b/app/partials/includes/modules/admin-menu.jade @@ -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") diff --git a/app/partials/includes/modules/admin-submenu-third-parties.jade b/app/partials/includes/modules/admin-submenu-third-parties.jade index f491ebb1..2e30e607 100644 --- a/app/partials/includes/modules/admin-submenu-third-parties.jade +++ b/app/partials/includes/modules/admin-submenu-third-parties.jade @@ -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 diff --git a/app/partials/views/modules/admin/admin-third-parties-webhooks.jade b/app/partials/views/modules/admin/admin-third-parties-webhooks.jade new file mode 100644 index 00000000..e70dbcb8 --- /dev/null +++ b/app/partials/views/modules/admin/admin-third-parties-webhooks.jade @@ -0,0 +1,71 @@ +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", + 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 + 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 + + section.webhooks-table.basic-table + div.table-header + div.row + div.webhook-service + span Name + div.webhook-url + span URL + div.webhook-options + div.table-body + form.row + div.webhook-service + input(type="text", name="service-name", placeholder="Type the service 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") + div.webhook-options + a.icon.icon-floppy(href="", title="Save Webhook") + a.icon.icon-delete(href="", title="Cancel Webhook") + div.row + div.webhook-service + span Slack + div.webhook-url + span http://slack.kjrw3543m/nwdlkw4m535/ffm + a(href="", title="Test history") Test history + div.webhook-options + a.icon.icon-floppy(href="", title="Save Webhook") + a.icon.icon-edit(href="", title="Edit Webhook") + a.icon.icon-cancel(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") + span.icon.icon-help + span Do you need help? Check out our support page! diff --git a/app/styles/modules/admin/admin-menu.scss b/app/styles/modules/admin/admin-menu.scss index ff92c3b4..99b82728 100644 --- a/app/styles/modules/admin/admin-menu.scss +++ b/app/styles/modules/admin/admin-menu.scss @@ -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; diff --git a/app/styles/modules/admin/admin-third-parties-webhooks.scss b/app/styles/modules/admin/admin-third-parties-webhooks.scss new file mode 100644 index 00000000..f7928beb --- /dev/null +++ b/app/styles/modules/admin/admin-third-parties-webhooks.scss @@ -0,0 +1,34 @@ +.admin-webhooks { + .webhooks-options { + margin-bottom: 1rem; + text-align: right; + } + + .webhook-service, + .webhook-url { + margin-right: .5rem; + } + .webhook-service { + flex-basis: 200px; + flex-grow: 0; + min-width: 200px; + } + .webhook-url { + flex-basis: 400px; + flex-grow: 8; + } + .webhook-options { + flex-basis: 100px; + flex-grow: 0; + min-width: 100px; + text-align: center; + } + .webhook-url-inputs { + display: flex; + flex-direction: row; + input { + flex-basis: 1; + margin-right: .3rem; + } + } +} diff --git a/app/styles/modules/admin/third-parties.scss b/app/styles/modules/admin/third-parties.scss index 0c3608c7..92331d11 100644 --- a/app/styles/modules/admin/third-parties.scss +++ b/app/styles/modules/admin/third-parties.scss @@ -19,7 +19,7 @@ textarea { height: 10rem; } - .button-green { + .submit-button { color: $white; display: block; text-align: center; diff --git a/main-sass.js b/main-sass.js index 40f1cbce..1f266882 100644 --- a/main-sass.js +++ b/main-sass.js @@ -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 From 8c25f0bad1f5226c6970ed5f560b7e80537e5121 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xavier=20Juli=C3=A1n?= Date: Wed, 21 Jan 2015 14:16:39 +0100 Subject: [PATCH 2/8] webhooks form and single view --- .../admin/admin-third-parties-webhooks.jade | 15 +++--- .../admin/admin-third-parties-webhooks.scss | 50 +++++++++++++++++++ 2 files changed, 57 insertions(+), 8 deletions(-) diff --git a/app/partials/views/modules/admin/admin-third-parties-webhooks.jade b/app/partials/views/modules/admin/admin-third-parties-webhooks.jade index e70dbcb8..c9596251 100644 --- a/app/partials/views/modules/admin/admin-third-parties-webhooks.jade +++ b/app/partials/views/modules/admin/admin-third-parties-webhooks.jade @@ -19,10 +19,8 @@ block content section.webhooks-table.basic-table div.table-header div.row - div.webhook-service - span Name - div.webhook-url - span URL + div.webhook-service Name + div.webhook-url URL div.webhook-options div.table-body form.row @@ -40,11 +38,12 @@ block content span Slack div.webhook-url span http://slack.kjrw3543m/nwdlkw4m535/ffm - a(href="", title="Test history") Test history + a(href="", title="Test history") (See test history) div.webhook-options - a.icon.icon-floppy(href="", title="Save Webhook") - a.icon.icon-edit(href="", title="Edit Webhook") - a.icon.icon-cancel(href="", title="Cancel Webhook") + 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") // form diff --git a/app/styles/modules/admin/admin-third-parties-webhooks.scss b/app/styles/modules/admin/admin-third-parties-webhooks.scss index f7928beb..caa9de38 100644 --- a/app/styles/modules/admin/admin-third-parties-webhooks.scss +++ b/app/styles/modules/admin/admin-third-parties-webhooks.scss @@ -1,4 +1,26 @@ .admin-webhooks { + .webhooks-table { + .row { + 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; + } + } + .webhooks-options { margin-bottom: 1rem; text-align: right; @@ -16,12 +38,40 @@ .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; From cdca5a2553d78e990abe2a1e75f12a2740bce408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xavier=20Juli=C3=A1n?= Date: Wed, 21 Jan 2015 14:41:24 +0100 Subject: [PATCH 3/8] Single history item --- .../admin/admin-third-parties-webhooks.jade | 46 ++++++++++++++----- .../admin/admin-third-parties-webhooks.scss | 37 +++++++++++++-- 2 files changed, 69 insertions(+), 14 deletions(-) diff --git a/app/partials/views/modules/admin/admin-third-parties-webhooks.jade b/app/partials/views/modules/admin/admin-third-parties-webhooks.jade index c9596251..8a97ba98 100644 --- a/app/partials/views/modules/admin/admin-third-parties-webhooks.jade +++ b/app/partials/views/modules/admin/admin-third-parties-webhooks.jade @@ -33,17 +33,41 @@ block content div.webhook-options a.icon.icon-floppy(href="", title="Save Webhook") a.icon.icon-delete(href="", title="Cancel Webhook") - 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.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 // form diff --git a/app/styles/modules/admin/admin-third-parties-webhooks.scss b/app/styles/modules/admin/admin-third-parties-webhooks.scss index caa9de38..aaac4f1a 100644 --- a/app/styles/modules/admin/admin-third-parties-webhooks.scss +++ b/app/styles/modules/admin/admin-third-parties-webhooks.scss @@ -1,6 +1,7 @@ .admin-webhooks { .webhooks-table { .row { + border-bottom: 0; padding: .5rem 0; } .row:hover { @@ -20,7 +21,9 @@ color: $gray; } } - + .single-webhook-wrapper { + border-bottom: 1px solid $whitish; + } .webhooks-options { margin-bottom: 1rem; text-align: right; @@ -31,9 +34,8 @@ margin-right: .5rem; } .webhook-service { - flex-basis: 200px; + flex-basis: 20%; flex-grow: 0; - min-width: 200px; } .webhook-url { flex-basis: 400px; @@ -81,4 +83,33 @@ margin-right: .3rem; } } + .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-response { + background: $gray; + border-radius: 25%; + display: inline-block; + height: .8rem; + margin-right: .5rem; + vertical-align: middle; + width: .8rem; + &.history-success { + background: $fresh-taiga; + } + &.history-error { + background: $red; + } + } } From 7585ad34508558f2744321f590763e39b7e38884 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xavier=20Juli=C3=A1n?= Date: Wed, 21 Jan 2015 15:22:46 +0100 Subject: [PATCH 4/8] Webhook responses --- CHANGELOG.md | 2 + app/coffee/modules/admin/third-parties.coffee | 230 ++++++++++++++++++ app/coffee/modules/resources.coffee | 6 + .../modules/resources/webhooklogs.coffee | 17 ++ app/coffee/modules/resources/webhooks.coffee | 17 ++ .../admin/admin-third-parties-webhooks.jade | 137 ++++++----- app/styles/dependencies/helpers.scss | 1 + app/styles/dependencies/mixins.scss | 2 +- .../admin/admin-third-parties-webhooks.scss | 93 ++++++- 9 files changed, 432 insertions(+), 73 deletions(-) create mode 100644 app/coffee/modules/resources/webhooklogs.coffee create mode 100644 app/coffee/modules/resources/webhooks.coffee diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b87d34f..507c9ba7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/app/coffee/modules/admin/third-parties.coffee b/app/coffee/modules/admin/third-parties.coffee index fc7abc4c..96eb6ff1 100644 --- a/app/coffee/modules/admin/third-parties.coffee +++ b/app/coffee/modules/admin/third-parties.coffee @@ -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 diff --git a/app/coffee/modules/resources.coffee b/app/coffee/modules/resources.coffee index ac877edf..9f067e78 100644 --- a/app/coffee/modules/resources.coffee +++ b/app/coffee/modules/resources.coffee @@ -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 ]) diff --git a/app/coffee/modules/resources/webhooklogs.coffee b/app/coffee/modules/resources/webhooklogs.coffee new file mode 100644 index 00000000..02d9eced --- /dev/null +++ b/app/coffee/modules/resources/webhooklogs.coffee @@ -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]) diff --git a/app/coffee/modules/resources/webhooks.coffee b/app/coffee/modules/resources/webhooks.coffee new file mode 100644 index 00000000..06907c32 --- /dev/null +++ b/app/coffee/modules/resources/webhooks.coffee @@ -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]) diff --git a/app/partials/views/modules/admin/admin-third-parties-webhooks.jade b/app/partials/views/modules/admin/admin-third-parties-webhooks.jade index 8a97ba98..9297fca6 100644 --- a/app/partials/views/modules/admin/admin-third-parties-webhooks.jade +++ b/app/partials/views/modules/admin/admin-third-parties-webhooks.jade @@ -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! diff --git a/app/styles/dependencies/helpers.scss b/app/styles/dependencies/helpers.scss index a17e8dff..54a5114d 100644 --- a/app/styles/dependencies/helpers.scss +++ b/app/styles/dependencies/helpers.scss @@ -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); diff --git a/app/styles/dependencies/mixins.scss b/app/styles/dependencies/mixins.scss index 81e547d2..1b6bf72f 100644 --- a/app/styles/dependencies/mixins.scss +++ b/app/styles/dependencies/mixins.scss @@ -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; diff --git a/app/styles/modules/admin/admin-third-parties-webhooks.scss b/app/styles/modules/admin/admin-third-parties-webhooks.scss index aaac4f1a..96ca1ed1 100644 --- a/app/styles/modules/admin/admin-third-parties-webhooks.scss +++ b/app/styles/modules/admin/admin-third-parties-webhooks.scss @@ -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; + } + } } From b61dc2155dec7da6b5fd5c37e4b4ac718c5243fb Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Mon, 26 Jan 2015 12:03:42 +0100 Subject: [PATCH 5/8] Updating includes --- .../modules => }/admin/admin-third-parties-webhooks.jade | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename app/partials/{views/modules => }/admin/admin-third-parties-webhooks.jade (97%) diff --git a/app/partials/views/modules/admin/admin-third-parties-webhooks.jade b/app/partials/admin/admin-third-parties-webhooks.jade similarity index 97% rename from app/partials/views/modules/admin/admin-third-parties-webhooks.jade rename to app/partials/admin/admin-third-parties-webhooks.jade index 9297fca6..a6a7794d 100644 --- a/app/partials/views/modules/admin/admin-third-parties-webhooks.jade +++ b/app/partials/admin/admin-third-parties-webhooks.jade @@ -5,12 +5,12 @@ block content div.wrapper.roles(ng-controller="WebhooksController as ctrl", ng-init="section='admin'") sidebar.menu-secondary.sidebar(tg-admin-navigation="Webhooks") - include ../admin-menu + include ../includes/modules/admin-menu sidebar.menu-tertiary.sidebar(tg-admin-navigation="third-parties-webhooks") - include ../admin-submenu-third-parties + include ../includes/modules/admin-submenu-third-parties section.main.admin-common.admin-webhooks(tg-new-webhook) - include ../../components/mainTitle + include ../includes/components/mainTitle p.admin-subtitle Webhooks notify external services about events in Taiga, like comments, user stories.... div.webhooks-options From 379b21b407c59f9ec422649a964ae6e3ce0db4b2 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Mon, 26 Jan 2015 12:15:39 +0100 Subject: [PATCH 6/8] Fixing show history when executing test --- app/coffee/modules/admin/third-parties.coffee | 7 +++++-- app/partials/admin/admin-third-parties-webhooks.jade | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/coffee/modules/admin/third-parties.coffee b/app/coffee/modules/admin/third-parties.coffee index 96eb6ff1..5e88b3fb 100644 --- a/app/coffee/modules/admin/third-parties.coffee +++ b/app/coffee/modules/admin/third-parties.coffee @@ -94,6 +94,7 @@ WebhookDirective = ($rs, $repo, $confirm, $loading) -> log.prettySentHeaders = JSON.stringify(log.request_headers, undefined, 2) log.prettyDate = moment(log.created).format("DD MMM YYYY [at] hh:mm:ss") + webhook.logs_counter = webhooklogs.length webhook.logs = webhooklogs updateShowHideHistoryText() @@ -113,6 +114,9 @@ WebhookDirective = ($rs, $repo, $confirm, $loading) -> $el.find(".visualization-mode").addClass("hidden") $el.find(".edition-mode").removeClass("hidden") + openHistory = () -> + $el.find(".webhooks-history").addClass("open") + cancel = () -> showVisualizationMode() $scope.$apply -> @@ -132,10 +136,9 @@ WebhookDirective = ($rs, $repo, $confirm, $loading) -> form.setErrors(data) $el.on "click", ".test-webhook", () -> + openHistory() $rs.webhooks.test(webhook.id).then => updateLogs() - $el.find(".webhooks-history").addClass("open") - updateShowHideHistoryText() $el.on "click", ".edit-webhook", () -> showEditMode() diff --git a/app/partials/admin/admin-third-parties-webhooks.jade b/app/partials/admin/admin-third-parties-webhooks.jade index a6a7794d..6b8810de 100644 --- a/app/partials/admin/admin-third-parties-webhooks.jade +++ b/app/partials/admin/admin-third-parties-webhooks.jade @@ -44,7 +44,7 @@ block content 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) + a.show-history.toggle-history(href="", title="Toggle history", ng-show="webhook.logs_counter") (Show history) div.webhook-options div.webhook-options-wrapper From be8476584e37a5ad66d38ef1b17d31b32f6d668e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Mon, 26 Jan 2015 17:46:10 +0100 Subject: [PATCH 7/8] Some fixes on webhooks interface --- app/coffee/modules/admin/third-parties.coffee | 18 ++++++++---------- .../admin/admin-third-parties-webhooks.jade | 2 +- .../admin/admin-third-parties-webhooks.scss | 5 +---- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/app/coffee/modules/admin/third-parties.coffee b/app/coffee/modules/admin/third-parties.coffee index 5e88b3fb..3e611b46 100644 --- a/app/coffee/modules/admin/third-parties.coffee +++ b/app/coffee/modules/admin/third-parties.coffee @@ -86,13 +86,11 @@ WebhookDirective = ($rs, $repo, $confirm, $loading) -> 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" + 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.prettySentHeaders = JSON.stringify(log.request_headers, undefined, 2) - log.prettyDate = moment(log.created).format("DD MMM YYYY [at] hh:mm:ss") + log.prettyDate = moment(log.created).format("DD MMM YYYY [at] hh:mm:ss") # TODO: i18n webhook.logs_counter = webhooklogs.length webhook.logs = webhooklogs @@ -102,9 +100,9 @@ WebhookDirective = ($rs, $repo, $confirm, $loading) -> textElement = $el.find(".toggle-history") historyElement = textElement.parents(".single-webhook-wrapper").find(".webhooks-history") if historyElement.hasClass("open") - textElement.text("(Hide history)") + textElement.text("(Hide history)") # TODO: i18n else - textElement.text("(Show history)") + textElement.text("(Show history)") # TODO: i18n showVisualizationMode = () -> $el.find(".edition-mode").addClass("hidden") @@ -160,8 +158,8 @@ WebhookDirective = ($rs, $repo, $confirm, $loading) -> cancel(target) $el.on "click", ".delete-webhook", () -> - title = "Delete webhook" #TODO: i18in - message = "Webhook '#{webhook.name}'" #TODO: i18in + title = "Delete webhook" #TODO: i18n + message = "Webhook '#{webhook.name}'" #TODO: i18n $confirm.askOnDelete(title, message).then (finish) => onSucces = -> diff --git a/app/partials/admin/admin-third-parties-webhooks.jade b/app/partials/admin/admin-third-parties-webhooks.jade index 6b8810de..e56468a5 100644 --- a/app/partials/admin/admin-third-parties-webhooks.jade +++ b/app/partials/admin/admin-third-parties-webhooks.jade @@ -56,7 +56,7 @@ block content 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-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 diff --git a/app/styles/modules/admin/admin-third-parties-webhooks.scss b/app/styles/modules/admin/admin-third-parties-webhooks.scss index 96ca1ed1..e8638bb2 100644 --- a/app/styles/modules/admin/admin-third-parties-webhooks.scss +++ b/app/styles/modules/admin/admin-third-parties-webhooks.scss @@ -135,15 +135,12 @@ } .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; + padding: 1.5rem 0 .5rem 0; span:first-child { @extend %bold; color: $gray-light; From 21ed451832308ee8a7954564a169211301e1e1c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Mon, 26 Jan 2015 17:52:28 +0100 Subject: [PATCH 8/8] Remove debugger --- app/coffee/modules/common/loading.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/app/coffee/modules/common/loading.coffee b/app/coffee/modules/common/loading.coffee index b8f5ad7a..916fa044 100644 --- a/app/coffee/modules/common/loading.coffee +++ b/app/coffee/modules/common/loading.coffee @@ -27,7 +27,6 @@ class TgLoadingService extends taiga.Service target.data('loading-old-content', target.html()) target.addClass('loading') target.html("loading...") - debugger finish: (target) -> if target.hasClass('loading')