Browse Source

Fixes issue #865 - Freshdesk import

Thorsten Eckel 3 years ago
parent
commit
8790e389be

+ 2 - 0
.rubocop/todo.yml

@@ -38,6 +38,7 @@ Metrics/AbcSize:
     - 'app/controllers/getting_started_controller.rb'
     - 'app/controllers/import_otrs_controller.rb'
     - 'app/controllers/import_zendesk_controller.rb'
+    - 'app/controllers/import_freshdesk_controller.rb'
     - 'app/controllers/integration/check_mk_controller.rb'
     - 'app/controllers/integration/cti_controller.rb'
     - 'app/controllers/integration/idoit_controller.rb'
@@ -466,6 +467,7 @@ Metrics/CyclomaticComplexity:
     - 'app/controllers/getting_started_controller.rb'
     - 'app/controllers/import_otrs_controller.rb'
     - 'app/controllers/import_zendesk_controller.rb'
+    - 'app/controllers/import_freshdesk_controller.rb'
     - 'app/controllers/integration/check_mk_controller.rb'
     - 'app/controllers/integration/smime_controller.rb'
     - 'app/controllers/knowledge_base/public/categories_controller.rb'

+ 194 - 0
app/assets/javascripts/app/controllers/import_freshdesk.coffee

@@ -0,0 +1,194 @@
+class ImportFreshdesk extends App.ControllerWizardFullScreen
+  className: 'getstarted fit'
+  elements:
+    '.input-feedback':                         'urlStatus'
+    '[data-target=freshdesk-credentials]':     'nextEnterCredentials'
+    '[data-target=freshdesk-start-migration]': 'nextStartMigration'
+    '#freshdesk-subdomain':                    'freshdeskSubdomain'
+    '#freshdesk-subdomain-addon':              'freshdeskSubdomainAddon'
+    '.freshdesk-subdomain-error':              'linkErrorMessage'
+    '.freshdesk-api-token-error':              'apiTokenErrorMessage'
+    '#freshdesk-email':                        'freshdeskEmail'
+    '#freshdesk-api-token':                    'freshdeskApiToken'
+    '.js-ticket-count-info':                   'ticketCountInfo'
+  updateMigrationDisplayLoop: 0
+
+  events:
+    'click .js-freshdesk-credentials': 'showCredentials'
+    'click .js-migration-start':     'startMigration'
+    'keyup #freshdesk-subdomain':           'updateUrl'
+    'keyup #freshdesk-api-token':     'updateApiToken'
+
+  constructor: ->
+    super
+
+    # set title
+    @title 'Import'
+
+    @freshdeskDomain = '.freshdesk.com'
+
+    # redirect to login if master user already exists
+    if @Config.get('system_init_done')
+      @navigate '#login'
+      return
+
+    @fetch()
+
+  fetch: ->
+
+    # get data
+    @ajax(
+      id:          'getting_started'
+      type:        'GET'
+      url:         "#{@apiPath}/getting_started"
+      processData: true
+      success:     (data, status, xhr) =>
+
+        # check if import is active
+        if data.import_mode == true && data.import_backend != 'freshdesk'
+          @navigate "#import/#{data.import_backend}", { emptyEl: true }
+          return
+
+        # render page
+        @render()
+
+        if data.import_mode == true
+          @showImportState()
+          @updateMigration()
+    )
+
+  render: ->
+    @replaceWith App.view('import/freshdesk')(
+      freshdeskDomain: @freshdeskDomain
+    )
+
+  updateUrl: (e) =>
+    @urlStatus.attr('data-state', 'loading')
+    @freshdeskSubdomainAddon.attr('style', 'padding-right: 42px')
+    @linkErrorMessage.text('')
+
+    # get data
+    callback = =>
+      @ajax(
+        id:          'import_freshdesk_url'
+        type:        'POST'
+        url:         "#{@apiPath}/import/freshdesk/url_check"
+        data:        JSON.stringify(url: "https://#{@freshdeskSubdomain.val()}#{@freshdeskDomain}")
+        processData: true
+        success:     (data, status, xhr) =>
+
+          # validate form
+          if data.result is 'ok'
+            @urlStatus.attr('data-state', 'success')
+            @linkErrorMessage.text('')
+            @nextEnterCredentials.removeClass('hide')
+          else
+            @urlStatus.attr('data-state', 'error')
+            @linkErrorMessage.text( data.message_human || data.message)
+            @nextEnterCredentials.addClass('hide')
+
+      )
+    @delay( callback, 700, 'import_freshdesk_url' )
+
+  updateApiToken: (e) =>
+    @urlStatus.attr('data-state', 'loading')
+    @apiTokenErrorMessage.text('')
+
+    # get data
+    callback = =>
+      @ajax(
+        id:          'import_freshdesk_api_token'
+        type:        'POST'
+        url:         "#{@apiPath}/import/freshdesk/credentials_check"
+        data:        JSON.stringify(token: @freshdeskApiToken.val())
+        processData: true
+        success:     (data, status, xhr) =>
+
+          # validate form
+          if data.result is 'ok'
+            @urlStatus.attr('data-state', 'success')
+            @apiTokenErrorMessage.text('')
+            @nextStartMigration.removeClass('hide')
+          else
+            @urlStatus.attr('data-state', 'error')
+            @apiTokenErrorMessage.text(data.message_human || data.message)
+            @nextStartMigration.addClass('hide')
+
+      )
+    @delay(callback, 700, 'import_freshdesk_api_token')
+
+  showCredentials: (e) =>
+    e.preventDefault()
+    @urlStatus.attr('data-state', '')
+    @$('[data-slide=freshdesk-subdomain]').toggleClass('hide')
+    @$('[data-slide=freshdesk-credentials]').toggleClass('hide')
+
+  showImportState: =>
+    @$('[data-slide=freshdesk-subdomain]').addClass('hide')
+    @$('[data-slide=freshdesk-credentials]').addClass('hide')
+    @$('[data-slide=freshdesk-import]').removeClass('hide')
+
+  startMigration: (e) =>
+    e.preventDefault()
+    @showImportState()
+    @ajax(
+      id:          'import_start'
+      type:        'POST'
+      url:         "#{@apiPath}/import/freshdesk/import_start"
+      processData: true
+      success:     (data, status, xhr) =>
+
+        # validate form
+        if data.result is 'ok'
+          @delay(@updateMigration, 3000)
+    )
+
+  updateMigration: =>
+    @updateMigrationDisplayLoop += 1
+    @showImportState()
+    @ajax(
+      id:          'import_status'
+      type:        'GET'
+      url:         "#{@apiPath}/import/freshdesk/import_status"
+      processData: true
+      success:     (data, status, xhr) =>
+
+        if _.isEmpty(data.result) && @updateMigrationDisplayLoop > 16
+          @$('.js-error').removeClass('hide')
+          @$('.js-error').html(App.i18n.translateContent('Background process did not start or has not finished! Please contact your support.'))
+          return
+
+        if !_.isEmpty(data.result['error'])
+          @$('.js-error').removeClass('hide')
+          @$('.js-error').html(App.i18n.translateContent(data.result['error']))
+        else
+          @$('.js-error').addClass('hide')
+
+        if !_.isEmpty(data.finished_at) && _.isEmpty(data.result['error'])
+          window.location.reload()
+          return
+
+        if !_.isEmpty(data.result)
+          for model, stats of data.result
+            if stats.sum > stats.total
+              stats.sum = stats.total
+
+            element = @$('.js-' + model.toLowerCase() )
+            element.find('.js-done').text(stats.sum)
+            element.find('.js-total').text(stats.total)
+            element.find('progress').attr('max', stats.total )
+            element.find('progress').attr('value', stats.sum )
+            if stats.total <= stats.sum
+              element.addClass('is-done')
+            else
+              element.removeClass('is-done')
+        @delay(@updateMigration, 5000)
+    )
+
+App.Config.set('import/freshdesk', ImportFreshdesk, 'Routes')
+App.Config.set('freshdesk', {
+  title: 'Freshdesk'
+  name:  'Freshdesk'
+  class: 'js-freshdesk'
+  url:   '#import/freshdesk'
+}, 'ImportPlugins')

