diff --git a/source/app/mocks/api/github/graphql/followup.repository.collaborators.mjs b/source/app/mocks/api/github/graphql/followup.repository.collaborators.mjs new file mode 100644 index 00000000..a5f1beb4 --- /dev/null +++ b/source/app/mocks/api/github/graphql/followup.repository.collaborators.mjs @@ -0,0 +1,11 @@ +/**Mocked data */ +export default function({faker, query, login = faker.internet.userName()}) { + console.debug("metrics/compute/mocks > mocking graphql api result > followup/repository/collaborators") + return ({ + repository:{ + collaborators:{ + nodes:["github-user"] + } + }, + }) +} diff --git a/source/app/mocks/api/github/graphql/followup.repository.mjs b/source/app/mocks/api/github/graphql/followup.repository.mjs new file mode 100644 index 00000000..6baef9fe --- /dev/null +++ b/source/app/mocks/api/github/graphql/followup.repository.mjs @@ -0,0 +1,14 @@ +/**Mocked data */ +export default function({faker, query, login = faker.internet.userName()}) { + console.debug("metrics/compute/mocks > mocking graphql api result > followup/repository") + return ({ + issues_open:{issueCount:faker.datatype.number(100)}, + issues_drafts:{issueCount:faker.datatype.number(100)}, + issues_skipped:{issueCount:faker.datatype.number(100)}, + issues_closed:{issueCount:faker.datatype.number(100)}, + pr_open:{issueCount:faker.datatype.number(100)}, + pr_drafts:{issueCount:faker.datatype.number(100)}, + pr_closed:{issueCount:faker.datatype.number(100)}, + pr_merged:{issueCount:faker.datatype.number(100)}, + }) +} diff --git a/source/app/mocks/api/github/graphql/followup.user.mjs b/source/app/mocks/api/github/graphql/followup.user.mjs index f9d1934d..e3458ce6 100644 --- a/source/app/mocks/api/github/graphql/followup.user.mjs +++ b/source/app/mocks/api/github/graphql/followup.user.mjs @@ -2,12 +2,13 @@ export default function({faker, query, login = faker.internet.userName()}) { console.debug("metrics/compute/mocks > mocking graphql api result > followup/user") return ({ - user:{ - issues_open:{totalCount:faker.datatype.number(100)}, - issues_closed:{totalCount:faker.datatype.number(100)}, - pr_open:{totalCount:faker.datatype.number(100)}, - pr_closed:{totalCount:faker.datatype.number(100)}, - pr_merged:{totalCount:faker.datatype.number(100)}, - }, + issues_open:{issueCount:faker.datatype.number(100)}, + issues_drafts:{issueCount:faker.datatype.number(100)}, + issues_skipped:{issueCount:faker.datatype.number(100)}, + issues_closed:{issueCount:faker.datatype.number(100)}, + pr_open:{issueCount:faker.datatype.number(100)}, + pr_drafts:{issueCount:faker.datatype.number(100)}, + pr_closed:{issueCount:faker.datatype.number(100)}, + pr_merged:{issueCount:faker.datatype.number(100)}, }) } diff --git a/source/app/web/statics/app.placeholder.js b/source/app/web/statics/app.placeholder.js index 8d3798dd..0995ff6a 100644 --- a/source/app/web/statics/app.placeholder.js +++ b/source/app/web/statics/app.placeholder.js @@ -178,36 +178,59 @@ sections: options["followup.sections"].split(",").map(x => x.trim()).filter(x => ["user", "repositories"].includes(x)), issues: { get count() { - return this.open + this.closed + return this.open + this.closed + this.drafts + this.skipped }, open: faker.datatype.number(1000), closed: faker.datatype.number(1000), + drafts: faker.datatype.number(100), + skipped: faker.datatype.number(100), + get collaborators() { + return { + open: faker.datatype.number(this.open), + closed: faker.datatype.number(this.closed), + drafts: faker.datatype.number(this.drafts), + skipped: faker.datatype.number(this.skipped), + } + } }, pr: { get count() { - return this.open + this.merged + return this.open + this.closed + this.merged + this.drafts }, open: faker.datatype.number(1000), closed: faker.datatype.number(1000), merged: faker.datatype.number(1000), + drafts: faker.datatype.number(100), + get collaborators() { + return { + open: faker.datatype.number(this.open), + closed: faker.datatype.number(this.closed), + merged: faker.datatype.number(this.skipped), + drafts: faker.datatype.number(this.drafts), + } + } }, user: { issues: { get count() { - return this.open + this.closed + return this.open + this.closed + this.drafts + this.skipped }, open: faker.datatype.number(1000), closed: faker.datatype.number(1000), + drafts: faker.datatype.number(100), + skipped: faker.datatype.number(100), }, pr: { get count() { - return this.open + this.merged + return this.open + this.closed + this.merged + this.drafts }, open: faker.datatype.number(1000), closed: faker.datatype.number(1000), merged: faker.datatype.number(1000), + drafts: faker.datatype.number(100), }, }, + indepth:options["followup.indepth"] ? {} : null }, }) : null), diff --git a/source/plugins/followup/index.mjs b/source/plugins/followup/index.mjs index fb60ce56..6c08e289 100644 --- a/source/plugins/followup/index.mjs +++ b/source/plugins/followup/index.mjs @@ -1,5 +1,5 @@ //Setup -export default async function({login, data, computed, imports, q, graphql, queries, account}, {enabled = false} = {}) { +export default async function({login, data, computed, imports, q, graphql, queries, account}, {enabled = false, extras = false} = {}) { //Plugin execution try { //Check if plugin is enabled and requirements are met @@ -7,14 +7,14 @@ export default async function({login, data, computed, imports, q, graphql, queri return null //Load inputs - let {sections} = imports.metadata.plugins.followup.inputs({data, account, q}) + let {sections, indepth} = imports.metadata.plugins.followup.inputs({data, account, q}) //Define getters const followup = { sections, issues:{ get count() { - return this.open + this.closed + return this.open + this.closed + this.drafts + this.skipped }, get open() { return computed.repositories.issues_open @@ -22,10 +22,18 @@ export default async function({login, data, computed, imports, q, graphql, queri get closed() { return computed.repositories.issues_closed }, + drafts:0, + skipped:0, + collaborators:{ + open:0, + closed:0, + drafts:0, + skipped:0, + } }, pr:{ get count() { - return this.open + this.closed + this.merged + return this.open + this.closed + this.merged + this.drafts }, get open() { return computed.repositories.pr_open @@ -36,27 +44,72 @@ export default async function({login, data, computed, imports, q, graphql, queri get merged() { return computed.repositories.pr_merged }, + drafts:0, + collaborators:{ + open:0, + closed:0, + merged:0, + drafts:0, + } }, } + //Extras features + if (extras) { + + //Indepth mode + if (indepth) { + console.debug(`metrics/compute/${login}/plugins > followup > indepth`) + followup.indepth = {repositories:{}} + + //Process repositories + for (const {name:repo, owner:{login:owner}} of data.user.repositories.nodes) { + try { + console.debug(`metrics/compute/${login}/plugins > followup > processing ${owner}/${repo}`) + followup.indepth.repositories[`${owner}/${repo}`] = {stats:{}} + //Fetch users with push access + let {repository:{collaborators:{nodes:collaborators}}} = await graphql(queries.followup["repository.collaborators"]({repo, owner})) + console.debug(`metrics/compute/${login}/plugins > followup > found ${collaborators.length} collaborators`) + followup.indepth.repositories[`${owner}/${repo}`].collaborators = collaborators.map(({login}) => login) + //Fetch issues and pull requests created by collaborators + collaborators = collaborators.map(({login}) => `-author:${login}`).join(" ") + const stats = await graphql(queries.followup.repository({repo, owner, collaborators})) + followup.indepth.repositories[`${owner}/${repo}`] = stats + //Aggregate global stats + for (const [key, {issueCount:count}] of Object.entries(stats)) { + const [section, type] = key.split("_") + followup[section].collaborators[type] += count + } + } + catch (error) { + console.debug(error) + console.debug(`metrics/compute/${login}/plugins > followup > an error occured while processing ${owner}/${repo}, skipping...`) + } + } + } + } + //Load user issues and pull requests if ((account === "user")&&(sections.includes("user"))) { - const {user} = await graphql(queries.followup.user({login})) + const search = await graphql(queries.followup.user({login})) followup.user = { issues:{ get count() { - return this.open + this.closed + return this.open + this.closed + this.drafts + this.skipped }, - open:user.issues_open.totalCount, - closed:user.issues_closed.totalCount, + open:search.issues_open.issueCount, + closed:search.issues_closed.issueCount, + drafts:search.issues_drafts.issueCount, + skipped:search.issues_skipped.issueCount, }, pr:{ get count() { - return this.open + this.closed + this.merged + return this.open + this.closed + this.merged + this.drafts }, - open:user.pr_open.totalCount, - closed:user.pr_closed.totalCount, - merged:user.pr_merged.totalCount, + open:search.pr_open.issueCount, + closed:search.pr_closed.issueCount, + merged:search.pr_merged.issueCount, + drafts:search.pr_drafts.issueCount, }, } } diff --git a/source/plugins/followup/metadata.yml b/source/plugins/followup/metadata.yml index 42210f2c..2c74e9b7 100644 --- a/source/plugins/followup/metadata.yml +++ b/source/plugins/followup/metadata.yml @@ -22,3 +22,9 @@ inputs: values: - repositories # Overall status of issues and pull requests on your repositories - user # Overall status of issues and pull requests you have created on GitHub + + # Compute issues and pull requests per repositories with special highlighting for maintainers and specified users + plugin_followup_indepth: + description: Indepth follow-up processing + type: boolean + default: no \ No newline at end of file diff --git a/source/plugins/followup/queries/repository.collaborators.graphql b/source/plugins/followup/queries/repository.collaborators.graphql new file mode 100644 index 00000000..ab04916c --- /dev/null +++ b/source/plugins/followup/queries/repository.collaborators.graphql @@ -0,0 +1,9 @@ +query FollowupRepositoryCollaborators { + repository(name: "$repo", owner: "$owner") { + collaborators { + nodes { + login + } + } + } +} diff --git a/source/plugins/followup/queries/repository.graphql b/source/plugins/followup/queries/repository.graphql new file mode 100644 index 00000000..578251b6 --- /dev/null +++ b/source/plugins/followup/queries/repository.graphql @@ -0,0 +1,26 @@ +query FollowupRepository { + issues_open:search(query: "repo:$owner/$repo is:issue $collaborators is:open", type: ISSUE, first: 0) { + issueCount + } + issues_drafts:search(query: "repo:$owner/$repo is:issue $collaborators draft:true", type: ISSUE, first: 0) { + issueCount + } + issues_skipped:search(query: "repo:$owner/$repo is:issue $collaborators is:closed label:wontfix,duplicate", type: ISSUE, first: 0) { + issueCount + } + issues_closed:search(query: "repo:$owner/$repo is:issue $collaborators is:closed", type: ISSUE, first: 0) { + issueCount + } + pr_open:search(query: "repo:$owner/$repo is:pr $collaborators is:open draft:false", type: ISSUE, first: 0) { + issueCount + } + pr_drafts:search(query: "repo:$owner/$repo is:pr $collaborators draft:true", type: ISSUE, first: 0) { + issueCount + } + pr_closed:search(query: "repo:$owner/$repo is:pr $collaborators is:unmerged draft:false", type: ISSUE, first: 0) { + issueCount + } + pr_merged:search(query: "repo:$owner/$repo is:pr $collaborators is:merged", type: ISSUE, first: 0) { + issueCount + } +} \ No newline at end of file diff --git a/source/plugins/followup/queries/user.graphql b/source/plugins/followup/queries/user.graphql index 9aa5c07e..9dc8aba7 100644 --- a/source/plugins/followup/queries/user.graphql +++ b/source/plugins/followup/queries/user.graphql @@ -1,19 +1,26 @@ query FollowupUser { - user(login: "$login") { - issues_open:issues(states: OPEN) { - totalCount - } - issues_closed:issues(states: CLOSED) { - totalCount - } - pr_open:pullRequests(states: OPEN) { - totalCount - } - pr_closed:pullRequests(states: CLOSED) { - totalCount - } - pr_merged:pullRequests(states: MERGED) { - totalCount - } + issues_open:search(query: "is:issue author:$login is:open", type: ISSUE, first: 0) { + issueCount + } + issues_drafts:search(query: "is:issue author:$login draft:true", type: ISSUE, first: 0) { + issueCount + } + issues_skipped:search(query: "is:issue author:$login is:closed label:wontfix,duplicate", type: ISSUE, first: 0) { + issueCount + } + issues_closed:search(query: "is:issue author:$login is:closed", type: ISSUE, first: 0) { + issueCount + } + pr_open:search(query: "is:pr author:$login is:open draft:false", type: ISSUE, first: 0) { + issueCount + } + pr_drafts:search(query: "is:pr author:$login draft:true", type: ISSUE, first: 0) { + issueCount + } + pr_closed:search(query: "is:pr author:$login is:unmerged draft:false", type: ISSUE, first: 0) { + issueCount + } + pr_merged:search(query: "is:pr author:$login is:merged", type: ISSUE, first: 0) { + issueCount } } \ No newline at end of file diff --git a/source/templates/classic/partials/followup.ejs b/source/templates/classic/partials/followup.ejs index b2094504..2d73100a 100644 --- a/source/templates/classic/partials/followup.ejs +++ b/source/templates/classic/partials/followup.ejs @@ -26,19 +26,67 @@ - - + <% { const {open, drafts, closed, skipped, count, collaborators = {open:0, drafts:0, closed:0, skipped:0}} = section.issues, width = 220; let x = 0; for (const {p, fill} of [ + {p:(open-collaborators.open)/count, fill:"#238636"}, + {p:collaborators.open/count, fill:"#56d364"}, + {p:(drafts-collaborators.drafts)/count, fill:"#8B949E"}, + {p:collaborators.drafts/count, fill:"#c9d1d9"}, + {p:(closed-collaborators.closed)/count, fill:"#8957e5"}, + {p:collaborators.closed/count, fill:"#d2a8ff"}, + {p:(skipped-collaborators.skipped)/count, fill:"#8B949E"}, + {p:collaborators.skipped/count, fill:"#c9d1d9"}, + ]) { %> + + <% x += p*width }} %> -
-
+ <% if ((plugins.followup.indepth)&&(section.issues.collaborators)) { %> +
+ + From communities +
+ <% } %> +
+
<%= section.issues.open %> open
-
- +
+ <%= section.issues.closed %> closed
+
+ + <%= section.issues.drafts %> draft<%= s(section.issues.drafts) %> +
+
+ + <%= section.issues.skipped %> skipped +
+ <% if ((plugins.followup.indepth)&&(section.issues.collaborators)) { %> +
+ + From self and collaborators +
+
+
+ + <%= section.issues.collaborators.open %> open +
+
+ + <%= section.issues.collaborators.closed %> closed +
+
+ + <%= section.issues.collaborators.drafts %> draft<%= s(section.issues.collaborators.drafts) %> +
+
+ + <%= section.issues.collaborators.skipped %> skipped +
+
+ <% } %>

