diff --git a/domain/section/jira/jira.go b/domain/section/jira/jira.go
index c3dfaa2a..38cc8d74 100644
--- a/domain/section/jira/jira.go
+++ b/domain/section/jira/jira.go
@@ -12,13 +12,12 @@
package jira
import (
- // "encoding/base64"
+ "bytes"
"encoding/json"
"fmt"
- // "io/ioutil"
+ "html/template"
+ "io/ioutil"
"net/http"
- // "bytes"
- // "html/template"
"github.com/documize/community/core/env"
"github.com/documize/community/domain"
@@ -131,218 +130,129 @@ func (p *Provider) Command(ctx *provider.Context, w http.ResponseWriter, r *http
}
switch method {
- // case "secrets":
- // secs(ctx, p.Store, w, r)
+ case "preview":
+ preview(ctx, p.Store, w, r)
case "auth":
auth(ctx, p.Store, w, r)
- // case "workspace":
- // workspace(ctx, p.Store, w, r)
- // case "items":
- // items(ctx, p.Store, w, r)
}
}
func auth(ctx *provider.Context, store *domain.Store, w http.ResponseWriter, r *http.Request) {
- var login = jiraLogin{}
- creds, err := store.Setting.GetUser(ctx.OrgID, "", "jira", "")
- err = json.Unmarshal([]byte(creds), &login)
+ creds, err := getCredentials(ctx, store)
if err != nil {
provider.WriteForbidden(w)
return
}
- tp := jira.BasicAuthTransport{Username: login.Username, Password: login.Secret}
- client, err := jira.NewClient(tp.Client(), login.URL)
-
- u, _, err := client.User.Get(login.Username)
- fmt.Printf("\nEmail: %v\nSuccess!\n", u.EmailAddress)
-
- // req, err := http.NewRequest("GET", fmt.Sprintf("%s/", login.URL), nil)
- // header := []byte(fmt.Sprintf("%s:%s", login.Username, login.Secret))
- // req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString(header))
-
- // client := &http.Client{}
- // res, err := client.Do(req)
-
- // if err != nil {
- // provider.WriteError(w, logID, err)
- // return
- // }
- // if res.StatusCode != http.StatusOK {
- // provider.WriteForbidden(w)
- // return
- // }
-
- // defer res.Body.Close()
- // var g = geminiUser{}
-
- // dec := json.NewDecoder(res.Body)
- // err = dec.Decode(&g)
-
- // if err != nil {
- // provider.WriteError(w, logID, err)
- // return
- // }
+ // Authenticate
+ _, _, err = authenticate(creds)
+ if err != nil {
+ fmt.Println(err)
+ provider.WriteError(w, logID, err)
+ return
+ }
provider.WriteJSON(w, "OK")
}
-/*
-func workspace(ctx *provider.Context, store *domain.Store, w http.ResponseWriter, r *http.Request) {
- defer r.Body.Close()
- body, err := ioutil.ReadAll(r.Body)
-
+func preview(ctx *provider.Context, store *domain.Store, w http.ResponseWriter, r *http.Request) {
+ creds, err := getCredentials(ctx, store)
if err != nil {
- provider.WriteMessage(w, logID, "Bad payload")
+ provider.WriteForbidden(w)
return
}
- var config = geminiConfig{}
- err = json.Unmarshal(body, &config)
-
- if err != nil {
- provider.WriteMessage(w, logID, "Bad payload")
- return
- }
-
- config.Clean(ctx, store)
-
- if len(config.URL) == 0 {
- provider.WriteMessage(w, logID, "Missing URL value")
- return
- }
-
- if len(config.Username) == 0 {
- provider.WriteMessage(w, logID, "Missing Username value")
- return
- }
-
- if len(config.APIKey) == 0 {
- provider.WriteMessage(w, logID, "Missing APIKey value")
- return
- }
-
- if config.UserID == 0 {
- provider.WriteMessage(w, logID, "Missing UserId value")
- return
- }
-
- req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/navigationcards/users/%d", config.URL, config.UserID), nil)
-
- creds := []byte(fmt.Sprintf("%s:%s", config.Username, config.APIKey))
- req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString(creds))
-
- client := &http.Client{}
- res, err := client.Do(req)
-
+ client, _, err := authenticate(creds)
if err != nil {
fmt.Println(err)
provider.WriteError(w, logID, err)
return
}
- if res.StatusCode != http.StatusOK {
- provider.WriteForbidden(w)
- return
- }
-
- defer res.Body.Close()
- var workspace interface{}
-
- dec := json.NewDecoder(res.Body)
- err = dec.Decode(&workspace)
-
- if err != nil {
- provider.WriteError(w, logID, err)
- return
- }
-
- provider.WriteJSON(w, workspace)
-}
-
-func items(ctx *provider.Context, store *domain.Store, w http.ResponseWriter, r *http.Request) {
- defer r.Body.Close()
- body, err := ioutil.ReadAll(r.Body)
-
- if err != nil {
- provider.WriteMessage(w, logID, "Bad payload")
- return
- }
-
- var config = geminiConfig{}
- err = json.Unmarshal(body, &config)
-
- if err != nil {
- provider.WriteMessage(w, logID, "Bad payload")
- return
- }
-
- config.Clean(ctx, store)
-
- if len(config.URL) == 0 {
- provider.WriteMessage(w, logID, "Missing URL value")
- return
- }
-
- if len(config.Username) == 0 {
- provider.WriteMessage(w, logID, "Missing Username value")
- return
- }
-
- if len(config.APIKey) == 0 {
- provider.WriteMessage(w, logID, "Missing APIKey value")
- return
- }
-
- creds := []byte(fmt.Sprintf("%s:%s", config.Username, config.APIKey))
-
- filter, err := json.Marshal(config.Filter)
- if err != nil {
- provider.WriteError(w, logID, err)
- return
- }
-
- var jsonFilter = []byte(string(filter))
- req, err := http.NewRequest("POST", fmt.Sprintf("%s/api/items/filtered", config.URL), bytes.NewBuffer(jsonFilter))
- req.Header.Set("Content-Type", "application/json")
- req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString(creds))
-
- client := &http.Client{}
- res, err := client.Do(req)
-
- if err != nil {
- provider.WriteError(w, logID, err)
- return
- }
-
- if res.StatusCode != http.StatusOK {
- provider.WriteForbidden(w)
- return
- }
-
- defer res.Body.Close()
- var items interface{}
-
- dec := json.NewDecoder(res.Body)
- err = dec.Decode(&items)
-
+ config, err := readConfig(ctx, store, w, r)
if err != nil {
fmt.Println(err)
provider.WriteError(w, logID, err)
return
}
- provider.WriteJSON(w, items)
+ issues, err := getIssues(config, client)
+
+ w.Header().Set("Content-Type", "html; charset=utf-8")
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte(generateGrid(issues)))
}
-func secs(ctx *provider.Context, store *domain.Store, w http.ResponseWriter, r *http.Request) {
- sec, _ := getSecrets(ctx, store)
- provider.WriteJSON(w, sec)
+// Pull config from HTTP request.
+func readConfig(ctx *provider.Context, store *domain.Store, w http.ResponseWriter, r *http.Request) (config jiraConfig, err error) {
+ defer r.Body.Close()
+ body, err := ioutil.ReadAll(r.Body)
+ if err != nil {
+ return
+ }
+
+ err = json.Unmarshal(body, &config)
+ if err != nil {
+ return
+ }
+
+ return
+}
+
+// Get Jira connector configuration.
+func getCredentials(ctx *provider.Context, store *domain.Store) (login jiraLogin, err error) {
+ creds, err := store.Setting.GetUser(ctx.OrgID, "", "jira", "")
+
+ err = json.Unmarshal([]byte(creds), &login)
+ if err != nil {
+ return login, err
+ }
+
+ return
+}
+
+// Perform Jira login.
+func authenticate(login jiraLogin) (c *jira.Client, u *jira.User, err error) {
+ tp := jira.BasicAuthTransport{Username: login.Username, Password: login.Secret}
+ c, err = jira.NewClient(tp.Client(), login.URL)
+ if err != nil {
+ return
+ }
+
+ u, _, err = c.User.Get(login.Username)
+ if err != nil {
+ return
+ }
+
+ return
+}
+
+// Fetch Jira issues using configuration criteria.
+func getIssues(config jiraConfig, client *jira.Client) (issues []jira.Issue, err error) {
+ opts := &jira.SearchOptions{Expand: "", MaxResults: 500, StartAt: 0}
+ issues, _, err = client.Issue.Search(config.JQL, opts)
+
+ return
+}
+
+// Generate issues grid
+func generateGrid(issues []jira.Issue) string {
+ t := template.New("issues")
+ t, _ = t.Parse(renderTemplate)
+
+ payload := jiraGrid{}
+ payload.ItemCount = len(issues)
+ payload.Issues = issues
+
+ buffer := new(bytes.Buffer)
+ t.Execute(buffer, payload)
+
+ return buffer.String()
+
}
-*/
type jiraConfig struct {
- JQL int64 `json:"jql"`
+ JQL string `json:"jql"`
ItemCount int `json:"itemCount"`
Filter map[string]interface{} `json:"filter"`
}
@@ -352,3 +262,41 @@ type jiraLogin struct {
Username string `json:"username"`
Secret string `json:"secret"`
}
+
+type jiraGrid struct {
+ Issues []jira.Issue `json:"issues"`
+ ItemCount int `json:"itemCount"`
+}
+
+// the HTML that is rendered by this section.
+const renderTemplate = `
+
Showing {{.ItemCount}} Jira issues
+
+
+
+ Key |
+ T |
+ Status |
+ P |
+ Component |
+ Summary |
+ Assignee |
+ Fix Version/s |
+
+
+
+ {{range $item := .Issues}}
+
+ {{ $item.Key }} |
+  |
+ {{ $item.Fields.Status.Name }} |
+  |
+ |
+ {{ $item.Fields.Summary }} |
+ {{ $item.Fields.Assignee.DisplayName }} |
+ |
+
+ {{end}}
+
+
+`
diff --git a/gui/app/components/section/jira/type-editor.js b/gui/app/components/section/jira/type-editor.js
index e190881b..f45b6be3 100644
--- a/gui/app/components/section/jira/type-editor.js
+++ b/gui/app/components/section/jira/type-editor.js
@@ -9,9 +9,6 @@
//
// https://documize.com
-import $ from 'jquery';
-import { set } from '@ember/object';
-import { schedule } from '@ember/runloop';
import { inject as service } from '@ember/service';
import Component from '@ember/component';
import SectionMixin from '../../../mixins/section';
@@ -22,6 +19,7 @@ export default Component.extend(SectionMixin, TooltipMixin, {
isDirty: false,
waiting: false,
authenticated: false,
+ issuesGrid: '',
init() {
this._super(...arguments);
@@ -48,96 +46,16 @@ export default Component.extend(SectionMixin, TooltipMixin, {
}
this.set('config', config);
-
- // let self = this;
- // self.set('waiting', true);
- // this.get('sectionService').fetch(this.get('page'), "secrets", this.get('config'))
- // .then(function (response) {
- // self.set('waiting', false);
- // self.set('config.APIKey', response.apikey);
- // self.set('config.url', response.url);
- // self.set('config.username', response.username);
-
- // if (response.apikey.length > 0 && response.url.length > 0 && response.username.length > 0) {
- // self.send('auth');
- // }
- // }, function (reason) { // eslint-disable-line no-unused-vars
- // self.set('waiting', false);
- // if (self.get('config.userId') > 0) {
- // self.send('auth');
- // }
- // });
- },
-
- getWorkspaces() {
- let page = this.get('page');
- let self = this;
this.set('waiting', true);
- this.get('sectionService').fetch(page, "workspace", this.get('config'))
- .then(function (response) {
- // console.log(response);
- let workspaceId = self.get('config.workspaceId');
-
- if (response.length > 0 && workspaceId === 0) {
- workspaceId = response[0].Id;
- }
-
- self.set("config.workspaceId", workspaceId);
- self.set('workspaces', response);
- self.selectWorkspace(workspaceId);
-
- schedule('afterRender', () => {
- window.scrollTo(0, document.body.scrollHeight);
- self.renderTooltips();
- });
- self.set('waiting', false);
- }, function (reason) { // eslint-disable-line no-unused-vars
- self.set('workspaces', []);
- self.set('waiting', false);
- });
- },
-
- getItems() {
- let page = this.get('page');
- let self = this;
-
- this.set('waiting', true);
-
- this.get('sectionService').fetch(page, "items", this.get('config'))
- .then(function (response) {
- if (self.get('isDestroyed') || self.get('isDestroying')) {
- return;
- }
- self.set('items', response);
- self.set('config.itemCount', response.length);
- self.set('waiting', false);
- }, function (reason) { // eslint-disable-line no-unused-vars
- if (self.get('isDestroyed') || self.get('isDestroying')) {
- return;
- }
- self.set('items', []);
- self.set('waiting', false);
- });
- },
-
- selectWorkspace(id) {
- let self = this;
- let w = this.get('workspaces');
-
- w.forEach(function (w) {
- set(w, 'selected', w.Id === id);
-
- if (w.Id === id) {
- self.set("config.filter", w.Filter);
- self.set("config.workspaceId", id);
- self.set("config.workspaceName", w.Title);
- // console.log(self.get('config'));
- }
+ this.get('sectionService').fetch(this.get('page'), "auth", this.get('config'))
+ .then((response) => { // eslint-disable-line no-unused-vars
+ this.set('authenticated', true);
+ this.set('waiting', false);
+ }, (reason) => { // eslint-disable-line no-unused-vars
+ this.set('authenticated', false);
+ this.set('waiting', false);
});
-
- this.set('workspaces', w);
- this.getItems();
},
actions: {
@@ -145,55 +63,20 @@ export default Component.extend(SectionMixin, TooltipMixin, {
return this.get('isDirty');
},
- auth() {
- // missing data?
- if (is.empty(this.get('config.url'))) {
- $("#gemini-url").addClass("is-invalid").focus();
- return;
- }
- if (is.empty(this.get('config.username'))) {
- $("#gemini-username").addClass("is-invalid").focus();
- return;
- }
- if (is.empty(this.get('config.APIKey'))) {
- $("#gemini-apikey").addClass("is-invalid").focus();
- return;
- }
-
- // knock out spaces
- this.set('config.url', this.get('config.url').trim());
- this.set('config.username', this.get('config.username').trim());
- this.set('config.APIKey', this.get('config.APIKey').trim());
-
- // remove trailing slash in URL
- let url = this.get('config.url');
- if (url.indexOf("/", url.length - 1) !== -1) {
- this.set('config.url', url.substring(0, url.length - 1));
- }
-
- let page = this.get('page');
- let self = this;
-
+ onPreview() {
this.set('waiting', true);
- this.get('sectionService').fetch(page, "auth", this.get('config'))
- .then(function (response) {
- self.set('authenticated', true);
- self.set('user', response);
- self.set('config.userId', response.BaseEntity.id);
- self.set('waiting', false);
- self.getWorkspaces();
- }, function (reason) { // eslint-disable-line no-unused-vars
- self.set('authenticated', false);
- self.set('user', null);
- self.set('config.userId', 0);
- self.set('waiting', false);
- });
- },
-
- onWorkspaceChange(id) {
- this.set('isDirty', true);
- this.selectWorkspace(id);
+ this.get('sectionService').fetchText(this.get('page'), 'preview', this.get('config'))
+ .then((response) => { // eslint-disable-line no-unused-vars
+ this.set('issuesGrid', response);
+ this.set('authenticated', true);
+ this.set('waiting', false);
+ }, (reason) => { // eslint-disable-line no-unused-vars
+ console.log(reason);
+ this.set('issuesGrid', '');
+ this.set('authenticated', false);
+ this.set('waiting', false);
+ });
},
onCancel() {
diff --git a/gui/app/pods/customize/integrations/controller.js b/gui/app/pods/customize/integrations/controller.js
index 71230b02..fe1fb43c 100644
--- a/gui/app/pods/customize/integrations/controller.js
+++ b/gui/app/pods/customize/integrations/controller.js
@@ -9,16 +9,7 @@
//
// https://documize.com
-import { inject as service } from '@ember/service';
import Controller from '@ember/controller';
export default Controller.extend({
- orgService: service('organization'),
-
- actions: {
- save() {
- return this.get('orgService').save(this.model.general).then(() => {
- });
- }
- }
});
diff --git a/gui/app/pods/customize/integrations/template.hbs b/gui/app/pods/customize/integrations/template.hbs
index a7e5b418..0ee6ba68 100644
--- a/gui/app/pods/customize/integrations/template.hbs
+++ b/gui/app/pods/customize/integrations/template.hbs
@@ -1 +1 @@
-{{customize/integration-settings jira=jira}}
+{{customize/integration-settings jira=model.jira}}
diff --git a/gui/app/services/organization.js b/gui/app/services/organization.js
index ebbdd67d..35922b43 100644
--- a/gui/app/services/organization.js
+++ b/gui/app/services/organization.js
@@ -47,6 +47,8 @@ export default Service.extend({
getOrgSetting(orgId, key) {
return this.get('ajax').request(`organization/${orgId}/setting?key=${key}`, {
method: 'GET'
+ }).then((response) => {
+ return JSON.parse(response);
});
},
diff --git a/gui/app/services/section.js b/gui/app/services/section.js
index 2f111b54..9384586a 100644
--- a/gui/app/services/section.js
+++ b/gui/app/services/section.js
@@ -48,6 +48,20 @@ export default BaseService.extend({
});
},
+ // Requests data from the specified section handler, passing the method and document ID
+ // and POST payload.
+ fetchText(page, method, data) {
+ let documentId = page.get('documentId');
+ let section = page.get('contentType');
+ let url = `sections?documentID=${documentId}§ion=${section}&method=${method}`;
+
+ return this.get('ajax').post(url, {
+ data: JSON.stringify(data),
+ contentType: "application/json",
+ dataType: "html"
+ });
+ },
+
// Did any dynamic sections change? Fetch and send up for rendering?
refresh(documentId) {
let url = `sections/refresh?documentID=${documentId}`;
diff --git a/gui/app/styles/section/jira.scss b/gui/app/styles/section/jira.scss
index d30340da..4c6a396a 100644
--- a/gui/app/styles/section/jira.scss
+++ b/gui/app/styles/section/jira.scss
@@ -3,3 +3,16 @@
padding: 0;
}
+.section-jira-table {
+ font-size: 1rem;
+ width: auto !important;
+
+ a:hover {
+ text-decoration: underline;
+ }
+}
+
+.section-jira-icon {
+ height: 16px;
+ width: 16px;
+}
diff --git a/gui/app/templates/components/section/jira/type-editor.hbs b/gui/app/templates/components/section/jira/type-editor.hbs
index 32345aee..03ddb8b4 100644
--- a/gui/app/templates/components/section/jira/type-editor.hbs
+++ b/gui/app/templates/components/section/jira/type-editor.hbs
@@ -1,12 +1,17 @@
{{#section/base-editor document=document folder=folder page=page busy=waiting tip="Jira issue tracking" isDirty=(action 'isDirty') onCancel=(action 'onCancel') onAction=(action 'onAction')}}
-
-
-
- {{#unless authenticated}}
-
Your Documize administrator needs to provide Jira connection details before usage.
- {{/unless}}
+
+ {{#if session.isAdmin}}
+ {{#link-to 'customize.integrations' class="btn btn-outline-secondary font-weight-bold"}}
+ Configire Jira Connector
+ {{/link-to}}
+ {{else}}
+ {{#unless authenticated}}
+
Your Documize administrator needs to provide Jira connection details before usage.
+ {{/unless}}
+ {{/if}}
+
@@ -15,9 +20,15 @@
- {{focus-input id="jira-jql" type="text" value=config.query class="form-control"}}
+ {{focus-input id="jira-jql" type="text" value=config.jql class="form-control"}}
Find issues using JQL
+
+
+
+
{{/if}}