diff --git a/source/app/mocks/api/github/rest/activity/listEventsForAuthenticatedUser.mjs b/source/app/mocks/api/github/rest/activity/listEventsForAuthenticatedUser.mjs index 65efd5d1..af5b2b8e 100644 --- a/source/app/mocks/api/github/rest/activity/listEventsForAuthenticatedUser.mjs +++ b/source/app/mocks/api/github/rest/activity/listEventsForAuthenticatedUser.mjs @@ -30,6 +30,7 @@ }, }, created_at:faker.date.recent(7), + public:true, }, { id:"10000000001", @@ -58,6 +59,7 @@ }, }, created_at:faker.date.recent(7), + public:true, }, { id:"10000000002", @@ -81,6 +83,7 @@ }, }, created_at:faker.date.recent(7), + public:true, }, { id:"10000000003", @@ -103,6 +106,7 @@ ], }, created_at:faker.date.recent(7), + public:true, }, { id:"10000000004", @@ -135,6 +139,7 @@ }, }, created_at:faker.date.recent(7), + public:true, }, { id:"10000000005", @@ -152,6 +157,7 @@ }, }, created_at:faker.date.recent(7), + public:true, }, { id:"10000000006", @@ -181,6 +187,7 @@ }, }, created_at:faker.date.recent(7), + public:true, }, { id:"10000000007", @@ -201,6 +208,7 @@ }, }, created_at:faker.date.recent(7), + public:true, }, { id:"10000000008", @@ -217,6 +225,7 @@ master_branch:"master", }, created_at:faker.date.recent(7), + public:true, }, { id:"100000000009", @@ -229,6 +238,7 @@ }, payload:{action:"started"}, created_at:faker.date.recent(7), + public:true, }, { id:"10000000010", @@ -244,6 +254,7 @@ ref_type:faker.random.arrayElement(["tag", "branch"]), }, created_at:faker.date.recent(7), + public:true, }, { id:"10000000011", @@ -266,6 +277,7 @@ ], }, created_at:faker.date.recent(7), + public:true, }, { id:"10000000012", @@ -291,6 +303,7 @@ }, }, created_at:faker.date.recent(7), + public:true, }, { id:"10000000013", @@ -308,6 +321,7 @@ action:"added", }, created_at:faker.date.recent(7), + public:true, }, { id:"10000000014", @@ -320,6 +334,7 @@ }, payload:{}, created_at:faker.date.recent(7), + public:true, }, ], }) diff --git a/source/app/mocks/api/github/rest/activity/listRepoEvents.mjs b/source/app/mocks/api/github/rest/activity/listRepoEvents.mjs new file mode 100644 index 00000000..f6a2174b --- /dev/null +++ b/source/app/mocks/api/github/rest/activity/listRepoEvents.mjs @@ -0,0 +1,8 @@ +// + import listEventsForAuthenticatedUser from "./listEventsForAuthenticatedUser.mjs" + +/**Mocked data */ + export default function({faker}, target, that, [{username:login, page, per_page}]) { + console.debug("metrics/compute/mocks > mocking rest api result > rest.activity.listRepoEvents") + return listEventsForAuthenticatedUser(...arguments) + } diff --git a/source/app/web/statics/app.placeholder.js b/source/app/web/statics/app.placeholder.js index 5c41ba10..a04906c9 100644 --- a/source/app/web/statics/app.placeholder.js +++ b/source/app/web/statics/app.placeholder.js @@ -511,13 +511,15 @@ //Activity ...(set.plugins.enabled.activity ? ({ activity:{ + timestamps:options["activity.timestamps"], events:new Array(Number(options["activity.limit"])).fill(null).map(_ => [ { type:"push", repo:`${faker.random.word()}/${faker.random.word()}`, size:1, branch:"master", - commits: [ { sha:faker.git.shortSha(), message:faker.lorem.sentence()} ] + commits: [ { sha:faker.git.shortSha(), message:faker.lorem.sentence()} ], + timestamp:faker.date.recent(), }, { type:"comment", @@ -528,6 +530,7 @@ mobile:null, number:faker.git.shortSha(), title:"", + timestamp:faker.date.recent(), }, { type:"comment", @@ -538,6 +541,7 @@ mobile:null, number:faker.random.number(100), title:faker.lorem.sentence(), + timestamp:faker.date.recent(), }, { type:"comment", @@ -548,6 +552,7 @@ mobile:null, number:faker.random.number(100), title:faker.lorem.sentence(), + timestamp:faker.date.recent(), }, { type:"issue", @@ -556,6 +561,7 @@ user:set.user, number:faker.random.number(100), title:faker.lorem.sentence(), + timestamp:faker.date.recent(), }, { type:"pr", @@ -564,16 +570,19 @@ user:set.user, number:faker.random.number(100), title:faker.lorem.sentence(), - lines:{added:faker.random.number(1000), deleted:faker.random.number(1000)}, files:{changed:faker.random.number(10)} + lines:{added:faker.random.number(1000), deleted:faker.random.number(1000)}, files:{changed:faker.random.number(10)}, + timestamp:faker.date.recent(), }, { type:"wiki", repo:`${faker.random.word()}/${faker.random.word()}`, - pages:[faker.lorem.sentence(), faker.lorem.sentence()] + pages:[faker.lorem.sentence(), faker.lorem.sentence()], + timestamp:faker.date.recent(), }, { type:"fork", repo:`${faker.random.word()}/${faker.random.word()}`, + timestamp:faker.date.recent(), }, { type:"review", @@ -581,6 +590,7 @@ user:set.user, number:faker.random.number(100), title:faker.lorem.sentence(), + timestamp:faker.date.recent(), }, { type:"release", @@ -589,30 +599,36 @@ name:faker.random.words(4), draft:faker.random.boolean(), prerelease:faker.random.boolean(), + timestamp:faker.date.recent(), }, { type:"ref/create", repo:`${faker.random.word()}/${faker.random.word()}`, - ref:{name:faker.lorem.slug(), type:faker.random.arrayElement(["tag", "branch"]),} + ref:{name:faker.lorem.slug(), type:faker.random.arrayElement(["tag", "branch"])}, + timestamp:faker.date.recent(), }, { type:"ref/delete", repo:`${faker.random.word()}/${faker.random.word()}`, - ref:{name:faker.lorem.slug(), type:faker.random.arrayElement(["tag", "branch"]),} + ref:{name:faker.lorem.slug(), type:faker.random.arrayElement(["tag", "branch"])}, + timestamp:faker.date.recent(), }, { type:"member", repo:`${faker.random.word()}/${faker.random.word()}`, - user:set.user + user:set.user, + timestamp:faker.date.recent(), }, { type:"public", repo:`${faker.random.word()}/${faker.random.word()}`, + timestamp:faker.date.recent(), }, { type:"star", repo:`${faker.random.word()}/${faker.random.word()}`, - action:"started" + action:"started", + timestamp:faker.date.recent(), }, ][Math.floor(Math.random()*15)]) } @@ -647,6 +663,9 @@ return text return `${text.substring(0, length)}…` } + data.f.date = function (string, options) { + return new Intl.DateTimeFormat("en-GB", options).format(new Date(string)) + } //Render return await ejs.render(image, data, {async:true, rmWhitespace:true}) } diff --git a/source/plugins/activity/index.mjs b/source/plugins/activity/index.mjs index 6b76dd28..445cd6c8 100644 --- a/source/plugins/activity/index.mjs +++ b/source/plugins/activity/index.mjs @@ -6,111 +6,121 @@ if ((!enabled)||(!q.activity)) return null + //Context + let context = {mode:"user"} + if (q.repo) { + console.debug(`metrics/compute/${login}/plugins > activity > switched to repository mode`) + const {owner, repo} = data.user.repositories.nodes.map(({name:repo, owner:{login:owner}}) => ({repo, owner})).shift() + context = {...context, mode:"repository", owner, repo} + } + //Load inputs - let {limit, days, filter} = imports.metadata.plugins.activity.inputs({data, q, account}) + let {limit, days, filter, visibility, timestamps} = imports.metadata.plugins.activity.inputs({data, q, account}) if (!days) days = Infinity //Get user recent activity console.debug(`metrics/compute/${login}/plugins > activity > querying api`) - const {data:events} = await rest.activity.listEventsForAuthenticatedUser({username:login, per_page:100}) + const {data:events} = context.mode === "repository" ? await rest.activity.listRepoEvents({owner:context.owner, repo:context.repo}) : await rest.activity.listEventsForAuthenticatedUser({username:login, per_page:100}) console.debug(`metrics/compute/${login}/plugins > activity > ${events.length} events loaded`) //Extract activity events const activity = events .filter(({actor}) => account === "organization" ? true : actor.login === login) .filter(({created_at}) => Number.isFinite(days) ? new Date(created_at) > new Date(Date.now()-days*24*60*60*1000) : true) - .map(({type, payload, repo:{name:repo}}) => { + .filter(event => visibility === "public" ? event.public : true) + .map(({type, payload, actor:{login:actor}, repo:{name:repo}, created_at}) => { //See https://docs.github.com/en/free-pro-team@latest/developers/webhooks-and-events/github-event-types + const timestamp = new Date(created_at) switch (type) { //Commented on a commit case "CommitCommentEvent":{ if (!["created"].includes(payload.action)) return null const {comment:{user:{login:user}, commit_id:sha, body:content}} = payload - return {type:"comment", on:"commit", repo, content, user, mobile:null, number:sha.substring(0, 7), title:""} + return {type:"comment", on:"commit", actor, timestamp, repo, content, user, mobile:null, number:sha.substring(0, 7), title:""} } //Created a git branch or tag case "CreateEvent":{ const {ref:name, ref_type:type} = payload - return {type:"ref/create", repo, ref:{name, type}} + return {type:"ref/create", actor, timestamp, repo, ref:{name, type}} } //Deleted a git branch or tag case "DeleteEvent":{ const {ref:name, ref_type:type} = payload - return {type:"ref/delete", repo, ref:{name, type}} + return {type:"ref/delete", actor, timestamp, repo, ref:{name, type}} } //Forked repository case "ForkEvent":{ - return {type:"fork", repo} + return {type:"fork", actor, timestamp, repo} } //Wiki editions case "GollumEvent":{ const {pages} = payload - return {type:"wiki", repo, pages:pages.map(({title}) => title)} + return {type:"wiki", actor, timestamp, repo, pages:pages.map(({title}) => title)} } //Commented on an issue case "IssueCommentEvent":{ if (!["created"].includes(payload.action)) return null const {issue:{user:{login:user}, title, number}, comment:{body:content, performed_via_github_app:mobile}} = payload - return {type:"comment", on:"issue", repo, content, user, mobile, number, title} + return {type:"comment", on:"issue", actor, timestamp, repo, content, user, mobile, number, title} } //Issue event case "IssuesEvent":{ if (!["opened", "closed", "reopened"].includes(payload.action)) return null const {action, issue:{user:{login:user}, title, number}} = payload - return {type:"issue", repo, action, user, number, title} + return {type:"issue", actor, timestamp, repo, action, user, number, title} } //Activity from repository collaborators case "MemberEvent":{ if (!["added"].includes(payload.action)) return null const {member:{login:user}} = payload - return {type:"member", repo, user} + return {type:"member", actor, timestamp, repo, user} } //Made repository public case "PublicEvent":{ - return {type:"public", repo} + return {type:"public", actor, timestamp, repo} } //Pull requests events case "PullRequestEvent":{ if (!["opened", "closed"].includes(payload.action)) return null const {action, pull_request:{user:{login:user}, title, number, additions:added, deletions:deleted, changed_files:changed}} = payload - return {type:"pr", repo, action, user, title, number, lines:{added, deleted}, files:{changed}} + return {type:"pr", actor, timestamp, repo, action, user, title, number, lines:{added, deleted}, files:{changed}} } //Reviewed a pull request case "PullRequestReviewEvent":{ const {review:{state:review}, pull_request:{user:{login:user}, number, title}} = payload - return {type:"review", repo, review, user, number, title} + return {type:"review", actor, timestamp, repo, review, user, number, title} } //Commented on a pull request case "PullRequestReviewCommentEvent":{ if (!["created"].includes(payload.action)) return null const {pull_request:{user:{login:user}, title, number}, comment:{body:content, performed_via_github_app:mobile}} = payload - return {type:"comment", on:"pr", repo, content, user, mobile, number, title} + return {type:"comment", on:"pr", actor, timestamp, repo, content, user, mobile, number, title} } //Pushed commits case "PushEvent":{ const {size, commits, ref} = payload - return {type:"push", repo, size, branch:ref.match(/refs.heads.(?.*)/)?.groups?.branch ?? null, commits:commits.map(({sha, message}) => ({sha:sha.substring(0, 7), message}))} + return {type:"push", actor, timestamp, repo, size, branch:ref.match(/refs.heads.(?.*)/)?.groups?.branch ?? null, commits:commits.map(({sha, message}) => ({sha:sha.substring(0, 7), message}))} } //Released case "ReleaseEvent":{ if (!["published"].includes(payload.action)) return null const {action, release:{name, prerelease, draft}} = payload - return {type:"release", repo, action, name, prerelease, draft} + return {type:"release", actor, timestamp, repo, action, name, prerelease, draft} } //Starred a repository case "WatchEvent":{ if (!["started"].includes(payload.action)) return null const {action} = payload - return {type:"star", repo, action} + return {type:"star", actor, timestamp, repo, action} } //Unknown event default:{ @@ -123,7 +133,7 @@ .slice(0, limit) //Results - return {events:activity} + return {timestamps, events:activity} } //Handle errors catch (error) { diff --git a/source/plugins/activity/metadata.yml b/source/plugins/activity/metadata.yml index 69b9bb25..9444c809 100644 --- a/source/plugins/activity/metadata.yml +++ b/source/plugins/activity/metadata.yml @@ -4,6 +4,7 @@ categorie: github supports: - user - organization + - repository inputs: # Enable or disable plugin @@ -51,3 +52,18 @@ inputs: - star # Display starred repositories - member # Display collaborators additions - public # Display repositories made public + + # Set events visibility (use this to restrict events when using a "repo" token) + plugin_activity_visibility: + description: Set events visibility + type: string + default: all + values: + - public + - all + + # Display events timestamps + plugin_activity_timestamps: + description: Display events timestamps + type: boolean + default: no diff --git a/source/templates/classic/partials/activity.ejs b/source/templates/classic/partials/activity.ejs index 610896ae..0d0f5cba 100644 --- a/source/templates/classic/partials/activity.ejs +++ b/source/templates/classic/partials/activity.ejs @@ -18,7 +18,7 @@ No recent activity <% } %> - <% for (const {type, repo, ...event} of plugins.activity.events) { %> + <% for (const {actor, type, repo, timestamp, ...event} of plugins.activity.events) { const _ = letter => account === "organization" ? `
${actor}
${letter.toLocaleLowerCase()}` : letter %>
<% if (/^ref/.test(type)) { %> @@ -28,7 +28,7 @@ <% } else { %> <% } %> - <%= /create/.test(type) ? "Created new" : "Deleted" %> + <%- /create/.test(type) ? `${_("C")}reated new` : `${_("D")}eleted` %> <%= event.ref.type %>
<%= event.ref.name %>
in
<%= repo %>
<% } %> @@ -39,7 +39,7 @@ <% } else if ((event.on === "issue")||(event.on === "commit")) { %> <% } %> - Commented on
#<%= event.number %> <%= event.title %>
+ <%- _("C") %>ommented on
#<%= event.number %> <%= event.title %>
<%= event.on === "commit" ? "committed" : "opened" %> by <%= event.user %> in
<%= repo %>
@@ -49,7 +49,7 @@ <% if (type === "wiki") { %>
- Updated <%= event.pages.length %> wiki page<%= s(event.pages.length) %> in
<%= repo %>
+ <%- _("U") %>pdated <%= event.pages.length %> wiki page<%= s(event.pages.length) %> in
<%= repo %>
<% for (const page of event.pages) { %> @@ -62,7 +62,7 @@ <% if (type === "pr") { %>
- <%= event.action === "opened" ? "Opened" : "Merged" %>
#<%= event.number %> <%= event.title %>
+ <%- event.action === "opened" ? `${_("O")}pened` : `${_("M")}erged` %>
#<%= event.number %> <%= event.title %>
opened <%= user.login !== event.user ? `by ${event.user}` : "" %> in
<%= repo %>
@@ -72,7 +72,7 @@ <% if (type === "issue") { %>
- <%= event.action === "opened" ? "Opened" : event.action === "reopened" ? "Reopened" : "Closed" %>
#<%= event.number %> <%= event.title %>
+ <%- event.action === "opened" ? `${_("O")}pened` : event.action === "reopened" ? `${_("R")}eopened` : `${_("C")}losed` %>
#<%= event.number %> <%= event.title %>
opened <%= user.login !== event.user ? `by ${event.user}` : "" %> in
<%= repo %>
@@ -81,19 +81,19 @@ <% if (type === "fork") { %>
- Forked
<%= repo %>
+ <%- _("F") %>orked
<%= repo %>
<% } %> <% if (type === "public") { %>
- Made
<%= repo %>
public + <%- _("M") %>ade
<%= repo %>
public
<% } %> <% if (type === "review") { %>
- Reviewed
#<%= event.number %> <%= event.title %>
+ <%- _("R") %>eviewed
#<%= event.number %> <%= event.title %>
opened <%= user.login !== event.user ? `by ${event.user}` : "" %> in
<%= repo %>
@@ -102,7 +102,7 @@ <% if (type === "push") { %>
- Pushed <%= event.size %> commit<%= s(event.size) %> in
<%= repo %>
+ <%- _("P") %>ushed <%= event.size %> commit<%= s(event.size) %> in
<%= repo %>
<% if (event.branch) { %> @@ -119,20 +119,25 @@ <% if (type === "release") { %>
- <%= event.draft ? "Drafted release" : event.prerelease ? "Pre-released" : "Released" %> + <%- event.draft ? `${_("D")}rafted release` : event.prerelease ? `${_("P")}re-released` : `${_("R")}eleased` %>
<%= event.name %>
of
<%= repo %>
<% } %> <% if (type === "star") { %>
- Starred
<%= repo %>
+ <%- _("S") %>tarred
<%= repo %>
<% } %> <% if (type === "member") { %>
- Added <%= event.user %> as collaborator in
<%= repo %>
+ <%- _("A") %>dded <%= event.user %> as collaborator in
<%= repo %>
+
+ <% } %> + <% if (plugins.activity.timestamps) { %> +
+ <%= f.date(timestamp, {timeStyle:"short", dateStyle:"short"}) %>
<% } %> diff --git a/source/templates/classic/style.css b/source/templates/classic/style.css index e6cf6efb..2ba04860 100644 --- a/source/templates/classic/style.css +++ b/source/templates/classic/style.css @@ -473,6 +473,13 @@ margin: 0 4px; } + .activity .issue { + flex-grow: 1; + width: 0%; + text-overflow: ellipsis; + overflow: hidden; + } + .activity .code { background-color: #7777771F; padding: 1px 5px; @@ -483,12 +490,12 @@ margin: 0 4px -3px; } - .activity .bold { + .activity .bold, .activity .user { font-weight: 600; margin: 0 4px; } - .activity .details { + .activity .details, .activity .timestamp { padding-left: 42px; display: flex; flex-direction: column; @@ -496,6 +503,11 @@ color: #666666; } + .activity .timestamp { + font-size: 10px; + margin-top: 4px; + } + .activity .details > div { display: flex; align-items: center; diff --git a/source/templates/repository/partials/_.json b/source/templates/repository/partials/_.json index 76964226..d5dc9d61 100644 --- a/source/templates/repository/partials/_.json +++ b/source/templates/repository/partials/_.json @@ -5,5 +5,6 @@ "projects", "pagespeed", "stargazers", - "people" + "people", + "activity" ] \ No newline at end of file diff --git a/source/templates/repository/partials/activity.ejs b/source/templates/repository/partials/activity.ejs new file mode 100644 index 00000000..5e10d649 --- /dev/null +++ b/source/templates/repository/partials/activity.ejs @@ -0,0 +1,150 @@ +<% if (plugins.activity) { %> +
+

+ + Recent activity +

+
+
+ <% if (plugins.activity.error) { %> +
+ + <%= plugins.activity.error.message %> +
+ <% } else { %> + <% if (!plugins.activity.events.length) { %> +
+ + No recent activity +
+ <% } %> + <% for (const {actor, type, repo, timestamp, ...event} of plugins.activity.events) { const _ = letter => `
${actor}
${letter.toLocaleLowerCase()}` %> +
+
+ <% if (/^ref/.test(type)) { %> +
+ <% if (event.ref.type === "branch") { %> + + <% } else { %> + + <% } %> + <%- /create/.test(type) ? `${_("C")}reated new` : `${_("D")}eleted` %> + <%= event.ref.type %>
<%= event.ref.name %>
in
<%= repo %>
+
+ <% } %> + <% if (type === "comment") { %> +
+ <% if (event.on === "pr") { %> + + <% } else if ((event.on === "issue")||(event.on === "commit")) { %> + + <% } %> + <%- _("C") %>ommented on
#<%= event.number %> <%= event.title %>
+
+
+
<%= event.on === "commit" ? "committed" : "opened" %> by <%= event.user %> in
<%= repo %>
+
<%= event.content %>
+
+ <% } %> + <% if (type === "wiki") { %> +
+ + <%- _("U") %>pdated <%= event.pages.length %> wiki page<%= s(event.pages.length) %> in
<%= repo %>
+
+
+ <% for (const page of event.pages) { %> +
+ <%= page %> +
+ <% } %> +
+ <% } %> + <% if (type === "pr") { %> +
+ + <%- event.action === "opened" ? `${_("O")}pened` : `${_("M")}erged` %>
#<%= event.number %> <%= event.title %>
+
+
+
opened <%= user.login !== event.user ? `by ${event.user}` : "" %> in
<%= repo %>
+
<%= event.files.changed %> file<%= s(event.files.changed) %> changed
++<%= event.lines.added %> --<%= event.lines.deleted%>
+
+ <% } %> + <% if (type === "issue") { %> +
+ + <%- event.action === "opened" ? `${_("O")}pened` : event.action === "reopened" ? `${_("R")}eopened` : `${_("C")}losed` %>
#<%= event.number %> <%= event.title %>
+
+
+
opened <%= user.login !== event.user ? `by ${event.user}` : "" %> in
<%= repo %>
+
+ <% } %> + <% if (type === "fork") { %> +
+ + <%- _("F") %>orked
<%= repo %>
+
+ <% } %> + <% if (type === "public") { %> +
+ + <%- _("M") %>ade
<%= repo %>
public +
+ <% } %> + <% if (type === "review") { %> +
+ + <%- _("R") %>eviewed
#<%= event.number %> <%= event.title %>
+
+
+
opened <%= user.login !== event.user ? `by ${event.user}` : "" %> in
<%= repo %>
+
+ <% } %> + <% if (type === "push") { %> +
+ + <%- _("P") %>ushed <%= event.size %> commit<%= s(event.size) %> in
<%= repo %>
+
+
+ <% if (event.branch) { %> +
on branch
<%= event.branch %>
+ <% } %> + <% for (const commit of event.commits) { %> +
+
#<%= commit.sha %>
+
<%= commit.message %>
+
+ <% } %> +
+ <% } %> + <% if (type === "release") { %> +
+ + <%- event.draft ? `${_("D")}rafted release` : event.prerelease ? `${_("P")}re-released` : `${_("R")}eleased` %> +
<%= event.name %>
of
<%= repo %>
+
+ <% } %> + <% if (type === "star") { %> +
+ + <%- _("S") %>tarred
<%= repo %>
+
+ <% } %> + <% if (type === "member") { %> +
+ + <%- _("A") %>dded <%= event.user %> as collaborator in
<%= repo %>
+
+ <% } %> + <% if (plugins.activity.timestamps) { %> +
+ <%= f.date(timestamp, {timeStyle:"short", dateStyle:"short"}) %> +
+ <% } %> +
+
+ <% } %> + <% } %> +
+
+
+<% } %> \ No newline at end of file