Fixes #435 - GitLab integration.
This commit is contained in:
parent
20d3c5027f
commit
97c9d541e9
42 changed files with 1116 additions and 20 deletions
|
@ -70,7 +70,7 @@ include:
|
||||||
RAILS_ENV: "test"
|
RAILS_ENV: "test"
|
||||||
script:
|
script:
|
||||||
- bundle exec rake zammad:ci:test:prepare
|
- bundle exec rake zammad:ci:test:prepare
|
||||||
- bundle exec rspec --fail-fast -t type:system
|
- bundle exec rspec --fail-fast -t type:system -t ~integration
|
||||||
|
|
||||||
# we need at least one job to store and include this template
|
# we need at least one job to store and include this template
|
||||||
# $IGNORE is not defined
|
# $IGNORE is not defined
|
||||||
|
|
|
@ -1,12 +1,29 @@
|
||||||
include:
|
include:
|
||||||
|
|
||||||
# browser-integration
|
# browser-integration
|
||||||
|
- local: '/.gitlab/ci/browser-integration/capybara_chrome.yml'
|
||||||
|
- local: '/.gitlab/ci/browser-integration/capybara_ff.yml'
|
||||||
- local: '/.gitlab/ci/browser-integration/facebook_chrome.yml'
|
- local: '/.gitlab/ci/browser-integration/facebook_chrome.yml'
|
||||||
- local: '/.gitlab/ci/browser-integration/facebook_ff.yml'
|
- local: '/.gitlab/ci/browser-integration/facebook_ff.yml'
|
||||||
- local: '/.gitlab/ci/browser-integration/idoit_chrome.yml'
|
- local: '/.gitlab/ci/browser-integration/idoit_chrome.yml'
|
||||||
- local: '/.gitlab/ci/browser-integration/otrs_chrome.yml'
|
- local: '/.gitlab/ci/browser-integration/otrs_chrome.yml'
|
||||||
- local: '/.gitlab/ci/browser-integration/zendesk_chrome.yml'
|
- local: '/.gitlab/ci/browser-integration/zendesk_chrome.yml'
|
||||||
|
|
||||||
|
.template_browser-integration_capybara: &template_browser-integration_capybara
|
||||||
|
stage: browser-integration
|
||||||
|
dependencies:
|
||||||
|
- browser:build
|
||||||
|
extends:
|
||||||
|
- .env_base
|
||||||
|
- .variables_app_restart_cmd
|
||||||
|
- .variables_es
|
||||||
|
- .services_mysql_postgresql_elasticsearch_selenium_imap
|
||||||
|
variables:
|
||||||
|
RAILS_ENV: "test"
|
||||||
|
script:
|
||||||
|
- bundle exec rake zammad:ci:test:prepare
|
||||||
|
- bundle exec rspec --fail-fast --pattern "spec/system/**/*_spec.rb" -t integration
|
||||||
|
|
||||||
.template_browser-integration: &template_browser-integration
|
.template_browser-integration: &template_browser-integration
|
||||||
stage: browser-integration
|
stage: browser-integration
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
5
.gitlab/ci/browser-integration/capybara_chrome.yml
Normal file
5
.gitlab/ci/browser-integration/capybara_chrome.yml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
integration_capybara_chrome:
|
||||||
|
extends:
|
||||||
|
- .template_browser-integration_capybara
|
||||||
|
variables:
|
||||||
|
BROWSER: "chrome"
|
5
.gitlab/ci/browser-integration/capybara_ff.yml
Normal file
5
.gitlab/ci/browser-integration/capybara_ff.yml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
integration_capybara_ff:
|
||||||
|
extends:
|
||||||
|
- .template_browser-integration_capybara
|
||||||
|
variables:
|
||||||
|
BROWSER: "firefox"
|
|
@ -274,6 +274,7 @@ RSpec/ExampleLength:
|
||||||
- 'spec/requests/integration/check_mk_spec.rb'
|
- 'spec/requests/integration/check_mk_spec.rb'
|
||||||
- 'spec/requests/integration/cti_spec.rb'
|
- 'spec/requests/integration/cti_spec.rb'
|
||||||
- 'spec/requests/integration/idoit_spec.rb'
|
- 'spec/requests/integration/idoit_spec.rb'
|
||||||
|
- 'spec/requests/integration/gitlab_spec.rb'
|
||||||
- 'spec/requests/integration/monitoring_spec.rb'
|
- 'spec/requests/integration/monitoring_spec.rb'
|
||||||
- 'spec/requests/integration/object_manager_attributes_spec.rb'
|
- 'spec/requests/integration/object_manager_attributes_spec.rb'
|
||||||
- 'spec/requests/integration/placetel_spec.rb'
|
- 'spec/requests/integration/placetel_spec.rb'
|
||||||
|
@ -554,6 +555,7 @@ RSpec/MultipleExpectations:
|
||||||
- 'spec/requests/integration/check_mk_spec.rb'
|
- 'spec/requests/integration/check_mk_spec.rb'
|
||||||
- 'spec/requests/integration/cti_spec.rb'
|
- 'spec/requests/integration/cti_spec.rb'
|
||||||
- 'spec/requests/integration/idoit_spec.rb'
|
- 'spec/requests/integration/idoit_spec.rb'
|
||||||
|
- 'spec/requests/integration/gitlab_spec.rb'
|
||||||
- 'spec/requests/integration/monitoring_spec.rb'
|
- 'spec/requests/integration/monitoring_spec.rb'
|
||||||
- 'spec/requests/integration/object_manager_attributes_spec.rb'
|
- 'spec/requests/integration/object_manager_attributes_spec.rb'
|
||||||
- 'spec/requests/integration/placetel_spec.rb'
|
- 'spec/requests/integration/placetel_spec.rb'
|
||||||
|
|
1
Gemfile
1
Gemfile
|
@ -124,6 +124,7 @@ gem 'acts_as_list'
|
||||||
|
|
||||||
# integrations
|
# integrations
|
||||||
gem 'clearbit'
|
gem 'clearbit'
|
||||||
|
gem 'graphql-client'
|
||||||
gem 'net-ldap'
|
gem 'net-ldap'
|
||||||
gem 'slack-notifier'
|
gem 'slack-notifier'
|
||||||
gem 'zendesk_api'
|
gem 'zendesk_api'
|
||||||
|
|
|
@ -218,6 +218,10 @@ GEM
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
gmail_xoauth (0.4.2)
|
gmail_xoauth (0.4.2)
|
||||||
oauth (>= 0.3.6)
|
oauth (>= 0.3.6)
|
||||||
|
graphql (1.11.6)
|
||||||
|
graphql-client (0.16.0)
|
||||||
|
activesupport (>= 3.0)
|
||||||
|
graphql (~> 1.8)
|
||||||
guard (2.15.0)
|
guard (2.15.0)
|
||||||
formatador (>= 0.2.4)
|
formatador (>= 0.2.4)
|
||||||
listen (>= 2.7, < 4.0)
|
listen (>= 2.7, < 4.0)
|
||||||
|
@ -613,6 +617,7 @@ DEPENDENCIES
|
||||||
faker
|
faker
|
||||||
github_changelog_generator
|
github_changelog_generator
|
||||||
gmail_xoauth
|
gmail_xoauth
|
||||||
|
graphql-client
|
||||||
guard
|
guard
|
||||||
guard-livereload
|
guard-livereload
|
||||||
guard-symlink
|
guard-symlink
|
||||||
|
|
|
@ -225,7 +225,7 @@
|
||||||
"license": ""
|
"license": ""
|
||||||
},
|
},
|
||||||
"gitlab-button.svg": {
|
"gitlab-button.svg": {
|
||||||
"author": "Gitlab",
|
"author": "GitLab",
|
||||||
"url": "",
|
"url": "",
|
||||||
"license": ""
|
"license": ""
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
class GitLab extends App.ControllerIntegrationBase
|
||||||
|
featureIntegration: 'gitlab_integration'
|
||||||
|
featureName: 'GitLab'
|
||||||
|
featureConfig: 'gitlab_config'
|
||||||
|
description: [
|
||||||
|
['This service allows you to connect %s with %s.', 'GitLab', 'Zammad']
|
||||||
|
]
|
||||||
|
events:
|
||||||
|
'change .js-switch input': 'switch'
|
||||||
|
|
||||||
|
render: =>
|
||||||
|
super
|
||||||
|
new Form(
|
||||||
|
el: @$('.js-form')
|
||||||
|
)
|
||||||
|
|
||||||
|
class Form extends App.Controller
|
||||||
|
events:
|
||||||
|
'submit form': 'update'
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
@render()
|
||||||
|
|
||||||
|
render: =>
|
||||||
|
config = App.Setting.get('gitlab_config')
|
||||||
|
|
||||||
|
@html App.view('integration/gitlab')(
|
||||||
|
config: config
|
||||||
|
)
|
||||||
|
|
||||||
|
update: (e) =>
|
||||||
|
e.preventDefault()
|
||||||
|
config = @formParam(e.target)
|
||||||
|
@validateAndSave(config)
|
||||||
|
|
||||||
|
validateAndSave: (config) =>
|
||||||
|
App.Ajax.request(
|
||||||
|
id: 'gitlab'
|
||||||
|
type: 'POST'
|
||||||
|
url: "#{@apiPath}/integration/gitlab/verify"
|
||||||
|
data: JSON.stringify(
|
||||||
|
api_token: config.api_token
|
||||||
|
endpoint: config.endpoint
|
||||||
|
)
|
||||||
|
success: (data, status, xhr) =>
|
||||||
|
if data.result is 'failed'
|
||||||
|
new App.ControllerErrorModal(
|
||||||
|
message: data.message
|
||||||
|
container: @el.closest('.content')
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
config.schema = data.response
|
||||||
|
App.Setting.set('gitlab_config', config)
|
||||||
|
|
||||||
|
error: (data, status) ->
|
||||||
|
|
||||||
|
return if status is 'abort'
|
||||||
|
|
||||||
|
details = data.responseJSON || {}
|
||||||
|
App.Event.trigger 'notify', {
|
||||||
|
type: 'error'
|
||||||
|
msg: App.i18n.translateContent(details.error_human || details.error || 'Unable to save!')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
class State
|
||||||
|
@current: ->
|
||||||
|
App.Setting.get('gitlab_integration')
|
||||||
|
|
||||||
|
App.Config.set(
|
||||||
|
'IntegrationGitLab'
|
||||||
|
{
|
||||||
|
name: 'GitLab'
|
||||||
|
target: '#system/integration/gitlab'
|
||||||
|
description: 'Link GitLab issues to your tickets.'
|
||||||
|
controller: GitLab
|
||||||
|
state: State
|
||||||
|
}
|
||||||
|
'NavBarIntegrations'
|
||||||
|
)
|
|
@ -0,0 +1,23 @@
|
||||||
|
class App.GitIssueLinkModal extends App.ControllerModal
|
||||||
|
buttonClose: true
|
||||||
|
buttonCancel: true
|
||||||
|
buttonSubmit: true
|
||||||
|
|
||||||
|
constructor: (params) ->
|
||||||
|
@placeholder = params.placeholder
|
||||||
|
@head = params.head
|
||||||
|
super
|
||||||
|
|
||||||
|
content: ->
|
||||||
|
$(App.view('integration/git_issue_link_modal')(
|
||||||
|
placeholder: @placeholder
|
||||||
|
))
|
||||||
|
|
||||||
|
onSubmit: (e) =>
|
||||||
|
form = @el.find('.js-result')
|
||||||
|
params = @formParam(form)
|
||||||
|
return if _.isEmpty(params.link)
|
||||||
|
|
||||||
|
@formDisable(form)
|
||||||
|
@callback(params.link, @)
|
||||||
|
|
|
@ -0,0 +1,167 @@
|
||||||
|
class App.SidebarGitIssue extends App.Controller
|
||||||
|
provider: '_need_to_be_defined_' # GitLab
|
||||||
|
urlPlaceholder: '_need_to_be_defined_' # https://git.example.com/group1/project1/-/issues/1
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
@issueLinks = []
|
||||||
|
@providerIdentifier = @provider.toLowerCase()
|
||||||
|
|
||||||
|
sidebarItem: =>
|
||||||
|
return if !@Config.get("#{@providerIdentifier}_integration")
|
||||||
|
@item = {
|
||||||
|
name: @providerIdentifier
|
||||||
|
badgeCallback: @badgeRender
|
||||||
|
sidebarHead: @provider
|
||||||
|
sidebarCallback: @showObjects
|
||||||
|
sidebarActions: [
|
||||||
|
{
|
||||||
|
title: 'Link issue'
|
||||||
|
name: 'link-issue'
|
||||||
|
callback: @linkIssue
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
@item
|
||||||
|
|
||||||
|
shown: ->
|
||||||
|
return if !@ticket
|
||||||
|
return if !@ticket.id
|
||||||
|
@showIssues()
|
||||||
|
|
||||||
|
metaBadge: =>
|
||||||
|
counter = ''
|
||||||
|
counter = @issueLinks.length
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'customer'
|
||||||
|
icon: "#{@providerIdentifier}-logo"
|
||||||
|
counterPossible: true
|
||||||
|
counter: counter
|
||||||
|
}
|
||||||
|
|
||||||
|
badgeRender: (el) =>
|
||||||
|
@badgeEl = el
|
||||||
|
@badgeRenderLocal()
|
||||||
|
|
||||||
|
badgeRenderLocal: =>
|
||||||
|
@badgeEl.html(App.view('generic/sidebar_tabs_item')(@metaBadge()))
|
||||||
|
|
||||||
|
linkIssue: =>
|
||||||
|
new App.GitIssueLinkModal(
|
||||||
|
head: @provider
|
||||||
|
placeholder: @urlPlaceholder
|
||||||
|
taskKey: @taskKey
|
||||||
|
container: @el.closest('.content')
|
||||||
|
callback: (link, ui) =>
|
||||||
|
if @ticket && @ticket.id
|
||||||
|
@saveTicketIssues = true
|
||||||
|
ui.close()
|
||||||
|
@showIssues([link])
|
||||||
|
)
|
||||||
|
|
||||||
|
showObjects: (el) =>
|
||||||
|
@el = el
|
||||||
|
|
||||||
|
# show placeholder
|
||||||
|
if @ticket && @ticket.preferences && @ticket.preferences[@providerIdentifier] && @ticket.preferences[@providerIdentifier].issue_links
|
||||||
|
@issueLinks = @ticket.preferences[@providerIdentifier].issue_links
|
||||||
|
queryParams = @queryParam()
|
||||||
|
|
||||||
|
# TODO: what is 'gitlab_issue_links'
|
||||||
|
if queryParams && queryParams.gitlab_issue_links
|
||||||
|
@issueLinks.push queryParams.gitlab_issue_links
|
||||||
|
@showIssues()
|
||||||
|
|
||||||
|
showIssues: (issueLinks) =>
|
||||||
|
if issueLinks
|
||||||
|
@issueLinks = _.uniq(@issueLinks.concat(issueLinks))
|
||||||
|
|
||||||
|
# show placeholder
|
||||||
|
if _.isEmpty(@issueLinks)
|
||||||
|
@html("<div>#{App.i18n.translateInline('No linked issues')}</div>")
|
||||||
|
return
|
||||||
|
|
||||||
|
# AJAX call to show items
|
||||||
|
@ajax(
|
||||||
|
id: "#{@providerIdentifier}-#{@taskKey}"
|
||||||
|
type: 'POST'
|
||||||
|
url: "#{@apiPath}/integration/#{@providerIdentifier}"
|
||||||
|
data: JSON.stringify(links: @issueLinks)
|
||||||
|
success: (data, status, xhr) =>
|
||||||
|
if data.response
|
||||||
|
@showList(data.response)
|
||||||
|
if @saveTicketIssues
|
||||||
|
@saveTicketIssues = false
|
||||||
|
@issueLinks = data.response.map((issue) -> issue.url)
|
||||||
|
@updateTicket(@ticket.id, @issueLinks)
|
||||||
|
return
|
||||||
|
@showError('Unable to load data...')
|
||||||
|
|
||||||
|
error: (xhr, status, error) =>
|
||||||
|
|
||||||
|
# do not close window if request is aborted
|
||||||
|
return if status is 'abort'
|
||||||
|
|
||||||
|
# show error message
|
||||||
|
@showError('Unable to load data...')
|
||||||
|
)
|
||||||
|
|
||||||
|
showList: (issues) =>
|
||||||
|
list = $(App.view('ticket_zoom/sidebar_git_issue')(
|
||||||
|
issues: issues
|
||||||
|
))
|
||||||
|
list.delegate('.js-delete', 'click', (e) =>
|
||||||
|
e.preventDefault()
|
||||||
|
issueLink = $(e.currentTarget).attr 'data-issue-id'
|
||||||
|
@delete(issueLink)
|
||||||
|
)
|
||||||
|
@html(list)
|
||||||
|
@badgeRenderLocal()
|
||||||
|
|
||||||
|
showError: (message) =>
|
||||||
|
@html App.i18n.translateInline(message)
|
||||||
|
|
||||||
|
reload: =>
|
||||||
|
@showIssues()
|
||||||
|
|
||||||
|
delete: (issueLink) =>
|
||||||
|
localLinks = []
|
||||||
|
for localLink in @issueLinks
|
||||||
|
if issueLink.toString() isnt localLink.toString()
|
||||||
|
localLinks.push localLink
|
||||||
|
@issueLinks = localLinks
|
||||||
|
if @ticket && @ticket.id
|
||||||
|
@updateTicket(@ticket.id, @issueLinks)
|
||||||
|
@showIssues()
|
||||||
|
|
||||||
|
postParams: (args) =>
|
||||||
|
return if !args.ticket
|
||||||
|
return if args.ticket.created_at
|
||||||
|
return if !@issueLinks
|
||||||
|
return if _.isEmpty(@issueLinks)
|
||||||
|
args.ticket.preferences ||= {}
|
||||||
|
args.ticket.preferences[@providerIdentifier] ||= {}
|
||||||
|
args.ticket.preferences[@providerIdentifier].issue_links = @issueLinks
|
||||||
|
|
||||||
|
updateTicket: (ticket_id, issueLinks) =>
|
||||||
|
App.Ajax.request(
|
||||||
|
id: "#{@providerIdentifier}-update-#{ticket_id}"
|
||||||
|
type: 'POST'
|
||||||
|
url: "#{@apiPath}/integration/#{@providerIdentifier}_ticket_update"
|
||||||
|
data: JSON.stringify(ticket_id: ticket_id, issue_links: issueLinks)
|
||||||
|
success: (data, status, xhr) =>
|
||||||
|
@badgeRenderLocal()
|
||||||
|
error: (xhr, status, details) =>
|
||||||
|
|
||||||
|
# do not close window if request is aborted
|
||||||
|
return if status is 'abort'
|
||||||
|
|
||||||
|
# show error message
|
||||||
|
@log 'errors', details
|
||||||
|
@notify(
|
||||||
|
type: 'error'
|
||||||
|
msg: App.i18n.translateContent(details.error_human || details.error || 'Unable to update object!')
|
||||||
|
timeout: 6000
|
||||||
|
)
|
||||||
|
)
|
|
@ -0,0 +1,6 @@
|
||||||
|
class SidebarGitLab extends App.SidebarGitIssue
|
||||||
|
provider: 'GitLab'
|
||||||
|
urlPlaceholder: 'https://git.example.com/group1/project1/-/issues/1'
|
||||||
|
|
||||||
|
App.Config.set('500-GitLab', SidebarGitLab, 'TicketCreateSidebar')
|
||||||
|
App.Config.set('500-GitLab', SidebarGitLab, 'TicketZoomSidebar')
|
|
@ -122,6 +122,12 @@ class App.Sidebar extends App.Controller
|
||||||
# remember current tab
|
# remember current tab
|
||||||
@currentTab = name
|
@currentTab = name
|
||||||
|
|
||||||
|
# get current sidebar controller
|
||||||
|
for item in @items
|
||||||
|
itemLocal = item.sidebarItem()
|
||||||
|
if itemLocal && itemLocal.name && itemLocal.name is @currentTab && item.shown
|
||||||
|
item.shown()
|
||||||
|
|
||||||
# show sidebar if not shown
|
# show sidebar if not shown
|
||||||
@showSidebar()
|
@showSidebar()
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<% if @counterPossible is true: %>
|
<% if @counterPossible is true and @counter and @counter > 0: %>
|
||||||
<div class="tabsSidebar-tab-count js-tabCounter <% if !@counter || @counter is 0: %>hide<% end %><% if @cssClass: %><%= @cssClass %><% end %>"><%= @counter %></div>
|
<div class="tabsSidebar-tab-count js-tabCounter <% if @cssClass: %><%= @cssClass %><% end %>"><%= @counter %></div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%- @Icon(@icon) %>
|
<%- @Icon(@icon) %>
|
|
@ -0,0 +1,3 @@
|
||||||
|
<form class="flex horizontal js-result">
|
||||||
|
<input type="text" name="link" value="" autocomplete="off" placeholder="<%= @placeholder %>" class="form-control"/>
|
||||||
|
</form>
|
22
app/assets/javascripts/app/views/integration/gitlab.jst.eco
Normal file
22
app/assets/javascripts/app/views/integration/gitlab.jst.eco
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<form>
|
||||||
|
<h2><%- @T('Settings') %></h2>
|
||||||
|
<div class="settings-entry">
|
||||||
|
<table class="settings-list" style="width: 100%;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th width="20%"><%- @T('Name') %>
|
||||||
|
<th width="80%"><%- @T('Value') %>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="settings-list-row-control"><%- @T('Endpoint') %> *
|
||||||
|
<td class="settings-list-control-cell"><input type="text" class="form-control form-control--small" value="<%= @config.endpoint %>" placeholder="https://git.example.com/api/graphql" name="endpoint">
|
||||||
|
<tr>
|
||||||
|
<td class="settings-list-row-control"><%- @T('API token') %> *
|
||||||
|
<td class="settings-list-control-cell"><input type="text" class="form-control form-control--small" value="<%= @config.api_token %>" name="api_token">
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn--primary js-submit"><%- @T('Save') %></button>
|
||||||
|
</form>
|
|
@ -8,12 +8,12 @@
|
||||||
<th width="80%"><%- @T('Value') %>
|
<th width="80%"><%- @T('Value') %>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
|
||||||
<td class="settings-list-row-control"><%- @T('API token') %> *
|
|
||||||
<td class="settings-list-control-cell"><input type="text" class="form-control form-control--small" value="<%= @config.api_token %>" name="api_token">
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="settings-list-row-control"><%- @T('Endpoint') %> *
|
<td class="settings-list-row-control"><%- @T('Endpoint') %> *
|
||||||
<td class="settings-list-control-cell"><input type="text" class="form-control form-control--small" value="<%= @config.endpoint %>" placeholder="https://idoit.example.com/i-doit/" name="endpoint">
|
<td class="settings-list-control-cell"><input type="text" class="form-control form-control--small" value="<%= @config.endpoint %>" placeholder="https://idoit.example.com/i-doit/" name="endpoint">
|
||||||
|
<tr>
|
||||||
|
<td class="settings-list-row-control"><%- @T('API token') %> *
|
||||||
|
<td class="settings-list-control-cell"><input type="text" class="form-control form-control--small" value="<%= @config.api_token %>" name="api_token">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="settings-list-row-control"><%- @T('Client ID') %>
|
<td class="settings-list-row-control"><%- @T('Client ID') %>
|
||||||
<td class="settings-list-control-cell"><input type="text" class="form-control form-control--small" value="<%= @config.client_id %>" name="client_id">
|
<td class="settings-list-control-cell"><input type="text" class="form-control form-control--small" value="<%= @config.client_id %>" name="client_id">
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
<% for issue in @issues: %>
|
||||||
|
<div class="sidebar-git-issue-delete">
|
||||||
|
<span class="list-item-delete js-delete" data-issue-id="<%= issue.url %>" data-type="remove">
|
||||||
|
<%- @Icon('diagonal-cross') %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-git-issue-content">
|
||||||
|
<div class="sidebar-block">
|
||||||
|
<a href="<%- issue.url %>" target="_blank">
|
||||||
|
<span class="icon-holder" title="<%= @T(issue.icon_state) %>">
|
||||||
|
<%- @Icon('task-state', issue.icon_state) %>
|
||||||
|
</span>
|
||||||
|
#<%= issue.id %> <%= issue.title %>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<% if issue.milestone: %>
|
||||||
|
<div class="sidebar-block">
|
||||||
|
<label><%- @T('Milestone') %></label>
|
||||||
|
<%= @T(issue.milestone) %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% if issue.assignees.length > 0: %>
|
||||||
|
<div class="sidebar-block">
|
||||||
|
<label><%- @T('Assignee') %></label>
|
||||||
|
<%= issue.assignees.join(', ') %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% if issue.labels.length > 0: %>
|
||||||
|
<div class="sidebar-block">
|
||||||
|
<label><%- @T('Labels') %></label>
|
||||||
|
<% for label in issue.labels: %><span class="badge" style="color: <%- label.text_color %>; background-color: <%- label.color %>"><%= label.title %></span> <% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<hr class="clearfix">
|
||||||
|
<% end %>
|
|
@ -44,6 +44,7 @@
|
||||||
.icon-full-logo { width: 175px; height: 50px; }
|
.icon-full-logo { width: 175px; height: 50px; }
|
||||||
.icon-github-button { width: 29px; height: 24px; }
|
.icon-github-button { width: 29px; height: 24px; }
|
||||||
.icon-gitlab-button { width: 29px; height: 24px; }
|
.icon-gitlab-button { width: 29px; height: 24px; }
|
||||||
|
.icon-gitlab-logo { width: 24px; height: 24px; }
|
||||||
.icon-google-button { width: 29px; height: 24px; }
|
.icon-google-button { width: 29px; height: 24px; }
|
||||||
.icon-group { width: 24px; height: 24px; }
|
.icon-group { width: 24px; height: 24px; }
|
||||||
.icon-help { width: 16px; height: 16px; }
|
.icon-help { width: 16px; height: 16px; }
|
||||||
|
|
|
@ -5108,6 +5108,15 @@ footer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar-git-issue-delete {
|
||||||
|
text-align: right;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-git-issue-content {
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
.main + .sidebar {
|
.main + .sidebar {
|
||||||
border-right: none;
|
border-right: none;
|
||||||
border-left: 1px solid #e6e6e6;
|
border-left: 1px solid #e6e6e6;
|
||||||
|
|
53
app/controllers/integration/gitlab_controller.rb
Normal file
53
app/controllers/integration/gitlab_controller.rb
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
|
class Integration::GitLabController < ApplicationController
|
||||||
|
prepend_before_action { authentication_check && authorize! }
|
||||||
|
|
||||||
|
def verify
|
||||||
|
gitlab = ::GitLab.new(params[:endpoint], params[:api_token])
|
||||||
|
render json: {
|
||||||
|
result: 'ok',
|
||||||
|
response: gitlab.schema.to_json,
|
||||||
|
}
|
||||||
|
rescue => e
|
||||||
|
logger.error e
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
result: 'failed',
|
||||||
|
message: e.message,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def query
|
||||||
|
config = Setting.get('gitlab_config')
|
||||||
|
|
||||||
|
gitlab = ::GitLab.new(config['endpoint'], config['api_token'], schema: config['schema'])
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
result: 'ok',
|
||||||
|
response: gitlab.issues_by_urls(params[:links]),
|
||||||
|
}
|
||||||
|
rescue => e
|
||||||
|
logger.error e
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
result: 'failed',
|
||||||
|
message: e.message,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
ticket = Ticket.find(params[:ticket_id])
|
||||||
|
ticket.with_lock do
|
||||||
|
authorize!(ticket, :show?)
|
||||||
|
ticket.preferences[:gitlab] ||= {}
|
||||||
|
ticket.preferences[:gitlab][:issue_links] = Array(params[:issue_links]).uniq
|
||||||
|
ticket.save!
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
result: 'ok',
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
|
@ -0,0 +1,5 @@
|
||||||
|
class Controllers::Integration::GitLabControllerPolicy < Controllers::ApplicationControllerPolicy
|
||||||
|
permit! %i[query update], to: 'ticket.agent'
|
||||||
|
permit! :verify, to: 'admin.integration.gitlab'
|
||||||
|
default_permit!(['agent.integration.gitlab', 'admin.integration.gitlab'])
|
||||||
|
end
|
|
@ -21,4 +21,5 @@ ActiveSupport::Inflector.inflections(:en) do |inflect|
|
||||||
# see: KnowledgeBase.table_name.singularize
|
# see: KnowledgeBase.table_name.singularize
|
||||||
inflect.singular(/(knowledge_base)s$/i, '\1')
|
inflect.singular(/(knowledge_base)s$/i, '\1')
|
||||||
inflect.acronym 'SMIME'
|
inflect.acronym 'SMIME'
|
||||||
|
inflect.acronym 'GitLab'
|
||||||
end
|
end
|
||||||
|
|
|
@ -32,7 +32,7 @@ Rails.application.config.middleware.use OmniAuth::Builder do
|
||||||
provider :github_database, 'not_change_will_be_set_by_database', 'not_change_will_be_set_by_database'
|
provider :github_database, 'not_change_will_be_set_by_database', 'not_change_will_be_set_by_database'
|
||||||
|
|
||||||
# gitlab database connect
|
# gitlab database connect
|
||||||
provider :gitlab_database, 'not_change_will_be_set_by_database', 'not_change_will_be_set_by_database', {
|
provider :git_lab_database, 'not_change_will_be_set_by_database', 'not_change_will_be_set_by_database', {
|
||||||
client_options: {
|
client_options: {
|
||||||
site: 'https://not_change_will_be_set_by_database',
|
site: 'https://not_change_will_be_set_by_database',
|
||||||
authorize_url: '/oauth/authorize',
|
authorize_url: '/oauth/authorize',
|
||||||
|
|
9
config/routes/integration_gitlab.rb
Normal file
9
config/routes/integration_gitlab.rb
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
Zammad::Application.routes.draw do
|
||||||
|
api_path = Rails.configuration.api_path
|
||||||
|
|
||||||
|
match api_path + '/integration/gitlab', to: 'integration/gitlab#query', via: :post
|
||||||
|
match api_path + '/integration/gitlab', to: 'integration/gitlab#query', via: :get
|
||||||
|
match api_path + '/integration/gitlab/verify', to: 'integration/gitlab#verify', via: :post
|
||||||
|
match api_path + '/integration/gitlab_ticket_update', to: 'integration/gitlab#update', via: :post
|
||||||
|
|
||||||
|
end
|
Binary file not shown.
|
@ -1,4 +1,4 @@
|
||||||
class Issue2595GitlabPlaceholder < ActiveRecord::Migration[5.2]
|
class Issue2595GitLabPlaceholder < ActiveRecord::Migration[5.2]
|
||||||
def change
|
def change
|
||||||
# return if it's a new setup
|
# return if it's a new setup
|
||||||
return if !Setting.exists?(name: 'system_init_done')
|
return if !Setting.exists?(name: 'system_init_done')
|
||||||
|
|
51
db/migrate/20210113000001_gitlab_support.rb
Normal file
51
db/migrate/20210113000001_gitlab_support.rb
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
class GitLabSupport < ActiveRecord::Migration[4.2]
|
||||||
|
def up
|
||||||
|
|
||||||
|
# return if it's a new setup
|
||||||
|
return if !Setting.exists?(name: 'system_init_done')
|
||||||
|
|
||||||
|
Setting.create_if_not_exists(
|
||||||
|
title: 'GitLab integration',
|
||||||
|
name: 'gitlab_integration',
|
||||||
|
area: 'Integration::Switch',
|
||||||
|
description: 'Defines if the GitLab (http://www.gitlab.com) integration is enabled or not.',
|
||||||
|
options: {
|
||||||
|
form: [
|
||||||
|
{
|
||||||
|
display: '',
|
||||||
|
null: true,
|
||||||
|
name: 'gitlab_integration',
|
||||||
|
tag: 'boolean',
|
||||||
|
options: {
|
||||||
|
true => 'yes',
|
||||||
|
false => 'no',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
state: false,
|
||||||
|
preferences: {
|
||||||
|
prio: 1,
|
||||||
|
authentication: true,
|
||||||
|
permission: ['admin.integration'],
|
||||||
|
},
|
||||||
|
frontend: true
|
||||||
|
)
|
||||||
|
Setting.create_if_not_exists(
|
||||||
|
title: 'GitLab config',
|
||||||
|
name: 'gitlab_config',
|
||||||
|
area: 'Integration::GitLab',
|
||||||
|
description: 'Stores the GitLab configuration.',
|
||||||
|
options: {},
|
||||||
|
state: {
|
||||||
|
endpoint: 'https://gitlab.com/api/graphql',
|
||||||
|
},
|
||||||
|
preferences: {
|
||||||
|
prio: 2,
|
||||||
|
permission: ['admin.integration'],
|
||||||
|
},
|
||||||
|
frontend: false,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
|
@ -1427,18 +1427,18 @@ Setting.create_if_not_exists(
|
||||||
preferences: {
|
preferences: {
|
||||||
controller: 'SettingsAreaSwitch',
|
controller: 'SettingsAreaSwitch',
|
||||||
sub: ['auth_gitlab_credentials'],
|
sub: ['auth_gitlab_credentials'],
|
||||||
title_i18n: ['Gitlab'],
|
title_i18n: ['GitLab'],
|
||||||
description_i18n: ['Gitlab', 'Gitlab Applications', 'https://your-gitlab-host/admin/applications'],
|
description_i18n: ['GitLab', 'GitLab Applications', 'https://your-gitlab-host/admin/applications'],
|
||||||
permission: ['admin.security'],
|
permission: ['admin.security'],
|
||||||
},
|
},
|
||||||
state: false,
|
state: false,
|
||||||
frontend: true
|
frontend: true
|
||||||
)
|
)
|
||||||
Setting.create_if_not_exists(
|
Setting.create_if_not_exists(
|
||||||
title: 'Gitlab App Credentials',
|
title: 'GitLab App Credentials',
|
||||||
name: 'auth_gitlab_credentials',
|
name: 'auth_gitlab_credentials',
|
||||||
area: 'Security::ThirdPartyAuthentication::Gitlab',
|
area: 'Security::ThirdPartyAuthentication::GitLab',
|
||||||
description: 'Enables user authentication via Gitlab.',
|
description: 'Enables user authentication via GitLab.',
|
||||||
options: {
|
options: {
|
||||||
form: [
|
form: [
|
||||||
{
|
{
|
||||||
|
@ -4046,6 +4046,48 @@ Setting.create_if_not_exists(
|
||||||
},
|
},
|
||||||
frontend: false,
|
frontend: false,
|
||||||
)
|
)
|
||||||
|
Setting.create_if_not_exists(
|
||||||
|
title: 'GitLab integration',
|
||||||
|
name: 'gitlab_integration',
|
||||||
|
area: 'Integration::Switch',
|
||||||
|
description: 'Defines if the GitLab (http://www.gitlab.com) integration is enabled or not.',
|
||||||
|
options: {
|
||||||
|
form: [
|
||||||
|
{
|
||||||
|
display: '',
|
||||||
|
null: true,
|
||||||
|
name: 'gitlab_integration',
|
||||||
|
tag: 'boolean',
|
||||||
|
options: {
|
||||||
|
true => 'yes',
|
||||||
|
false => 'no',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
state: false,
|
||||||
|
preferences: {
|
||||||
|
prio: 1,
|
||||||
|
authentication: true,
|
||||||
|
permission: ['admin.integration'],
|
||||||
|
},
|
||||||
|
frontend: true
|
||||||
|
)
|
||||||
|
Setting.create_if_not_exists(
|
||||||
|
title: 'GitLab config',
|
||||||
|
name: 'gitlab_config',
|
||||||
|
area: 'Integration::GitLab',
|
||||||
|
description: 'Stores the GitLab configuration.',
|
||||||
|
options: {},
|
||||||
|
state: {
|
||||||
|
endpoint: 'https://gitlab.com/api/graphql',
|
||||||
|
},
|
||||||
|
preferences: {
|
||||||
|
prio: 2,
|
||||||
|
permission: ['admin.integration'],
|
||||||
|
},
|
||||||
|
frontend: false,
|
||||||
|
)
|
||||||
Setting.create_if_not_exists(
|
Setting.create_if_not_exists(
|
||||||
title: 'Defines sync transaction backend.',
|
title: 'Defines sync transaction backend.',
|
||||||
name: '0100_trigger',
|
name: '0100_trigger',
|
||||||
|
|
27
lib/gitlab.rb
Normal file
27
lib/gitlab.rb
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
|
class GitLab
|
||||||
|
extend Forwardable
|
||||||
|
|
||||||
|
attr_reader :client
|
||||||
|
|
||||||
|
def_delegator :client, :schema
|
||||||
|
|
||||||
|
def initialize(*args, **kargs)
|
||||||
|
@client = GitLab::Client.new(*args, **kargs)
|
||||||
|
end
|
||||||
|
|
||||||
|
def issues_by_urls(urls)
|
||||||
|
urls.uniq.each_with_object([]) do |url, result|
|
||||||
|
issue = issue_by_url(url)
|
||||||
|
next if issue.blank?
|
||||||
|
|
||||||
|
result << issue
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def issue_by_url(url)
|
||||||
|
issue = GitLab::LinkedIssue.new(client)
|
||||||
|
issue.find_by(url)&.to_h
|
||||||
|
end
|
||||||
|
end
|
38
lib/gitlab/client.rb
Normal file
38
lib/gitlab/client.rb
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
require 'graphql/client'
|
||||||
|
|
||||||
|
class GitLab
|
||||||
|
class Client
|
||||||
|
|
||||||
|
delegate_missing_to :client
|
||||||
|
|
||||||
|
attr_reader :endpoint
|
||||||
|
|
||||||
|
def initialize(endpoint, api_token, schema: nil)
|
||||||
|
@endpoint = endpoint
|
||||||
|
@api_token = api_token
|
||||||
|
schema(schema) if schema.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def schema(source = http_client)
|
||||||
|
@schema ||= ::GraphQL::Client.load_schema(source)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def http_client
|
||||||
|
@http_client ||= GitLab::HttpClient.new(@endpoint, @api_token)
|
||||||
|
end
|
||||||
|
|
||||||
|
def client
|
||||||
|
@client ||= begin
|
||||||
|
GraphQL::Client.new(
|
||||||
|
schema: schema,
|
||||||
|
execute: http_client,
|
||||||
|
).tap do |client|
|
||||||
|
client.allow_dynamic_queries = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
23
lib/gitlab/http_client.rb
Normal file
23
lib/gitlab/http_client.rb
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
require 'graphql/client'
|
||||||
|
require 'graphql/client/http'
|
||||||
|
|
||||||
|
class GitLab
|
||||||
|
class HttpClient < ::GraphQL::Client::HTTP
|
||||||
|
|
||||||
|
def initialize(endpoint, api_token)
|
||||||
|
raise 'api_token required' if api_token.blank?
|
||||||
|
raise 'endpoint required' if endpoint.blank?
|
||||||
|
|
||||||
|
@api_token = api_token
|
||||||
|
|
||||||
|
super(endpoint)
|
||||||
|
end
|
||||||
|
|
||||||
|
def headers(_context)
|
||||||
|
{
|
||||||
|
"PRIVATE-TOKEN": @api_token
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
115
lib/gitlab/linked_issue.rb
Normal file
115
lib/gitlab/linked_issue.rb
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
class GitLab
|
||||||
|
class LinkedIssue
|
||||||
|
|
||||||
|
STATES_MAPPING = {
|
||||||
|
'opened' => 'open'
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
QUERY = <<-'GRAPHQL'.freeze
|
||||||
|
query($fullpath: ID!, $issue_id: String) {
|
||||||
|
project(fullPath: $fullpath) {
|
||||||
|
issue(iid: $issue_id) {
|
||||||
|
iid
|
||||||
|
title
|
||||||
|
state
|
||||||
|
milestone {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
assignees {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
labels {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
title
|
||||||
|
color
|
||||||
|
textColor
|
||||||
|
description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GRAPHQL
|
||||||
|
|
||||||
|
attr_reader :client
|
||||||
|
|
||||||
|
def initialize(client)
|
||||||
|
@client = client
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_by(url)
|
||||||
|
@result = query_by_url(url)
|
||||||
|
return if @result.blank?
|
||||||
|
|
||||||
|
to_h.merge(url: url)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def to_h
|
||||||
|
{
|
||||||
|
id: @result['iid'],
|
||||||
|
title: @result['title'],
|
||||||
|
icon_state: STATES_MAPPING.fetch(@result['state'], @result['state']),
|
||||||
|
milestone: milestone,
|
||||||
|
assignees: assignees,
|
||||||
|
labels: labels,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def assignees
|
||||||
|
@result['assignees']['edges'].map do |assignee|
|
||||||
|
assignee['node']['name']
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def labels
|
||||||
|
@result['labels']['edges'].map do |label|
|
||||||
|
{
|
||||||
|
text_color: label['node']['textColor'],
|
||||||
|
color: label['node']['color'],
|
||||||
|
title: label['node']['title']
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def milestone
|
||||||
|
@result['milestone']['title']
|
||||||
|
end
|
||||||
|
|
||||||
|
def query
|
||||||
|
@query ||= client.parse GitLab::LinkedIssue::QUERY
|
||||||
|
end
|
||||||
|
|
||||||
|
def query_by_url(url)
|
||||||
|
variables = variables(url)
|
||||||
|
return if variables.blank?
|
||||||
|
|
||||||
|
response = client.query(query, variables: variables)
|
||||||
|
|
||||||
|
response&.data&.project&.issue&.to_h&.deep_dup
|
||||||
|
end
|
||||||
|
|
||||||
|
def variables(url)
|
||||||
|
return if url !~ %r{^https://([^/]+)/(.*)/-/issues/(\d+)$}
|
||||||
|
|
||||||
|
host = $1
|
||||||
|
fullpath = $2
|
||||||
|
id = $3
|
||||||
|
|
||||||
|
return if client.endpoint.exclude?(host)
|
||||||
|
|
||||||
|
{
|
||||||
|
fullpath: fullpath,
|
||||||
|
issue_id: id
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,4 +1,4 @@
|
||||||
class GitlabDatabase < OmniAuth::Strategies::GitLab
|
class GitLabDatabase < OmniAuth::Strategies::GitLab
|
||||||
option :name, 'gitlab'
|
option :name, 'gitlab'
|
||||||
|
|
||||||
def initialize(app, *args, &block)
|
def initialize(app, *args, &block)
|
||||||
|
|
|
@ -324,6 +324,11 @@
|
||||||
<path d="M24.906 9.481l1.333 4.101a.908.908 0 0 1-.33 1.015l-11.533 8.38L24.906 9.48z" fill="#FF998A"/>
|
<path d="M24.906 9.481l1.333 4.101a.908.908 0 0 1-.33 1.015l-11.533 8.38L24.906 9.48z" fill="#FF998A"/>
|
||||||
<path d="M25.066 10.022H17.99l3.41-8.668a.454.454 0 0 1 .864 0l2.8 8.668z" fill="#FFF"/>
|
<path d="M25.066 10.022H17.99l3.41-8.668a.454.454 0 0 1 .864 0l2.8 8.668z" fill="#FFF"/>
|
||||||
</g>
|
</g>
|
||||||
|
</symbol><symbol id="icon-gitlab-logo" viewBox="0 0 24 24">
|
||||||
|
<title>
|
||||||
|
gitlab-logo
|
||||||
|
</title>
|
||||||
|
<path d="M23.955 13.64l-1.343-4.133-2.661-8.19a.457.457 0 0 0-.87 0l-2.876 8.726H7.599l-2.68-8.727a.457.457 0 0 0-.87 0L1.388 9.507.045 13.64a.915.915 0 0 0 .332 1.023L12 23.108l11.623-8.445a.915.915 0 0 0 .332-1.023" fill-rule="evenodd"/>
|
||||||
</symbol><symbol id="icon-google-button" viewBox="0 0 29 24">
|
</symbol><symbol id="icon-google-button" viewBox="0 0 29 24">
|
||||||
<title>
|
<title>
|
||||||
google-button
|
google-button
|
||||||
|
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
7
public/assets/images/icons/gitlab-logo.svg
Normal file
7
public/assets/images/icons/gitlab-logo.svg
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<title>gitlab-logo</title>
|
||||||
|
<g id="gitlab-logo" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<path d="M23.9551616,10.4673469 L22.6122253,14.6003733 L19.9507177,22.7917715 C19.8138186,23.2131893 19.2175573,23.2131893 19.0805932,22.7917715 L16.2048292,14.0650133 L7.59876258,14.0650133 L4.91936909,22.7917715 C4.78247003,23.2131893 4.18620865,23.2131893 4.04924461,22.7917715 L1.38773702,14.6003733 L0.0448656821,10.4673469 C-0.0776742518,10.0903709 0.0565609036,9.67739968 0.377204898,9.44440484 L11.9999811,1 L23.6228224,9.44440484 C23.9434664,9.67739968 24.0776366,10.0903709 23.9551616,10.4673469" fill="#50E3C2" transform="translate(12.000000, 12.053917) scale(1, -1) translate(-12.000000, -12.053917) "></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 948 B |
95
spec/integration/gitlab_spec.rb
Normal file
95
spec/integration/gitlab_spec.rb
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
RSpec.describe GitLab, type: :integration do # rubocop:disable RSpec/FilePath
|
||||||
|
|
||||||
|
before(:all) do # rubocop:disable RSpec/BeforeAfterAll
|
||||||
|
required_envs = %w[GITLAB_ENDPOINT GITLAB_APITOKEN]
|
||||||
|
required_envs.each do |key|
|
||||||
|
skip("NOTICE: Missing environment variable #{key} for test! (Please fill up: #{required_envs.join(' && ')})") if ENV[key].blank?
|
||||||
|
end
|
||||||
|
|
||||||
|
# request schema only once for performance reasons
|
||||||
|
@cached_schema = described_class.new(ENV['GITLAB_ENDPOINT'], ENV['GITLAB_APITOKEN']).schema
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:instance) { described_class.new(ENV['GITLAB_ENDPOINT'], ENV['GITLAB_APITOKEN'], schema: schema) }
|
||||||
|
let(:schema) { @cached_schema } # rubocop:disable RSpec/InstanceVariable
|
||||||
|
let(:issue_data) do
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
title: 'Example issue',
|
||||||
|
url: ENV['GITLAB_ISSUE_LINK'],
|
||||||
|
icon_state: 'open',
|
||||||
|
milestone: 'important milestone',
|
||||||
|
assignees: ['zammad-robot'],
|
||||||
|
labels: [
|
||||||
|
{
|
||||||
|
color: '#FF0000',
|
||||||
|
text_color: '#FFFFFF',
|
||||||
|
title: 'critical'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: '#0033CC',
|
||||||
|
text_color: '#FFFFFF',
|
||||||
|
title: 'label1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: '#D1D100',
|
||||||
|
text_color: '#FFFFFF',
|
||||||
|
title: 'special'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
end
|
||||||
|
let(:invalid_issue_url) { 'https://git.example.com/group/project/-/issues/1' }
|
||||||
|
|
||||||
|
describe '#schema' do
|
||||||
|
it 'returns GraphQL schema' do
|
||||||
|
expect(instance.schema).to respond_to(:to_graphql)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#issues_by_urls' do
|
||||||
|
let(:result) { instance.issues_by_urls([ issue_url ]) }
|
||||||
|
|
||||||
|
context 'when issue exists' do
|
||||||
|
let(:issue_url) { ENV['GITLAB_ISSUE_LINK'] }
|
||||||
|
|
||||||
|
it 'returns a result list' do
|
||||||
|
expect(result.size).to eq(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns issue data in the result list' do
|
||||||
|
expect(result[0]).to eq(issue_data)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when issue does not exists' do
|
||||||
|
let(:issue_url) { invalid_issue_url }
|
||||||
|
|
||||||
|
it 'returns no result' do
|
||||||
|
expect(result.size).to eq(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#issue_by_url' do
|
||||||
|
|
||||||
|
let(:result) { instance.issue_by_url(issue_url) }
|
||||||
|
|
||||||
|
context 'when issue exists' do
|
||||||
|
let(:issue_url) { ENV['GITLAB_ISSUE_LINK'] }
|
||||||
|
|
||||||
|
it 'returns issue data' do
|
||||||
|
expect(result).to eq(issue_data)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when issue does not exists' do
|
||||||
|
let(:issue_url) { invalid_issue_url }
|
||||||
|
|
||||||
|
it 'returns nil' do
|
||||||
|
expect(result).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,5 +1,3 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe ActiveRecord::Calculations do # rubocop:disable RSpec/FilePath
|
RSpec.describe ActiveRecord::Calculations do # rubocop:disable RSpec/FilePath
|
||||||
|
|
115
spec/requests/integration/gitlab_spec.rb
Normal file
115
spec/requests/integration/gitlab_spec.rb
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
# rubocop:disable RSpec/StubbedMock,RSpec/MessageSpies
|
||||||
|
|
||||||
|
RSpec.describe 'GitLab', type: :request do
|
||||||
|
|
||||||
|
let(:token) { 't0k3N' }
|
||||||
|
let(:endpoint) { 'https://git.example.com/api/graphql' }
|
||||||
|
|
||||||
|
let!(:admin) do
|
||||||
|
create(:admin, groups: Group.all)
|
||||||
|
end
|
||||||
|
|
||||||
|
let!(:agent) do
|
||||||
|
create(:agent, groups: Group.all)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:issue_data) do
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
title: 'Example issue',
|
||||||
|
url: ENV['GITLAB_ISSUE_LINK'],
|
||||||
|
icon_state: 'open',
|
||||||
|
milestone: 'important milestone',
|
||||||
|
assignees: ['zammad-robot'],
|
||||||
|
labels: [
|
||||||
|
{
|
||||||
|
color: '#FF0000',
|
||||||
|
text_color: '#FFFFFF',
|
||||||
|
title: 'critical'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: '#0033CC',
|
||||||
|
text_color: '#FFFFFF',
|
||||||
|
title: 'label1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: '#D1D100',
|
||||||
|
text_color: '#FFFFFF',
|
||||||
|
title: 'special'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:dummy_schema) do
|
||||||
|
{
|
||||||
|
a: :b
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'request handling' do
|
||||||
|
it 'does verify integration' do
|
||||||
|
params = {
|
||||||
|
endpoint: endpoint,
|
||||||
|
api_token: token,
|
||||||
|
}
|
||||||
|
authenticated_as(agent)
|
||||||
|
post '/api/v1/integration/gitlab/verify', params: params, as: :json
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
expect(json_response).to be_a_kind_of(Hash)
|
||||||
|
expect(json_response).not_to be_blank
|
||||||
|
expect(json_response['error']).to eq('Not authorized (user)!')
|
||||||
|
|
||||||
|
authenticated_as(admin)
|
||||||
|
instance = instance_double('GitLab')
|
||||||
|
expect(GitLab).to receive(:new).with(endpoint, token).and_return instance
|
||||||
|
expect(instance).to receive(:schema).and_return(dummy_schema)
|
||||||
|
|
||||||
|
post '/api/v1/integration/gitlab/verify', params: params, as: :json
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(json_response).to be_a_kind_of(Hash)
|
||||||
|
expect(json_response).not_to be_blank
|
||||||
|
expect(json_response['result']).to eq('ok')
|
||||||
|
expect(json_response['response']).to eq(dummy_schema.to_json)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does query objects' do
|
||||||
|
params = {
|
||||||
|
links: [ ENV['GITLAB_ISSUE_LINK'] ],
|
||||||
|
}
|
||||||
|
authenticated_as(agent)
|
||||||
|
instance = instance_double('GitLab')
|
||||||
|
expect(GitLab).to receive(:new).and_return instance
|
||||||
|
expect(instance).to receive(:issues_by_urls).and_return([issue_data])
|
||||||
|
|
||||||
|
post '/api/v1/integration/gitlab', params: params, as: :json
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
|
expect(json_response).to be_a_kind_of(Hash)
|
||||||
|
expect(json_response).not_to be_blank
|
||||||
|
expect(json_response['result']).to eq('ok')
|
||||||
|
expect(json_response['response']).to eq([issue_data.deep_stringify_keys])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does save ticket issues' do
|
||||||
|
ticket = create(:ticket, group: Group.first)
|
||||||
|
|
||||||
|
params = {
|
||||||
|
ticket_id: ticket.id,
|
||||||
|
issue_links: [ ENV['GITLAB_ISSUE_LINK'] ],
|
||||||
|
}
|
||||||
|
authenticated_as(agent)
|
||||||
|
post '/api/v1/integration/gitlab_ticket_update', params: params, as: :json
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(json_response).to be_a_kind_of(Hash)
|
||||||
|
expect(json_response).not_to be_blank
|
||||||
|
expect(json_response['result']).to eq('ok')
|
||||||
|
|
||||||
|
expect(ticket.reload.preferences[:gitlab][:issue_links]).to eq(params[:issue_links])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# rubocop:enable RSpec/StubbedMock,RSpec/MessageSpies
|
|
@ -1,4 +1,4 @@
|
||||||
VCR_IGNORE_MATCHING_HOSTS = %w[zammad.com google.com elasticsearch selenium login.microsoftonline.com zammad.org].freeze
|
VCR_IGNORE_MATCHING_HOSTS = %w[elasticsearch selenium zammad.org zammad.com znuny.com google.com login.microsoftonline.com].freeze
|
||||||
VCR_IGNORE_MATCHING_REGEXPS = [/^192\.168\.\d+\.\d+$/].freeze
|
VCR_IGNORE_MATCHING_REGEXPS = [/^192\.168\.\d+\.\d+$/].freeze
|
||||||
|
|
||||||
VCR.configure do |config|
|
VCR.configure do |config|
|
||||||
|
|
|
@ -341,4 +341,62 @@ RSpec.describe 'Ticket Create', type: :system do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'GitLab Integration', :integration, authenticated_as: :authenticate do
|
||||||
|
let(:customer) { create(:customer) }
|
||||||
|
let(:agent) { create(:agent, groups: [Group.find_by(name: 'Users')]) }
|
||||||
|
let!(:template) { create(:template, :dummy_data, group: Group.find_by(name: 'Users'), owner: agent, customer: customer) }
|
||||||
|
|
||||||
|
before(:all) do # rubocop:disable RSpec/BeforeAfterAll
|
||||||
|
required_envs = %w[GITLAB_ENDPOINT GITLAB_APITOKEN]
|
||||||
|
required_envs.each do |key|
|
||||||
|
skip("NOTICE: Missing environment variable #{key} for test! (Please fill up: #{required_envs.join(' && ')})") if ENV[key].blank?
|
||||||
|
end
|
||||||
|
|
||||||
|
# request schema only once for performance reasons
|
||||||
|
@cached_schema = GitLab.new(ENV['GITLAB_ENDPOINT'], ENV['GITLAB_APITOKEN']).schema.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
def authenticate
|
||||||
|
Setting.set('gitlab_integration', true)
|
||||||
|
Setting.set('gitlab_config', {
|
||||||
|
api_token: ENV['GITLAB_APITOKEN'],
|
||||||
|
endpoint: ENV['GITLAB_ENDPOINT'],
|
||||||
|
schema: @cached_schema, # rubocop:disable RSpec/InstanceVariable
|
||||||
|
})
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a ticket with links' do
|
||||||
|
visit 'ticket/create'
|
||||||
|
within(:active_content) do
|
||||||
|
use_template(template)
|
||||||
|
|
||||||
|
# switch to gitlab sidebar
|
||||||
|
click('.tabsSidebar-tab[data-tab=gitlab]')
|
||||||
|
click('.sidebar-header-headline.js-headline')
|
||||||
|
|
||||||
|
# add issue
|
||||||
|
click_on 'Link issue'
|
||||||
|
fill_in 'link', with: ENV['GITLAB_ISSUE_LINK']
|
||||||
|
click_on 'Submit'
|
||||||
|
await_empty_ajax_queue
|
||||||
|
|
||||||
|
# verify issue
|
||||||
|
content = find('.sidebar-git-issue-content')
|
||||||
|
expect(content).to have_text('#1 Example issue')
|
||||||
|
expect(content).to have_text('critical')
|
||||||
|
expect(content).to have_text('special')
|
||||||
|
expect(content).to have_text('important milestone')
|
||||||
|
expect(content).to have_text('zammad-robot')
|
||||||
|
|
||||||
|
# create Ticket
|
||||||
|
click '.js-submit'
|
||||||
|
await_empty_ajax_queue
|
||||||
|
|
||||||
|
# check stored data
|
||||||
|
expect(Ticket.last.preferences[:gitlab][:issue_links][0]).to eq(ENV['GITLAB_ISSUE_LINK'])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1474,4 +1474,68 @@ RSpec.describe 'Ticket zoom', type: :system do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'GitLab Integration', :integration, authenticated_as: :authenticate do
|
||||||
|
let!(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) }
|
||||||
|
|
||||||
|
before(:all) do # rubocop:disable RSpec/BeforeAfterAll
|
||||||
|
required_envs = %w[GITLAB_ENDPOINT GITLAB_APITOKEN]
|
||||||
|
required_envs.each do |key|
|
||||||
|
skip("NOTICE: Missing environment variable #{key} for test! (Please fill up: #{required_envs.join(' && ')})") if ENV[key].blank?
|
||||||
|
end
|
||||||
|
|
||||||
|
# request schema only once for performance reasons
|
||||||
|
@cached_schema = GitLab.new(ENV['GITLAB_ENDPOINT'], ENV['GITLAB_APITOKEN']).schema.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
def authenticate
|
||||||
|
Setting.set('gitlab_integration', true)
|
||||||
|
Setting.set('gitlab_config', {
|
||||||
|
api_token: ENV['GITLAB_APITOKEN'],
|
||||||
|
endpoint: ENV['GITLAB_ENDPOINT'],
|
||||||
|
schema: @cached_schema, # rubocop:disable RSpec/InstanceVariable
|
||||||
|
})
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates links and removes them' do
|
||||||
|
visit "#ticket/zoom/#{ticket.id}"
|
||||||
|
within(:active_content) do
|
||||||
|
|
||||||
|
# switch to GitLab sidebar
|
||||||
|
click('.tabsSidebar-tab[data-tab=gitlab]')
|
||||||
|
click('.sidebar-header-headline.js-headline')
|
||||||
|
|
||||||
|
# add issue
|
||||||
|
click_on 'Link issue'
|
||||||
|
fill_in 'link', with: ENV['GITLAB_ISSUE_LINK']
|
||||||
|
click_on 'Submit'
|
||||||
|
await_empty_ajax_queue
|
||||||
|
|
||||||
|
# verify issue
|
||||||
|
content = find('.sidebar-git-issue-content')
|
||||||
|
expect(content).to have_text('#1 Example issue')
|
||||||
|
expect(content).to have_text('critical')
|
||||||
|
expect(content).to have_text('special')
|
||||||
|
expect(content).to have_text('important milestone')
|
||||||
|
expect(content).to have_text('zammad-robot')
|
||||||
|
|
||||||
|
expect(ticket.reload.preferences[:gitlab][:issue_links][0]).to eq(ENV['GITLAB_ISSUE_LINK'])
|
||||||
|
|
||||||
|
# check sidebar counter increased to 1
|
||||||
|
expect(find('.tabsSidebar-tab[data-tab=gitlab] .js-tabCounter')).to have_text('1')
|
||||||
|
|
||||||
|
# delete issue
|
||||||
|
click(".sidebar-git-issue-delete span[data-issue-id='#{ENV['GITLAB_ISSUE_LINK']}']")
|
||||||
|
await_empty_ajax_queue
|
||||||
|
|
||||||
|
content = find('.sidebar[data-tab=gitlab] .sidebar-content')
|
||||||
|
expect(content).to have_text('No linked issues')
|
||||||
|
expect(ticket.reload.preferences[:gitlab][:issue_links][0]).to be nil
|
||||||
|
|
||||||
|
# check that counter got removed
|
||||||
|
expect(page).to have_no_selector('.tabsSidebar-tab[data-tab=gitlab] .js-tabCounter')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue