Skip to content
This repository has been archived by the owner on Oct 9, 2020. It is now read-only.

Commit

Permalink
Merge pull request #2 from CCristi/master
Browse files Browse the repository at this point in the history
Add [readonly] jira integration
  • Loading branch information
AlexanderC authored Feb 19, 2018
2 parents 677319d + 9ed22be commit 918bf30
Show file tree
Hide file tree
Showing 9 changed files with 287 additions and 16 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
}
},
"dependencies": {
"axios": "^0.17.1",
"cron": "^1.3.0",
"d3-timelines": "^1.3.1",
"desktop-idle": "^1.1.1",
Expand Down
40 changes: 25 additions & 15 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Registry from './registry';
import DB from './db';
import Env from './env';
import RedmineProvider from './remote/redmine';
import JiraProvider from './remote/jira';
import IdleMonitor from './idle-monitor';
import Notifier from './notifier';
import Cron from './cron';
Expand Down Expand Up @@ -51,6 +52,22 @@ if (Env.isDebug()) {
}
})();

const createProviderSyncCommand = (providerName, createProvider) => async (...args) => {
const provider = createProvider(...args);

if (!cron.exists('jira')) {
cron.add(
'jira',
registry.config().get('remoteSyncCron'),
() => provider.synchronize().then(() => provider.report(registry.config().get('minLogTime'))),
true,
);
}

await provider.synchronize();
await provider.report(registry.config().get('minLogTime'));
};