+ 2 - 5
app/assets/javascripts/app/controllers/import_zendesk.coffee

@@ -73,14 +73,13 @@ class ImportZendesk extends App.ControllerWizardFullScreen
         success:     (data, status, xhr) =>
 
           # validate form
-          console.log(data)
           if data.result is 'ok'
             @urlStatus.attr('data-state', 'success')
             @linkErrorMessage.text('')
             @nextEnterCredentials.removeClass('hide')
           else
             @urlStatus.attr('data-state', 'error')
-            @linkErrorMessage.text( data.message_human || data.message)
+            @linkErrorMessage.text( data.message_human || data.message)
             @nextEnterCredentials.addClass('hide')
 
       )
@@ -101,14 +100,13 @@ class ImportZendesk extends App.ControllerWizardFullScreen
         success:     (data, status, xhr) =>
 
           # validate form
-          console.log(data)
           if data.result is 'ok'
             @urlStatus.attr('data-state', 'success')
             @apiTokenErrorMessage.text('')
             @nextStartMigration.removeClass('hide')
           else
             @urlStatus.attr('data-state', 'error')
-            @apiTokenErrorMessage.text(data.message_human || data.message)
+            @apiTokenErrorMessage.text(data.message_human || data.message)
             @nextStartMigration.addClass('hide')
 
       )
