From ebb57e75a1a61b8869852a6003e59215a84ad2c9 Mon Sep 17 00:00:00 2001 From: Suresh Kumar Date: Tue, 23 Nov 2021 19:29:24 +0530 Subject: [PATCH] added jira cloud integration removed duplicated code + event tracking + error msg string added japenese tx message + refactor refactor to use jira project hash added feature flag IRENE_SHOW_JIRA_CLOUD to show/hide connect jira button added translate messages for cloud integration show jira cloud integration section by default JIRA Cloud Integration support --- app/components/jira-account.js | 125 ---------- app/components/jira-integration/index.hbs | 157 +++++++++++++ app/components/jira-integration/index.js | 159 +++++++++++++ app/components/jira-integration/index.scss | 20 ++ app/components/jira-project.js | 218 ++++++++++-------- app/models/jira-repo.js | 5 +- app/models/organization-jiraproject.js | 5 +- .../authenticated/organization/settings.hbs | 38 +-- app/templates/components/jira-account.emblem | 34 --- config/environment.js | 9 + .../components/jira-account-test.js | 50 ---- .../components/jira-integration-test.js | 189 +++++++++++++++ translations/en.json | 5 +- translations/ja.json | 5 +- 14 files changed, 686 insertions(+), 333 deletions(-) delete mode 100644 app/components/jira-account.js create mode 100644 app/components/jira-integration/index.hbs create mode 100644 app/components/jira-integration/index.js create mode 100644 app/components/jira-integration/index.scss delete mode 100644 app/templates/components/jira-account.emblem delete mode 100644 tests/integration/components/jira-account-test.js create mode 100644 tests/integration/components/jira-integration-test.js diff --git a/app/components/jira-account.js b/app/components/jira-account.js deleted file mode 100644 index 2a70cd84e..000000000 --- a/app/components/jira-account.js +++ /dev/null @@ -1,125 +0,0 @@ -import Component from '@ember/component'; -import { inject as service } from '@ember/service'; -import { computed } from '@ember/object'; -import ENV from 'irene/config/environment'; -import { t } from 'ember-intl'; -import triggerAnalytics from 'irene/utils/trigger-analytics'; -import { task } from 'ember-concurrency'; -import lookupValidator from 'ember-changeset-validations'; -import Changeset from 'ember-changeset'; -import JIRAValidation from '../validations/jiraintegrate'; - -const JiraAccountComponent = Component.extend({ - - intl: service(), - ajax: service(), - organization: service(), - notify: service('notifications'), - user: null, - jiraHost: "", - jiraUsername: "", - jiraPassword: "", - jiraPOJO: {}, - - isRevokingJIRA: false, - isIntegratingJIRA: false, - tInValidCredentials: t("tInValidCredentials"), - tJiraIntegrated: t("jiraIntegrated"), - tJiraWillBeRevoked: t("jiraWillBeRevoked"), - tPleaseEnterAllDetails: t("pleaseEnterAllDetails"), - isJIRAConnected: false, - connectedHost: "", - connectedUsername: "", - init() { - this._super(...arguments); - const jiraPOJO = this.get('jiraPOJO'); - const changeset = new Changeset( - jiraPOJO, lookupValidator(JIRAValidation), JIRAValidation - ); - this.set('changeset', changeset); - }, - didInsertElement() { -this._super(...arguments); - this.get('checkJIRA').perform(); - }, - baseURL: computed('organization.selected.id', '', function(){ - return [ - '/api/organizations', - this.get('organization.selected.id'), ENV.endpoints.integrateJira - ].join('/') - }), - confirmCallback() { - this.get('revokeJIRA').perform(); - }, - - checkJIRA: task(function *() { - try { - const data = yield this.get("ajax").request(this.get('baseURL')); - this.set("isJIRAConnected", true); - this.set("connectedHost", data.host); - this.set("connectedUsername", data.username); - } catch(error) { - if(error.status == 404) { - this.set("isJIRAConnected", false); - } - } - }).drop(), - revokeJIRA: task(function*(){ - try { - this.send("closeRevokeJIRAConfirmBox"); - yield this.get("ajax").delete(this.get('baseURL')); - const tJiraWillBeRevoked = this.get("tJiraWillBeRevoked"); - this.get("notify").success(tJiraWillBeRevoked); - this.get('checkJIRA').perform(); - } catch(error) { - this.get("notify").error("Sorry something went wrong, please try again"); - } - }).drop(), - integrateJIRA: task(function *(changeset){ - const tJiraIntegrated = this.get("tJiraIntegrated"); - yield changeset.validate() - if(!changeset.get('isValid')) { - if(changeset.get('errors') && changeset.get('errors')[0].validation) { - this.get("notify").error( - changeset.get('errors')[0].validation[0], - ENV.notifications - ); - } - return; - } - const host = changeset.get('host').trim(); - const username = changeset.get('username').trim(); - const password = changeset.get('password'); - const data = { - host, - username, - password - }; - try { - yield this.get("ajax").post(this.get('baseURL'), {data}) - this.get('checkJIRA').perform(); - this.get("notify").success(tJiraIntegrated); - triggerAnalytics('feature',ENV.csb.integrateJIRA); - } catch(error) { - if(error.payload) { - if(error.payload.host) { - this.get("notify").error(error.payload.host[0], ENV.notifications) - } - if(error.payload.username || error.payload.password) { - this.get("notify").error(this.get('tInValidCredentials')) - } - } - } - }).drop(), - actions: { - openRevokeJIRAConfirmBox() { - this.set("showRevokeJIRAConfirmBox", true); - }, - - closeRevokeJIRAConfirmBox() { - this.set("showRevokeJIRAConfirmBox", false); - } - } -}); - -export default JiraAccountComponent; diff --git a/app/components/jira-integration/index.hbs b/app/components/jira-integration/index.hbs new file mode 100644 index 000000000..0022fcce2 --- /dev/null +++ b/app/components/jira-integration/index.hbs @@ -0,0 +1,157 @@ +
+ {{! Cloud Integration start }} + {{#if this.isJiraCloudEnabled}} +
+ {{#if (or this.isIntegratedByCloud (not this.isJIRAIntegrated))}} +
+ {{t 'jiraCloudIntegration'}} +
+ {{/if}} + {{#unless this.isJIRAIntegrated}} + + {{/unless}} +
+ {{#unless this.isJIRAIntegrated}} +
+ {{/unless}} + {{/if}} + {{! Cloud Integration end }} + {{! On-Premise Integration start}} +
+ {{#if (or this.isIntegratedByCred (not this.isJIRAIntegrated))}} +
+ {{t 'jiraOnPremiseIntegration'}} +
+ {{/if}} + {{#unless this.isJIRAIntegrated}} +
+
+ +
+
+
+ + +
+
+ +
+ {{/unless}} +
+ {{! On-Premise Integration end }} + {{! JIRA integrated info container }} + {{#if this.isJIRAIntegrated}} +
+
+ +
+ +
+ {{/if}} +
+{{! Revoke JIRA integration confirmation modal }} +{{#if this.showRevokeJIRAConfirmBox}} + +
+ {{t 'confirmBox.revokeJira'}} +
+
+ + {{t 'ok'}} + + + {{t 'cancel'}} + +
+
+{{/if}} diff --git a/app/components/jira-integration/index.js b/app/components/jira-integration/index.js new file mode 100644 index 000000000..c83425a37 --- /dev/null +++ b/app/components/jira-integration/index.js @@ -0,0 +1,159 @@ +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import ENV from 'irene/config/environment'; +import triggerAnalytics from 'irene/utils/trigger-analytics'; +import { task } from 'ember-concurrency'; +import lookupValidator from 'ember-changeset-validations'; +import Changeset from 'ember-changeset'; +import JIRAValidation from '../../validations/jiraintegrate'; +import { tracked } from '@glimmer/tracking'; +import parseError from 'irene/utils/parse-error'; + +export default class JiraIntegration extends Component { + @service intl; + @service ajax; + @service('notifications') notify; + @service organization; + + @tracked isJIRAIntegrated = false; + @tracked connectedHost = null; + @tracked connectedUsername = ''; + @tracked isIntegratedByCred = false; + @tracked isIntegratedByCloud = false; + @tracked isJiraCloudEnabled = ENV.showJiraCloud; + @tracked showRevokeJIRAConfirmBox = false; + @tracked changeset = {}; + + get baseURL() { + return [ + '/api/organizations', + this.organization.selected.id, + ENV.endpoints.integrateJira, + ].join('/'); + } + + @action + initComp() { + this.checkJIRA.perform(); + this.changeset = new Changeset( + {}, + lookupValidator(JIRAValidation), + JIRAValidation + ); + } + + @action + onRevokeJira() { + this.toggleConfirmBox(); + this.revokeJIRA.perform(); + } + + @action + toggleConfirmBox() { + this.showRevokeJIRAConfirmBox = !this.showRevokeJIRAConfirmBox; + } + + @action + onIntegrateJIRAByCred() { + this.integrateJIRAByCred.perform(); + } + + @action + onIntegrateJiraCloud() { + this.integrateJiraCloud.perform(); + } + + @task(function* () { + return yield this.ajax.request( + `/api/integrations/jira-cloud/${this.organization.selected.id}/redirect/` + ); + }) + redirectAPI; + + @task(function* () { + try { + const data = yield this.ajax.request(this.baseURL); + if (data.type == 'jira_cloud_oauth') { + this.isJIRAIntegrated = true; + this.isIntegratedByCloud = true; + this.connectedHost = data.accounts; + } else if (data.type == 'jira') { + this.isIntegratedByCred = true; + this.isJIRAIntegrated = true; + this.connectedHost = data.host; + this.connectedUsername = data.username; + } + } catch (error) { + if (error.status == 404) { + this.isJIRAIntegrated = false; + } + } + }) + checkJIRA; + + @task(function* () { + try { + yield this.ajax.delete(this.baseURL); + this.notify.success(this.intl.t('jiraWillBeRevoked')); + this.checkJIRA.perform(); + } catch (error) { + this.notify.error( + parseError(error), + 'Sorry something went wrong, please try again' + ); + } + }) + revokeJIRA; + + @task(function* () { + yield this.changeset.validate(); + if (!this.changeset.get('isValid')) { + if ( + this.changeset.get('errors') && + this.changeset.get('errors')[0].validation + ) { + this.notify.error( + this.changeset.get('errors')[0].validation, + ENV.notifications + ); + } + return; + } + const host = this.changeset.get('host').trim(); + const username = this.changeset.get('username').trim(); + const password = this.changeset.get('password'); + const data = { + host, + username, + password, + }; + try { + yield this.ajax.post(this.baseURL, { data }); + this.checkJIRA.perform(); + this.notify.success(this.intl.t('jiraIntegrated')); + triggerAnalytics('feature', ENV.csb.integrateJIRA); + } catch (error) { + if (error.payload) { + if (error.payload.host) { + this.notify.error(error.payload.host[0], ENV.notifications); + } + if (error.payload.username || error.payload.password) { + this.notify.error(this.intl.t('inValidCredentials')); + } + } + } + }) + integrateJIRAByCred; + + @task(function* () { + try { + triggerAnalytics('feature', ENV.csb.integrateJIRACloud); + let data = yield this.redirectAPI.perform(); + window.location.href = data.url; + } catch (err) { + this.notify.error(this.intl.t('tJiraErrorIntegration')); + } + }) + integrateJiraCloud; +} diff --git a/app/components/jira-integration/index.scss b/app/components/jira-integration/index.scss new file mode 100644 index 000000000..31d7aa23f --- /dev/null +++ b/app/components/jira-integration/index.scss @@ -0,0 +1,20 @@ +.jira-integration-subtitle { + font-weight: 700; + font-size: 1.2em; + margin-bottom: 0.8em; +} + +.subsection-divider { + margin-top: 0.8em; + margin-bottom: 0.8em; + display: inline-block; +} + +.confirm-box-action-btns { + display: flex; + column-gap: 1em; +} + +.confirm-box-label { + margin-bottom: 1em; +} diff --git a/app/components/jira-project.js b/app/components/jira-project.js index ab3ff47a2..c74b8a5fd 100644 --- a/app/components/jira-project.js +++ b/app/components/jira-project.js @@ -6,185 +6,205 @@ import { t } from 'ember-intl'; import ENUMS from 'irene/enums'; import { on } from '@ember/object/evented'; - const JiraProjectComponent = Component.extend({ intl: service(), ajax: service(), notify: service('notifications'), project: null, jiraProjects: null, - tIntegratedJIRA: t("integratedJIRA"), - tProjectRemoved: t("projectRemoved"), - tRepoNotIntegrated: t("repoNotIntegrated"), - tFetchJIRAProjectFailed: t("fetchProjectFailed"), - thresholds: computed(function(){ - return ENUMS.THRESHOLD.CHOICES.filter(c => c.key !== 'UNKNOWN').map(c => c.value) + tIntegratedJIRA: t('integratedJIRA'), + tProjectRemoved: t('projectRemoved'), + tRepoNotIntegrated: t('repoNotIntegrated'), + tFetchJIRAProjectFailed: t('fetchProjectFailed'), + thresholds: computed(function () { + return ENUMS.THRESHOLD.CHOICES.filter((c) => c.key !== 'UNKNOWN').map( + (c) => c.value + ); }), selectedThreshold: ENUMS.THRESHOLD.LOW, noIntegration: false, noAccess: false, currentJiraProject: null, selectedRepo: null, - tInvalidRepo: t("invalidProject"), - tInvalidRisk: t("tInvalidRisk"), + tInvalidRepo: t('invalidProject'), + tInvalidRisk: t('tInvalidRisk'), hasJIRAProject: computed.gt('jiraProjects.length', 0), - setCurrentJiraRepo: task(function *(){ - return yield this.get("store").findRecord( - 'jira-repo', this.get('project.id')); + setCurrentJiraRepo: task(function* () { + return yield this.get('store').findRecord( + 'jira-repo', + this.get('project.id') + ); }).evented(), - setCurrentJiraRepoErrored: on('setCurrentJiraRepo:errored', function(_, err){ - if (err.errors[0].detail && err.errors[0].detail==="JIRA not integrated"){ - this.set('noIntegration', true); - return - } - if (err.errors[0].detail && err.errors[0].detail==="JIRA integration failed"){ - this.set('reconnect', true); - return + setCurrentJiraRepoErrored: on( + 'setCurrentJiraRepo:errored', + function (_, err) { + if ( + err.errors[0].detail && + err.errors[0].detail === 'JIRA not integrated' + ) { + this.set('noIntegration', true); + return; + } + if ( + err.errors[0].detail && + err.errors[0].detail === 'JIRA integration failed' + ) { + this.set('reconnect', true); + return; + } } - this.get("notify").error(this.get('tFetchJIRAProjectFailed')); - }), + ), - setCurrentJiraRepoSucceded: on('setCurrentJiraRepo:succeeded', function(instance){ - this.set('currentJiraProject', instance.value) - this.set('selectedRepo',{ + setCurrentJiraRepoSucceded: on( + 'setCurrentJiraRepo:succeeded', + function (instance) { + this.set('currentJiraProject', instance.value); + this.set('selectedRepo', { key: instance.value.get('project_key'), - name: instance.value.get('project_name') - }) - }), + name: instance.value.get('project_name'), + jira_cloud_project_hash: instance.value.get('jira_cloud_project_hash'), + }); + } + ), didInsertElement() { -this._super(...arguments); + this._super(...arguments); this.get('fetchJIRAProjects').perform(); this.get('setCurrentJiraRepo').perform(); }, - fetchJIRAProjects: task(function* (){ + fetchJIRAProjects: task(function* () { this.set('noAccess', false); this.set('noIntegration', false); this.set('jiraProjects', null); - try{ + try { const jiraprojects = yield this.get('store').query( - 'organizationJiraproject', {} + 'organizationJiraproject', + {} ); this.set('jiraProjects', jiraprojects); } catch (error) { - if(error.errors) { + console.log('error', error); + if (error.errors) { const status = error.errors[0].status; - if(status == 403) { + if (status == 403) { this.set('noAccess', true); - return - } else if(status == 404) { + return; + } else if (status == 404) { this.set('noIntegration', true); - return + return; } throw error; } } }), - deleteRepo: task(function *(){ - return yield this.get('currentJiraProject').destroyRecord() + deleteRepo: task(function* () { + return yield this.get('currentJiraProject').destroyRecord(); }).evented(), - deleteRepoErrored: on('deleteRepo:errored', function(_, err){ - this.get("notify").error(err.payload.detail); - this.send("closeDeleteJIRAConfirmBox"); + deleteRepoErrored: on('deleteRepo:errored', function (_, err) { + this.get('notify').error(err.payload.detail); + this.send('closeDeleteJIRAConfirmBox'); }), - deleteRepoSucceded: on('deleteRepo:succeeded', function(instance){ - const tProjectRemoved = this.get("tProjectRemoved"); - this.get('store')._removeFromIdMap(instance.value._internalModel) + deleteRepoSucceded: on('deleteRepo:succeeded', function () { + const tProjectRemoved = this.get('tProjectRemoved'); this.get('currentJiraProject').unloadRecord(); - this.get("notify").success(tProjectRemoved); - this.send("closeDeleteJIRAConfirmBox"); + this.get('notify').success(tProjectRemoved); + this.send('closeDeleteJIRAConfirmBox'); this.set('currentJiraProject', null); this.set('selectedRepo', null); this.set('selectedThreshold', 1); }), - confirmCallback(){ + confirmCallback() { this.get('deleteRepo').perform(); }, - selectProject: task( function *(){ + selectProject: task(function* () { let jiraProject = this.get('currentJiraProject'); - if (jiraProject){ - jiraProject.setProperties( - { - project_key: this.get('selectedRepo.key'), - project_name: this.get('selectedRepo.name'), - risk_threshold: this.get('selectedThreshold') - } - ); - }else{ - jiraProject = this.get('store').createRecord( - 'jira-repo',{ - id: this.get('project.id'), - project_key: this.get('selectedRepo.key'), - project_name:this.get('selectedRepo.name'), - risk_threshold: this.get('selectedThreshold'), - project: this.get('project') - } - ) + if (jiraProject) { + jiraProject.setProperties({ + project_key: this.get('selectedRepo.key'), + project_name: this.get('selectedRepo.name'), + risk_threshold: this.get('selectedThreshold'), + jira_cloud_project_hash: this.get( + 'selectedRepo.jira_cloud_project_hash' + ), + }); + } else { + jiraProject = this.get('store').createRecord('jira-repo', { + id: this.get('project.id'), + project_key: this.get('selectedRepo.key'), + project_name: this.get('selectedRepo.name'), + risk_threshold: this.get('selectedThreshold'), + project: this.get('project'), + jira_cloud_project_hash: this.get( + 'selectedRepo.jira_cloud_project_hash' + ), + }); } - try{ + try { yield jiraProject.save(); - this.set("currentJiraProject",jiraProject); - this.get("notify").success(this.get('tIntegratedJIRA')); - yield this.set("showEditJiraModal", false ) - }catch(error){ + this.set('currentJiraProject', jiraProject); + this.get('notify').success(this.get('tIntegratedJIRA')); + yield this.set('showEditJiraModal', false); + } catch (error) { yield jiraProject.rollbackAttributes(); - yield this.get('store')._removeFromIdMap(jiraProject._internalModel) - throw error + yield this.get('store')._removeFromIdMap(jiraProject._internalModel); + throw error; } }).evented(), - selectProjectErrored: on('selectProject:errored', function(_, error){ - if (error.errors[0].detail==="JIRA not integrated"){ - this.set("showEditJiraModal", false ) + selectProjectErrored: on('selectProject:errored', function (_, error) { + if (error.errors[0].detail === 'JIRA not integrated') { + this.set('showEditJiraModal', false); this.set('jiraProjects', null); this.set('noIntegration', true); - this.set("currentJiraProject", null); - this.get("notify").error(error.errors[0].detail); - return + this.set('currentJiraProject', null); + this.get('notify').error(error.errors[0].detail); + return; } - if (error.errors[0].source.pointer==="/data/attributes/project_key" || - error.errors[0].source.pointer==="/data/attributes/project_key"){ - this.get("notify").error(this.get('tInvalidRepo')) - return - } - if (error.errors[0].source.pointer==="/data/attributes/risk_threshold"){ - this.get("notify").error(this.get('tInvalidRisk')) - return + if ( + error.errors[0].source.pointer === '/data/attributes/project_key' || + error.errors[0].source.pointer === '/data/attributes/project_key' + ) { + this.get('notify').error(this.get('tInvalidRepo')); + return; } - this.get("notify").error(error.errors[0].detail) + if (error.errors[0].source.pointer === '/data/attributes/risk_threshold') { + this.get('notify').error(this.get('tInvalidRisk')); + return; + } + this.get('notify').error(error.errors[0].detail); }), - editJiraRepoModal: task(function *(){ - yield this.set("showEditJiraModal", true ) + editJiraRepoModal: task(function* () { + yield this.set('showEditJiraModal', true); }), - closeJiraRepoModal: task(function *(){ - yield this.set("showEditJiraModal", false ) + closeJiraRepoModal: task(function* () { + yield this.set('showEditJiraModal', false); }), - selectRepo: task(function *(repo){ + selectRepo: task(function* (repo) { yield this.set('selectedRepo', repo.toJSON()); }), - selectThreshold: task(function *(threshold){ + selectThreshold: task(function* (threshold) { yield this.set('selectedThreshold', threshold); }), actions: { openDeleteJIRAConfirmBox() { - this.set("showDeleteJIRAConfirmBox", true); + this.set('showDeleteJIRAConfirmBox', true); }, closeDeleteJIRAConfirmBox() { - this.set("showDeleteJIRAConfirmBox", false); - } - } + this.set('showDeleteJIRAConfirmBox', false); + }, + }, }); export default JiraProjectComponent; diff --git a/app/models/jira-repo.js b/app/models/jira-repo.js index 061559bff..25d110009 100644 --- a/app/models/jira-repo.js +++ b/app/models/jira-repo.js @@ -1,8 +1,9 @@ -import Model, { attr, belongsTo } from '@ember-data/model'; +import Model, { attr, belongsTo } from '@ember-data/model'; export default Model.extend({ project: belongsTo('project'), project_key: attr("string"), project_name: attr("string"), - risk_threshold: attr('number') + risk_threshold: attr('number'), + jira_cloud_project_hash: attr("string", { defaultValue: null }), }); diff --git a/app/models/organization-jiraproject.js b/app/models/organization-jiraproject.js index 8fbc33ce7..551a80bcb 100644 --- a/app/models/organization-jiraproject.js +++ b/app/models/organization-jiraproject.js @@ -1,6 +1,7 @@ -import Model, { attr } from '@ember-data/model'; +import Model, { attr } from '@ember-data/model'; export default Model.extend({ key: attr('string'), - name: attr('string') + name: attr('string'), + jira_cloud_project_hash: attr("string", { defaultValue: null }), }); diff --git a/app/templates/authenticated/organization/settings.hbs b/app/templates/authenticated/organization/settings.hbs index b5140bbf8..d867f8fae 100644 --- a/app/templates/authenticated/organization/settings.hbs +++ b/app/templates/authenticated/organization/settings.hbs @@ -1,36 +1,36 @@ {{page-title 'Settings'}} -
+
{{#if @model.organization.features.sso}} -
+
{{/if}} -
-
-
+
+
+
{{t 'emailDomainRestriction'}}
-
+
-
-
-
-
+
+
+
+
{{t 'jiraIntegration'}}
- +
-
-
-
-
+
+
+
+
{{t 'githubIntegration'}}
-
-
-
+
+
+
-
\ No newline at end of file +
diff --git a/app/templates/components/jira-account.emblem b/app/templates/components/jira-account.emblem deleted file mode 100644 index a03a49664..000000000 --- a/app/templates/components/jira-account.emblem +++ /dev/null @@ -1,34 +0,0 @@ -if isJIRAConnected - .integration.integration-jira - .integration-logo-container - img.integration-logo src="/images/jira-icon.png" - .integration-account-container - .integration-account - div - .text-lightgray.padding-b-q - | #{connectedHost} - .black-text - | #{connectedUsername} - button.is-primary.mp-jira-revoke click="openRevokeJIRAConfirmBox" - = t "disconnect" -else - if checkJIRA.isRunning - i.fa class="fa-spinner fa-spin margin-r-h" - | #{t "loading"}... - else - form - .input-wrap - = input class="input-field" classNameBindings="changeset.error.host:has-error" placeholder=(t "jiraHost") type="text" value=changeset.host - .input-wrap - .half-wrap - = input class="input-field" classNameBindings="changeset.error.username:has-error" placeholder=(t "username") type="text" value=changeset.username - = input class="input-field" classNameBindings="changeset.error.password:has-error" placeholder=(t "apiKey") type="password" value=changeset.password autocomplete="jira-password" - - button.is-primary click={(perform integrateJIRA changeset)} class="mp-jira-integrate" disabled=isIntegratingJIRA - if integrateJIRA.isRunning - .fa-font-size - i.fa class="fa-spinner fa-spin" - |   - = t "integrateJIRA" - -= confirm-box isActive=showRevokeJIRAConfirmBox title=(t 'confirmBox.revokeJira') delegate=this disabled=isRevokingJIRA diff --git a/config/environment.js b/config/environment.js index ea9712183..ed3e520b0 100644 --- a/config/environment.js +++ b/config/environment.js @@ -13,6 +13,7 @@ const possibleENVS = [ 'WHITELABEL_LOGO', 'WHITELABEL_THEME', 'WHITELABEL_FAVICON', + 'IRENE_SHOW_JIRA_CLOUD', ]; const ENVHandlerCONST = { @@ -31,6 +32,7 @@ const ENVHandlerCONST = { WHITELABEL_NAME: '', WHITELABEL_LOGO: '', WHITELABEL_THEME: 'dark', + IRENE_SHOW_JIRA_CLOUD: true, }, processENV: Object.keys(process.env).reduce((acc, key) => { @@ -144,6 +146,7 @@ module.exports = function (environment) { var devicefarmHost = handler.getEnv('IRENE_DEVICEFARM_HOST'); var isEnterprise = handler.getBoolean('ENTERPRISE'); var showLicense = handler.getBoolean('IRENE_SHOW_LICENSE'); + var showJiraCloud = handler.getBoolean('IRENE_SHOW_JIRA_CLOUD'); var ENV = { ENVHandlerCONST: ENVHandlerCONST, version: Date.now(), @@ -151,6 +154,7 @@ module.exports = function (environment) { isAppknox: false, isEnterprise: isEnterprise, showLicense: showLicense, + showJiraCloud: showJiraCloud, exportApplicationGlobal: true, devknoxPrice: 9, // This should also change in `mycroft/settings.py` platform: -1, @@ -469,6 +473,11 @@ module.exports = function (environment) { module: 'Report', product: 'Appknox', }, + integrateJIRACloud: { + feature: 'Integrate JIRA Cloud', + module: 'Report', + product: 'Appknox', + }, changePassword: { feature: 'Change Password', module: 'Setup', diff --git a/tests/integration/components/jira-account-test.js b/tests/integration/components/jira-account-test.js deleted file mode 100644 index 33f3b1e2d..000000000 --- a/tests/integration/components/jira-account-test.js +++ /dev/null @@ -1,50 +0,0 @@ -// import { getOwner } from '@ember/application'; -// import tHelper from 'ember-i18n/helper'; -// import localeConfig from 'ember-i18n/config/en'; -// import { test, moduleForComponent } from 'ember-qunit'; -// import { startMirage } from 'irene/initializers/ember-cli-mirage'; -// import { run } from '@ember/runloop'; -// -// moduleForComponent('jira-account', 'Integration | Component | jira account', { -// unit: true, -// needs: [ -// 'service:i18n', -// 'service:ajax', -// 'service:notifications', -// 'service:session', -// 'locale:en/translations', -// 'locale:en/config', -// 'util:i18n/missing-message', -// 'util:i18n/compile-template', -// 'config:environment' -// ], -// beforeEach() { -// // set the locale and the config -// getOwner(this).lookup('service:i18n').set('locale', 'en'); -// this.register('locale:en/config', localeConfig); -// -// // register t helper -// this.register('helper:t', tHelper); -// -// // start Mirage -// this.server = startMirage(); -// }, -// afterEach() { -// // shutdown Mirage -// this.server.shutdown(); -// } -// }); -// -// test('tapping button fires an external action', function(assert) { -// -// var component = this.subject(); -// -// run(function() { -// component.send('openRevokeJIRAConfirmBox'); -// assert.equal(component.get('showRevokeJIRAConfirmBox'),true, "Open Modal"); -// component.send('closeRevokeJIRAConfirmBox'); -// assert.equal(component.get('showRevokeJIRAConfirmBox'),false, "Close Modal"); -// -// assert.equal(component.confirmCallback(),undefined, "Confirm Callback"); -// }); -// }); diff --git a/tests/integration/components/jira-integration-test.js b/tests/integration/components/jira-integration-test.js new file mode 100644 index 000000000..dc7dda0fb --- /dev/null +++ b/tests/integration/components/jira-integration-test.js @@ -0,0 +1,189 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render, click } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { setupIntl } from 'ember-intl/test-support'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import ENV from 'irene/config/environment'; +import Service from '@ember/service'; + +class NotificationsStub extends Service { + errorMsg = null; + successMsg = null; + error(msg) { + return (this.errorMsg = msg); + } + success(msg) { + return (this.successMsg = msg); + } +} + +module('Integration | Component | jira-integration', function (hooks) { + setupRenderingTest(hooks); + setupMirage(hooks); + setupIntl(hooks); + + hooks.beforeEach(async function () { + await this.server.createList('organization', 2); + await this.owner.lookup('service:organization').load(); + this.owner.register('service:notifications', NotificationsStub); + }); + + test('it should render on-premise integration', async function (assert) { + ENV.showJiraCloud = false; + this.server.get('organizations/:orgId/integrate_jira', () => { + return Response(404); + }); + await render(hbs``); + assert.dom(`[data-test-jira-cloud-integration]`).doesNotExist(); + assert.dom(`[data-test-jira-on-premise-integration-title]`).exists(); + assert + .dom(`[data-test-jira-on-premise-integration-title]`) + .hasText(`t:jiraOnPremiseIntegration:()`); + assert.dom(`[data-test-jira-on-premise-integration-form]`).exists(); + assert.dom(`[data-test-jira-integrated]`).doesNotExist(); + }); + + test('it should render both cloud & on-premise integration', async function (assert) { + ENV.showJiraCloud = true; + await render(hbs``); + + assert.dom(`[data-test-jira-cloud-integration]`).exists(); + assert.dom(`[data-test-jira-on-premise-integration-title]`).exists(); + assert + .dom(`[data-test-jira-cloud-integration-title]`) + .hasText(`t:jiraCloudIntegration:()`); + assert.dom(`[data-test-jira-cloud-integration-btn]`).exists(); + assert.dom(`[data-test-jira-integrated]`).doesNotExist(); + }); + + test('it should render jira cloud integrated container', async function (assert) { + ENV.showJiraCloud = true; + this.server.get('organizations/:orgId/integrate_jira', () => { + return { + type: 'jira_cloud_oauth', + accounts: 'http://test.com', + }; + }); + await render(hbs``); + + assert.dom(`[data-test-jira-on-premise-integration-title]`).doesNotExist(); + assert.dom(`[data-test-jira-cloud-integration-title]`).exists(); + assert.dom(`[data-test-jira-cloud-integration-btn]`).doesNotExist(); + assert.dom(`[data-test-jira-integrated-host]`).hasText('http://test.com'); + }); + + test('it should render jira on-premise integrated container', async function (assert) { + this.server.get('organizations/:orgId/integrate_jira', () => { + return { + type: 'jira', + host: 'http://test.com', + username: 'testuser', + }; + }); + await render(hbs``); + + assert.dom(`[data-test-jira-on-premise-integration-title]`).exists(); + assert.dom(`[data-test-jira-cloud-integration-title]`).doesNotExist(); + assert.dom(`[data-test-jira-cloud-integration-btn]`).doesNotExist(); + assert.dom(`[data-test-jira-integrated-host]`).hasText('http://test.com'); + assert.dom(`[data-test-jira-integrated-username]`).hasText('testuser'); + }); + + test('it should render jira integrated container', async function (assert) { + this.server.get('organizations/:orgId/integrate_jira', () => { + return { + type: 'jira', + host: 'http://test.com', + username: 'testuser', + }; + }); + await render(hbs``); + + assert + .dom(`[data-test-jira-logo]`) + .hasAttribute('src', '/images/jira-icon.png'); + assert.dom(`[data-test-jira-integrated-host]`).hasText('http://test.com'); + assert.dom(`[data-test-jira-integrated-username]`).hasText('testuser'); + assert.dom(`[data-test-jira-disconnect-btn]`).hasText(`t:disconnect:()`); + }); + + test('it should handle disconnect jira integration', async function (assert) { + this.server.get('organizations/:orgId/integrate_jira', () => { + return { + type: 'jira', + host: 'http://test.com', + username: 'testuser', + }; + }); + + this.server.delete('organizations/:orgId/integrate_jira', () => { + return {}; + }); + await render(hbs``); + + assert.dom(`[data-test-jira-disconnect-confirm-label]`).doesNotExist(); + await click(`[data-test-jira-disconnect-btn]`); + assert.dom(`[data-test-jira-disconnect-confirm-label]`).exists(); + assert + .dom(`[data-test-jira-disconnect-confirm-label]`) + .hasText(`t:confirmBox.revokeJira:()`); + assert.dom(`[data-test-jira-disconnect-confirm-ok-btn]`).hasText(`t:ok:()`); + assert + .dom(`[data-test-jira-disconnect-confirm-cancel-btn]`) + .hasText(`t:cancel:()`); + + await click(`[data-test-jira-disconnect-confirm-ok-btn]`); + this.notifyService = this.owner.lookup('service:notifications'); + assert.equal( + this.notifyService.get('successMsg'), + `t:jiraWillBeRevoked:()` + ); + assert.dom(`[data-test-jira-disconnect-confirm-label]`).doesNotExist(); + }); + + test('it should close confirm modal while clicking at cancel btn', async function (assert) { + this.server.get('organizations/:orgId/integrate_jira', () => { + return { + type: 'jira', + host: 'http://test.com', + username: 'testuser', + }; + }); + + await render(hbs``); + + assert.dom(`[data-test-jira-disconnect-confirm-label]`).doesNotExist(); + await click(`[data-test-jira-disconnect-btn]`); + assert.dom(`[data-test-jira-disconnect-confirm-label]`).exists(); + + await click(`[data-test-jira-disconnect-confirm-cancel-btn]`); + + assert.dom(`[data-test-jira-disconnect-confirm-label]`).doesNotExist(); + }); + + test('it should handle disconnect jira integration failed', async function (assert) { + this.server.get('organizations/:orgId/integrate_jira', () => { + return { + type: 'jira', + host: 'http://test.com', + username: 'testuser', + }; + }); + + this.server.delete('organizations/:orgId/integrate_jira', () => { + return Response(500); + }); + await render(hbs``); + + await click(`[data-test-jira-disconnect-btn]`); + + await click(`[data-test-jira-disconnect-confirm-ok-btn]`); + this.notifyService = this.owner.lookup('service:notifications'); + assert.equal( + this.notifyService.get('errorMsg'), + 'Request was rejected due to server error' + ); + assert.dom(`[data-test-jira-disconnect-confirm-label]`).doesNotExist(); + }); +}); diff --git a/translations/en.json b/translations/en.json index 448ab28a2..fbf57ff05 100644 --- a/translations/en.json +++ b/translations/en.json @@ -313,6 +313,7 @@ "idpMetadataUpload": "Upload your IdP Metadata XML file", "impact": "Impact", "improved": "Improved", + "inValidCredentials": "Invalid credentials", "inactive": "inactive", "inactiveCaptital": "Inactive", "includeInactiveMembers": "Include inactive members", @@ -356,10 +357,13 @@ "jaHTMLReport": "JA HTML Report", "jenkinsPipeline": "Jenkins Pipeline", "jira": "Jira", + "jiraCloudIntegration": "JIRA Cloud Integration", + "jiraErrorIntegration": "Intergration failed, please contact support", "jiraHost": "Jira Host", "jiraIntegrated": "JIRA integrated", "jiraIntegration": "JIRA Integration", "jiraNoProject": "Linked JIRA account doesn't have any projects", + "jiraOnPremiseIntegration": "JIRA On-Premise Integration", "jiraPageConfigure": "page to configure JIRA integration", "jiraWillBeRevoked": "Your JIRA authorization will be revoked in a moment", "language": "Language", @@ -812,7 +816,6 @@ "sync": "Sync More", "system": "System", "systemStatus": "System Status", - "tInValidCredentials": "Invalid credentials", "tablet": "Tablet", "team": "Team", "teamCreated": "Team Created Successfully", diff --git a/translations/ja.json b/translations/ja.json index 1854adf54..8f8d7ef67 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -313,6 +313,7 @@ "idpMetadataUpload": "Upload your IdP Metadata XML file", "impact": "Impact", "improved": "改善", + "inValidCredentials": "Invalid credentials", "inactive": "inactive", "inactiveCaptital": "Inactive", "includeInactiveMembers": "Include inactive members", @@ -356,10 +357,13 @@ "jaHTMLReport": "JA HTML Report", "jenkinsPipeline": "Jenkins Pipeline", "jira": "JIRA", + "jiraCloudIntegration": "JIRA Cloud Integration", + "jiraErrorIntegration": "Intergration failed, please contact support", "jiraHost": "JIRAホスト", "jiraIntegrated": "JIRAと連携されました", "jiraIntegration": "JIRA連携", "jiraNoProject": "Linked JIRA account doesn't have any projects", + "jiraOnPremiseIntegration": "JIRA On-Premise Integration", "jiraPageConfigure": "JIRA連携設定ページ", "jiraWillBeRevoked": "JIRA認証は直ちに取り消されます", "language": "言語", @@ -812,7 +816,6 @@ "sync": "同期する", "system": "System", "systemStatus": "System Status", - "tInValidCredentials": "Invalid credentials", "tablet": "タブレット", "team": "チーム", "teamCreated": "チームが作成されました",