From b06d9ff898f2b8aaeed0a953ac7fec2fd2ea4604 Mon Sep 17 00:00:00 2001 From: Simon Lecoq <22963968+lowlighter@users.noreply.github.com> Date: Tue, 5 Jan 2021 12:39:04 +0100 Subject: [PATCH] Add Stargazers plugin (#37) --- .github/pull_request_template.md | 25 ++++++++++- README.md | 43 +++++++++++++++++- action.yml | 6 +++ settings.example.json | 5 ++- source/app/action/index.mjs | 1 + source/app/mocks.mjs | 20 +++++++++ source/app/web/statics/app.js | 1 + source/app/web/statics/index.html | 16 ++++--- source/app/web/statics/style.css | 4 +- source/plugins/stargazers/index.mjs | 65 +++++++++++++++++++++++++++ source/queries/stargazers.graphql | 10 +++++ source/templates/classic/image.svg | 48 ++++++++++++++++++++ source/templates/classic/style.css | 12 +++++ source/templates/repository/image.svg | 48 ++++++++++++++++++++ tests/metrics.test.js | 3 ++ 15 files changed, 295 insertions(+), 12 deletions(-) create mode 100644 source/plugins/stargazers/index.mjs create mode 100644 source/queries/stargazers.graphql diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index d7e1989a..6fdf9bb1 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -6,7 +6,30 @@ --> **Pull request description** - + **Additional context and screenshots** diff --git a/README.md b/README.md index 651bb863..82f3ce7c 100644 --- a/README.md +++ b/README.md @@ -147,20 +147,32 @@ But there's more with [plugins](https://github.com/lowlighter/metrics/tree/maste + ✨ Stargazers evolution 🌟 Recently starred repositories - 🗃️ Header special features + + + + + + + + 🗃️ Header special features + + + + @@ -516,6 +528,7 @@ Used template defaults to the `classic` one. 💡 🎫 🌟 + Classic @@ -534,6 +547,7 @@ Used template defaults to the `classic` one. ✔️ ✔️ ✔️M + ✔️M Terminal @@ -552,6 +566,7 @@ Used template defaults to the `classic` one. ❌ ✔️ ❌ + ❌ RepositoryR @@ -570,6 +585,7 @@ Used template defaults to the `classic` one. ❌ ❌ ❌ + ✔️M @@ -584,7 +600,7 @@ Used template defaults to the `classic` one. To use `repository` template, you'll need to provide a repository name in `query` option. -If repository owner is different from `token` owner, use `user` option to specify it. +If repository owner is different from `token` owner, use `user` option to specify it. Add the following to your workflow : ```yaml @@ -1281,6 +1297,29 @@ Add the following to your workflow : +### ✨ Stargazers + + 🚧 This feature is available on @master + +The *stargazers* plugin displays your stargazers evolution across all of your repositories over the last two weeks. + +![Stargazers plugin](https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.stargazers.svg) + +
+💬 About + +It will consume additional GitHub requests per repository per set of 100 stargazers. + +Add the following to your workflow : +```yaml +- uses: lowlighter/metrics@latest + with: + # ... other options + plugin_stargazers: yes +``` + +
+ ### 🔧 Other options A few additional options are available. diff --git a/action.yml b/action.yml index 44980521..3b002136 100644 --- a/action.yml +++ b/action.yml @@ -357,6 +357,12 @@ inputs: description: Number of recently starred repositories to display default: 4 + # Display stargazers evolution over the last two weeks + # It shows total stargazers along with increase rate per day + plugin_stagazers: + description: Display stargazers evolution over the last two weeks + default: no + # ==================================================================================== # Options below are mostly used for testing diff --git a/settings.example.json b/settings.example.json index cca8e548..815ce845 100644 --- a/settings.example.json +++ b/settings.example.json @@ -60,7 +60,10 @@ "token":null, "//":"Twitter token (required when enabled)" }, "stars":{ "//":"Stars plugin", - "enabled":true, "//":"Enable or disable recently starred repositories display" + "enabled":false, "//":"Enable or disable recently starred repositories display" + }, + "stargazers":{ "//":"Stargazers plugin", + "enabled":false, "//":"Enable or disable stargazers charts display" } } } \ No newline at end of file diff --git a/source/app/action/index.mjs b/source/app/action/index.mjs index 3ef0ddd4..bbbbbcd5 100644 --- a/source/app/action/index.mjs +++ b/source/app/action/index.mjs @@ -140,6 +140,7 @@ projects:{enabled:input.bool("plugin_projects")}, tweets:{enabled:input.bool("plugin_tweets")}, stars:{enabled:input.bool("plugin_stars")}, + stargazers:{enabled:input.bool("plugin_stargazers")}, } let q = Object.fromEntries(Object.entries(plugins).filter(([key, plugin]) => plugin.enabled).map(([key]) => [key, true])) info("Plugins enabled", Object.entries(plugins).filter(([key, plugin]) => plugin.enabled).map(([key]) => key)) diff --git a/source/app/mocks.mjs b/source/app/mocks.mjs index 7a262d60..462f3818 100644 --- a/source/app/mocks.mjs +++ b/source/app/mocks.mjs @@ -296,6 +296,26 @@ } }) } + //Stargazers query + if (/^query Stargazers /.test(query)) { + console.debug(`metrics/compute/mocks > mocking graphql api result > Stargazers`) + return /after: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"/m.test(query) ? ({ + repository:{ + stargazers:{ + edges:[], + } + } + }) : ({ + repository:{ + stargazers:{ + edges:new Array(Math.ceil(20+80*Math.random())).fill(null).map(() => ({ + starredAt:new Date(Date.now()-Math.floor(30*Math.random())*24*60*60*1000).toISOString(), + cursor:"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + })) + } + } + }) + } //Unmocked call return target(...args) } diff --git a/source/app/web/statics/app.js b/source/app/web/statics/app.js index 0d6d0a0f..7aca6c91 100644 --- a/source/app/web/statics/app.js +++ b/source/app/web/statics/app.js @@ -47,6 +47,7 @@ projects:"🗂️ Projects", tweets:"🐤 Latest tweets", stars:"🌟 Recently starred repositories", + stargazers:"✨ Stargazers over last weeks", "base.header":` Header`, diff --git a/source/app/web/statics/index.html b/source/app/web/statics/index.html index 2b6d8c59..194c848b 100644 --- a/source/app/web/statics/index.html +++ b/source/app/web/statics/index.html @@ -17,8 +17,10 @@

- - Metrics v{{ version }} + + + Metrics v{{ version }} +

@@ -29,14 +31,14 @@ Overview -
- - Markdown -
Action
+
+ + Markdown +
@@ -242,7 +244,7 @@
- Once generated, click on tabs above to see the code to embed them on your real readme ! + Once generated, click on tabs above to see the code to embed them on your real readme !
diff --git a/source/app/web/statics/style.css b/source/app/web/statics/style.css index 8e42ae53..2b921daa 100644 --- a/source/app/web/statics/style.css +++ b/source/app/web/statics/style.css @@ -10,7 +10,6 @@ background-color: var(--color-bg-canvas); display: flex; flex-direction: column; - overflow: hidden; } /* Title */ .title { @@ -36,6 +35,7 @@ display: flex; border-bottom: 1px solid var(--color-border-secondary); flex-shrink: 0; + overflow-x: auto; } nav .tab { flex-shrink: 0; @@ -287,6 +287,7 @@ } nav { margin: 32px 0 24px; + overflow: hidden; } nav .left { display: block; @@ -297,6 +298,7 @@ main { height: 100vh; width: 100vw; + overflow: hidden; } .avatar { display: flex; diff --git a/source/plugins/stargazers/index.mjs b/source/plugins/stargazers/index.mjs new file mode 100644 index 00000000..94a3e87d --- /dev/null +++ b/source/plugins/stargazers/index.mjs @@ -0,0 +1,65 @@ +//Setup + export default async function ({login, graphql, data, q, queries, imports}, {enabled = false} = {}) { + //Plugin execution + try { + //Check if plugin is enabled and requirements are met + if ((!enabled)||(!q.stargazers)) + return null + //Retrieve stargazers from graphql api + console.debug(`metrics/compute/${login}/plugins > stargazers > querying api`) + const repositories = data.user.repositories.nodes.map(({name}) => name).slice(0, 2) + const dates = [] + for (const repository of repositories) { + //Iterate through stargazers + console.debug(`metrics/compute/${login}/plugins > stargazers > retrieving stargazers of ${repository}`) + let cursor = null + let pushed = 0 + do { + console.debug(`metrics/compute/${login}/plugins > stargazers > retrieving stargazers of ${repository} after ${cursor}`) + const {repository:{stargazers:{edges}}} = await graphql(queries.stargazers({login, repository, after:cursor ? `after: "${cursor}"` : ""})) + cursor = edges?.[edges?.length-1]?.cursor + console.log(edges) + dates.push(...edges.map(({starredAt}) => new Date(starredAt))) + pushed = edges.length + } while ((pushed)&&(cursor)) + //Limit repositories + console.debug(`metrics/compute/${login}/plugins > stargazers > loaded ${dates.length} stargazers for ${repository}`) + } + console.debug(`metrics/compute/${login}/plugins > stargazers > loaded ${dates.length} stargazers in total`) + //Compute stargazers increments + const days = 14 + const increments = {dates:Object.fromEntries([...new Array(days).fill(null).map((_, i) => [new Date(Date.now()-i*24*60*60*1000).toISOString().slice(0, 10), 0]).reverse()]), max:NaN, min:NaN} + dates + .map(date => date.toISOString().slice(0, 10)) + .filter(date => date in increments.dates) + .map(date => increments.dates[date]++) + increments.min = Math.min(...Object.values(increments.dates)) + increments.max = Math.max(...Object.values(increments.dates)) + //Compute total stargazers + let stargazers = data.computed.repositories.stargazers + const total = {dates:{...increments.dates}, max:NaN, min:NaN} + { + const dates = Object.keys(total.dates) + for (let i = dates.length-1; i >= 0; i--) { + const date = dates[i], tomorrow = dates[i+1] + stargazers -= (increments.dates[tomorrow] ?? 0) + total.dates[date] = stargazers + } + } + total.min = Math.min(...Object.values(total.dates)) + total.max = Math.max(...Object.values(total.dates)) + //Format values + for (const date in increments.dates) + increments.dates[date] = `${increments.dates[date] > 0 ? "+" : ""}${imports.format(increments.dates[date])}` + for (const date in total.dates) + total.dates[date] = imports.format(total.dates[date]) + //Months name + const months = ["", "Jan.", "Feb.", "Mar.", "Apr.", "May", "June", "July", "Aug.", "Sep.", "Oct.", "Nov.", "Dec."] + //Results + return {total, increments, months} + } + //Handle errors + catch (error) { + throw {error:{message:"An error occured", instance:error}} + } + } diff --git a/source/queries/stargazers.graphql b/source/queries/stargazers.graphql new file mode 100644 index 00000000..fed1601a --- /dev/null +++ b/source/queries/stargazers.graphql @@ -0,0 +1,10 @@ +query Stargazers { + repository(name: "$repository", owner: "$login") { + stargazers($after first: 100, orderBy: {field: STARRED_AT, direction: ASC}) { + edges { + starredAt + cursor + } + } + } +} \ No newline at end of file diff --git a/source/templates/classic/image.svg b/source/templates/classic/image.svg index 9de19ff7..5b27a563 100644 --- a/source/templates/classic/image.svg +++ b/source/templates/classic/image.svg @@ -847,6 +847,54 @@
<% } %> + <% if (plugins.stargazers) { %> +
+

+ + Stargazers over the last two weeks +

+ <% if (plugins.stargazers.error) { %> +
+ + <%= plugins.stargazers.error.message %> +
+ <% } else { %> +
+
+

Total stargazers

+
+ <% { let previous = null; for (const [date, value] of Object.entries(plugins.stargazers.total.dates)) { const p = 0.05+0.95*(value-plugins.stargazers.total.min)/(plugins.stargazers.total.max-plugins.stargazers.total.min); const [y, m, d] = date.split("-").map(Number) %> +
+ <%= (value-(previous ?? 0)) ? value : "" %> +
+ <%= d %> + <% if ((previous === null)||(d === 1)) { %> +
<%= plugins.stargazers.months[m] %>
+ <% } %> +
+ <% previous = value } } %> +
+
+
+

New stargazers per day

+
+ <% { let previous = true; for (const [date, value] of Object.entries(plugins.stargazers.increments.dates)) { const p = value/plugins.stargazers.increments.max; const [y, m, d] = date.split("-").map(Number) %> +
+ <%= value != 0 ? value : "" %> +
+ <%= d %> + <% if ((previous === null)||(d === 1)) { %> +
<%= plugins.stargazers.months[m] %>
+ <% } %> +
+ <% previous = value } } %> +
+
+
+ <% } %> +
+ <% } %> + <% if (base.metadata) { %>