mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-09-10 07:05:57 +08:00
Merge pull request #3463 from biosistemika/features/webhooks
Features/webhooks
This commit is contained in:
commit
f24207588c
40 changed files with 1275 additions and 4 deletions
|
@ -6,6 +6,7 @@ AllCops:
|
|||
Exclude:
|
||||
- "vendor/**/*"
|
||||
- "db/schema.rb"
|
||||
- "spec/**/*"
|
||||
NewCops: enable
|
||||
UseCache: false
|
||||
TargetRubyVersion: 2.6
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* global animateSpinner globalActivities */
|
||||
/* global animateSpinner globalActivities HelperModule */
|
||||
|
||||
'use strict';
|
||||
|
||||
|
@ -59,6 +59,37 @@
|
|||
});
|
||||
}
|
||||
|
||||
function validateActivityFilterName() {
|
||||
let filterName = $('#saveFilterModal .activity-filter-name-input').val();
|
||||
$('#saveFilterModal .btn-confirm').prop('disabled', filterName.length === 0);
|
||||
}
|
||||
|
||||
$('#saveFilterModal')
|
||||
.on('keyup', '.activity-filter-name-input', function() {
|
||||
validateActivityFilterName();
|
||||
})
|
||||
.on('click', '.btn-confirm', function() {
|
||||
$.ajax({
|
||||
url: this.dataset.saveFilterUrl,
|
||||
type: 'POST',
|
||||
global: false,
|
||||
dataType: 'json',
|
||||
data: {
|
||||
name: $('#saveFilterModal .activity-filter-name-input').val(),
|
||||
filter: globalActivities.getFilters()
|
||||
},
|
||||
success: function(data) {
|
||||
HelperModule.flashAlertMsg(data.message, 'success');
|
||||
$('#saveFilterModal .activity-filter-name-input').val('');
|
||||
validateActivityFilterName();
|
||||
$('#saveFilterModal').modal('hide');
|
||||
},
|
||||
error: function(response) {
|
||||
HelperModule.flashAlertMsg(response.responseJSON.errors.join(','), 'danger');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
initExpandCollapseAllButtons();
|
||||
initShowMoreButton();
|
||||
}());
|
||||
|
|
70
app/assets/javascripts/users/settings/webhooks/index.js
Normal file
70
app/assets/javascripts/users/settings/webhooks/index.js
Normal file
|
@ -0,0 +1,70 @@
|
|||
/* global dropdownSelector */
|
||||
|
||||
(function() {
|
||||
function initDeleteFilterModal() {
|
||||
$('.activity-filters-list').on('click', '.delete-filter', function() {
|
||||
$('#deleteFilterModal').find('.description b').text(this.dataset.name);
|
||||
$('#deleteFilterModal').find('#filter_id').val(this.dataset.id);
|
||||
$('#deleteFilterModal').modal('show');
|
||||
});
|
||||
}
|
||||
|
||||
function initFilterInfoDropdown() {
|
||||
$('.info-container').on('show.bs.dropdown', function() {
|
||||
var tagsList = $(this).find('.tags-list');
|
||||
if (tagsList.is(':empty')) {
|
||||
$.get(this.dataset.url, function(data) {
|
||||
$.each(data.filter_elements, function(i, element) {
|
||||
let tag = $('<span class="filter-info-tag"></span>');
|
||||
tag.text(element);
|
||||
tagsList.append(tag);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
$('.activity-filters-list').on('ajax:error', '.webhook-form', function(e, data) {
|
||||
$(this).renderFormErrors('webhook', data.responseJSON.errors);
|
||||
});
|
||||
|
||||
$('.activity-filters-list').on('click', '.create-webhook', function() {
|
||||
let filterElement = $(this).closest('.filter-element');
|
||||
filterElement.find('.webhooks-list').collapse('show');
|
||||
filterElement.find('.create-webhook-container').removeClass('hidden');
|
||||
});
|
||||
|
||||
$('.activity-filters-list').on('click', '.create-webhook-container .cancel-action', function(e) {
|
||||
let webhookContainer = $(this).closest('.create-webhook-container');
|
||||
e.preventDefault();
|
||||
webhookContainer.addClass('hidden');
|
||||
webhookContainer.find('.url-input').val('');
|
||||
});
|
||||
|
||||
$('.activity-filters-list').on('click', '.edit-webhook', function(e) {
|
||||
let webhookContainer = $(this).closest('.webhook');
|
||||
e.preventDefault();
|
||||
webhookContainer.find('.view-mode').addClass('hidden');
|
||||
webhookContainer.find('.edit-webhook-container').removeClass('hidden');
|
||||
});
|
||||
|
||||
$('.activity-filters-list').on('click', '.edit-webhook-container .cancel-action', function(e) {
|
||||
let webhookContainer = $(this).closest('.webhook');
|
||||
e.preventDefault();
|
||||
webhookContainer.find('.view-mode').removeClass('hidden');
|
||||
webhookContainer.find('.edit-webhook-container').addClass('hidden');
|
||||
});
|
||||
|
||||
|
||||
$('.webhook-method-container select').each(function() {
|
||||
dropdownSelector.init($(this), {
|
||||
singleSelect: true,
|
||||
closeOnSelect: true,
|
||||
noEmptyOption: true,
|
||||
selectAppearance: 'simple',
|
||||
disableSearch: true
|
||||
});
|
||||
});
|
||||
|
||||
initDeleteFilterModal();
|
||||
initFilterInfoDropdown();
|
||||
}());
|
146
app/assets/stylesheets/settings/webhooks.scss
Normal file
146
app/assets/stylesheets/settings/webhooks.scss
Normal file
|
@ -0,0 +1,146 @@
|
|||
// scss-lint:disable SelectorDepth NestingDepth
|
||||
|
||||
.webhooks-index {
|
||||
.webhooks-description {
|
||||
@include font-main;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.activity-filters-list {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.filter-element {
|
||||
border-left: 3px solid $color-concrete;
|
||||
list-style: none;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.filter-block {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
padding-left: 1em;
|
||||
|
||||
.fa-caret-down {
|
||||
cursor: pointer;
|
||||
margin-right: 1em;
|
||||
|
||||
&.collapsed {
|
||||
@include rotate(-90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.create-webhook {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.filter-name {
|
||||
@include font-h3;
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
.info-container {
|
||||
.dropdown-menu {
|
||||
padding: .5em;
|
||||
width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-info-title {
|
||||
@include font-small;
|
||||
padding-left: .25em;
|
||||
}
|
||||
|
||||
.tags-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.filter-info-tag {
|
||||
@include font-small;
|
||||
background: $color-concrete;
|
||||
flex-shrink: 0;
|
||||
margin: .25em;
|
||||
padding: .25em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.webhooks-list {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.webhook-form {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
||||
.form-group {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.form-text {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.webhook-method-container {
|
||||
margin: .5em;
|
||||
}
|
||||
|
||||
.url-input-container {
|
||||
margin: .5em;
|
||||
}
|
||||
}
|
||||
|
||||
.webhook {
|
||||
.view-mode {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
||||
.method {
|
||||
margin: 0 .5em;
|
||||
}
|
||||
|
||||
.webhook-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.active-webhook,
|
||||
.disabled-webhook {
|
||||
flex-basis: 110px;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
padding: 0 1em;
|
||||
text-align: right;
|
||||
|
||||
.fas {
|
||||
margin-right: .25em;
|
||||
}
|
||||
}
|
||||
|
||||
.active-webhook {
|
||||
color: $brand_success;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
@include font-button;
|
||||
|
||||
.fas {
|
||||
margin-right: .25em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.active) {
|
||||
.view-mode {
|
||||
color: $color-silver-chalice;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#deleteFilterModal {
|
||||
.delete-filter-form {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,6 +3,8 @@
|
|||
class GlobalActivitiesController < ApplicationController
|
||||
include InputSanitizeHelper
|
||||
|
||||
before_action :check_create_activity_filter_permissions, only: :save_activity_filter
|
||||
|
||||
def index
|
||||
# Preload filter format
|
||||
# {
|
||||
|
@ -106,8 +108,21 @@ class GlobalActivitiesController < ApplicationController
|
|||
render json: get_objects(Report)
|
||||
end
|
||||
|
||||
def save_activity_filter
|
||||
activity_filter = ActivityFilter.new(activity_filter_params)
|
||||
if activity_filter.save
|
||||
render json: { message: t('global_activities.index.activity_filter_saved') }
|
||||
else
|
||||
render json: { errors: activity_filter.errors.full_messages }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_create_activity_filter_permissions
|
||||
render_403 && return unless can_create_acitivity_filters?
|
||||
end
|
||||
|
||||
def get_objects(subject)
|
||||
query = subject_search_params[:query]
|
||||
teams =
|
||||
|
@ -138,6 +153,10 @@ class GlobalActivitiesController < ApplicationController
|
|||
matched.map { |pr| { value: pr[0], label: escape_input(pr[1]) } }
|
||||
end
|
||||
|
||||
def activity_filter_params
|
||||
params.permit(:name, filter: {})
|
||||
end
|
||||
|
||||
def activity_filters
|
||||
params.permit(
|
||||
:page, :starting_timestamp, :from_date, :to_date, types: [], subjects: {}, users: [], teams: []
|
||||
|
|
101
app/controllers/users/settings/webhooks_controller.rb
Normal file
101
app/controllers/users/settings/webhooks_controller.rb
Normal file
|
@ -0,0 +1,101 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Users
|
||||
module Settings
|
||||
class WebhooksController < ApplicationController
|
||||
layout 'fluid'
|
||||
|
||||
before_action :can_manage_filters
|
||||
before_action :load_filter, except: :index
|
||||
before_action :load_webhook, only: %i(update destroy)
|
||||
before_action :set_sort, except: :filter_info
|
||||
|
||||
def index
|
||||
@activity_filters = ActivityFilter.includes(:webhooks).order(name: (@current_sort == 'atoz' ? :asc : :desc))
|
||||
end
|
||||
|
||||
def destroy_filter
|
||||
@filter.destroy
|
||||
redirect_to users_settings_webhooks_path(sort: @current_sort)
|
||||
end
|
||||
|
||||
def filter_info
|
||||
render json: { filter_elements: load_filter_elements(@filter) }
|
||||
end
|
||||
|
||||
def create
|
||||
@webhook = @filter.webhooks.create(webhook_params)
|
||||
if @webhook.errors.any?
|
||||
render json: { errors: @webhook.errors.messages }, status: :unprocessable_entity
|
||||
else
|
||||
flash[:success] = t('webhooks.index.webhook_created')
|
||||
redirect_to users_settings_webhooks_path(sort: @current_sort)
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@webhook.update(webhook_params)
|
||||
if @webhook.errors.any?
|
||||
render json: { errors: @webhook.errors.messages }, status: :unprocessable_entity
|
||||
else
|
||||
flash[:success] = t('webhooks.index.webhook_updated')
|
||||
redirect_to users_settings_webhooks_path(sort: @current_sort)
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@webhook.destroy
|
||||
flash[:success] = t('webhooks.index.webhook_deleted')
|
||||
redirect_to users_settings_webhooks_path(sort: @current_sort)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def can_manage_filters
|
||||
render_403 && return unless can_create_acitivity_filters?
|
||||
end
|
||||
|
||||
def set_sort
|
||||
@current_sort = params[:sort] || 'atoz'
|
||||
end
|
||||
|
||||
def load_filter
|
||||
@filter = ActivityFilter.find_by(id: params[:filter_id])
|
||||
|
||||
render_404 && return unless @filter
|
||||
end
|
||||
|
||||
def load_webhook
|
||||
@webhook = Webhook.find_by(id: params[:id])
|
||||
|
||||
render_404 && return unless @webhook
|
||||
end
|
||||
|
||||
def webhook_params
|
||||
params.require(:webhook).permit(:http_method, :url, :active)
|
||||
end
|
||||
|
||||
def load_filter_elements(filter)
|
||||
result = []
|
||||
filters = filter.filter
|
||||
result += Team.where(id: filters['teams']).pluck(:name) if filters['teams']
|
||||
result += User.where(id: filters['users']).pluck(:full_name) if filters['users']
|
||||
|
||||
if filters['types']
|
||||
result += Activity.type_ofs.select { |_k, v| filters['types'].include?(v.to_s) }
|
||||
.map { |k, _v| I18n.t("global_activities.activity_name.#{k}") }
|
||||
end
|
||||
|
||||
if filters['to_date'] || filters['from_date']
|
||||
result.push("#{t('global_activities.index.period_label')} #{filters['from_date']} - #{filters['to_date']}")
|
||||
end
|
||||
|
||||
filters['subjects']&.each do |subject, ids|
|
||||
result += subject.constantize.where(id: ids).pluck(:name)
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -23,6 +23,11 @@ module UserSettingsHelper
|
|||
action_name.in?(%w(index new create show audits_index))
|
||||
end
|
||||
|
||||
def on_settings_webhook_page?
|
||||
controller_name.in?(%w(webhooks)) &&
|
||||
action_name.in?(%w(index))
|
||||
end
|
||||
|
||||
def on_settings_account_connected_accounts_page?
|
||||
controller_name == 'connected_accounts'
|
||||
end
|
||||
|
|
19
app/jobs/activities/dispatch_webhooks_job.rb
Normal file
19
app/jobs/activities/dispatch_webhooks_job.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Activities
|
||||
class DispatchWebhooksJob < ApplicationJob
|
||||
queue_as :high_priority
|
||||
|
||||
def perform(activity)
|
||||
webhooks =
|
||||
Webhook.active.where(
|
||||
activity_filter_id:
|
||||
Activities::ActivityFilterMatchingService.new(activity).activity_filters.select(:id)
|
||||
)
|
||||
|
||||
webhooks.each do |webhook|
|
||||
Activities::SendWebhookJob.perform_later(webhook, activity)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
13
app/jobs/activities/send_webhook_job.rb
Normal file
13
app/jobs/activities/send_webhook_job.rb
Normal file
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Activities
|
||||
class SendWebhookJob < ApplicationJob
|
||||
queue_as :webhooks
|
||||
|
||||
retry_on StandardError, attempts: 3, wait: :exponentially_longer
|
||||
|
||||
def perform(webhook, activity)
|
||||
Activities::ActivityWebhookService.new(webhook, activity).send_webhook
|
||||
end
|
||||
end
|
||||
end
|
|
@ -74,6 +74,8 @@ class Activity < ApplicationRecord
|
|||
breadcrumbs: {}
|
||||
)
|
||||
|
||||
after_create ->(activity) { Activities::DispatchWebhooksJob.perform_later(activity) }
|
||||
|
||||
def self.activity_types_list
|
||||
activity_list = type_ofs.map do |key, value|
|
||||
[
|
||||
|
|
8
app/models/activity_filter.rb
Normal file
8
app/models/activity_filter.rb
Normal file
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ActivityFilter < ApplicationRecord
|
||||
validates :name, presence: true
|
||||
validates :filter, presence: true
|
||||
|
||||
has_many :webhooks, dependent: :destroy
|
||||
end
|
20
app/models/webhook.rb
Normal file
20
app/models/webhook.rb
Normal file
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Webhook < ApplicationRecord
|
||||
enum http_method: { get: 0, post: 1, patch: 2 }
|
||||
|
||||
belongs_to :activity_filter
|
||||
validates :http_method, presence: true
|
||||
validates :url, presence: true
|
||||
validate :valid_url
|
||||
|
||||
scope :active, -> { where(active: true) }
|
||||
|
||||
private
|
||||
|
||||
def valid_url
|
||||
unless /\A#{URI::DEFAULT_PARSER.make_regexp(%w(http https))}\z/.match?(url)
|
||||
errors.add(:url, I18n.t('activerecord.errors.models.webhook.attributes.url.not_valid'))
|
||||
end
|
||||
end
|
||||
end
|
|
@ -8,5 +8,9 @@ module Organization
|
|||
can :create_teams do |_|
|
||||
true
|
||||
end
|
||||
|
||||
can :create_acitivity_filters do
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
55
app/services/activities/activity_filter_matching_service.rb
Normal file
55
app/services/activities/activity_filter_matching_service.rb
Normal file
|
@ -0,0 +1,55 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Activities
|
||||
class ActivityFilterMatchingService
|
||||
def initialize(activity)
|
||||
@activity = activity
|
||||
@activity_filters = ActivityFilter.all
|
||||
end
|
||||
|
||||
def activity_filters
|
||||
filter_date!
|
||||
filter_users!
|
||||
filter_types!
|
||||
filter_teams!
|
||||
filter_subjects!
|
||||
|
||||
@activity_filters
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def filter_date!
|
||||
@activity_filters = @activity_filters.where(
|
||||
"((filter ->> 'from_date') = '' AND (filter ->> 'to_date') = '') OR " \
|
||||
"((?)::date BETWEEN (filter ->> 'from_date')::date AND (filter ->> 'to_date')::date)",
|
||||
@activity.created_at.to_date
|
||||
)
|
||||
end
|
||||
|
||||
def filter_users!
|
||||
@activity_filters = @activity_filters.where(
|
||||
"NOT(filter ? 'users') OR filter -> 'users' @> '\"#{@activity.owner_id}\"'"
|
||||
)
|
||||
end
|
||||
|
||||
def filter_types!
|
||||
@activity_filters = @activity_filters.where(
|
||||
"NOT(filter ? 'types') OR filter -> 'types' @> '\"#{@activity.type_of_before_type_cast}\"'"
|
||||
)
|
||||
end
|
||||
|
||||
def filter_teams!
|
||||
@activity_filters = @activity_filters.where(
|
||||
"NOT(filter ? 'teams') OR filter -> 'teams' @> '\"#{@activity.team_id}\"'"
|
||||
)
|
||||
end
|
||||
|
||||
def filter_subjects!
|
||||
@activity_filters = @activity_filters.where(
|
||||
"NOT(filter ? 'subjects') OR "\
|
||||
"filter -> 'subjects' -> '#{@activity.subject_type}' @> '\"#{@activity.subject_id}\"'"
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
21
app/services/activities/activity_webhook_service.rb
Normal file
21
app/services/activities/activity_webhook_service.rb
Normal file
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Activities
|
||||
class ActivityWebhookService
|
||||
def initialize(webhook, activity)
|
||||
@webhook = webhook
|
||||
@activity = activity
|
||||
end
|
||||
|
||||
def send_webhook
|
||||
WebhookService.new(@webhook, activity_payload).send_webhook
|
||||
end
|
||||
|
||||
def activity_payload
|
||||
@activity.values.merge(
|
||||
type: @activity.type_of,
|
||||
created_at: @activity.created_at
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -76,4 +76,8 @@ class ActivitiesService
|
|||
*subjects_with_children.to_h.flatten
|
||||
)
|
||||
end
|
||||
|
||||
def self.activity_matches_filter?(user, teams, activity, activity_filter)
|
||||
load_activities(user, teams, activity_filter.filter).where(id: activity.id).any?
|
||||
end
|
||||
end
|
||||
|
|
53
app/services/webhook_service.rb
Normal file
53
app/services/webhook_service.rb
Normal file
|
@ -0,0 +1,53 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class WebhookService
|
||||
class InactiveWebhookSendException < StandardError; end
|
||||
|
||||
class RequestFailureException < StandardError; end
|
||||
|
||||
def initialize(webhook, payload)
|
||||
@webhook = webhook
|
||||
@payload = payload
|
||||
end
|
||||
|
||||
def send_webhook
|
||||
unless @webhook.active?
|
||||
raise(
|
||||
InactiveWebhookSendException,
|
||||
'Refused to send inactive webhook.'
|
||||
)
|
||||
end
|
||||
|
||||
response = HTTParty.public_send(
|
||||
@webhook.http_method,
|
||||
@webhook.url,
|
||||
{
|
||||
headers: { 'Content-Type' => 'application/json' },
|
||||
body: @payload
|
||||
}
|
||||
)
|
||||
|
||||
unless response.success?
|
||||
error_description = "#{response.code}: #{response.message}"
|
||||
log_error!(error_description)
|
||||
|
||||
raise(
|
||||
RequestFailureException,
|
||||
error_description
|
||||
)
|
||||
end
|
||||
|
||||
response
|
||||
rescue Net::ReadTimeout, Net::OpenTimeout, SocketError => e
|
||||
log_error!(e)
|
||||
raise e
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def log_error!(message)
|
||||
@webhook.update!(
|
||||
last_error: message
|
||||
)
|
||||
end
|
||||
end
|
25
app/views/global_activities/_save_filter_modal.html.erb
Normal file
25
app/views/global_activities/_save_filter_modal.html.erb
Normal file
|
@ -0,0 +1,25 @@
|
|||
<div class="modal" id="saveFilterModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="<%= t('general.close') %>"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title" id="regenerate-report-modal-label">
|
||||
<%= t('global_activities.index.save_filter_modal.title') %>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
<%= t('global_activities.index.save_filter_modal.description') %>
|
||||
</p>
|
||||
<div class="sci-input-container">
|
||||
<label><%= t('global_activities.index.save_filter_modal.filter_name_label') %></label>
|
||||
<input type="text" class="sci-input-field activity-filter-name-input" placeholder="<%= t('global_activities.index.save_filter_modal.filter_name_placeholder') %>"></input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal"><%= t('general.cancel') %></button>
|
||||
<button type="button" class="btn btn-primary btn-confirm" data-save-filter-url="<%= save_activity_filter_global_activities_path %>" disabled><%= t('general.save') %></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -17,6 +17,11 @@
|
|||
</div>
|
||||
<div class="ga-tags-container">
|
||||
<div class="ga-tags perfect-scrollbar"></div>
|
||||
<% if can_create_acitivity_filters? %>
|
||||
<div class="btn btn-light save-filter" data-toggle="modal" data-target="#saveFilterModal">
|
||||
<i class="fas fa-save"></i><%= t('global_activities.index.save_filter') %>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="btn btn-light clear-container">
|
||||
<i class="fas fa-times-circle"></i><%= t('global_activities.index.clear_filters') %>
|
||||
</div>
|
||||
|
|
|
@ -32,6 +32,8 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<%= render partial: 'save_filter_modal' %>
|
||||
|
||||
<script>
|
||||
var gaUrlQueryParams= <%= raw @filters.to_json %>
|
||||
</script>
|
||||
|
|
|
@ -32,5 +32,12 @@
|
|||
class: "sidebar-link #{'selected' if on_settings_team_page?}"
|
||||
%>
|
||||
</li>
|
||||
|
||||
<li class="sidebar-leaf" >
|
||||
<%= link_to t("users.settings.sidebar.webhooks"),
|
||||
users_settings_webhooks_path,
|
||||
class: "sidebar-link #{'selected' if on_settings_webhook_page?}"
|
||||
%>
|
||||
</li>
|
||||
</ul>
|
||||
<% end %>
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
<div class="modal" id="deleteFilterModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="<%= t('general.close') %>"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title">
|
||||
<%= t('webhooks.index.delete_filter_modal.title') %>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="description">
|
||||
<%= t('webhooks.index.delete_filter_modal.description_html') %>
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal"><%= t('general.cancel') %></button>
|
||||
<%= form_with url: destroy_filter_users_settings_webhooks_path(sort: @current_sort), class: "delete-filter-form" do |f| %>
|
||||
<%= hidden_field_tag :filter_id %>
|
||||
<%= f.button t('general.delete'), class: "btn btn-danger" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
16
app/views/users/settings/webhooks/_webhook_form.html.erb
Normal file
16
app/views/users/settings/webhooks/_webhook_form.html.erb
Normal file
|
@ -0,0 +1,16 @@
|
|||
<span class="form-text"><%= t("webhooks.index.webhook_trigger") %></span>
|
||||
<div class="webhook-method-container">
|
||||
<%= f.select :http_method, options_for_select(Webhook.http_methods.map{ |k,_v| [k.upcase, k] }, f.object.http_method) %>
|
||||
</div>
|
||||
<span class="form-text"><%= t("webhooks.index.target") %></span>
|
||||
<div class="sci-input-container url-input-container form-group">
|
||||
<%= f.text_field :url, class: "sci-input-field url-input", placeholder: t("webhooks.index.url_placeholder") %>
|
||||
</div>
|
||||
<button class="btn btn-light cancel-action">
|
||||
<i class="fas fa-times"></i>
|
||||
<%= t('general.cancel') %>
|
||||
</button>
|
||||
<%= f.button class: "btn btn-primary save-webhook" do %>
|
||||
<i class="fas fa-save"></i>
|
||||
<%= t('general.save') %>
|
||||
<% end %>
|
131
app/views/users/settings/webhooks/index.html.erb
Normal file
131
app/views/users/settings/webhooks/index.html.erb
Normal file
|
@ -0,0 +1,131 @@
|
|||
<% provide(:head_title, t('webhooks.index.title')) %>
|
||||
<% provide(:container_class, "no-second-nav-container") %>
|
||||
|
||||
<%= render partial: "users/settings/sidebar.html.erb" %>
|
||||
|
||||
<div class="content-pane flexible webhooks-index">
|
||||
<div class="content-header sticky-header">
|
||||
<div class="title-row">
|
||||
<h1>
|
||||
<%= t('webhooks.index.title') %>
|
||||
</h1>
|
||||
<div class="header-actions">
|
||||
<div class="dropdown sort-menu">
|
||||
<button class="btn btn-light dropdown-toggle" type="button" id="sortMenu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
|
||||
<span><i class="fas fa-sort-amount-down"></i></span>
|
||||
</button>
|
||||
<ul id="sortMenuDropdown" class="dropdown-menu dropdown-menu-right" aria-labelledby="sortMenu">
|
||||
<% %w(atoz ztoa).each do |sort| %>
|
||||
<li>
|
||||
<%= link_to t("general.sort.#{sort}_html"), users_settings_webhooks_path(sort: sort), class: (@current_sort == sort ? 'selected' : '') %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="webhooks-description">
|
||||
<%= t('webhooks.index.description') %>
|
||||
</div>
|
||||
<ul class="activity-filters-list">
|
||||
<% @activity_filters.each do |filter| %>
|
||||
<li class="filter-element">
|
||||
<div class="filter-block">
|
||||
<i class="fas fa-caret-down collapsed" data-toggle="collapse" href="#activityFilter<%= filter.id %>" aria-expanded="false"></i>
|
||||
<span class="filter-name" title="<%= filter.name %>">
|
||||
<%= filter.name %>
|
||||
</span>
|
||||
<div class="info-container dropdown" data-url="<%= filter_info_users_settings_webhooks_path(filter_id: filter.id) %>" >
|
||||
<div id="filter-info-<%= filter.id %>-button" class="btn btn-light show-filter icon-btn" data-toggle="dropdown">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
</div>
|
||||
<div class="dropdown-menu" aria-labelledby="filter-info-<%= filter.id %>-button" role="menu">
|
||||
<p class="filter-info-title"><%= t('webhooks.index.applied_filters') %></p>
|
||||
<div class="tags-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn btn-light create-webhook">
|
||||
<i class="fas fa-plus"></i>
|
||||
<%= t('webhooks.index.new_webhook') %>
|
||||
</div>
|
||||
<div class="btn btn-light delete-filter icon-btn" data-id="<%= filter.id %>" data-name="<%= filter.name %>">
|
||||
<i class="fas fa-trash"></i>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="webhooks-list collapse" id="activityFilter<%= filter.id %>">
|
||||
<li class="create-webhook-container hidden">
|
||||
<%= form_with model: Webhook.new, url: users_settings_webhooks_path(filter_id: filter.id, sort: @current_sort), class: 'webhook-form' do |f| %>
|
||||
<%= render partial: 'webhook_form', locals: {f: f} %>
|
||||
<% end %>
|
||||
</li>
|
||||
<% filter.webhooks.order(created_at: :desc).each do |webhook| %>
|
||||
<li class="webhook <%= 'active' if webhook.active? %>">
|
||||
<div class="view-mode">
|
||||
<span class="webhook-text">
|
||||
<%= t('webhooks.index.webhook_text_html', method: webhook.http_method.upcase, url: webhook.url) %>
|
||||
</span>
|
||||
<% if webhook.active? %>
|
||||
<span class="active-webhook">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<%= t("webhooks.index.active") %>
|
||||
</span>
|
||||
<% else %>
|
||||
<span class="disabled-webhook">
|
||||
<i class="fas fa-unlink"></i>
|
||||
<%= t("webhooks.index.disabled") %>
|
||||
</span>
|
||||
<% end %>
|
||||
<div class="dropdown webhook-menu">
|
||||
<button class="btn btn-light icon-btn dropdown-toggle" type="button" id="webhookMenuButton<%= webhook.id %>" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
|
||||
<span><i class="fas fa-ellipsis-h"></i></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="webhookMenuButton<%= webhook.id %>">
|
||||
<li>
|
||||
<a href="#" class="edit-webhook">
|
||||
<i class="fas fa-pen"></i>
|
||||
<%= t('general.edit') %>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<% if webhook.active? %>
|
||||
<%= link_to users_settings_webhook_path(webhook, filter_id: filter.id, 'webhook[active]' => false, sort: @current_sort), method: :patch do %>
|
||||
<i class="fas fa-unlink"></i>
|
||||
<%= t("webhooks.index.disable") %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= link_to users_settings_webhook_path(webhook, filter_id: filter.id, 'webhook[active]' => true, sort: @current_sort), method: :patch do %>
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<%= t("webhooks.index.enable") %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</li>
|
||||
<li>
|
||||
<%= link_to users_settings_webhook_path(webhook, filter_id: filter.id, sort: @current_sort), method: :delete, data: { confirm: t('webhooks.index.delete_webhook_confimration') } do %>
|
||||
<i class="fas fa-trash"></i>
|
||||
<%= t('general.delete') %>
|
||||
<% end %>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="edit-webhook-container hidden">
|
||||
<%= form_with model: webhook, url: users_settings_webhook_path(webhook, filter_id: filter.id, sort: @current_sort), class: 'webhook-form', method: :patch do |f| %>
|
||||
<%= render partial: 'webhook_form', locals: {f: f} %>
|
||||
<% end %>
|
||||
</div>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
|
||||
<%= render partial: 'delete_filter_modal' %>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<%= javascript_include_tag "users/settings/webhooks/index" %>
|
||||
|
|
@ -26,6 +26,7 @@ Rails.application.config.assets.precompile += %w(users/settings/teams/datatable.
|
|||
Rails.application.config.assets.precompile += %w(users/settings/teams/add_user_modal.js)
|
||||
Rails.application.config.assets.precompile += %w(users/settings/teams/show.js)
|
||||
Rails.application.config.assets.precompile += %w(users/settings/teams/invite_users_modal.js)
|
||||
Rails.application.config.assets.precompile += %w(users/settings/webhooks/index.js)
|
||||
Rails.application.config.assets.precompile += %w(my_modules/activities.js)
|
||||
Rails.application.config.assets.precompile += %w(my_modules/protocols.js)
|
||||
Rails.application.config.assets.precompile += %w(my_modules/repositories.js)
|
||||
|
|
|
@ -24,9 +24,7 @@ module DelayedWorkerConfig
|
|||
# or left in the database with "failed_at" set dempends on the
|
||||
# DESTROY_FAILED_JOBS value
|
||||
def max_attempts
|
||||
value = ENV['DELAYED_WORKER_MAX_ATTEMPTS'].to_i
|
||||
return 6 if value.zero?
|
||||
value
|
||||
1 # We want ActiveJob to handle retries
|
||||
end
|
||||
|
||||
# The default DELAYED_WORKER_MAX_RUN_TIME is 30.minutes.
|
||||
|
@ -65,5 +63,6 @@ Delayed::Worker.read_ahead = DelayedWorkerConfig.read_ahead
|
|||
Delayed::Worker.default_queue_name = DelayedWorkerConfig.default_queue_name
|
||||
Delayed::Worker.queue_attributes = {
|
||||
high_priority: { priority: -10 },
|
||||
webhooks: { priority: 5 },
|
||||
low_priority: { priority: 10 }
|
||||
}
|
||||
|
|
|
@ -146,6 +146,10 @@ en:
|
|||
attributes:
|
||||
base:
|
||||
per_column_limit: "Too many items in the column"
|
||||
webhook:
|
||||
attributes:
|
||||
url:
|
||||
not_valid: 'Not valid URL'
|
||||
|
||||
helpers:
|
||||
label:
|
||||
|
@ -1619,6 +1623,7 @@ en:
|
|||
activities_counter_label: " activities"
|
||||
expand_all: "Expand all"
|
||||
collapse_all: "Collapse all"
|
||||
|
||||
modal:
|
||||
modal_title: "Activities"
|
||||
result_type:
|
||||
|
@ -1775,6 +1780,7 @@ en:
|
|||
sidebar:
|
||||
account: "Account"
|
||||
teams: "Teams"
|
||||
webhooks: "Webhooks"
|
||||
account_nav:
|
||||
profile: "Profile"
|
||||
preferences: "Preferences"
|
||||
|
@ -2400,6 +2406,27 @@ en:
|
|||
export_request_success: "Export request received!"
|
||||
export_error: "Error when creating zip export."
|
||||
|
||||
webhooks:
|
||||
index:
|
||||
title: 'Webhooks'
|
||||
description: 'Saved activity filters are listed below as collapsible sections. You can add more saved filters on the Activities page. Each saved filter can hold several webhooks.'
|
||||
new_webhook: "New webhook"
|
||||
applied_filters: "Applied activity filters"
|
||||
delete_filter_modal:
|
||||
title: "Delete filter"
|
||||
description_html: "Are you sure you want to delete <b></b>?"
|
||||
webhook_text_html: "Trigger <b class=\"method\">%{method}</b> target <b>%{url}</b>"
|
||||
enable: "Enable"
|
||||
disable: "Disable"
|
||||
active: "Active"
|
||||
disabled: "Disabled"
|
||||
webhook_trigger: "Webhook trigger"
|
||||
target: "target"
|
||||
url_placeholder: "Enter url or other html target"
|
||||
webhook_created: "Webhook successfully created"
|
||||
webhook_updated: "Webhook successfully updated"
|
||||
webhook_deleted: "Webhook successfully deleted"
|
||||
delete_webhook_confimration: "Are you sure you want to delete the webhook?"
|
||||
# This section contains general words that can be used in any parts of
|
||||
# application.
|
||||
tiny_mce:
|
||||
|
|
|
@ -36,6 +36,13 @@ en:
|
|||
select_reports: Select Reports
|
||||
no_name: "(unnamed)"
|
||||
content_generation_error: "Failed to render activity content with id: %{activity_id}"
|
||||
activity_filter_saved: "Filter successfully saved to the Webhooks page!"
|
||||
save_filter: "Save filter"
|
||||
save_filter_modal:
|
||||
title: "Save filter"
|
||||
description: "Saved filter will be located in the Webhooks section of the Settings. It can be used to trigger webhooks."
|
||||
filter_name_label: "Filter name"
|
||||
filter_name_placeholder: "Name your filter"
|
||||
content:
|
||||
create_project_html: "%{user} created project %{project}."
|
||||
rename_project_html: "%{user} renamed project %{project}."
|
||||
|
|
|
@ -104,6 +104,18 @@ Rails.application.routes.draw do
|
|||
to: 'users/settings/user_teams#destroy',
|
||||
as: 'destroy_user_team'
|
||||
|
||||
namespace :users do
|
||||
namespace :settings do
|
||||
resources :webhooks, only: %i(index create update destroy) do
|
||||
collection do
|
||||
post :destroy_filter
|
||||
get :filter_info
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Invite users
|
||||
devise_scope :user do
|
||||
post '/invite',
|
||||
|
@ -729,6 +741,7 @@ Rails.application.routes.draw do
|
|||
get :protocol_filter
|
||||
get :team_filter
|
||||
get :user_filter
|
||||
post :save_activity_filter
|
||||
end
|
||||
end
|
||||
|
||||
|
|
12
db/migrate/20210531114633_create_activity_filters.rb
Normal file
12
db/migrate/20210531114633_create_activity_filters.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateActivityFilters < ActiveRecord::Migration[6.0]
|
||||
def change
|
||||
create_table :activity_filters do |t|
|
||||
t.string :name, null: false
|
||||
t.jsonb :filter, null: false
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
14
db/migrate/20210603152345_create_webhooks_table.rb
Normal file
14
db/migrate/20210603152345_create_webhooks_table.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateWebhooksTable < ActiveRecord::Migration[6.0]
|
||||
def change
|
||||
create_table :webhooks do |t|
|
||||
t.references :activity_filter, null: false, index: true, foreign_key: true
|
||||
t.boolean :active, null: false, default: true
|
||||
t.string :url, null: false
|
||||
t.integer :method, null: false
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddErrorInfoToWebhooksAndRenameMethodColumn < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
change_table :webhooks, bulk: true do |t|
|
||||
t.text :last_error, :text
|
||||
t.rename :method, :http_method
|
||||
end
|
||||
end
|
||||
end
|
116
db/structure.sql
116
db/structure.sql
|
@ -193,6 +193,38 @@ CREATE SEQUENCE public.activities_id_seq
|
|||
ALTER SEQUENCE public.activities_id_seq OWNED BY public.activities.id;
|
||||
|
||||
|
||||
--
|
||||
-- Name: activity_filters; Type: TABLE; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE TABLE public.activity_filters (
|
||||
id bigint NOT NULL,
|
||||
name character varying NOT NULL,
|
||||
filter jsonb NOT NULL,
|
||||
created_at timestamp(6) without time zone NOT NULL,
|
||||
updated_at timestamp(6) without time zone NOT NULL
|
||||
);
|
||||
|
||||
|
||||
--
|
||||
-- Name: activity_filters_id_seq; Type: SEQUENCE; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE SEQUENCE public.activity_filters_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
--
|
||||
-- Name: activity_filters_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER SEQUENCE public.activity_filters_id_seq OWNED BY public.activity_filters.id;
|
||||
|
||||
|
||||
--
|
||||
-- Name: ar_internal_metadata; Type: TABLE; Schema: public; Owner: -
|
||||
--
|
||||
|
@ -2758,6 +2790,42 @@ CREATE SEQUENCE public.view_states_id_seq
|
|||
ALTER SEQUENCE public.view_states_id_seq OWNED BY public.view_states.id;
|
||||
|
||||
|
||||
--
|
||||
-- Name: webhooks; Type: TABLE; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE TABLE public.webhooks (
|
||||
id bigint NOT NULL,
|
||||
activity_filter_id bigint NOT NULL,
|
||||
active boolean DEFAULT true NOT NULL,
|
||||
url character varying NOT NULL,
|
||||
http_method integer NOT NULL,
|
||||
created_at timestamp(6) without time zone NOT NULL,
|
||||
updated_at timestamp(6) without time zone NOT NULL,
|
||||
last_error text,
|
||||
text text
|
||||
);
|
||||
|
||||
|
||||
--
|
||||
-- Name: webhooks_id_seq; Type: SEQUENCE; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE SEQUENCE public.webhooks_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
--
|
||||
-- Name: webhooks_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER SEQUENCE public.webhooks_id_seq OWNED BY public.webhooks.id;
|
||||
|
||||
|
||||
--
|
||||
-- Name: wopi_actions; Type: TABLE; Schema: public; Owner: -
|
||||
--
|
||||
|
@ -2918,6 +2986,13 @@ ALTER TABLE ONLY public.active_storage_variant_records ALTER COLUMN id SET DEFAU
|
|||
ALTER TABLE ONLY public.activities ALTER COLUMN id SET DEFAULT nextval('public.activities_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- Name: activity_filters id; Type: DEFAULT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.activity_filters ALTER COLUMN id SET DEFAULT nextval('public.activity_filters_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- Name: asset_text_data id; Type: DEFAULT; Schema: public; Owner: -
|
||||
--
|
||||
|
@ -3408,6 +3483,13 @@ ALTER TABLE ONLY public.users ALTER COLUMN id SET DEFAULT nextval('public.users_
|
|||
ALTER TABLE ONLY public.view_states ALTER COLUMN id SET DEFAULT nextval('public.view_states_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- Name: webhooks id; Type: DEFAULT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.webhooks ALTER COLUMN id SET DEFAULT nextval('public.webhooks_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- Name: wopi_actions id; Type: DEFAULT; Schema: public; Owner: -
|
||||
--
|
||||
|
@ -3468,6 +3550,14 @@ ALTER TABLE ONLY public.activities
|
|||
ADD CONSTRAINT activities_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: activity_filters activity_filters_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.activity_filters
|
||||
ADD CONSTRAINT activity_filters_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: ar_internal_metadata ar_internal_metadata_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
@ -4044,6 +4134,14 @@ ALTER TABLE ONLY public.view_states
|
|||
ADD CONSTRAINT view_states_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: webhooks webhooks_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.webhooks
|
||||
ADD CONSTRAINT webhooks_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: wopi_actions wopi_actions_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
@ -5805,6 +5903,13 @@ CREATE INDEX index_view_states_on_user_id ON public.view_states USING btree (use
|
|||
CREATE INDEX index_view_states_on_viewable ON public.view_states USING btree (viewable_type, viewable_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_webhooks_on_activity_filter_id; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX index_webhooks_on_activity_filter_id ON public.webhooks USING btree (activity_filter_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_wopi_actions_on_extension_and_action; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
@ -6147,6 +6252,14 @@ ALTER TABLE ONLY public.tags
|
|||
ADD CONSTRAINT fk_rails_5f245fd6a7 FOREIGN KEY (created_by_id) REFERENCES public.users(id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: webhooks fk_rails_61458d031d; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.webhooks
|
||||
ADD CONSTRAINT fk_rails_61458d031d FOREIGN KEY (activity_filter_id) REFERENCES public.activity_filters(id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: user_my_modules fk_rails_62fa90cb51; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
@ -7257,6 +7370,9 @@ INSERT INTO "schema_migrations" (version) VALUES
|
|||
('20210407143303'),
|
||||
('20210410100006'),
|
||||
('20210506125657'),
|
||||
('20210531114633'),
|
||||
('20210603152345'),
|
||||
('20210616071836'),
|
||||
('20210622101238'),
|
||||
('20210715125349');
|
||||
|
||||
|
|
8
spec/factories/activity_filters.rb
Normal file
8
spec/factories/activity_filters.rb
Normal file
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :activity_filter do
|
||||
name { 'type filter 1' }
|
||||
filter { { 'types' => %w(0), 'from_date' => '', 'to_date' => '' } }
|
||||
end
|
||||
end
|
9
spec/factories/webhooks.rb
Normal file
9
spec/factories/webhooks.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :webhook do
|
||||
activity_filter
|
||||
http_method { 'post' }
|
||||
url { 'https://www.example.com' }
|
||||
end
|
||||
end
|
25
spec/jobs/activities/dispatch_webhooks_job_spec.rb
Normal file
25
spec/jobs/activities/dispatch_webhooks_job_spec.rb
Normal file
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe Activities::DispatchWebhooksJob do
|
||||
let!(:activity_filter_1) { create :activity_filter }
|
||||
let!(:activity_filter_2) { create :activity_filter }
|
||||
let!(:non_matching_activity_filter) do
|
||||
create(
|
||||
:activity_filter,
|
||||
filter: { 'types' => ['163'], 'from_date' => '', 'to_date' => '' }
|
||||
)
|
||||
end
|
||||
let!(:webhook_1) { create :webhook, activity_filter: activity_filter_1 }
|
||||
let!(:webhook_2) { create :webhook, activity_filter: activity_filter_2 }
|
||||
let!(:webhook_3) { create :webhook, activity_filter: non_matching_activity_filter }
|
||||
let!(:activity) { create :activity }
|
||||
|
||||
it 'enqueues webhook jobs' do
|
||||
ActiveJob::Base.queue_adapter = :test
|
||||
|
||||
expect { Activities::DispatchWebhooksJob.new(activity).perform_now }
|
||||
.to have_enqueued_job(Activities::SendWebhookJob).exactly(2).times
|
||||
end
|
||||
end
|
14
spec/jobs/activities/send_webhook_job_spec.rb
Normal file
14
spec/jobs/activities/send_webhook_job_spec.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe Activities::SendWebhookJob do
|
||||
let!(:webhook) { create :webhook }
|
||||
let!(:activity) { create :activity }
|
||||
|
||||
it 'sends the webhook' do
|
||||
stub_request(:post, webhook.url).to_return(status: 200, body: "", headers: {})
|
||||
|
||||
expect(Activities::SendWebhookJob.new(webhook, activity).perform_now.response.code).to eq("200")
|
||||
end
|
||||
end
|
|
@ -4,6 +4,9 @@ require 'rails_helper'
|
|||
|
||||
describe Activity, type: :model do
|
||||
let(:activity) { build :activity }
|
||||
let(:user) { create :user }
|
||||
let(:team) { create :team }
|
||||
|
||||
let(:old_activity) { build :activity, :old }
|
||||
|
||||
it 'should be of class Activity' do
|
||||
|
@ -61,6 +64,14 @@ describe Activity, type: :model do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.create' do
|
||||
it 'enqueues webhook dispatch job' do
|
||||
ActiveJob::Base.queue_adapter = :test
|
||||
expect { Activity.create(owner: user, team: team, type_of: "generate_pdf_report") }
|
||||
.to have_enqueued_job(Activities::DispatchWebhooksJob)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.save' do
|
||||
it 'adds user to message items' do
|
||||
activity.save
|
||||
|
|
|
@ -0,0 +1,178 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe Activities::ActivityFilterMatchingService do
|
||||
let(:user) { create :user }
|
||||
let(:user_2) { create :user }
|
||||
let(:team) { create :team, :with_members }
|
||||
let(:team_2) { create :team }
|
||||
let(:project) do
|
||||
create :project, team: team, user_projects: []
|
||||
end
|
||||
let(:project_2) do
|
||||
create :project, team: team, user_projects: []
|
||||
end
|
||||
let(:activity) { create :activity }
|
||||
|
||||
it 'matches activity filters by activity date' do
|
||||
matching_filter = ActivityFilter.create(
|
||||
name: "date filter",
|
||||
filter: {"to_date"=>"2021-1-2", "from_date"=>"2021-1-1"}
|
||||
)
|
||||
|
||||
non_matching_filter = ActivityFilter.create(
|
||||
name: "date filter",
|
||||
filter: {"to_date"=>"2021-12-2", "from_date"=>"2021-12-1"}
|
||||
)
|
||||
|
||||
activity.update_column(:created_at, Date.parse("2021-1-1").to_time)
|
||||
|
||||
matched_activity_filters = Activities::ActivityFilterMatchingService.new(activity).activity_filters
|
||||
|
||||
expect(matched_activity_filters).to include(matching_filter)
|
||||
expect(matched_activity_filters).to_not include(non_matching_filter)
|
||||
end
|
||||
|
||||
it 'matches activity filters by activity user' do
|
||||
matching_filter = ActivityFilter.create(
|
||||
name: "user filter 1",
|
||||
filter: {"users" => [user.id.to_s], "from_date" => "", "to_date" => ""}
|
||||
)
|
||||
|
||||
non_matching_filter = ActivityFilter.create(
|
||||
name: "user filter 2",
|
||||
filter: {"users" => [user_2.id.to_s], "from_date" => "", "to_date" => ""}
|
||||
)
|
||||
|
||||
activity.update_column(:owner_id, user.id)
|
||||
|
||||
matched_activity_filters = Activities::ActivityFilterMatchingService.new(activity).activity_filters
|
||||
|
||||
expect(matched_activity_filters).to include(matching_filter)
|
||||
expect(matched_activity_filters).to_not include(non_matching_filter)
|
||||
end
|
||||
|
||||
it 'matches activity filters by activity type' do
|
||||
matching_filter = ActivityFilter.create(
|
||||
name: "type filter 1",
|
||||
filter: {"types" => ["163"], "from_date" => "", "to_date" => ""}
|
||||
)
|
||||
|
||||
non_matching_filter = ActivityFilter.create(
|
||||
name: "type filter 2",
|
||||
filter: {"types" => ["0"], "from_date" => "", "to_date" => ""}
|
||||
)
|
||||
|
||||
activity.update_column(:type_of, 163)
|
||||
|
||||
matched_activity_filters = Activities::ActivityFilterMatchingService.new(activity).activity_filters
|
||||
|
||||
expect(matched_activity_filters).to include(matching_filter)
|
||||
expect(matched_activity_filters).to_not include(non_matching_filter)
|
||||
end
|
||||
|
||||
it 'matches activity filters by activity team' do
|
||||
matching_filter = ActivityFilter.create(
|
||||
name: "team filter 1",
|
||||
filter: {"teams" => [team.id.to_s], "from_date" => "", "to_date" => ""}
|
||||
)
|
||||
|
||||
non_matching_filter = ActivityFilter.create(
|
||||
name: "team filter 2",
|
||||
filter: {"teams" => [team_2.id.to_s], "from_date" => "", "to_date" => ""}
|
||||
)
|
||||
|
||||
activity.update_column(:team_id, team.id)
|
||||
|
||||
matched_activity_filters = Activities::ActivityFilterMatchingService.new(activity).activity_filters
|
||||
|
||||
expect(matched_activity_filters).to include(matching_filter)
|
||||
expect(matched_activity_filters).to_not include(non_matching_filter)
|
||||
end
|
||||
|
||||
it 'matches activity filters by activity subject' do
|
||||
matching_filter = ActivityFilter.create(
|
||||
name: "subject filter 1",
|
||||
filter: {"subjects" => { "Project" => [project.id.to_s] }, "from_date" => "", "to_date" => ""}
|
||||
)
|
||||
|
||||
non_matching_filter = ActivityFilter.create(
|
||||
name: "subject filter 2",
|
||||
filter: {"subjects" => { "Project" => [project_2.id.to_s] }, "from_date" => "", "to_date" => ""}
|
||||
)
|
||||
|
||||
activity.update_columns(subject_type: "Project", subject_id: project.id)
|
||||
|
||||
matched_activity_filters = Activities::ActivityFilterMatchingService.new(activity).activity_filters
|
||||
|
||||
expect(matched_activity_filters).to include(matching_filter)
|
||||
expect(matched_activity_filters).to_not include(non_matching_filter)
|
||||
end
|
||||
|
||||
it 'matches activity filters by a combination of filters' do
|
||||
matching_filter = ActivityFilter.create(
|
||||
name: "mixed filter 1",
|
||||
filter: {
|
||||
"subjects" => { "Project" => [project.id.to_s] },
|
||||
"to_date"=>"2021-1-2",
|
||||
"from_date"=>"2021-1-1",
|
||||
"teams"=>[team.id.to_s],
|
||||
"users"=>[user.id.to_s],
|
||||
"types"=>["163"]
|
||||
}
|
||||
)
|
||||
|
||||
activity.update_columns(
|
||||
created_at: Date.parse("2021-1-1").to_time,
|
||||
owner_id: user.id,
|
||||
type_of: 163,
|
||||
subject_type: "Project",
|
||||
subject_id: project.id,
|
||||
team_id: team.id
|
||||
)
|
||||
|
||||
non_matching_filter_1 = ActivityFilter.create(
|
||||
name: "mixed filter 1",
|
||||
filter: {
|
||||
"subjects" => { "Project" => [project.id.to_s] },
|
||||
"to_date"=>"2021-10-2",
|
||||
"from_date"=>"2021-10-1",
|
||||
"teams"=>[team.id.to_s],
|
||||
"users"=>[user.id.to_s],
|
||||
"types"=>["163"]
|
||||
}
|
||||
)
|
||||
|
||||
non_matching_filter_2 = ActivityFilter.create(
|
||||
name: "mixed filter 2",
|
||||
filter: {
|
||||
"subjects" => { "Project" => [project.id.to_s] },
|
||||
"to_date"=>"2021-1-2",
|
||||
"from_date"=>"2021-1-1",
|
||||
"teams"=>[team_2.id.to_s],
|
||||
"users"=>[user.id.to_s],
|
||||
"types"=>["163"]
|
||||
}
|
||||
)
|
||||
|
||||
non_matching_filter_3 = ActivityFilter.create(
|
||||
name: "mixed filter 3",
|
||||
filter: {
|
||||
"subjects" => { "Project" => [project_2.id.to_s] },
|
||||
"to_date"=>"2021-1-2",
|
||||
"from_date"=>"2021-1-1",
|
||||
"teams"=>[team.id.to_s],
|
||||
"users"=>[user.id.to_s],
|
||||
"types"=>["163"]
|
||||
}
|
||||
)
|
||||
|
||||
matched_activity_filters = Activities::ActivityFilterMatchingService.new(activity).activity_filters
|
||||
|
||||
expect(matched_activity_filters).to include(matching_filter)
|
||||
expect(matched_activity_filters).to_not include(non_matching_filter_1)
|
||||
expect(matched_activity_filters).to_not include(non_matching_filter_2)
|
||||
expect(matched_activity_filters).to_not include(non_matching_filter_3)
|
||||
end
|
||||
end
|
45
spec/services/webhook_service_spec.rb
Normal file
45
spec/services/webhook_service_spec.rb
Normal file
|
@ -0,0 +1,45 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe Activities::CreateActivityService do
|
||||
let(:webhook) { create :webhook }
|
||||
|
||||
context 'when webhook is valid' do
|
||||
it 'executes webhook' do
|
||||
stub_request(:post, webhook.url).to_return(status: 200, body: "", headers: {})
|
||||
|
||||
expect(WebhookService.new(webhook, { payload: "payload" }).send_webhook.response.code).to eq("200")
|
||||
end
|
||||
end
|
||||
|
||||
context 'when webhook gets non-success HTTP response' do
|
||||
it 'logs error' do
|
||||
stub_request(:post, webhook.url).to_return(status: 500, body: "", headers: {})
|
||||
|
||||
expect { WebhookService.new(webhook, { payload: "payload" }).send_webhook }.to(
|
||||
raise_error(WebhookService::RequestFailureException)
|
||||
)
|
||||
|
||||
expect(webhook.last_error).to eq("500: ")
|
||||
end
|
||||
end
|
||||
|
||||
context 'when webhook times out' do
|
||||
it 'logs error' do
|
||||
stub_request(:post, webhook.url).to_timeout
|
||||
|
||||
expect { WebhookService.new(webhook, { payload: "payload" }).send_webhook }.to raise_error(Net::OpenTimeout)
|
||||
expect(webhook.last_error).to eq("execution expired")
|
||||
end
|
||||
end
|
||||
|
||||
context 'when webhook url cannot be resolved' do
|
||||
it 'logs error' do
|
||||
stub_request(:post, webhook.url).to_raise(SocketError)
|
||||
|
||||
expect { WebhookService.new(webhook, { payload: "payload" }).send_webhook }.to raise_error(SocketError)
|
||||
expect(webhook.last_error).to eq("Exception from WebMock")
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Add table
Reference in a new issue