@@ -139,7 +137,6 @@ class ImportZendesk extends App.ControllerWizardFullScreen
       success:     (data, status, xhr) =>
 
         # validate form
-        console.log(data)
         if data.result is 'ok'
           @delay(@updateMigration, 3000)
     )

+ 109 - 0
app/assets/javascripts/app/views/import/freshdesk.jst.eco

@@ -0,0 +1,109 @@
+<div class="main flex vertical centered darkBackground">
+  <%- @Icon('full-logo', 'wizard-logo') %>
+  <div class="import wizard">
+    <div class="wizard-slide vertical" data-slide="freshdesk-subdomain">
+      <h2><%- @T('%s URL', 'Freshdesk') %></h2>
+      <div class="wizard-body flex vertical justified">
+        <p>
+          <%- @T('Enter the Subdomain of your %s system', 'Freshdesk') %>:
+        </p>
+        <div class="form-group">
+          <label for="freshdesk-subdomain"><%- @T('%s Subdomain', 'Freshdesk') %></label>
+          <div class="u-positionOrigin">
+            <div class="input-group">
+              <input type="text" id="freshdesk-subdomain" class="form-control" placeholder="example" name="freshdesk-subdomain" aria-describedby="freshdesk-subdomain-addon">
+              <span class="input-group-addon" id="freshdesk-subdomain-addon"><%- @freshdeskDomain %></span>
+            </div>
+            <div class="input-feedback input-feedback--no-background centered">
+              <div class="small loading icon"></div>
+              <%- @Icon('diagonal-cross', 'icon-error') %>
+              <%- @Icon('checkmark') %>
+            </div>
+          </div>
+          <div class="error freshdesk-subdomain-error"></div>
+        </div>
+      </div>
+      <div class="wizard-controls horizontal center">
+        <a class="btn btn--text btn--secondary" href="#import"><%- @T('Go Back') %></a>
+        <div class="btn btn--primary align-right hide js-freshdesk-credentials" data-target="freshdesk-credentials"><%- @T('Enter credentials') %></div>
+      </div>
+    </div>
+
+    <div class="wizard-slide vertical hide" data-slide="freshdesk-credentials">
+      <h2><%- @T('%s credentials', 'Freshdesk') %></h2>
+      <div class="wizard-body flex vertical justified">
+        <p>
+          <a class="js-freshdeskUrlApiToken" href="https://support.freshdesk.com/support/solutions/articles/215517-how-to-find-your-api-key" target="_blank"><%- @T('Enter your %s API token gained from your account profile settings.', 'Freshdesk') %></a>
+        </p>
+        <p>
+          <%- @T('Attention: These will be your login password after the import is completed.') %>
+        </p>
+        <div class="form-group">
+          <label for="freshdesk-api-token"><%- @T('API token') %></label>
+          <div class="u-positionOrigin">
+            <input type="text" id="freshdesk-api-token" class="form-control" placeholder="XYZ3133723421111" name="freshdesk-api-token">
+            <div class="input-feedback centered">
+              <div class="small loading icon"></div>
+              <%- @Icon('diagonal-cross', 'icon-error') %>
+              <%- @Icon('checkmark') %>
+            </div>
+          </div>
+          <div class="error freshdesk-api-token-error"></div>
+        </div>
+      </div>
+      <div class="wizard-controls horizontal center">
+        <a class="btn btn--text btn--secondary" href="#import"><%- @T('Go Back') %></a>
+        <div class="btn btn--primary align-right hide js-migration-start" data-target="freshdesk-start-migration"><%- @T('Migrate %s Data', 'Freshdesk') %></div>
+      </div>
+    </div>
+
+    <div class="wizard-slide vertical hide" data-slide="freshdesk-import">
+      <h2><%- @T('%s Migration', 'Freshdesk') %></h2>
+      <div class="alert alert--danger hide js-error" role="alert"></div>
+      <div class="wizard-body flex vertical justified">
+        <table class="progressTable">
+          <tr class="js-groups">
+            <td><span class="js-done">-</span>/<span class="js-total">-</span>
+            <td><span><%- @T('Groups') %></span>
+            <td class="progressTable-progressCell">
+              <div class="horizontal center">
+                <div class="flex"><progress max="42" value="42"></progress></div>
+                <%- @Icon('checkmark') %>
+              </div>
+          </tr>
+          <tr class="js-organizations">
+            <td><span class="js-done">-</span>/<span class="js-total">-</span>
+            <td><span><%- @T('Organizations') %></span>
+            <td class="progressTable-progressCell">
+              <div class="horizontal center">
+                <div class="flex"><progress max="42" value="42"></progress></div>
+                <%- @Icon('checkmark') %>
+              </div>
+          </tr>
+          <tr class="js-users">
+            <td><span class="js-done">-</span>/<span class="js-total">-</span>
+            <td><span><%- @T('Users') %></span>
+            <td class="progressTable-progressCell">
+              <div class="horizontal center">
+                <div class="flex"><progress max="42" value="42"></progress></div>
+                <%- @Icon('checkmark') %>
+              </div>
+          </tr>
+          <tr class="js-tickets">
+            <td><span class="js-done">-</span>/<span class="js-total">-</span>
+            <td><span><%- @T('Tickets') %></span>
+            <td class="progressTable-progressCell">
+              <div class="horizontal center">
+                <div class="flex"><progress max="42" value="42"></progress></div>
+                <%- @Icon('checkmark') %>
+              </div>
+          </tr>
+        </table>
+      </div>
+      <div class="wizard-controls horizontal center">
+        <a href="#" class="btn btn--primary align-right hide js-finished"><%- @T('done') %></a>
+      </div>
+    </div>
+
+  </div>
+</div>

