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

+ + + + + + + + + + + + + + + {{range $item := .Issues}} + + + + + + + + + + + {{end}} + +
KeyTStatusPComponentSummaryAssigneeFix Version/s
{{ $item.Key }}{{ $item.Fields.Status.Name }}{{ $item.Fields.Summary }}{{ $item.Fields.Assignee.DisplayName }}
+` 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
+ +
+
+
+
+ {{{issuesGrid}}}
{{/if}}