From 88f9e1a41f09606f3a6872f60594858a4741ea6b Mon Sep 17 00:00:00 2001 From: Simon Lecoq <22963968+lowlighter@users.noreply.github.com> Date: Fri, 30 Jul 2021 23:05:12 +0200 Subject: [PATCH 1/2] Add new Discussions plugin (#430) [skip ci] --- .../github/graphql/discussions.categories.mjs | 27 +++++++++++ .../github/graphql/discussions.statistics.mjs | 11 +++++ source/app/web/statics/app.placeholder.js | 14 ++++++ source/plugins/discussions/README.md | 21 ++++++++ source/plugins/discussions/index.mjs | 46 ++++++++++++++++++ source/plugins/discussions/metadata.yml | 12 +++++ .../discussions/queries/categories.graphql | 15 ++++++ .../discussions/queries/statistics.graphql | 13 +++++ source/plugins/discussions/tests.yml | 5 ++ source/templates/classic/partials/_.json | 1 + .../classic/partials/discussions.ejs | 48 +++++++++++++++++++ 11 files changed, 213 insertions(+) create mode 100644 source/app/mocks/api/github/graphql/discussions.categories.mjs create mode 100644 source/app/mocks/api/github/graphql/discussions.statistics.mjs create mode 100644 source/plugins/discussions/README.md create mode 100644 source/plugins/discussions/index.mjs create mode 100644 source/plugins/discussions/metadata.yml create mode 100644 source/plugins/discussions/queries/categories.graphql create mode 100644 source/plugins/discussions/queries/statistics.graphql create mode 100644 source/plugins/discussions/tests.yml create mode 100644 source/templates/classic/partials/discussions.ejs diff --git a/source/app/mocks/api/github/graphql/discussions.categories.mjs b/source/app/mocks/api/github/graphql/discussions.categories.mjs new file mode 100644 index 00000000..e5fa8fdc --- /dev/null +++ b/source/app/mocks/api/github/graphql/discussions.categories.mjs @@ -0,0 +1,27 @@ +/**Mocked data */ +export default function({faker, query, login = faker.internet.userName()}) { + console.debug("metrics/compute/mocks > mocking graphql api result > contributors/commit") + return /after: "MOCKED_CURSOR"/m.test(query) + ? ({ + user:{ + repositoryDiscussions:{ + edges:[], + nodes:[], + } + } + }) + : ({ + user:{ + repositoryDiscussions:{ + edges:new Array(100).fill(null).map(_ => ({cursor:"MOCKED_CURSOR"})), + nodes:new Array(100).fill(null).map(_ => ({ + category:{ + emoji:faker.random.arrayElement([":chart_with_upwards_trend:", ":chart_with_downwards_trend:", ":bar_char:"]), + name:faker.lorem.slug() + } + })) + } + } + }) +} + diff --git a/source/app/mocks/api/github/graphql/discussions.statistics.mjs b/source/app/mocks/api/github/graphql/discussions.statistics.mjs new file mode 100644 index 00000000..b899f7ea --- /dev/null +++ b/source/app/mocks/api/github/graphql/discussions.statistics.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 > contributors/commit") + return ({ + user:{ + started:{totalCount:faker.datatype.number(1000)}, + comments:{totalCount:faker.datatype.number(1000)}, + answers:{totalCount:faker.datatype.number(1000)} + } + }) +} \ No newline at end of file diff --git a/source/app/web/statics/app.placeholder.js b/source/app/web/statics/app.placeholder.js index 63d4cc51..2c327712 100644 --- a/source/app/web/statics/app.placeholder.js +++ b/source/app/web/statics/app.placeholder.js @@ -586,6 +586,20 @@ }, }) : null), + //Discussions + ...(set.plugins.enabled.discussions + ? ({ + discussions: { + categories: { + stats: { '🙏 Q&A': faker.datatype.number(100), 'đŸ“Ŗ Announcements': faker.datatype.number(100), '💡 Ideas': faker.datatype.number(100), 'đŸ’Ŧ General': faker.datatype.number(100) }, + favorite: 'đŸ“Ŗ Announcements' + }, + started: faker.datatype.number(1000), + comments: faker.datatype.number(1000), + answers: faker.datatype.number(1000), + }, + }) + : null), //Posts ...(set.plugins.enabled.posts ? ({ diff --git a/source/plugins/discussions/README.md b/source/plugins/discussions/README.md new file mode 100644 index 00000000..23bae3c4 --- /dev/null +++ b/source/plugins/discussions/README.md @@ -0,0 +1,21 @@ +### đŸ’Ŧ Discussions + +The *discussions* plugin displays your GitHub discussions metrics. + + + +
+ + +
+ +#### â„šī¸ Examples workflows + +[âžĄī¸ Available options for this plugin](metadata.yml) + +```yaml +- uses: lowlighter/metrics@latest + with: + # ... other options + plugin_discussions: yes +``` \ No newline at end of file diff --git a/source/plugins/discussions/index.mjs b/source/plugins/discussions/index.mjs new file mode 100644 index 00000000..8327fb68 --- /dev/null +++ b/source/plugins/discussions/index.mjs @@ -0,0 +1,46 @@ +//Setup + export default async function({login, q, imports, graphql, queries, data, account}, {enabled = false} = {}) { + //Plugin execution + try { + //Check if plugin is enabled and requirements are met + if ((!enabled)||(!q.discussions)) + return null + + //Load inputs + imports.metadata.plugins.discussions.inputs({data, account, q}) + const discussions = {categories:{}} + + //Fetch general statistics + const stats = Object.fromEntries(Object.entries((await graphql(queries.discussions.statistics({login}))).user).map(([key, value]) => [key, value.totalCount])) + Object.assign(discussions, stats) + + //Load started discussions + { + const fetched = [] + const categories = {} + let cursor = null + let pushed = 0 + do { + console.debug(`metrics/compute/${login}/discussions > retrieving discussions after ${cursor}`) + const {user:{repositoryDiscussions:{edges = [], nodes = []} = {}}} = await graphql(queries.discussions.categories({login, after:cursor ? `after: "${cursor}"` : ""})) + cursor = edges?.[edges?.length - 1]?.cursor + fetched.push(...nodes) + pushed = nodes.length + console.debug(`metrics/compute/${login}/discussions > retrieved ${pushed} discussions after ${cursor}`) + } while ((pushed) && (cursor)) + + //Compute favorite category + for (const category of [...fetched.map(({category:{emoji, name}}) => `${imports.emoji.get(emoji)} ${name}`)]) + categories[category] = (categories[category] ?? 0) + 1 + discussions.categories.stats = categories + discussions.categories.favorite = Object.entries(categories).sort((a, b) => b[1] - a[1]).map(([name]) => name).shift() ?? null + } + + //Results + return discussions + } + //Handle errors + catch (error) { + throw {error:{message:"An error occured", instance:error}} + } + } diff --git a/source/plugins/discussions/metadata.yml b/source/plugins/discussions/metadata.yml new file mode 100644 index 00000000..84058a96 --- /dev/null +++ b/source/plugins/discussions/metadata.yml @@ -0,0 +1,12 @@ +name: "đŸ’Ŧ Discussions" +cost: 1 GraphQL request + 1 GraphQL request per 100 discussions started +category: github +supports: + - user +inputs: + + # Enable or disable plugin + plugin_discussions: + description: GitHub discussions metrics + type: boolean + default: no \ No newline at end of file diff --git a/source/plugins/discussions/queries/categories.graphql b/source/plugins/discussions/queries/categories.graphql new file mode 100644 index 00000000..cc6ba0f6 --- /dev/null +++ b/source/plugins/discussions/queries/categories.graphql @@ -0,0 +1,15 @@ +query DiscussionsCategories { + user(login: "$login") { + repositoryDiscussions($after first: 100, orderBy: {field: CREATED_AT, direction: DESC}) { + edges { + cursor + } + nodes { + category { + emoji + name + } + } + } + } +} \ No newline at end of file diff --git a/source/plugins/discussions/queries/statistics.graphql b/source/plugins/discussions/queries/statistics.graphql new file mode 100644 index 00000000..281bf769 --- /dev/null +++ b/source/plugins/discussions/queries/statistics.graphql @@ -0,0 +1,13 @@ +query DiscussionsStatistics { + user(login: "$login") { + started: repositoryDiscussions { + totalCount + } + comments: repositoryDiscussionComments { + totalCount + } + answers: repositoryDiscussionComments(onlyAnswers: true) { + totalCount + } + } +} \ No newline at end of file diff --git a/source/plugins/discussions/tests.yml b/source/plugins/discussions/tests.yml new file mode 100644 index 00000000..d1a46e9a --- /dev/null +++ b/source/plugins/discussions/tests.yml @@ -0,0 +1,5 @@ +- name: Discussions plugin (default) + uses: lowlighter/metrics@latest + with: + token: MOCKED_TOKEN + plugin_discussions: yes \ No newline at end of file diff --git a/source/templates/classic/partials/_.json b/source/templates/classic/partials/_.json index cc4d8fc5..0834249c 100644 --- a/source/templates/classic/partials/_.json +++ b/source/templates/classic/partials/_.json @@ -25,6 +25,7 @@ "anilist", "wakatime", "skyline", + "discussions", "support", "stackoverflow", "stock", diff --git a/source/templates/classic/partials/discussions.ejs b/source/templates/classic/partials/discussions.ejs new file mode 100644 index 00000000..e8587b2f --- /dev/null +++ b/source/templates/classic/partials/discussions.ejs @@ -0,0 +1,48 @@ +<% if (plugins.discussions) { %> +
+

+ + <% if (!plugins.discussions.error) { %> + <%= plugins.discussions.started %> Discussion<%= s(plugins.discussions.started) %> started + <% } else { %> + Discussions + <% } %> +

+ <% if (plugins.discussions.error) { %> +
+
+
+ + <%= plugins.discussions.error.message %> +
+
+
+ <% } else { %> +
+
+
+ + <%= plugins.discussions.comments %> Comment<%= s(plugins.discussions.comments) %> +
+
+
+
+ + <%= plugins.discussions.answers %> Answer<%= s(plugins.discussions.answers) %> +
+
+
+ <% if (Object.keys(plugins.discussions.categories.stats).length) { %> +
+
+
+ <% for (const [category, posts] of Object.entries(plugins.discussions.categories.stats)) { %> +
<%= category %>
<%= posts %>
+ <% } %> +
+
+
+ <% } %> + <% } %> +
+<% } %> \ No newline at end of file From 012748529febd79cf8012ad55a8c1bd7579e64b3 Mon Sep 17 00:00:00 2001 From: Simon Lecoq <22963968+lowlighter@users.noreply.github.com> Date: Fri, 30 Jul 2021 23:05:27 +0200 Subject: [PATCH 2/2] Add new Repositories plugin (#431) [skip ci] --- .../graphql/repositories.repository.mjs | 29 +++++++ source/app/web/statics/app.placeholder.js | 26 ++++++- source/plugins/repositories/README.md | 26 +++++++ source/plugins/repositories/index.mjs | 38 +++++++++ source/plugins/repositories/metadata.yml | 22 ++++++ .../repositories/queries/repository.graphql | 26 +++++++ source/plugins/repositories/tests.yml | 6 ++ source/templates/classic/partials/_.json | 1 + .../classic/partials/repositories.ejs | 78 +++++++++++++++++++ 9 files changed, 251 insertions(+), 1 deletion(-) create mode 100644 source/app/mocks/api/github/graphql/repositories.repository.mjs create mode 100644 source/plugins/repositories/README.md create mode 100644 source/plugins/repositories/index.mjs create mode 100644 source/plugins/repositories/metadata.yml create mode 100644 source/plugins/repositories/queries/repository.graphql create mode 100644 source/plugins/repositories/tests.yml create mode 100644 source/templates/classic/partials/repositories.ejs diff --git a/source/app/mocks/api/github/graphql/repositories.repository.mjs b/source/app/mocks/api/github/graphql/repositories.repository.mjs new file mode 100644 index 00000000..488dd422 --- /dev/null +++ b/source/app/mocks/api/github/graphql/repositories.repository.mjs @@ -0,0 +1,29 @@ +/**Mocked data */ +export default function({faker, query, login = faker.internet.userName()}) { + console.debug("metrics/compute/mocks > mocking graphql api result > stars/default") + return ({ + repository:{ + createdAt: faker.date.past(), + description:"📊 An image generator with 20+ metrics about your GitHub account such as activity, community, repositories, coding habits, website performances, music played, starred topics, etc. that you can put on your profile or elsewhere !", + forkCount:faker.datatype.number(100), + isFork:false, + issues:{ + totalCount:faker.datatype.number(100), + }, + nameWithOwner:"lowlighter/metrics", + openGraphImageUrl:"https://repository-images.githubusercontent.com/293860197/7fd72080-496d-11eb-8fe0-238b38a0746a", + pullRequests:{ + totalCount:faker.datatype.number(100), + }, + stargazerCount:faker.datatype.number(10000), + licenseInfo:{ + nickname:null, + name:"MIT License", + }, + primaryLanguage:{ + color:"#f1e05a", + name:"JavaScript", + }, + }, + }) +} diff --git a/source/app/web/statics/app.placeholder.js b/source/app/web/statics/app.placeholder.js index 2c327712..aa3761ec 100644 --- a/source/app/web/statics/app.placeholder.js +++ b/source/app/web/statics/app.placeholder.js @@ -678,7 +678,31 @@ }, }) : null), - //Stars + //Repositories + ...(set.plugins.enabled.repositories + ? ({ + repositories: { + list: new Array(Number(options["repositories.featured"].split(",").length) - 1).fill(null).map((_, i) => ({ + created: faker.date.past(), + description: faker.lorem.sentence(), + forkCount: faker.datatype.number(100), + isFork: faker.datatype.boolean(), + issues: { + totalCount: faker.datatype.number(100), + }, + nameWithOwner: `${faker.random.word()}/${faker.random.word()}`, + openGraphImageUrl: faker.internet.url(), + pullRequests: { + totalCount: faker.datatype.number(100), + }, + stargazerCount: faker.datatype.number(10000), + licenseInfo: { nickname: null, name: "License" }, + primaryLanguage: { color: faker.internet.color(), name: faker.lorem.word() }, + })), + }, + }) + : null), + //Stargazers ...(set.plugins.enabled.stargazers ? ({ get stargazers() { diff --git a/source/plugins/repositories/README.md b/source/plugins/repositories/README.md new file mode 100644 index 00000000..826f64d9 --- /dev/null +++ b/source/plugins/repositories/README.md @@ -0,0 +1,26 @@ +### 📓 Repositories + +The *repositories* plugin can display a list of chosen featured repositories. + + + +
+ + +
+ +It is mostly intended for external usage as [pinned repositories](https://www.google.com/search?client=firefox-b-d&q=github+pinned+repositories) is probably a better alternative if you want to embed them on your profile. + +Because of limitations of using SVG inside of `` tags, people won't be able to click on it. + +#### â„šī¸ Examples workflows + +[âžĄī¸ Available options for this plugin](metadata.yml) + +```yaml +- uses: lowlighter/metrics@latest + with: + # ... other options + plugin_repositories: yes + plugin_repositories_list: lowlighter/metrics, denoland/deno # List of repositories you want to feature +``` \ No newline at end of file diff --git a/source/plugins/repositories/index.mjs b/source/plugins/repositories/index.mjs new file mode 100644 index 00000000..163bdd46 --- /dev/null +++ b/source/plugins/repositories/index.mjs @@ -0,0 +1,38 @@ +//Setup +export default async function({login, q, imports, graphql, queries, data, account}, {enabled = false} = {}) { + //Plugin execution + try { + //Check if plugin is enabled and requirements are met + if ((!enabled)||(!q.repositories)) + return null + + //Load inputs + let {featured} = imports.metadata.plugins.repositories.inputs({data, account, q}) + + //Initialization + const repositories = {list:[]} + + //Fetch repositories informations + for (const repo of featured) { + const {owner = login, name} = repo.match(/^(?:(?[\s\S]*)[/])?(?[\s\S]+)$/)?.groups ?? {} + const {repository} = await graphql(queries.repositories.repository({owner, name})) + repositories.list.push(repository) + + //Format date + const time = (Date.now() - new Date(repository.createdAt).getTime()) / (24 * 60 * 60 * 1000) + let created = new Date(repository.createdAt).toDateString().substring(4) + if (time < 1) + created = `${Math.ceil(time * 24)} hour${Math.ceil(time * 24) >= 2 ? "s" : ""} ago` + else if (time < 30) + created = `${Math.floor(time)} day${time >= 2 ? "s" : ""} ago` + repository.created = created + } + + //Results + return repositories + } + //Handle errors + catch (error) { + throw {error:{message:"An error occured", instance:error}} + } +} \ No newline at end of file diff --git a/source/plugins/repositories/metadata.yml b/source/plugins/repositories/metadata.yml new file mode 100644 index 00000000..d18bca69 --- /dev/null +++ b/source/plugins/repositories/metadata.yml @@ -0,0 +1,22 @@ +name: "📓 Repositories" +cost: 1 GraphQL request per repository +category: github +supports: + - user + - organization +inputs: + + # Enable or disable plugin + plugin_repositories: + description: Display chosen featured repositories + type: boolean + default: no + + # Featured repositories to display + # If no owner is specified, it will implicitly use the current account login as owner + plugin_repositories_featured: + description: List of repositories to display + type: array + format: comma-separated + default: "" + example: lowlighter/metrics diff --git a/source/plugins/repositories/queries/repository.graphql b/source/plugins/repositories/queries/repository.graphql new file mode 100644 index 00000000..b4bcb788 --- /dev/null +++ b/source/plugins/repositories/queries/repository.graphql @@ -0,0 +1,26 @@ +query RepositoriesRepository { + repository(owner: "$owner", name: "$name") { + createdAt + description + forkCount + isFork + issues { + totalCount + } + nameWithOwner + openGraphImageUrl + licenseInfo { + nickname + spdxId + name + } + pullRequests { + totalCount + } + stargazerCount + primaryLanguage { + color + name + } + } +} diff --git a/source/plugins/repositories/tests.yml b/source/plugins/repositories/tests.yml new file mode 100644 index 00000000..77f25749 --- /dev/null +++ b/source/plugins/repositories/tests.yml @@ -0,0 +1,6 @@ +- name: Repositories plugin (default) + uses: lowlighter/metrics@latest + with: + token: MOCKED_TOKEN + plugin_repositories: yes + plugin_repositories_list: metrics \ No newline at end of file diff --git a/source/templates/classic/partials/_.json b/source/templates/classic/partials/_.json index 0834249c..f88ec37e 100644 --- a/source/templates/classic/partials/_.json +++ b/source/templates/classic/partials/_.json @@ -7,6 +7,7 @@ "languages", "notable", "projects", + "repositories", "gists", "pagespeed", "habits", diff --git a/source/templates/classic/partials/repositories.ejs b/source/templates/classic/partials/repositories.ejs new file mode 100644 index 00000000..879e2aa2 --- /dev/null +++ b/source/templates/classic/partials/repositories.ejs @@ -0,0 +1,78 @@ +<% if (plugins.repositories) { %> +
+

+ + Featured repositories +

+
+
+ <% if (plugins.repositories.error) { %> +
+ + <%= plugins.repositories.error.message %> +
+ <% } else if (plugins.repositories.list.length) { %> + <% for (const repository of plugins.repositories.list) { %> +
+
+
+ <% if (repository.isFork) { %> + + <% } else { %> + + <% } %> +
+ <%= repository.nameWithOwner %> + created <%= repository.created %> +
+
+
+ <%= repository.description %> +
+
+ <% if (repository.primaryLanguage) { %> +
+ + <%= repository.primaryLanguage.name %> +
+ <% } %> + <% if (repository.licenseInfo) { %> +
+ + <%= f.license(repository.licenseInfo) %> +
+ <% } %> +
+ + <%= f(repository.stargazerCount) %> +
+
+ + <%= f(repository.forkCount) %> +
+
+ + <%= f(repository.issues.totalCount) %> +
+
+ + <%= f(repository.pullRequests.totalCount) %> +
+
+
+
+ <% } %> + <% } else { %> +
+
+
+ + Configure this plugin with repositories you want to feature! +
+
+
+ <% } %> +
+
+
+<% } %> \ No newline at end of file