+ 2 - 2
app/assets/javascripts/app/views/import/zendesk.jst.eco

@@ -42,7 +42,7 @@
           </div>
           <label for="zendesk-api-token"><%- @T('API token') %></label>
           <div class="u-positionOrigin">
-            <input type="email" id="zendesk-api-token" class="form-control" placeholder="XYZ3133723421111" name="zendesk-api-token">
+            <input type="text" id="zendesk-api-token" class="form-control" placeholder="XYZ3133723421111" name="zendesk-api-token">
             <div class="input-feedback centered">
               <div class="small loading icon"></div>
               <%- @Icon('diagonal-cross', 'icon-error') %>
@@ -108,4 +108,4 @@
     </div>
 
   </div>
-</div>
+</div>

+ 8 - 0
app/assets/stylesheets/zammad.scss

@@ -9196,6 +9196,14 @@ label + .wizard-buttonList {
   width: 52px;
   border-radius: 0 5px 5px 0;
   background: linear-gradient(to right, rgba(255,255,255,0), white 33%);
+
+  &--no-background {
+    background: none;
+  }
+}
+
+.input-feedback--no-background {
+  background: none;
 }
 
 .input-feedback .icon {

+ 143 - 0
app/controllers/import_freshdesk_controller.rb

@@ -0,0 +1,143 @@
+# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
+class ImportFreshdeskController < ApplicationController
+
+  def url_check
+    return if setup_done_response
+
+    # validate
+    if params[:url].blank? || params[:url] !~ %r{^(http|https)://.+?$}
+      render json: {
+        result:  'invalid',
+        message: 'Invalid URL!',
+      }
+      return
+    end
+
+    response = UserAgent.request(params[:url])
+
+    if !response.success?
+      render json: {
+        result:        'invalid',
+        message_human: url_check_human_error_message(response.error.to_s),
+        message:       response.error.to_s,
+      }
+      return
+    end
+
+    # Check if maybe a redirect is implemented.
+    if !response.body.match?(%r{#{params[:url]}})
+      render json: {
+        result:        'invalid',
+        message_human: 'Hostname not found!',
+      }
+      return
+    end
+
+    endpoint = "#{params[:url]}/api/v2"
+    endpoint.gsub!(%r{([^:])//+}, '\\1/')
+
+    Setting.set('import_freshdesk_endpoint', endpoint)
+
+    render json: {
+      result: 'ok',
+      url:    params[:url],
+    }
+  end
+
+  def credentials_check
+    return if setup_done_response
+
+    if !params[:token]
+
+      render json: {
+        result:        'invalid',
+        message_human: 'Incomplete credentials',
+      }
+      return
+    end
+
+    Setting.set('import_freshdesk_endpoint_key', params[:token])
+
+    result = Sequencer.process('Import::Freshdesk::ConnectionTest')
+
+    if !result[:connected]
+
+      Setting.set('import_freshdesk_endpoint_key', nil)
+
+      render json: {
+        result:        'invalid',
+        message_human: 'Invalid credentials!',
+      }
+      return
+    end
+
+    render json: {
+      result: 'ok',
+    }
+  end
+
+  def import_start
+    return if setup_done_response
+
+    Setting.set('import_mode', true)
+    Setting.set('import_backend', 'freshdesk')
+
+    job = ImportJob.create(name: 'Import::Freshdesk')
+    AsyncImportJob.perform_later(job)
+
+    render json: {
+      result: 'ok',
+    }
+  end
+
+  def import_status
+    job = ImportJob.find_by(name: 'Import::Freshdesk')
+
+    if job.finished_at.present?
+      Setting.reload
+    end
+
+    model_show_render_item(job)
+  end
+
+  private
+
+  def setup_done
+    count = User.all.count()
+    done = true
+    if count <= 2
+      done = false
+    end
+    done
+  end
+
+  def setup_done_response
+    if !setup_done
+      return false
+    end
+
+    render json: {
+      setup_done: true,
+    }
+    true
+  end
+
+  def url_check_human_error_message(error)
+    translation_map = {
+      'No such file'                                              => 'Hostname not found!',
+      'getaddrinfo: nodename nor servname provided, or not known' => 'Hostname not found!',
+      'No route to host'                                          => 'No route to host!',
+      'Connection refused'                                        => 'Connection refused!',
+    }
+
+    message_human = ''
+    translation_map.each do |key, message|
+      if error.match?(%r{#{Regexp.escape(key)}}i)
+        message_human = message
+      end
+    end
+
+    message_human
+  end
+
+end

+ 1 - 0
app/controllers/import_zendesk_controller.rb

@@ -50,6 +50,7 @@ class ImportZendeskController < ApplicationController
 
     endpoint = "#{params[:url]}/api/v2"
     endpoint.gsub!(%r{([^:])//+}, '\\1/')
+
     Setting.set('import_zendesk_endpoint', endpoint)
 
     render json: {

+ 10 - 0
config/routes/import_freshdesk.rb

@@ -0,0 +1,10 @@
+Zammad::Application.routes.draw do
+  api_path = Rails.configuration.api_path
+
+  # import freshdesk
+  match api_path + '/import/freshdesk/url_check',          to: 'import_freshdesk#url_check',         via: :post
+  match api_path + '/import/freshdesk/credentials_check',  to: 'import_freshdesk#credentials_check', via: :post
+  match api_path + '/import/freshdesk/import_start',       to: 'import_freshdesk#import_start',      via: :post
+  match api_path + '/import/freshdesk/import_status',      to: 'import_freshdesk#import_status',     via: :get
+
+end

+ 38 - 0
db/seeds/settings.rb

@@ -3142,6 +3142,44 @@ Setting.create_if_not_exists(
   state:       '',
   frontend:    false
 )
+
+Setting.create_if_not_exists(
+  title:       'Import Endpoint',
+  name:        'import_freshdesk_endpoint',
+  area:        'Import::Freshdesk',
+  description: 'Defines Freshdesk endpoint to import users, ticket, states and articles.',
+  options:     {
+    form: [
+      {
+        display: '',
+        null:    false,
+        name:    'import_freshdesk_endpoint',
+        tag:     'input',
+      },
+    ],
+  },
+  state:       'https://yours.freshdesk.com/api/v2',
+  frontend:    false
+)
+Setting.create_if_not_exists(
+  title:       'Import Key for requesting the Freshdesk API',
+  name:        'import_freshdesk_endpoint_key',
+  area:        'Import::Freshdesk',
+  description: 'Defines Freshdesk endpoint authentication key.',
+  options:     {
+    form: [
+      {
+        display: '',
+        null:    false,
+        name:    'import_freshdesk_endpoint_key',
+        tag:     'input',
+      },
+    ],
+  },
+  state:       '',
+  frontend:    false
+)
+
 Setting.create_if_not_exists(
   title:       'Import Backends',
   name:        'import_backends',

Some files were not shown because too many files changed in this diff