Pull requests

@@ -47,24 +95,67 @@ - - - + <% { const {open, drafts, closed, merged, count, collaborators = {open:0, drafts:0, closed:0, merged:0}} = section.pr, width = 220; let x = 0; for (const {p, fill} of [ + {p:(open-collaborators.open)/count, fill:"#238636"}, + {p:collaborators.open/count, fill:"#56d364"}, + {p:(drafts-collaborators.drafts)/count, fill:"#8B949E"}, + {p:collaborators.drafts/count, fill:"#c9d1d9"}, + {p:(closed-collaborators.closed)/count, fill:"#da3633"}, + {p:collaborators.closed/count, fill:"#ff7b72"}, + {p:(merged-collaborators.merged)/count, fill:"#8957e5"}, + {p:collaborators.merged/count, fill:"#d2a8ff"}, + ]) { %> + + <% x += p*width }} %> -
-
+ <% if ((plugins.followup.indepth)&&(section.pr.collaborators)) { %> +
+ + From communities +
+ <% } %> +
+
<%= section.pr.open %> open
-
- - <%= section.pr.closed %> closed -
-
+
<%= section.pr.merged %> merged
+
+ + <%= section.pr.drafts %> draft<%= s(section.pr.drafts) %> +
+
+ + <%= section.pr.closed %> closed +
+ <% if ((plugins.followup.indepth)&&(section.pr.collaborators)) { %> +
+ + From self and collaborators +
+
+
+ + <%= section.pr.collaborators.open %> open +
+
+ + <%= section.pr.collaborators.merged %> merged +
+
+ + <%= section.pr.collaborators.drafts %> draft<%= s(section.pr.collaborators.drafts) %> +
+
+ + <%= section.pr.collaborators.closed %> closed +
+
+ <% } %>
diff --git a/source/templates/classic/style.css b/source/templates/classic/style.css index d0c2f36e..2342fec9 100644 --- a/source/templates/classic/style.css +++ b/source/templates/classic/style.css @@ -274,6 +274,12 @@ /* Follow-up */ .followup.legend { font-size: 12px; + flex-wrap: wrap; + } + .followup.legend .field { + width: 46%; + justify-content: flex-start; + margin-left: 8px; } .followup.legend svg { margin: 0 3px; diff --git a/source/templates/repository/partials/followup.ejs b/source/templates/repository/partials/followup.ejs index dcea97ee..24e07cd5 100644 --- a/source/templates/repository/partials/followup.ejs +++ b/source/templates/repository/partials/followup.ejs @@ -1,76 +1,160 @@ <% if (plugins.followup) { %> -
- -
-

Issues

- <% if (plugins.followup.error) { %> -
-
- - <%= plugins.followup.error.message %> -
-
- <% } else { const section = plugins.followup, width = 220*(1+large) %> -
- - - - - - - - -
-
- - <%= section.issues.open %> open -
-
- - <%= section.issues.closed %> closed -
-
-
- <% } %> +
+

+ + Overall issues and pull requests status +

+
+ <% if (plugins.followup.error) { %> +
+
+ + <%= plugins.followup.error.message %> +
- -
-

Pull requests

- <% if (plugins.followup.error) { %> -
-
- - <%= plugins.followup.error.message %> -
-
- <% } else { const section = plugins.followup, width = 220*(1+large) %> -
- - - - - - - - - -
-
- - <%= section.pr.open %> open + <% } else { %> + <% const section = plugins.followup %> +
+
+
+

Issues

+ + + + + + <% { const {open, drafts, closed, skipped, count, collaborators = {open:0, drafts:0, closed:0, skipped:0}} = section.issues, width = 220; let x = 0; for (const {p, fill} of [ + {p:(open-collaborators.open)/count, fill:"#238636"}, + {p:collaborators.open/count, fill:"#56d364"}, + {p:(drafts-collaborators.drafts)/count, fill:"#8B949E"}, + {p:collaborators.drafts/count, fill:"#c9d1d9"}, + {p:(closed-collaborators.closed)/count, fill:"#8957e5"}, + {p:collaborators.closed/count, fill:"#d2a8ff"}, + {p:(skipped-collaborators.skipped)/count, fill:"#8B949E"}, + {p:collaborators.skipped/count, fill:"#c9d1d9"}, + ]) { %> + + <% x += p*width }} %> + + <% if ((plugins.followup.indepth)&&(section.issues.collaborators)) { %> +
+ + From community +
+ <% } %> +
+
+ + <%= section.issues.open %> open +
+
+ + <%= section.issues.closed %> closed +
+
+ + <%= section.issues.drafts %> draft<%= s(section.issues.drafts) %> +
+
+ + <%= section.issues.skipped %> skipped +
-
- - <%= section.pr.closed %> closed + <% if ((plugins.followup.indepth)&&(section.issues.collaborators)) { %> +
+ + From maintainers
-
- - <%= section.pr.merged %> merged +
+
+ + <%= section.issues.collaborators.open %> open +
+
+ + <%= section.issues.collaborators.closed %> closed +
+
+ + <%= section.issues.collaborators.drafts %> draft<%= s(section.issues.collaborators.drafts) %> +
+
+ + <%= section.issues.collaborators.skipped %> skipped +
-
-
- <% } %> -
- -
-<% } %> \ No newline at end of file + <% } %> + +
+

Pull requests

+ + + + + + <% { const {open, drafts, closed, merged, count, collaborators = {open:0, drafts:0, closed:0, merged:0}} = section.pr, width = 220; let x = 0; for (const {p, fill} of [ + {p:(open-collaborators.open)/count, fill:"#238636"}, + {p:collaborators.open/count, fill:"#56d364"}, + {p:(drafts-collaborators.drafts)/count, fill:"#8B949E"}, + {p:collaborators.drafts/count, fill:"#c9d1d9"}, + {p:(closed-collaborators.closed)/count, fill:"#da3633"}, + {p:collaborators.closed/count, fill:"#ff7b72"}, + {p:(merged-collaborators.merged)/count, fill:"#8957e5"}, + {p:collaborators.merged/count, fill:"#d2a8ff"}, + ]) { %> + + <% x += p*width }} %> + + <% if ((plugins.followup.indepth)&&(section.pr.collaborators)) { %> +
+ + From community +
+ <% } %> +
+
+ + <%= section.pr.open %> open +
+
+ + <%= section.pr.merged %> merged +
+
+ + <%= section.pr.drafts %> draft<%= s(section.pr.drafts) %> +
+
+ + <%= section.pr.closed %> closed +
+
+ <% if ((plugins.followup.indepth)&&(section.pr.collaborators)) { %> +
+ + From maintainers +
+
+
+ + <%= section.pr.collaborators.open %> open +
+
+ + <%= section.pr.collaborators.merged %> merged +
+
+ + <%= section.pr.collaborators.drafts %> draft<%= s(section.pr.collaborators.drafts) %> +
+
+ + <%= section.pr.collaborators.closed %> closed +
+
+ <% } %> +
+
+ + <% } %> +<% } %>