diff --git a/README.md b/README.md index 440a91ba..3f826b57 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,7 @@ But there's more with [plugins](https://github.com/lowlighter/metrics/tree/maste 🎫 Gists plugin - 🗃️ Header special features + 🧑‍🤝‍🧑 People plugin 🚧 @master @@ -172,11 +172,28 @@ But there's more with [plugins](https://github.com/lowlighter/metrics/tree/maste + + + + +
Followed people version + + + +
+ + + + 🗃️ Header special features + + + + @@ -534,6 +551,7 @@ Used template defaults to the `classic` one. 🌟 🎫 + 🧑‍🤝‍🧑 Classic @@ -550,10 +568,11 @@ Used template defaults to the `classic` one. ✔️ ✔️ ✔️ - ✔️M + ✔️M ✔️ ✔️ ✔️ + ✔️M Terminal @@ -574,6 +593,7 @@ Used template defaults to the `classic` one. ❌ ❌ ✔️ + ❌ RepositoryR @@ -594,6 +614,7 @@ Used template defaults to the `classic` one. ❌ ✔️ ❌ + ❌ @@ -1381,6 +1402,41 @@ Add the following to your workflow : +### 🧑‍🤝‍🧑 People + + 🚧 This plugin is available as pre-release on @master branch (unstable) + +The *people* plugin displays your followers and followed users' avatars. + +![People plugin](https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.people.svg) + +
+💬 About + +It will consume an additional GitHub request per group of 100 users fetched. + +Add the following to your workflow : +```yaml +- uses: lowlighter/metrics@master + with: + # ... other options + plugin_people: yes + plugin_people_types: followers, following + plugin_people_limit: 28 + plugin_people_size: 28 # Size in pixels of displayed avatars +``` + +It is possible to use [identicons](https://github.blog/2013-08-14-identicons/) instead of their avatar for privacy purposes. + +```yaml +- uses: lowlighter/metrics@master + with: + # ... other options + plugin_people_identicons: yes +``` + +
+ ### 🔧 Other options A few additional options are available. diff --git a/action.yml b/action.yml index f952af5e..ad4a5480 100644 --- a/action.yml +++ b/action.yml @@ -377,7 +377,7 @@ inputs: description: Number of activity events to display default: 5 - # Disacard older events + # Discard older events # Use 0 to display activity whatever the date plugin_activity_days: description: Maximum activity event age @@ -400,6 +400,35 @@ inputs: description: Events to display default: all + # Display followed and following users + plugin_people: + description: Display + default: no + + # Limit the number of users displayed + plugin_people_limit: + description: Number of users to display per categorie + default: 28 + + # Configure image size of users' avatar + plugin_people_size: + description: Size of users' avatars + default: 28 + + # List of users categories to display (comma separated) + # Supported values are: + # - "followers" + # - "following" + plugin_people_types: + description: Categories to display + default: followers, following + + # Display GitHub identicons instead of users' real avatar + # Mostly for privacy purposes + plugin_people_identicons: + description: Use identicons instead of real avatars + default: no + # ==================================================================================== # Options below are mostly used for testing @@ -497,7 +526,7 @@ runs: set -e echo "Is released version: $METRICS_IS_RELEASED" # Rebuild image for unreleased version - if [[ $METRICS_IS_RELEASED ]]; then + if [[ "$METRICS_IS_RELEASED" -gt "0" ]]; then echo "Using released version $METRICS_TAG, will pull docker image from GitHub registry" METRICS_IMAGE=ghcr.io/lowlighter/metrics:$METRICS_TAG # Use registry for released version diff --git a/settings.example.json b/settings.example.json index 6049aed6..3db0a2fb 100644 --- a/settings.example.json +++ b/settings.example.json @@ -67,6 +67,9 @@ }, "activity":{ "//":"Activity plugin", "enabled":false, "//":"Enable or disable recent activity display" + }, + "people":{ "//":"People plugin", + "enabled":false, "//":"Enable or disable people display" } } } \ No newline at end of file diff --git a/source/app/action/index.mjs b/source/app/action/index.mjs index e9f8d8ef..fceb7f3f 100644 --- a/source/app/action/index.mjs +++ b/source/app/action/index.mjs @@ -142,6 +142,7 @@ stars:{enabled:input.bool("plugin_stars")}, stargazers:{enabled:input.bool("plugin_stargazers")}, activity:{enabled:input.bool("plugin_activity")}, + people:{enabled:input.bool("plugin_people")}, } 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)) @@ -223,6 +224,15 @@ for (const option of ["filter"]) info(`Activity ${option}`, q[`activity.${option}`] = input.array(`plugin_activity_${option}`)) } + //People + if (plugins.people.enabled) { + for (const option of ["limit", "size"]) + info(`People ${option}`, q[`people.${option}`] = input.number(`plugin_people_${option}`)) + for (const option of ["types"]) + info(`People ${option}`, q[`people.${option}`] = input.array(`plugin_people_${option}`)) + for (const option of ["identicons"]) + info(`People ${option}`, q[`people.${option}`] = input.bool(`plugin_people_${option}`)) + } //Repositories to use const repositories = input.number("repositories") diff --git a/source/app/metrics.mjs b/source/app/metrics.mjs index 50f859ae..1a6b2cae 100644 --- a/source/app/metrics.mjs +++ b/source/app/metrics.mjs @@ -90,7 +90,7 @@ //Additional SVG transformations if (/svg/.test(mime)) { //Optimize rendering - if ((conf.optimize)&&(!q.raw)) { + if ((conf.settings?.optimize)&&(!q.raw)) { console.debug(`metrics/compute/${login} > optimize`) const svgo = new SVGO({full:true, plugins:[{cleanupAttrs:true}, {inlineStyles:false}]}) const {data:optimized} = await svgo.optimize(rendered) diff --git a/source/app/mocks.mjs b/source/app/mocks.mjs index f0e19e13..9846e5f6 100644 --- a/source/app/mocks.mjs +++ b/source/app/mocks.mjs @@ -316,6 +316,30 @@ } }) } + //People query + if (/^query People /.test(query)) { + console.debug(`metrics/compute/mocks > mocking graphql api result > People`) + const type = query.match(/(?followers|following)[(]/)?.groups?.type ?? "(unknown type)" + return /after: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"/m.test(query) ? ({ + user:{ + [type]:{ + edges:[], + } + } + }) : ({ + user:{ + [type]:{ + edges:new Array(Math.ceil(20+80*Math.random())).fill(null).map(() => ({ + cursor:"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + node:{ + login:"user", + avatarUrl:"https://github.com/identicons/user.png", + } + })) + } + } + }) + } //Unmocked call return target(...args) } diff --git a/source/plugins/people/index.mjs b/source/plugins/people/index.mjs new file mode 100644 index 00000000..18a38d9e --- /dev/null +++ b/source/plugins/people/index.mjs @@ -0,0 +1,53 @@ +//Setup + export default async function ({login, graphql, q, queries, imports}, {enabled = false} = {}) { + //Plugin execution + try { + //Check if plugin is enabled and requirements are met + if ((!enabled)||(!q.people)) + return null + + //Parameters override + let {"people.limit":limit = 28, "people.types":types = "followers, following", "people.size":size = 28, "people.identicons":identicons = false} = q + //Limit + limit = Math.max(1, limit) + //Repositories projects + types = decodeURIComponent(types ?? "").split(",").map(type => type.trim()).filter(type => ["followers", "following"].includes(type)) ?? [] + + //Retrieve followers from graphql api + console.debug(`metrics/compute/${login}/plugins > people > querying api`) + const result = {followers:[], following:[]} + for (const type of types) { + //Iterate through people + console.debug(`metrics/compute/${login}/plugins > people > retrieving ${type}`) + let cursor = null + let pushed = 0 + do { + console.debug(`metrics/compute/${login}/plugins > people > retrieving ${type} after ${cursor}`) + const {user:{[type]:{edges}}} = await graphql(queries.people({login, type, size, after:cursor ? `after: "${cursor}"` : ""})) + cursor = edges?.[edges?.length-1]?.cursor + result[type].push(...edges.map(({node}) => node)) + pushed = edges.length + } while ((pushed)&&(cursor)) + //Limit people + if (limit > 0) { + console.debug(`metrics/compute/${login}/plugins > people > keeping only ${limit} ${type}`) + result[type].splice(limit) + } + //Hide real avator with identicons if enabled + if (identicons) { + console.debug(`metrics/compute/${login}/plugins > people > using identicons`) + result[type].map(user => user.avatarUrl = `https://github.com/identicons/${user.login}.png`) + } + //Convert avatars to base64 + console.debug(`metrics/compute/${login}/plugins > people > loading avatars`) + await Promise.all(result[type].map(async user => user.avatar = await imports.imgb64(user.avatarUrl))) + } + + //Results + return {types, size, ...result} + } + //Handle errors + catch (error) { + throw {error:{message:"An error occured", instance:error}} + } + } diff --git a/source/plugins/projects/index.mjs b/source/plugins/projects/index.mjs index b70d1aa2..bb6d70d2 100644 --- a/source/plugins/projects/index.mjs +++ b/source/plugins/projects/index.mjs @@ -8,7 +8,7 @@ //Parameters override let {"projects.limit":limit = 4, "projects.repositories":repositories = ""} = q //Repositories projects - repositories = repositories?.split(",").map(repository => repository.trim()).filter(repository => /[-\w]+[/][-\w]+[/]projects[/]\d+/.test(repository)) ?? [] + repositories = decodeURIComponent(repositories ?? "").split(",").map(repository => repository.trim()).filter(repository => /[-\w]+[/][-\w]+[/]projects[/]\d+/.test(repository)) ?? [] //Limit limit = Math.max(repositories.length, Math.min(100, Number(limit))) //Retrieve user owned projects from graphql api diff --git a/source/queries/people.graphql b/source/queries/people.graphql new file mode 100644 index 00000000..c69041eb --- /dev/null +++ b/source/queries/people.graphql @@ -0,0 +1,14 @@ +query People { + user(login: "$login") { + login + $type($after first: 100) { + edges { + cursor + node { + login + avatarUrl(size: $size) + } + } + } + } +} diff --git a/source/templates/classic/image.svg b/source/templates/classic/image.svg index c5f1afe9..5719973f 100644 --- a/source/templates/classic/image.svg +++ b/source/templates/classic/image.svg @@ -895,6 +895,66 @@ <% } %> + <% if (plugins.people) { %> + <% if (plugins.people.error) { %> +
+

+ + +

+
+
+
+ + <%= plugins.people.error.message %> +
+
+
+
+ <% } else { %> + <% if (plugins.people.types?.includes("followers")) { %> +
+

+ + <%= user.followers.totalCount %> follower<%= s(user.followers.totalCount) %> +

+
+
+ <% if (plugins.people.error) { %> +
+ + <%= plugins.people.error.message %> +
+ <% } else { %> + <% for (const user of plugins.people.followers) { %><% } %> + <% } %> +
+
+
+ <% } %> + <% if (plugins.people.types?.includes("following")) { %> +
+

+ + <%= user.following.totalCount %> followed +

+
+
+ <% if (plugins.people.error) { %> +
+ + <%= plugins.people.error.message %> +
+ <% } else { %> + <% for (const user of plugins.people.following) { %><% } %> + <% } %> +
+
+
+ <% } %> + <% } %> + <% } %> + <% if (plugins.activity) { %>

diff --git a/source/templates/classic/style.css b/source/templates/classic/style.css index e2ecb764..9ae70f5f 100644 --- a/source/templates/classic/style.css +++ b/source/templates/classic/style.css @@ -85,7 +85,6 @@ /* User avatar */ .avatar { - background-color: #000000; border-radius: 50%; margin: 0 6px; } @@ -503,6 +502,15 @@ -webkit-box-orient: vertical; } +/* People */ + .people { + padding: 0 10px; + } + + .people .avatar { + margin: 0 2px; + } + /* Fade animation */ .af { opacity: 0; diff --git a/tests/metrics.test.js b/tests/metrics.test.js index b4cfc80c..80f7c6fd 100644 --- a/tests/metrics.test.js +++ b/tests/metrics.test.js @@ -255,6 +255,21 @@ ["Gists plugin (default)", { plugin_gists:true, }, {skip:["terminal", "repository"]}], + ["People plugin (default)", { + plugin_people:true, + }, {skip:["terminal", "repository"]}], + ["People plugin (followers)", { + plugin_people:true, + plugin_people_types:"followers", + }, {skip:["terminal", "repository"]}], + ["People plugin (following)", { + plugin_people:true, + plugin_people_types:"following", + }, {skip:["terminal", "repository"]}], + ["People plugin (identicons)", { + plugin_people:true, + plugin_people_identicons:true, + }, {skip:["terminal", "repository"]}], ] //Tests run