registry
.register('logger', logger)
.register('events', events)
Expand All @@ -61,21 +78,14 @@ registry
app.dock.setBadge(text);
}
})
.register('synchronizeRedmine', async (host, apiKey) => {
const redmine = new RedmineProvider(db, { host, apiKey });

if (!cron.exists('redmine')) {
cron.add(
'redmine',
registry.config().get('remoteSyncCron'),
() => redmine.synchronize().then(() => redmine.report(registry.config().get('minLogTime'))),
true,
);
}

await redmine.synchronize();
await redmine.report(registry.config().get('minLogTime'));
})
.register('synchronizeRedmine', createProviderSyncCommand(
'jira',
(host, apiKey) => new RedmineProvider(db, { host, apiKey }),
))
.register('synchronizeJira', createProviderSyncCommand(
'jira',
(host, username, password) => new JiraProvider(db, { host, username, password }),
))
.register('notify', Notifier.notify)
.register('openReport', async (projectId) => {
registry.config().set('lastReportProjectId', projectId);
Expand Down
7 changes: 7 additions & 0 deletions src/invariant.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function invariant(condition, message, ...args) {
if (!condition) {
let idx = 0;

throw new Error(message.replace(/%s/g, () => args[idx++])); // eslint-disable-line no-plusplus
}
}
64 changes: 64 additions & 0 deletions src/remote/jira.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import url from 'url';
import AbstractProvider from './abstract-provider';
import Logger from '../logger';
import JiraClient from './rest/client/jira-client';
import BasicAuthProvider from './rest/auth/basic-auth-provider';

export default class JiraProvider extends AbstractProvider {
setupClient() {
Logger.debug(`Connect to Jira on ${this.options.host}`);

JiraClient.updateOptions({
authProvider: new BasicAuthProvider(
this.options.username,
this.options.password,
),
baseURL: this.options.host,
});
}

generateId(issue) { // eslint-disable-line
return `JIRA-${issue.id}`;
}

generateLink(issue) { // eslint-disable-line
return url.resolve(this.options.host, `/browse/${issue.key}`);
}

generateName(issue) { // eslint-disable-line
return `[${issue.key}] ${issue.fields.summary}`;
}

async synchronize() {
this.setupClient();

const response = await JiraClient.search({
jql: 'assignee=currentuser() AND status!=closed',
});

const itemsToLookup = response.data.issues.map(issue => ({
projectId: issue.key,
name: this.generateName(issue),
issueId: this.generateId(issue),
link: this.generateLink(issue),
}));

itemsToLookup.forEach((item) => {
const { issueId } = item;

const existingItem = this.db.findOne({ issueId });

if (!existingItem) {
Logger.info(`Add new Jira issue: ${item.name} #${issueId}`);

this.db.save(item);
}
});
}

async report(minLogTime = 0) { // eslint-disable-line
Logger.info(`JiraProvider::report(${minLogTime})`);

return Promise.resolve();
}
}
5 changes: 5 additions & 0 deletions src/remote/rest/auth/anon-provider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default class AnonProvider {
sign(requestConfig) { // eslint-disable-line
return requestConfig;
}
}
16 changes: 16 additions & 0 deletions src/remote/rest/auth/basic-auth-provider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export default class BasicAuthProvider {
constructor(username, password) {
this.username = username;
this.password = password;
}

sign(requestConfig) {
const signedRequestConfig = Object.assign({}, requestConfig);

signedRequestConfig.headers = Object.assign(signedRequestConfig.headers || {}, {
Authorization: `Basic ${new Buffer(`${this.username}:${this.password}`).toString('base64')}`,
});

return signedRequestConfig;
}
}
28 changes: 28 additions & 0 deletions src/remote/rest/client/jira-client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import RestClient from './rest-client';

/**
* @callback ApiMethod
* @param {Object=} payload
* @returns {Promise<{status: Number, headers: Object, data: Object}>}
*/

/**
* @typedef {RestClient} JiraRestClient
*
* @property {ApiMethod} getIssue
* @property {ApiMethod} search
*
* @type {JiraRestClient}
*/
const JiraClient = new RestClient({
getIssue: {
method: 'GET',
path: 'rest/api/2/issue/{issueId}',
},
search: {
method: 'GET',
path: '/rest/api/2/search',
},
});

export default JiraClient;
72 changes: 72 additions & 0 deletions src/remote/rest/client/rest-client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import axios from 'axios';
import invariant from '../../../invariant';
import AnonProvider from '../auth/anon-provider';

export default class RestClient {
constructor(methodsMap = {}, options = {}) {
this.options = options;
this.options.authProvider = Object.assign(RestClient.defaults, options);

this.createApiMethods(methodsMap);
}

// @TODO: add options schema
updateOptions(newOptions) {
this.options = Object.assign(this.options, newOptions);
}

createApiMethods(methodsMap) {
Object.keys(methodsMap).forEach((methodName) => {
const methodDefinition = methodsMap[methodName];

this[methodName] = this.createApiMethod(methodDefinition);
});
}

createApiMethod(methodDefinition) {
return (payload = {}, overrideOptions = {}) => {
const options = this.options;
const requestConfig = Object.assign({
baseURL: options.baseURL,
method: methodDefinition.method,
adapter: methodDefinition.adapter,
responseType: 'json',
url: this.buildUrl(methodDefinition.path, payload),
[this.getParametersSendType(methodDefinition)]: payload,
}, overrideOptions);

invariant(options.baseURL, 'You should configure API baseURL before making any api calls');
invariant(options.authProvider, 'Missing authentication provider');

const signedRequestConfig = this.options.authProvider.sign(requestConfig);

return axios.request(signedRequestConfig);
};
}

buildUrl(urlTemplate, params = {}) { // eslint-disable-line
return urlTemplate.replace(/{\s*([^/]+)\s*}/g, (match, paramName) => {
invariant(
paramName in params,
'Missing "%s" parameter. Cannot build "%s" url template',
paramName,
urlTemplate,
);

const paramValue = params[paramName];
delete params[paramName]; // eslint-disable-line no-param-reassign

return paramValue.toString();
});
}

getParametersSendType(endpointDefinition) { // eslint-disable-line
return ['GET', 'HEAD'].includes(endpointDefinition.method.toUpperCase()) ? 'params' : 'data';
}

static get defaults() {
return {
authProvider: new AnonProvider(),
};
}
}
70 changes: 69 additions & 1 deletion src/view/component/projects.vue
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,11 @@
</md-button>
<md-button @click.prevent.stop="redmineDialog = true">
<md-icon>bug_report</md-icon>
Integrate Redmine
Redmine
</md-button>
<md-button @click.prevent.stop="jiraDialog = true">
<md-icon>bug_report</md-icon>
Jira
</md-button>
</md-list-item>
</md-list>
Expand All @@ -70,6 +74,32 @@
</md-dialog-actions>
</md-dialog>

<md-dialog v-if="jiraDialog" md-active>
<md-dialog-title>Jira Integration</md-dialog-title>

<md-content class="new-project">
<md-field>
<label>Host</label>
<md-input v-model="jiraHost" required></md-input>
</md-field>

<md-field>
<label>Username</label>
<md-input v-model="jiraUsername" required></md-input>
</md-field>

<md-field>
<label>Password</label>
<md-input v-model="jiraPassword" type="password"></md-input>
</md-field>
</md-content>

<md-dialog-actions>
<md-button class="md-raised" @click="jiraDialog = false">Close</md-button>
<md-button class="md-raised md-accent" @click="syncJira(); jiraDialog = false">Synchronize</md-button>
</md-dialog-actions>
</md-dialog>

<md-dialog v-if="newProjectDialog" md-active>
<md-dialog-title>Add a Project</md-dialog-title>

Expand Down Expand Up @@ -109,6 +139,7 @@ import validUrl from 'valid-url';
const PROJECT_SELECTED = 'property-selected';
const REDMINE = { HOST: 'redmine_host', API_KEY: 'redmine_apiKey' };
const JIRA = { HOST: 'jira_host', USERNAME: 'jira_username', PASSWORD: 'jira_password' };
export default {
PROJECT_SELECTED,
Expand All @@ -119,6 +150,10 @@ export default {
redmineDialog: false,
redmineHost: null,
redmineApiKey: null,
jiraDialog: false,
jiraHost: null,
jiraUsername: null,
jiraPassword: null,
newProjectDialog: false,
newProjectName: null,
newProjectLink: null,
Expand Down Expand Up @@ -181,6 +216,39 @@ export default {
}
}
},
// TODO: make separate component for synchronization
async syncJira (readConfig = false) {
if (readConfig) {
this.jiraHost = this.$registry.config().get(JIRA.HOST);
this.jiraUsername = this.$registry.config().get(JIRA.USERNAME);
this.jiraPassword = this.$registry.config().get(JIRA.PASSWORD);
}
this.$registry.get('logger').info('Trigger Jira synchronization');
this.$registry.config().set(this.jiraHost, JIRA.HOST);
this.$registry.config().set(this.jiraUsername, JIRA.USERNAME);
this.$registry.config().set(this.jiraPassword, JIRA.PASSWORD);
try {
await this.$registry.get('synchronizeJira')(this.jiraHost, this.jiraUsername, this.jiraPassword);
this.refresh();
this.$registry.get('notify')(
'TiTime - Jira Integration.',
`We've successfully synchronized w/ Jira (${ this.jiraHost }).`
);
} catch (error) {
this.$registry.get('logger').error(error.message);
this.$registry.get('notify')(
'TiTime - Jira Integration.',
`Failed to synchronized w/ Jira (${ this.jiraHost }). Error: ${ error.message }`
);
}
},
validUrl (val) {
return validUrl.isUri(val);
},
Expand Down

0 comments on commit 918bf30

Please sign in to comment.